Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .jules/palette.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## 2023-10-24 - μž₯μ‹μš© ν™”μ‚΄ν‘œ μ•„μ΄μ½˜ μ ‘κ·Όμ„± κ°œμ„ 
**Learning:** Lucide의 `Chevron` μ‹œλ¦¬μ¦ˆ λ“± λ‹¨μˆœ μž₯μ‹μš© μ•„μ΄μ½˜λ“€μ΄ 슀크린 λ¦¬λ”μ—μ„œ μ½νžˆλŠ” λ¬Έμ œκ°€ μžˆμ—ˆμŠ΅λ‹ˆλ‹€.
**Action:** μž₯μ‹μš© μ•„μ΄μ½˜μ—λŠ” 항상 `aria-hidden="true"`λ₯Ό μΆ”κ°€ν•˜μ—¬ 슀크린 리더 μ‚¬μš©μ„±μ„ κ°œμ„ ν•©λ‹ˆλ‹€.
15 changes: 9 additions & 6 deletions packages/web/src/components/dashboard/event-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,8 @@ function getSinglePreview(event: TimelineEvent): string {
return normalized.slice(0, 80);
}
if (event.isSkillCall && event.skillName) return `Skill: ${event.skillName}`;
if (event.isAgentCall && event.agentType) return `Subagent: ${event.agentType}`;
if (event.isAgentCall && event.agentType)
return `Subagent: ${event.agentType}`;
return event.toolName;
}

Expand Down Expand Up @@ -179,6 +180,7 @@ function RowView({
<span className="flex-1 min-w-0 flex items-center gap-1 text-sm text-muted-foreground">
{chevron !== undefined && (
<ChevronRight
aria-hidden="true"
className={cn(
"h-3 w-3 shrink-0 transition-transform",
chevron === "expanded" && "rotate-90",
Expand Down Expand Up @@ -231,11 +233,12 @@ function Row({
}

const label = row.labelOverride ?? getSingleLabel(row.event);
const preview = row.labelOverride === "Tool"
? row.event.kind === "tool"
? row.event.toolName
: getSinglePreview(row.event)
: getSinglePreview(row.event);
const preview =
row.labelOverride === "Tool"
? row.event.kind === "tool"
? row.event.toolName
: getSinglePreview(row.event)
: getSinglePreview(row.event);

return (
<div style={style} role="listitem">
Expand Down
44 changes: 27 additions & 17 deletions packages/web/src/components/dashboard/reports/context-section.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
'use client'
"use client";

import React, { useState, useId, type ReactNode } from 'react'
import { ChevronDown, ChevronUp } from 'lucide-react'
import { cn } from '@/lib/utils'
import React, { useState, useId, type ReactNode } from "react";
import { ChevronDown, ChevronUp } from "lucide-react";
import { cn } from "@/lib/utils";

interface ContextSectionProps {
title: string
children: ReactNode
defaultOpen?: boolean
title: string;
children: ReactNode;
defaultOpen?: boolean;
}

export function ContextSection({ title, children, defaultOpen = false }: ContextSectionProps) {
const [open, setOpen] = useState(defaultOpen)
const id = useId()
const buttonId = `context-section-btn-${id}`
const contentId = `context-section-content-${id}`
export function ContextSection({
title,
children,
defaultOpen = false,
}: ContextSectionProps) {
const [open, setOpen] = useState(defaultOpen);
const id = useId();
const buttonId = `context-section-btn-${id}`;
const contentId = `context-section-content-${id}`;

return (
<div className="rounded-xl bg-card ring-1 ring-foreground/10 overflow-hidden">
Expand All @@ -23,17 +27,23 @@ export function ContextSection({ title, children, defaultOpen = false }: Context
id={buttonId}
onClick={() => setOpen((v) => !v)}
className={cn(
'w-full flex items-center justify-between px-4 py-3 text-left',
'hover:bg-card-elevated transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
"w-full flex items-center justify-between px-4 py-3 text-left",
"hover:bg-card-elevated transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
)}
aria-expanded={open}
aria-controls={contentId}
>
<h2 className="text-base font-medium">{title}</h2>
{open ? (
<ChevronUp className="h-4 w-4 text-muted-foreground" />
<ChevronUp
className="h-4 w-4 text-muted-foreground"
aria-hidden="true"
/>
) : (
<ChevronDown className="h-4 w-4 text-muted-foreground" />
<ChevronDown
className="h-4 w-4 text-muted-foreground"
aria-hidden="true"
/>
)}
</button>
{open && (
Expand All @@ -47,5 +57,5 @@ export function ContextSection({ title, children, defaultOpen = false }: Context
</div>
)}
</div>
)
);
}
80 changes: 44 additions & 36 deletions packages/web/src/components/dashboard/reports/week-navigator.tsx
Original file line number Diff line number Diff line change
@@ -1,60 +1,68 @@
'use client'
"use client";

import Link from 'next/link'
import { usePathname, useSearchParams } from 'next/navigation'
import { ChevronLeft, ChevronRight } from 'lucide-react'
import { buttonVariants } from '@/components/ui/button'
import { cn } from '@/lib/utils'
import Link from "next/link";
import { usePathname, useSearchParams } from "next/navigation";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { buttonVariants } from "@/components/ui/button";
import { cn } from "@/lib/utils";

interface WeekNavigatorProps {
currentIsoKey: string // e.g. "2026-W16"
label: string // e.g. "2026-W16 (4/13~4/19)"
isCurrent: boolean // 였늘이 ν¬ν•¨λœ μ£Ό
currentIsoKey: string; // e.g. "2026-W16"
label: string; // e.g. "2026-W16 (4/13~4/19)"
isCurrent: boolean; // 였늘이 ν¬ν•¨λœ μ£Ό
}

/** "YYYY-Www" β†’ 직전/λ‹€μŒ μ£Ό ISO key 계산 */
function shiftWeek(iso: string, offset: number): string | null {
const m = iso.match(/^(\d{4})-W(\d{1,2})$/)
if (!m) return null
const year = parseInt(m[1], 10)
const week = parseInt(m[2], 10)
const m = iso.match(/^(\d{4})-W(\d{1,2})$/);
if (!m) return null;
const year = parseInt(m[1], 10);
const week = parseInt(m[2], 10);

// κ°„λ‹¨ν•œ μ ‘κ·Ό: ISO weekλ₯Ό ms둜 ν™˜μ‚°ν•˜μ§€ μ•Šκ³ , jan4 기반으둜 Β±offset μ£Ό 이동
const jan4 = new Date(Date.UTC(year, 0, 4))
const cur = new Date(jan4.getTime() + (week - 1 + offset) * 7 * 24 * 60 * 60 * 1000)
const jan4 = new Date(Date.UTC(year, 0, 4));
const cur = new Date(
jan4.getTime() + (week - 1 + offset) * 7 * 24 * 60 * 60 * 1000,
);

// ISO week year + week μž¬κ³„μ‚°
const tmp = new Date(cur.getTime())
const tmp = new Date(cur.getTime());
// ISO week: Thursdayκ°€ μ†ν•œ ν•΄κ°€ week year
tmp.setUTCDate(tmp.getUTCDate() + 4 - (tmp.getUTCDay() || 7))
const isoYear = tmp.getUTCFullYear()
const yearStart = new Date(Date.UTC(isoYear, 0, 1))
const isoWeek = Math.ceil(((tmp.getTime() - yearStart.getTime()) / (24 * 60 * 60 * 1000) + 1) / 7)
return `${isoYear}-W${String(isoWeek).padStart(2, '0')}`
tmp.setUTCDate(tmp.getUTCDate() + 4 - (tmp.getUTCDay() || 7));
const isoYear = tmp.getUTCFullYear();
const yearStart = new Date(Date.UTC(isoYear, 0, 1));
const isoWeek = Math.ceil(
((tmp.getTime() - yearStart.getTime()) / (24 * 60 * 60 * 1000) + 1) / 7,
);
return `${isoYear}-W${String(isoWeek).padStart(2, "0")}`;
}

export function WeekNavigator({ currentIsoKey, label, isCurrent }: WeekNavigatorProps) {
const pathname = usePathname()
const searchParams = useSearchParams()
export function WeekNavigator({
currentIsoKey,
label,
isCurrent,
}: WeekNavigatorProps) {
const pathname = usePathname();
const searchParams = useSearchParams();

const buildHref = (targetIso: string | null) => {
if (!targetIso) return pathname
const params = new URLSearchParams(searchParams.toString())
params.set('week', targetIso)
return `${pathname}?${params.toString()}`
}
if (!targetIso) return pathname;
const params = new URLSearchParams(searchParams.toString());
params.set("week", targetIso);
return `${pathname}?${params.toString()}`;
};

const prevIso = shiftWeek(currentIsoKey, -1)
const nextIso = shiftWeek(currentIsoKey, 1)
const prevIso = shiftWeek(currentIsoKey, -1);
const nextIso = shiftWeek(currentIsoKey, 1);

return (
<div className="flex items-center gap-2">
<Link
href={buildHref(prevIso)}
aria-label="이전 μ£Ό"
className={cn(buttonVariants({ variant: 'outline', size: 'icon-sm' }))}
className={cn(buttonVariants({ variant: "outline", size: "icon-sm" }))}
>
<ChevronLeft className="h-4 w-4" />
<ChevronLeft className="h-4 w-4" aria-hidden="true" />
</Link>
<div className="flex items-center gap-2 px-3 py-1.5 rounded-md bg-muted">
<span className="text-sm font-medium tabular-nums">{label}</span>
Expand All @@ -67,10 +75,10 @@ export function WeekNavigator({ currentIsoKey, label, isCurrent }: WeekNavigator
<Link
href={buildHref(nextIso)}
aria-label="λ‹€μŒ μ£Ό"
className={cn(buttonVariants({ variant: 'outline', size: 'icon-sm' }))}
className={cn(buttonVariants({ variant: "outline", size: "icon-sm" }))}
>
<ChevronRight className="h-4 w-4" />
<ChevronRight className="h-4 w-4" aria-hidden="true" />
</Link>
</div>
)
);
}
84 changes: 46 additions & 38 deletions packages/web/src/components/ui/pagination.tsx
Original file line number Diff line number Diff line change
@@ -1,47 +1,47 @@
'use client'
"use client";

import { ChevronLeftIcon, ChevronRightIcon } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { cn } from '@/lib/utils'
} from "@/components/ui/select";
import { cn } from "@/lib/utils";

type PaginationProps = {
page: number
pageSize: number
total: number
onPageChange: (page: number) => void
onPageSizeChange?: (pageSize: number) => void
pageSizeOptions?: number[]
className?: string
page: number;
pageSize: number;
total: number;
onPageChange: (page: number) => void;
onPageSizeChange?: (pageSize: number) => void;
pageSizeOptions?: number[];
className?: string;
/** νŽ˜μ΄μ§€ μˆ˜κ°€ 1 μ΄ν•˜μ—¬λ„ count 라벨은 ν‘œμ‹œ (κΈ°λ³Έ true). falseλ©΄ 전체 μˆ¨κΉ€ */
alwaysShowLabel?: boolean
}
alwaysShowLabel?: boolean;
};

/**
* 1 … x-1 [x] x+1 … N ν˜•νƒœλ‘œ μΆ•μ•½λœ νŽ˜μ΄μ§€ 번호λ₯Ό 계산.
* ellipsisλŠ” -1둜 ν‘œκΈ°.
*/
function getPageList(current: number, totalPages: number): (number | -1)[] {
if (totalPages <= 7) {
return Array.from({ length: totalPages }, (_, i) => i + 1)
return Array.from({ length: totalPages }, (_, i) => i + 1);
}

const pages: (number | -1)[] = [1]
const start = Math.max(2, current - 1)
const end = Math.min(totalPages - 1, current + 1)
const pages: (number | -1)[] = [1];
const start = Math.max(2, current - 1);
const end = Math.min(totalPages - 1, current + 1);

if (start > 2) pages.push(-1)
for (let p = start; p <= end; p++) pages.push(p)
if (end < totalPages - 1) pages.push(-1)
if (start > 2) pages.push(-1);
for (let p = start; p <= end; p++) pages.push(p);
if (end < totalPages - 1) pages.push(-1);

pages.push(totalPages)
return pages
pages.push(totalPages);
return pages;
}

export function Pagination({
Expand All @@ -54,19 +54,19 @@ export function Pagination({
className,
alwaysShowLabel = true,
}: PaginationProps) {
const totalPages = Math.max(1, Math.ceil(total / pageSize))
const safePage = Math.min(Math.max(1, page), totalPages)
const startIdx = total === 0 ? 0 : (safePage - 1) * pageSize + 1
const endIdx = Math.min(safePage * pageSize, total)
const totalPages = Math.max(1, Math.ceil(total / pageSize));
const safePage = Math.min(Math.max(1, page), totalPages);
const startIdx = total === 0 ? 0 : (safePage - 1) * pageSize + 1;
const endIdx = Math.min(safePage * pageSize, total);

if (totalPages <= 1 && !alwaysShowLabel) return null
if (totalPages <= 1 && !alwaysShowLabel) return null;

const pageList = getPageList(safePage, totalPages)
const pageList = getPageList(safePage, totalPages);

return (
<div
className={cn(
'flex flex-col gap-3 border-t border-border bg-muted/20 px-4 py-3 text-xs text-muted-foreground sm:flex-row sm:items-center sm:justify-between',
"flex flex-col gap-3 border-t border-border bg-muted/20 px-4 py-3 text-xs text-muted-foreground sm:flex-row sm:items-center sm:justify-between",
className,
)}
>
Expand All @@ -75,7 +75,11 @@ export function Pagination({
<span>κ²°κ³Ό μ—†μŒ</span>
) : (
<span>
총 <span className="text-foreground font-medium">{total.toLocaleString()}</span>개 쀑{' '}
총{" "}
<span className="text-foreground font-medium">
{total.toLocaleString()}
</span>
개 쀑{" "}
<span className="text-foreground font-medium">
{startIdx.toLocaleString()}–{endIdx.toLocaleString()}
</span>
Expand All @@ -90,15 +94,19 @@ export function Pagination({
<Select
value={String(pageSize)}
onValueChange={(value) => {
if (value) onPageSizeChange(Number(value))
if (value) onPageSizeChange(Number(value));
}}
>
<SelectTrigger size="sm" className="min-w-[60px]">
<SelectValue />
</SelectTrigger>
<SelectContent alignItemWithTrigger={false} className="p-1">
{pageSizeOptions.map((size) => (
<SelectItem key={size} value={String(size)} className="px-2 py-1.5">
<SelectItem
key={size}
value={String(size)}
className="px-2 py-1.5"
>
{size}
</SelectItem>
))}
Expand All @@ -116,7 +124,7 @@ export function Pagination({
disabled={safePage <= 1}
aria-label="이전 νŽ˜μ΄μ§€"
>
<ChevronLeftIcon />
<ChevronLeftIcon aria-hidden="true" />
</Button>

{/* λͺ¨λ°”일: ν˜„μž¬ νŽ˜μ΄μ§€ / μ „μ²΄λ§Œ ν‘œμ‹œ */}
Expand All @@ -137,11 +145,11 @@ export function Pagination({
) : (
<Button
key={p}
variant={p === safePage ? 'default' : 'ghost'}
variant={p === safePage ? "default" : "ghost"}
size="icon-sm"
onClick={() => onPageChange(p)}
className="tabular-nums"
aria-current={p === safePage ? 'page' : undefined}
aria-current={p === safePage ? "page" : undefined}
>
{p}
</Button>
Expand All @@ -156,11 +164,11 @@ export function Pagination({
disabled={safePage >= totalPages}
aria-label="λ‹€μŒ νŽ˜μ΄μ§€"
>
<ChevronRightIcon />
<ChevronRightIcon aria-hidden="true" />
</Button>
</div>
)}
</div>
</div>
)
);
}
Loading