From 60b647101a37850e72b0cf13840f76996f800b26 Mon Sep 17 00:00:00 2001 From: Pedro Nauck Date: Tue, 28 Apr 2026 13:16:25 -0300 Subject: [PATCH 1/6] refactor: badges design --- .../landing/__tests__/landing.test.tsx | 14 +- .../components/landing/bridges-section.tsx | 14 +- .../landing/network-protocol-visual.tsx | 14 +- .../components/landing/primitives/index.ts | 3 +- .../landing/primitives/kind-chip.tsx | 38 --- .../landing/primitives/mono-badge.tsx | 31 -- .../landing/primitives/network-kinds.ts | 16 ++ packages/ui/.storybook/preview.css | 2 + packages/ui/.storybook/preview.ts | 1 - packages/ui/README.md | 31 +- .../components/connection-indicator.test.tsx | 91 ------ packages/ui/src/components/input-group.tsx | 2 +- packages/ui/src/components/input.tsx | 2 +- packages/ui/src/components/kind-chip.test.tsx | 53 ---- packages/ui/src/components/kind-chip.tsx | 56 ---- .../ui/src/components/mono-badge.test.tsx | 79 ------ packages/ui/src/components/mono-badge.tsx | 63 ---- packages/ui/src/components/mono-chip.test.tsx | 22 -- packages/ui/src/components/mono-chip.tsx | 29 -- .../{pills.test.tsx => pill-group.test.tsx} | 63 ++-- packages/ui/src/components/pill-group.tsx | 101 +++++++ packages/ui/src/components/pill.test.tsx | 217 ++++++++++++++ packages/ui/src/components/pill.tsx | 200 +++++++++++++ packages/ui/src/components/pills.tsx | 154 ---------- .../ui/src/components/status-dot.test.tsx | 90 ------ packages/ui/src/components/status-dot.tsx | 66 ----- .../components/stories/accordion.stories.tsx | 1 - .../src/components/stories/alert.stories.tsx | 1 - .../src/components/stories/avatar.stories.tsx | 1 - .../src/components/stories/badge.stories.tsx | 1 - .../components/stories/breadcrumb.stories.tsx | 1 - .../stories/button-group.stories.tsx | 1 - .../src/components/stories/button.stories.tsx | 1 - .../src/components/stories/card.stories.tsx | 1 - .../stories/chat-message-bubble.stories.tsx | 5 +- .../components/stories/code-block.stories.tsx | 1 - .../stories/collapsible.stories.tsx | 1 - .../components/stories/combobox.stories.tsx | 1 - .../components/stories/command.stories.tsx | 1 - .../stories/connection-indicator.stories.tsx | 52 ---- .../src/components/stories/dialog.stories.tsx | 1 - .../components/stories/direction.stories.tsx | 1 - .../stories/dropdown-menu.stories.tsx | 1 - .../src/components/stories/empty.stories.tsx | 1 - .../src/components/stories/field.stories.tsx | 1 - .../stories/input-group.stories.tsx | 1 - .../src/components/stories/input.stories.tsx | 1 - .../src/components/stories/item.stories.tsx | 1 - .../ui/src/components/stories/kbd.stories.tsx | 1 - .../components/stories/kind-chip.stories.tsx | 48 ---- .../src/components/stories/label.stories.tsx | 1 - .../src/components/stories/logo.stories.tsx | 1 - .../src/components/stories/metric.stories.tsx | 1 - .../components/stories/mono-badge.stories.tsx | 65 ----- .../components/stories/mono-chip.stories.tsx | 35 --- .../stories/native-select.stories.tsx | 1 - .../stories/page-header.stories.tsx | 7 +- .../components/stories/pill-group.stories.tsx | 90 ++++++ .../src/components/stories/pill.stories.tsx | 268 ++++++++++++++++++ .../src/components/stories/pills.stories.tsx | 103 ------- .../components/stories/popover.stories.tsx | 1 - .../components/stories/progress.stories.tsx | 1 - .../stories/scroll-area.stories.tsx | 1 - .../stories/search-input.stories.tsx | 1 - .../components/stories/section.stories.tsx | 5 +- .../src/components/stories/select.stories.tsx | 1 - .../components/stories/separator.stories.tsx | 1 - .../src/components/stories/sheet.stories.tsx | 1 - .../components/stories/sidebar.stories.tsx | 1 - .../components/stories/skeleton.stories.tsx | 1 - .../src/components/stories/sonner.stories.tsx | 1 - .../components/stories/spinner.stories.tsx | 1 - .../components/stories/split-pane.stories.tsx | 1 - .../components/stories/status-dot.stories.tsx | 102 ------- .../src/components/stories/switch.stories.tsx | 1 - .../src/components/stories/table.stories.tsx | 1 - .../src/components/stories/tabs.stories.tsx | 1 - .../components/stories/textarea.stories.tsx | 1 - .../stories/toggle-group.stories.tsx | 1 - .../src/components/stories/toggle.stories.tsx | 1 - .../stories/tool-call-card.stories.tsx | 1 - .../components/stories/toolbar.stories.tsx | 9 +- .../components/stories/tooltip.stories.tsx | 1 - .../stories/typing-dots.stories.tsx | 1 - .../stories/ui-provider.stories.tsx | 1 - .../components/stories/wire-card.stories.tsx | 1 - .../components/stories/wire-chip.stories.tsx | 51 ---- packages/ui/src/components/tool-call-card.tsx | 8 +- packages/ui/src/components/toolbar.tsx | 2 +- packages/ui/src/components/wire-chip.test.tsx | 42 --- packages/ui/src/components/wire-chip.tsx | 57 ---- packages/ui/src/index.ts | 37 +-- skills-lock.json | 5 - web/AGENTS.md | 39 +-- web/CLAUDE.md | 39 +-- web/src/components/app-sidebar.test.tsx | 22 +- web/src/components/app-sidebar.tsx | 31 +- .../src/components/connection-indicator.tsx | 26 +- .../design-system-showcase.test.tsx | 10 +- web/src/components/design-system-showcase.tsx | 107 ++++--- .../stories/app-sidebar.stories.tsx | 8 +- web/src/hooks/routes/use-home-page.ts | 6 +- web/src/lib/kind-colors.ts | 19 ++ web/src/lib/pill-variant.ts | 10 +- web/src/routes/_app.tsx | 1 - web/src/routes/_app/-tasks.test.tsx | 2 +- web/src/routes/_app/bridges.tsx | 4 +- web/src/routes/_app/index.tsx | 18 +- web/src/routes/_app/knowledge.tsx | 4 +- web/src/routes/_app/sandbox.tsx | 6 +- .../_app/settings/-mcp-servers.test.tsx | 2 +- web/src/routes/_app/settings/general.tsx | 4 +- .../routes/_app/settings/hooks-extensions.tsx | 31 +- web/src/routes/_app/settings/mcp-servers.tsx | 13 +- .../routes/_app/settings/observability.tsx | 7 +- web/src/routes/_app/tasks.tsx | 4 +- .../packages-ui-storybook-config.test.ts | 5 +- .../agent/components/agent-info-panel.tsx | 6 +- .../agent/components/agent-page-header.tsx | 124 ++++---- .../agent/components/agent-sessions-list.tsx | 6 +- .../stories/agent-page-header.stories.tsx | 3 +- web/src/systems/agent/lib/session-status.ts | 4 +- .../components/automation-detail-panel.tsx | 48 ++-- .../components/automation-job-form.tsx | 8 +- .../components/automation-list-panel.tsx | 26 +- .../components/automation-operations-page.tsx | 4 +- .../components/automation-run-history.tsx | 11 +- .../components/automation-trigger-form.tsx | 12 +- .../components/bridge-create-dialog.tsx | 8 +- .../components/bridge-detail-panel.test.tsx | 2 +- .../components/bridge-detail-panel.tsx | 40 +-- .../bridges/components/bridge-edit-dialog.tsx | 4 +- .../bridges/components/bridge-list-panel.tsx | 14 +- .../components/bridge-provider-card.tsx | 22 +- .../bridge-test-delivery-dialog.tsx | 12 +- .../systems/daemon/hooks/use-daemon-health.ts | 2 +- .../components/knowledge-detail-panel.tsx | 14 +- .../components/knowledge-list-panel.tsx | 12 +- .../knowledge/lib/knowledge-formatters.ts | 8 +- .../systems/network/components/kind-chip.tsx | 35 +++ .../network-create-channel-dialog.tsx | 10 +- .../network-workspace-shell.test.tsx | 2 +- .../components/network-workspace-shell.tsx | 140 ++++++--- .../session/components/chat-header.test.tsx | 4 +- .../session/components/chat-header.tsx | 21 +- .../components/runtime-activity-notice.tsx | 6 +- .../session/components/session-inspector.tsx | 20 +- .../components/session-resume-failure.tsx | 6 +- .../components/tool-call-card.test.tsx | 6 +- .../settings/components/provider-card.tsx | 15 +- .../components/settings-field-row.test.tsx | 4 +- .../components/settings-source-badge.tsx | 14 +- .../components/settings-status-line.tsx | 4 +- .../skill/components/marketplace-view.tsx | 19 +- .../skill/components/skill-detail-panel.tsx | 18 +- .../skill/components/skill-list-panel.tsx | 6 +- web/src/systems/skill/lib/skill-formatters.ts | 6 +- .../tasks/components/task-card.test.tsx | 2 +- .../systems/tasks/components/task-card.tsx | 11 +- .../tasks/components/task-editor-surface.tsx | 40 ++- .../components/task-run-detail-header.tsx | 15 +- .../components/task-run-detail-panels.tsx | 22 +- .../tasks-dashboard-active-runs.tsx | 10 +- .../tasks-dashboard-queue-health.tsx | 2 +- .../tasks-dashboard-status-breakdown.tsx | 2 +- .../tasks-detail-children-panel.tsx | 12 +- .../tasks-detail-dependencies-panel.tsx | 10 +- .../components/tasks-detail-header.test.tsx | 2 +- .../tasks/components/tasks-detail-header.tsx | 18 +- .../tasks-detail-overview-panel.tsx | 10 +- .../components/tasks-detail-preview-panel.tsx | 16 +- .../components/tasks-detail-runs-panel.tsx | 10 +- .../tasks/components/tasks-empty-state.tsx | 2 +- .../tasks/components/tasks-inbox-item.tsx | 2 +- .../tasks/components/tasks-kanban-board.tsx | 8 +- .../tasks/components/tasks-list-panel.tsx | 4 +- .../tasks/components/tasks-list-row.test.tsx | 6 +- .../tasks/components/tasks-list-row.tsx | 8 +- .../components/tasks-multi-agent-panel.tsx | 10 +- .../tasks/components/tasks-timeline-panel.tsx | 14 +- web/src/systems/tasks/lib/task-formatters.ts | 4 +- .../workspace/components/workspace-setup.tsx | 4 +- 182 files changed, 1751 insertions(+), 2128 deletions(-) delete mode 100644 packages/site/components/landing/primitives/kind-chip.tsx delete mode 100644 packages/site/components/landing/primitives/mono-badge.tsx create mode 100644 packages/site/components/landing/primitives/network-kinds.ts delete mode 100644 packages/ui/src/components/connection-indicator.test.tsx delete mode 100644 packages/ui/src/components/kind-chip.test.tsx delete mode 100644 packages/ui/src/components/kind-chip.tsx delete mode 100644 packages/ui/src/components/mono-badge.test.tsx delete mode 100644 packages/ui/src/components/mono-badge.tsx delete mode 100644 packages/ui/src/components/mono-chip.test.tsx delete mode 100644 packages/ui/src/components/mono-chip.tsx rename packages/ui/src/components/{pills.test.tsx => pill-group.test.tsx} (56%) create mode 100644 packages/ui/src/components/pill-group.tsx create mode 100644 packages/ui/src/components/pill.test.tsx create mode 100644 packages/ui/src/components/pill.tsx delete mode 100644 packages/ui/src/components/pills.tsx delete mode 100644 packages/ui/src/components/status-dot.test.tsx delete mode 100644 packages/ui/src/components/status-dot.tsx delete mode 100644 packages/ui/src/components/stories/connection-indicator.stories.tsx delete mode 100644 packages/ui/src/components/stories/kind-chip.stories.tsx delete mode 100644 packages/ui/src/components/stories/mono-badge.stories.tsx delete mode 100644 packages/ui/src/components/stories/mono-chip.stories.tsx create mode 100644 packages/ui/src/components/stories/pill-group.stories.tsx create mode 100644 packages/ui/src/components/stories/pill.stories.tsx delete mode 100644 packages/ui/src/components/stories/pills.stories.tsx delete mode 100644 packages/ui/src/components/stories/status-dot.stories.tsx delete mode 100644 packages/ui/src/components/stories/wire-chip.stories.tsx delete mode 100644 packages/ui/src/components/wire-chip.test.tsx delete mode 100644 packages/ui/src/components/wire-chip.tsx rename {packages/ui => web}/src/components/connection-indicator.tsx (66%) create mode 100644 web/src/lib/kind-colors.ts create mode 100644 web/src/systems/network/components/kind-chip.tsx diff --git a/packages/site/components/landing/__tests__/landing.test.tsx b/packages/site/components/landing/__tests__/landing.test.tsx index 98952973d..c68d555f2 100644 --- a/packages/site/components/landing/__tests__/landing.test.tsx +++ b/packages/site/components/landing/__tests__/landing.test.tsx @@ -37,7 +37,9 @@ import { NetworkSection } from "../network-section"; import { InstallSection } from "../install-section"; import { Comparison } from "../comparison"; import { FinalCta } from "../final-cta"; -import { KindChip, KIND_MEANING, type NetworkKind } from "../primitives/kind-chip"; +import { Pill } from "@agh/ui"; + +import { KIND_MEANING, type NetworkKind } from "../primitives/network-kinds"; describe("Hero", () => { it("leads with the runtime + network headline and drops ACP from the fold", () => { @@ -339,8 +341,8 @@ describe("FinalCta", () => { }); }); -describe("KindChip", () => { - it("has a meaning string for every NetworkKind", () => { +describe("Network kind pill", () => { + it("has a meaning string for every NetworkKind and renders inside Pill", () => { const kinds: NetworkKind[] = [ "greet", "whois", @@ -352,7 +354,11 @@ describe("KindChip", () => { ]; for (const kind of kinds) { expect(KIND_MEANING[kind]).toBeDefined(); - render(); + render( + + {kind} + + ); expect(screen.getAllByText(kind)).toBeDefined(); } }); diff --git a/packages/site/components/landing/bridges-section.tsx b/packages/site/components/landing/bridges-section.tsx index 471b61ca1..34b805a7f 100644 --- a/packages/site/components/landing/bridges-section.tsx +++ b/packages/site/components/landing/bridges-section.tsx @@ -1,5 +1,6 @@ import { ArrowRight } from "lucide-react"; import type { ReactNode } from "react"; +import { Pill } from "@agh/ui"; import { DiscordLogo, GithubLogo, @@ -10,7 +11,6 @@ import { TelegramLogo, WhatsAppLogo, } from "@agh/ui/logos"; -import { MonoBadge } from "./primitives/mono-badge"; import { SectionFrame } from "./primitives/section-frame"; import { SectionHeader } from "./primitives/section-header"; @@ -84,9 +84,13 @@ export function BridgesSection() {
{bridge.logo}
{bridge.status === "live" ? ( - live + + live + ) : ( - next + + next + )}

{bridge.name}

@@ -104,7 +108,9 @@ export function BridgesSection() {

How a bridge delivers a session

- inside the daemon + + inside the daemon +
diff --git a/packages/site/components/landing/network-protocol-visual.tsx b/packages/site/components/landing/network-protocol-visual.tsx index 57665e0cc..7f9054789 100644 --- a/packages/site/components/landing/network-protocol-visual.tsx +++ b/packages/site/components/landing/network-protocol-visual.tsx @@ -2,10 +2,10 @@ import { useEffect, useReducer, useRef } from "react"; import { ArrowLeftRight, Pause, Play } from "lucide-react"; -import { Button } from "@agh/ui"; +import { Button, Pill } from "@agh/ui"; import { cn } from "@agh/ui/utils"; import { AnimatedDiagram } from "./primitives/animated-diagram"; -import { KindChip, type NetworkKind } from "./primitives/kind-chip"; +import { KIND_MEANING, type NetworkKind } from "./primitives/network-kinds"; type Lane = "A" | "NET" | "B"; type Direction = "->" | "<-" | ".."; @@ -232,7 +232,15 @@ function Inner({ active, reducedMotion }: { active: boolean; reducedMotion: bool
- + + {step.kind} + ; - -interface KindChipProps { - kind: NetworkKind; - className?: string; - /** Force a visual "active" / highlighted state. */ - active?: boolean; -} - -export function KindChip({ kind, className, active = false }: KindChipProps) { - return ( - - {kind} - - ); -} diff --git a/packages/site/components/landing/primitives/mono-badge.tsx b/packages/site/components/landing/primitives/mono-badge.tsx deleted file mode 100644 index d17a3f4c9..000000000 --- a/packages/site/components/landing/primitives/mono-badge.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import type { ReactNode } from "react"; -import { cn } from "@agh/ui/utils"; - -type Tone = "accent" | "neutral" | "success"; - -const TONE_CLASS: Record = { - accent: "bg-(--color-accent-tint) text-(--color-accent)", - neutral: "bg-(--color-surface-elevated) text-(--color-text-tertiary)", - success: "bg-(--color-success-tint) text-(--color-success)", -}; - -interface MonoBadgeProps { - children: ReactNode; - tone?: Tone; - className?: string; -} - -/** 11px uppercase mono chip — the ubiquitous eyebrow/label on the landing page. */ -export function MonoBadge({ children, tone = "accent", className }: MonoBadgeProps) { - return ( - - {children} - - ); -} diff --git a/packages/site/components/landing/primitives/network-kinds.ts b/packages/site/components/landing/primitives/network-kinds.ts new file mode 100644 index 000000000..e0256c837 --- /dev/null +++ b/packages/site/components/landing/primitives/network-kinds.ts @@ -0,0 +1,16 @@ +/** + * Wire-protocol kinds rendered on the landing diagrams. Kept as data-only so + * the chrome (a `Pill mono` from `@agh/ui`) can be composed inline by callers. + */ +export type NetworkKind = "greet" | "whois" | "say" | "direct" | "capability" | "receipt" | "trace"; + +/** One-line purpose for every kind — tooltip copy, alt text, and copy audit source. */ +export const KIND_MEANING = { + greet: "Announce presence + capabilities to a channel", + whois: "Ask the network which peers match a capability", + say: "Free-form operator chat to a channel", + direct: "Send a structured task to a named peer", + capability: "Transfer a full capability artifact to a peer", + receipt: "Confirm completion with status and trace IDs", + trace: "Stream progress updates during a task", +} as const satisfies Record; diff --git a/packages/ui/.storybook/preview.css b/packages/ui/.storybook/preview.css index 4cb5c8bd7..e72841f9e 100644 --- a/packages/ui/.storybook/preview.css +++ b/packages/ui/.storybook/preview.css @@ -1,3 +1,5 @@ @import "tailwindcss"; +@import "@agh/ui/tokens.css"; +@import "shadcn/tailwind.css"; @source "../src/**/*.{ts,tsx}"; diff --git a/packages/ui/.storybook/preview.ts b/packages/ui/.storybook/preview.ts index 1d598c756..a44e0b4a4 100644 --- a/packages/ui/.storybook/preview.ts +++ b/packages/ui/.storybook/preview.ts @@ -2,7 +2,6 @@ import type { Preview } from "@storybook/react-vite"; import { withThemeByClassName } from "@storybook/addon-themes"; import "./preview.css"; -import "@agh/ui/tokens.css"; export const themeDecorator = withThemeByClassName({ themes: { diff --git a/packages/ui/README.md b/packages/ui/README.md index 83e4a2836..75b500ccd 100644 --- a/packages/ui/README.md +++ b/packages/ui/README.md @@ -26,7 +26,7 @@ package live in the maintained docs below. Ask one question: **is this a domain-free shape that could serve a second surface without code changes?** -If yes — keep it here. Pull tokens from [`./src/tokens.css`](./src/tokens.css), wire the API in terms of slots (`rail`, `list`, `detail`, `leading`, `trailing`, …) and variants (`tone`, `size`, `density`), and hold no AGH-specific defaults in its props. Examples: `Sidebar`, `SplitPane`, `Metric`, `StatusDot`, `ConnectionIndicator`, `ChatMessageBubble` (the **shell**, not the session-aware message body). +If yes — keep it here. Pull tokens from [`./src/tokens.css`](./src/tokens.css), wire the API in terms of slots (`rail`, `list`, `detail`, `leading`, `trailing`, …) and variants (`tone`, `size`, `density`), and hold no AGH-specific defaults in its props. Examples: `Sidebar`, `SplitPane`, `Metric`, `Pill`, `ChatMessageBubble` (the **shell**, not the session-aware message body). If it reads session events, hits a TanStack query, consumes the `agh-openapi` types, or only makes sense inside one domain — keep it in `web/src/systems//components/`. The `@agh/ui` shell stays ignorant of that domain; the domain component composes the shell. This matches the package boundary documented above — `@agh/ui` does not import from `web/src/**`. @@ -99,23 +99,18 @@ Controls, selection, and the input scaffolding primitives. Status, alerting, progress, and density-sensitive signal primitives. -| Export | Story | Notes | -| -------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `Alert` · `AlertTitle` · `AlertDescription` · `AlertAction` · `alertVariants` · `AlertProps` | [`alert.stories.tsx`](./src/components/stories/alert.stories.tsx) | Inline alerts (default/destructive/warning/success/info/accent). | -| `Progress` · `ProgressTrack` · `ProgressIndicator` · `ProgressLabel` · `ProgressValue` | [`progress.stories.tsx`](./src/components/stories/progress.stories.tsx) | Linear progress. | -| `Badge` · `badgeVariants` | [`badge.stories.tsx`](./src/components/stories/badge.stories.tsx) | Tinted badges — use `MonoBadge` for status, `KindChip` for kind labels. | -| `Skeleton` | [`skeleton.stories.tsx`](./src/components/stories/skeleton.stories.tsx) | Shimmer placeholder. | -| `Spinner` | [`spinner.stories.tsx`](./src/components/stories/spinner.stories.tsx) | Spinner atom. | -| `Toaster` · `toast` · `ToasterProps` | [`sonner.stories.tsx`](./src/components/stories/sonner.stories.tsx) | `sonner` re-export. Mount `` once at the app root. Default `theme="system"`. | -| `StatusDot` · `StatusDotProps` · `StatusDotTone` · `StatusDotSize` | [`status-dot.stories.tsx`](./src/components/stories/status-dot.stories.tsx) | Live-status dot. Tone vocabulary: `success \| warning \| danger \| info \| accent \| neutral`. | -| `MonoBadge` · `monoBadgeVariants` · `MonoBadgeProps` · `MonoBadgeTone` | [`mono-badge.stories.tsx`](./src/components/stories/mono-badge.stories.tsx) | 11px mono status badge (`RUNNING`, `DONE`, `ERROR`, …). `tone="solid-accent"` is reserved for unread pills. | -| `MonoChip` · `MonoChipProps` | [`mono-chip.stories.tsx`](./src/components/stories/mono-chip.stories.tsx) | Neutral inline chip — capability descriptors, tag rows. For tinted semantic variants use `MonoBadge`. | -| `KindChip` · `KindChipProps` · `KIND_DOT_COLORS` | [`kind-chip.stories.tsx`](./src/components/stories/kind-chip.stories.tsx) | Wire-dot kind marker (`say`, `greet`, `direct`, `receipt`, `recipe`, `trace`, `whois`). Unknown kinds render without a dot. `KIND_DOT_COLORS` is the canonical kind→color map. | -| `WireChip` · `WireChipProps` | [`wire-chip.stories.tsx`](./src/components/stories/wire-chip.stories.tsx) | Free-floating filter chip with optional leading wire-dot. For a contained segmented toggle use `Pills` instead. | -| `ConnectionIndicator` · `ConnectionIndicatorProps` · `ConnectionStatus` | [`connection-indicator.stories.tsx`](./src/components/stories/connection-indicator.stories.tsx) | Live-connection composite (`StatusDot` + label). Default labels `Connected` / `Disconnected` / `Reconnecting`. | -| `Metric` · `MetricProps` · `MetricTone` | [`metric.stories.tsx`](./src/components/stories/metric.stories.tsx) | Dashboard metric with `detail` (inline mono unit) + `subtext` (secondary line) slots. | -| `Pill` · `Pills` · `pillVariants` · `pillToggleVariants` · `PillProps` · `PillsProps` · `PillsItem` · `PillVariant` · `PillSize` | [`pills.stories.tsx`](./src/components/stories/pills.stories.tsx) | `Pill` standalone + `Pills` tablist (`role="tab"`, `aria-selected`). | -| `Avatar` · `AvatarBadge` · `AvatarFallback` · `AvatarGroup` · `AvatarGroupCount` · `AvatarImage` | [`avatar.stories.tsx`](./src/components/stories/avatar.stories.tsx) | Identity avatar with grouping. | +| Export | Story | Notes | +| ------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `Alert` · `AlertTitle` · `AlertDescription` · `AlertAction` · `alertVariants` · `AlertProps` | [`alert.stories.tsx`](./src/components/stories/alert.stories.tsx) | Inline alerts (default/destructive/warning/success/info/accent). | +| `Progress` · `ProgressTrack` · `ProgressIndicator` · `ProgressLabel` · `ProgressValue` | [`progress.stories.tsx`](./src/components/stories/progress.stories.tsx) | Linear progress. | +| `Badge` · `badgeVariants` | [`badge.stories.tsx`](./src/components/stories/badge.stories.tsx) | shadcn baseline — kept untouched so we can pull future shadcn updates. Use `Pill` for the AGH design-system pill family. | +| `Skeleton` | [`skeleton.stories.tsx`](./src/components/stories/skeleton.stories.tsx) | Shimmer placeholder. | +| `Spinner` | [`spinner.stories.tsx`](./src/components/stories/spinner.stories.tsx) | Spinner atom. | +| `Toaster` · `toast` · `ToasterProps` | [`sonner.stories.tsx`](./src/components/stories/sonner.stories.tsx) | `sonner` re-export. Mount `` once at the app root. Default `theme="system"`. | +| `Pill` · `PillDot` · `pillVariants` · `PillProps` · `PillDotProps` · `PillTone` · `PillSize` | [`pill.stories.tsx`](./src/components/stories/pill.stories.tsx) | Unified semantic pill — `tone` × `size` × `mono` × `solid` + composable `Pill.Dot`. Replaces the legacy `MonoBadge`, `MonoChip`, `KindChip`, `StatusDot`, `WireChip`, `ConnectionIndicator`. Compose `label` for kind chips; `` standalone replaces `StatusDot`. | +| `PillGroup` · `pillGroupSegmentVariants` · `PillGroupProps` · `PillGroupItem` · `PillGroupSize` | [`pill-group.stories.tsx`](./src/components/stories/pill-group.stories.tsx) | Segmented toggle track — `items` + controlled `value`/`onChange`. Renamed from the legacy `Pills`. | +| `Metric` · `MetricProps` · `MetricTone` | [`metric.stories.tsx`](./src/components/stories/metric.stories.tsx) | Dashboard metric with `detail` (inline mono unit) + `subtext` (secondary line) slots. | +| `Avatar` · `AvatarBadge` · `AvatarFallback` · `AvatarGroup` · `AvatarGroupCount` · `AvatarImage` | [`avatar.stories.tsx`](./src/components/stories/avatar.stories.tsx) | Identity avatar with grouping. | ### Chat diff --git a/packages/ui/src/components/connection-indicator.test.tsx b/packages/ui/src/components/connection-indicator.test.tsx deleted file mode 100644 index 8bcf47868..000000000 --- a/packages/ui/src/components/connection-indicator.test.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import { render } from "@testing-library/react"; -import { MotionConfig } from "motion/react"; -import type { ReactNode } from "react"; -import { describe, expect, it } from "vitest"; - -import { ConnectionIndicator, type ConnectionStatus } from "./connection-indicator"; - -function WithMotion({ - reducedMotion, - children, -}: { - reducedMotion: "always" | "never"; - children: ReactNode; -}) { - return {children}; -} - -describe("ConnectionIndicator", () => { - it.each<{ - status: ConnectionStatus; - tone: string; - label: string; - }>([ - { status: "connected", tone: "success", label: "Connected" }, - { status: "disconnected", tone: "danger", label: "Disconnected" }, - { status: "reconnecting", tone: "warning", label: "Reconnecting" }, - ])( - "Should compose a StatusDot with the correct tone + default label for $status", - ({ status, tone, label }) => { - const { container } = render(); - const root = container.querySelector('[data-slot="connection-indicator"]'); - expect(root?.getAttribute("data-status")).toBe(status); - expect(root?.getAttribute("role")).toBe("status"); - expect(root?.getAttribute("aria-live")).toBe("polite"); - const dot = container.querySelector('[data-slot="status-dot"]'); - expect(dot?.getAttribute("data-tone")).toBe(tone); - const labelNode = container.querySelector( - '[data-slot="connection-indicator-label"]' - ); - expect(labelNode?.textContent).toBe(label); - } - ); - - it("Should pulse the dot while reconnecting", () => { - const { container } = render( - - - - ); - const dot = container.querySelector('[data-slot="status-dot"]'); - expect(dot?.className).toContain("animate-pulse"); - }); - - it("Should not pulse the dot while connected or disconnected", () => { - const { container: connected } = render( - - - - ); - expect( - connected.querySelector('[data-slot="status-dot"]')?.className - ).not.toContain("animate-pulse"); - const { container: disconnected } = render( - - - - ); - expect( - disconnected.querySelector('[data-slot="status-dot"]')?.className - ).not.toContain("animate-pulse"); - }); - - it("Should suppress the pulse when prefers-reduced-motion is reduce", () => { - const { container } = render( - - - - ); - expect( - container.querySelector('[data-slot="status-dot"]')?.className - ).not.toContain("animate-pulse"); - }); - - it("Should allow overriding the default label", () => { - const { container } = render(); - const labelNode = container.querySelector( - '[data-slot="connection-indicator-label"]' - ); - expect(labelNode?.textContent).toBe("Live"); - }); -}); diff --git a/packages/ui/src/components/input-group.tsx b/packages/ui/src/components/input-group.tsx index 5857ad8dc..ff4da8ee2 100644 --- a/packages/ui/src/components/input-group.tsx +++ b/packages/ui/src/components/input-group.tsx @@ -12,7 +12,7 @@ function InputGroup({ className, ...props }: React.ComponentProps<"div">) { data-slot="input-group" role="group" className={cn( - "group/input-group relative flex h-8 w-full min-w-0 items-center rounded-lg border border-input transition-colors outline-none in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:ring-0 has-disabled:bg-input/50 has-disabled:opacity-50 has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-3 has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-3 has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>textarea]:h-auto dark:bg-input/30 dark:has-disabled:bg-input/80 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5", + "group/input-group relative flex h-8 w-full min-w-0 items-center rounded-lg border border-input bg-[color:var(--color-surface-panel)] transition-colors outline-none in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:ring-0 has-disabled:bg-input/50 has-disabled:opacity-50 has-[[data-slot=input-group-control]:focus-visible]:border-[color:var(--color-text-tertiary)] has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-3 has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>textarea]:h-auto dark:has-disabled:bg-input/80 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5", className )} {...props} diff --git a/packages/ui/src/components/input.tsx b/packages/ui/src/components/input.tsx index bba61c23e..95b965d7c 100644 --- a/packages/ui/src/components/input.tsx +++ b/packages/ui/src/components/input.tsx @@ -9,7 +9,7 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) { type={type} data-slot="input" className={cn( - "h-9 w-full min-w-0 rounded-lg border border-input bg-[color:var(--color-surface-elevated)] px-3 py-0 text-sm text-[color:var(--color-text-primary)] transition-colors outline-none selection:bg-[color:var(--color-accent-tint-strong)] selection:text-[color:var(--color-text-primary)] file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-[color:var(--color-text-tertiary)] focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/20 disabled:pointer-events-none disabled:cursor-not-allowed disabled:border-[color:var(--color-surface-elevated)] disabled:bg-[color:var(--color-surface)] disabled:text-[color:var(--color-disabled)] disabled:opacity-100 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20", + "h-8 w-full min-w-0 rounded-lg border border-input bg-[color:var(--color-surface-panel)] px-3 py-0 text-sm text-[color:var(--color-text-primary)] transition-colors outline-none selection:bg-[color:var(--color-accent-tint-strong)] selection:text-[color:var(--color-text-primary)] file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-[color:var(--color-text-tertiary)] focus-visible:border-[color:var(--color-text-tertiary)] disabled:pointer-events-none disabled:cursor-not-allowed disabled:border-[color:var(--color-surface-panel)] disabled:bg-[color:var(--color-surface)] disabled:text-[color:var(--color-disabled)] disabled:opacity-100 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20", className )} {...props} diff --git a/packages/ui/src/components/kind-chip.test.tsx b/packages/ui/src/components/kind-chip.test.tsx deleted file mode 100644 index bfe6b6cb2..000000000 --- a/packages/ui/src/components/kind-chip.test.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { render } from "@testing-library/react"; -import { describe, expect, it } from "vitest"; - -import { KindChip, KIND_DOT_COLORS } from "./kind-chip"; - -describe("KindChip", () => { - it("Should render the kind label uppercase with the wire-dot chrome", () => { - const { container } = render(); - const chip = container.querySelector('[data-slot="kind-chip"]'); - expect(chip).not.toBeNull(); - expect(chip?.textContent).toBe("greet"); - expect(chip?.className).toContain("font-mono"); - expect(chip?.className).toContain("uppercase"); - expect(chip?.className).toContain("border-[color:var(--color-divider)]"); - expect(chip?.className).toContain("bg-transparent"); - expect(chip?.className).toContain("text-[color:var(--color-text-tertiary)]"); - expect(chip?.getAttribute("data-kind")).toBe("greet"); - }); - - it("Should render a colored 7px dot for known protocol kinds", () => { - const { container } = render(); - const dot = container.querySelector('[data-slot="kind-chip-dot"]'); - expect(dot).not.toBeNull(); - expect(dot).toHaveStyle({ background: KIND_DOT_COLORS.receipt }); - }); - - it("Should omit the dot for unknown kinds (platforms, event ids)", () => { - const { container } = render(); - expect(container.querySelector('[data-slot="kind-chip-dot"]')).toBeNull(); - }); - - it("Should display the explicit label when provided", () => { - const { container } = render(); - const chip = container.querySelector('[data-slot="kind-chip"]'); - expect(chip?.textContent).toBe("presence"); - }); - - it("Should forward className alongside the defaults", () => { - const { container } = render(); - const chip = container.querySelector('[data-slot="kind-chip"]'); - expect(chip?.className).toContain("custom-class"); - expect(chip?.className).toContain("border-[color:var(--color-divider)]"); - }); - - it("Should preserve internal data markers when conflicting attributes are passed", () => { - const { container } = render( - - ); - const chip = container.querySelector('[data-slot="kind-chip"]'); - expect(chip).not.toBeNull(); - expect(chip?.getAttribute("data-kind")).toBe("whois"); - }); -}); diff --git a/packages/ui/src/components/kind-chip.tsx b/packages/ui/src/components/kind-chip.tsx deleted file mode 100644 index feb2db259..000000000 --- a/packages/ui/src/components/kind-chip.tsx +++ /dev/null @@ -1,56 +0,0 @@ -"use client"; - -import * as React from "react"; - -import { cn } from "../lib/utils"; - -export interface KindChipProps extends Omit, "children"> { - kind: string; - /** Optional explicit label; defaults to `kind`. */ - label?: React.ReactNode; -} - -/** - * Protocol kind marker — mirrors `.intent-badge` + `.wire-dot` in - * `docs/design/web-inspiration/styles/app.css`. Transparent surface, neutral - * border + tertiary label, leading 7px colored dot keyed off the protocol - * kind. Unknown kinds (platform names, event ids) render without a dot. - */ -const KIND_DOT_COLORS: Record = { - say: "#8E8E93", - greet: "#5BA6FF", - direct: "var(--color-accent)", - receipt: "var(--color-success)", - recipe: "var(--color-warning)", - trace: "#B892FF", - whois: "#4FD1C5", -}; - -function KindChip({ kind, label, className, ...props }: KindChipProps) { - const dotColor = KIND_DOT_COLORS[kind.toLowerCase()]; - - return ( - - {dotColor ? ( - - ); -} - -export { KindChip, KIND_DOT_COLORS }; diff --git a/packages/ui/src/components/mono-badge.test.tsx b/packages/ui/src/components/mono-badge.test.tsx deleted file mode 100644 index a61cc88f3..000000000 --- a/packages/ui/src/components/mono-badge.test.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import { render } from "@testing-library/react"; -import { describe, expect, it } from "vitest"; - -import { MonoBadge, type MonoBadgeTone } from "./mono-badge"; - -describe("MonoBadge", () => { - it("Should render children uppercase in mono with the default outline tone", () => { - const { container } = render(agent-42); - const badge = container.querySelector('[data-slot="mono-badge"]'); - expect(badge).not.toBeNull(); - expect(badge?.textContent).toBe("agent-42"); - expect(badge?.className).toContain("font-mono"); - expect(badge?.className).toContain("uppercase"); - expect(badge?.className).toContain("rounded-[var(--radius-mono-badge)]"); - expect(badge?.className).toContain("border-[color:var(--color-divider)]"); - expect(badge?.getAttribute("data-tone")).toBe("default"); - }); - - it("Should respect uppercase={false} and keep the provided casing", () => { - const { container } = render(Agent); - const badge = container.querySelector('[data-slot="mono-badge"]'); - expect(badge?.className).not.toContain("uppercase"); - }); - - it.each<{ tone: MonoBadgeTone; background: string; text: string }>([ - { - tone: "accent", - background: "bg-[color:var(--color-accent-tint)]", - text: "text-[color:var(--color-accent)]", - }, - { - tone: "success", - background: "bg-[color:var(--color-success-tint)]", - text: "text-[color:var(--color-success)]", - }, - { - tone: "warning", - background: "bg-[color:var(--color-warning-tint)]", - text: "text-[color:var(--color-warning)]", - }, - { - tone: "danger", - background: "bg-[color:var(--color-danger-tint)]", - text: "text-[color:var(--color-danger)]", - }, - { - tone: "info", - background: "bg-[color:var(--color-info-tint)]", - text: "text-[color:var(--color-info)]", - }, - { - tone: "neutral", - background: "bg-[color:var(--color-neutral-tint)]", - text: "text-[color:var(--color-text-label)]", - }, - { - tone: "solid-accent", - background: "bg-[color:var(--color-accent)]", - text: "text-[color:var(--color-accent-ink)]", - }, - ])("Should apply the $tone tint tokens", ({ tone, background, text }) => { - const { container } = render(token); - const badge = container.querySelector('[data-slot="mono-badge"]'); - expect(badge?.getAttribute("data-tone")).toBe(tone); - expect(badge?.className).toContain(background); - expect(badge?.className).toContain(text); - }); - - it("Should preserve the requested slot while keeping the component tone marker stable", () => { - const { container } = render( - - token - - ); - const badge = container.querySelector('[data-slot="override-slot"]'); - expect(badge).not.toBeNull(); - expect(badge?.getAttribute("data-tone")).toBe("accent"); - }); -}); diff --git a/packages/ui/src/components/mono-badge.tsx b/packages/ui/src/components/mono-badge.tsx deleted file mode 100644 index d7b9be7b5..000000000 --- a/packages/ui/src/components/mono-badge.tsx +++ /dev/null @@ -1,63 +0,0 @@ -"use client"; - -import { cva, type VariantProps } from "class-variance-authority"; -import * as React from "react"; - -import { cn } from "../lib/utils"; - -const monoBadgeVariants = cva( - [ - "inline-flex items-center rounded-[var(--radius-mono-badge)] px-1.5 py-0.5", - "font-mono text-[11px] font-medium leading-[14px] tracking-[0.06em] whitespace-nowrap", - ].join(" "), - { - variants: { - tone: { - default: - "border border-[color:var(--color-divider)] bg-transparent text-[color:var(--color-text-label)]", - neutral: "bg-[color:var(--color-neutral-tint)] text-[color:var(--color-text-label)]", - accent: "bg-[color:var(--color-accent-tint)] text-[color:var(--color-accent)]", - "solid-accent": "bg-[color:var(--color-accent)] text-[color:var(--color-accent-ink)]", - success: "bg-[color:var(--color-success-tint)] text-[color:var(--color-success)]", - warning: "bg-[color:var(--color-warning-tint)] text-[color:var(--color-warning)]", - danger: "bg-[color:var(--color-danger-tint)] text-[color:var(--color-danger)]", - info: "bg-[color:var(--color-info-tint)] text-[color:var(--color-info)]", - }, - uppercase: { - true: "uppercase", - false: "", - }, - }, - defaultVariants: { - tone: "default", - uppercase: true, - }, - } -); - -export type MonoBadgeTone = NonNullable["tone"]>; - -export interface MonoBadgeProps - extends Omit, "color">, VariantProps { - "data-slot"?: string; -} - -/** - * Inline mono pill for identifiers (agent IDs, versions, protocol names) and - * status badges. Uppercase by default, with semantic tones using the DESIGN.md - * §4 tint formula and `solid-accent` reserved for accent-filled emphasis. - */ -function MonoBadge({ tone, uppercase, className, ...props }: MonoBadgeProps) { - const dataSlot = props["data-slot"] ?? "mono-badge"; - - return ( - - ); -} - -export { MonoBadge, monoBadgeVariants }; diff --git a/packages/ui/src/components/mono-chip.test.tsx b/packages/ui/src/components/mono-chip.test.tsx deleted file mode 100644 index 43d52b970..000000000 --- a/packages/ui/src/components/mono-chip.test.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { render } from "@testing-library/react"; -import { describe, expect, it } from "vitest"; - -import { MonoChip } from "./mono-chip"; - -describe("MonoChip", () => { - it("Should render a neutral mono chip with elevated surface", () => { - const { container } = render(code); - const chip = container.querySelector('[data-slot="mono-chip"]'); - expect(chip).not.toBeNull(); - expect(chip?.textContent).toBe("code"); - expect(chip?.className).toContain("font-mono"); - expect(chip?.className).toContain("bg-[color:var(--color-surface-elevated)]"); - expect(chip?.className).toContain("text-[color:var(--color-text-secondary)]"); - }); - - it("Should forward className", () => { - const { container } = render(tag); - const chip = container.querySelector('[data-slot="mono-chip"]'); - expect(chip?.className).toContain("custom-class"); - }); -}); diff --git a/packages/ui/src/components/mono-chip.tsx b/packages/ui/src/components/mono-chip.tsx deleted file mode 100644 index 0bbd3908c..000000000 --- a/packages/ui/src/components/mono-chip.tsx +++ /dev/null @@ -1,29 +0,0 @@ -"use client"; - -import * as React from "react"; - -import { cn } from "../lib/utils"; - -export interface MonoChipProps extends React.ComponentProps<"span"> {} - -/** - * Neutral inline chip — mirrors `.mono-chip` (default tone) in - * `docs/design/web-inspiration/styles/app.css`. Used for capability - * descriptors, tag rows, and other identifier strings rendered alongside - * message bodies. For tinted semantic variants use {@link MonoBadge}. - */ -function MonoChip({ className, ...props }: MonoChipProps) { - return ( - - ); -} - -export { MonoChip }; diff --git a/packages/ui/src/components/pills.test.tsx b/packages/ui/src/components/pill-group.test.tsx similarity index 56% rename from packages/ui/src/components/pills.test.tsx rename to packages/ui/src/components/pill-group.test.tsx index 22a44c482..fc2fc79be 100644 --- a/packages/ui/src/components/pills.test.tsx +++ b/packages/ui/src/components/pill-group.test.tsx @@ -2,62 +2,37 @@ import { render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { describe, expect, it, vi } from "vitest"; -import { Pill, Pills } from "./pills"; - -describe("Pill", () => { - it("Should render a semantic tag with the success tint token as background", () => { - render(Live); - const pill = screen.getByText("Live"); - expect(pill).toHaveAttribute("data-slot", "pill"); - expect(pill).toHaveAttribute("data-variant", "success"); - expect(pill.className).toContain("bg-[color:var(--color-success-tint)]"); - expect(pill.className).toContain("text-[color:var(--color-success)]"); - }); - - it("Should fall back to the default variant when none is provided", () => { - render(Neutral); - const pill = screen.getByText("Neutral"); - expect(pill).toHaveAttribute("data-variant", "default"); - expect(pill.className).toContain("border-[color:var(--color-divider)]"); - }); - - it("Should use the md size token when size='md' is requested", () => { - render(Filter); - const pill = screen.getByText("Filter"); - expect(pill).toHaveAttribute("data-size", "md"); - expect(pill.className).toContain("h-8"); - }); -}); +import { PillGroup } from "./pill-group"; -describe("Pills", () => { +describe("PillGroup", () => { const items = [ { value: "list", label: "List" }, { value: "kanban", label: "Kanban" }, { value: "inbox", label: "Inbox", badge: 3 }, ] as const; - it("Should fire onChange with the selected value when an item is clicked", async () => { + it("Should fire onChange with the selected value when a non-active item is clicked", async () => { const user = userEvent.setup(); const handle = vi.fn(); - render(); + render(); await user.click(screen.getByRole("button", { name: /kanban/i })); expect(handle).toHaveBeenCalledWith("kanban"); }); - it("Should not fire onChange when the active item is clicked", async () => { + it("Should not fire onChange when the active item is re-clicked", async () => { const user = userEvent.setup(); const handle = vi.fn(); - render(); + render(); await user.click(screen.getByRole("button", { name: /list/i })); expect(handle).not.toHaveBeenCalled(); }); - it("Should reflect the active item via aria-pressed + data-active", () => { - render( {}} items={items} />); + it("Should reflect the active item via aria-pressed and data-active", () => { + render( {}} items={items} />); const kanban = screen.getByRole("button", { name: /kanban/i }); const list = screen.getByRole("button", { name: /list/i }); expect(kanban).toHaveAttribute("aria-pressed", "true"); @@ -67,18 +42,20 @@ describe("Pills", () => { }); it("Should render the badge count next to the item label when badge > 0", () => { - render( {}} items={items} />); + render( {}} items={items} />); const inbox = screen.getByRole("button", { name: /inbox/i }); - const badge = inbox.querySelector('[data-slot="pills-badge"]'); + const badge = inbox.querySelector('[data-slot="pill-group-badge"]'); expect(badge).not.toBeNull(); expect(badge?.textContent).toBe("3"); + expect(badge?.className).toContain("bg-(--color-accent)"); + expect(badge?.className).toContain("text-(--color-accent-ink)"); }); it("Should not fire onChange for a disabled item", async () => { const user = userEvent.setup(); const handle = vi.fn(); render( - { it("Should expose testId as data-testid when provided", () => { render( - {}} items={[{ value: "list", label: "List", testId: "mode-list" }]} @@ -108,4 +85,16 @@ describe("Pills", () => { "mode-list" ); }); + + it("Should render the larger md segments by default and switch to sm when requested", () => { + const { container, rerender } = render( + {}} items={items} /> + ); + let segments = container.querySelectorAll('[data-slot="pill-group-item"]'); + expect(segments[0]?.className).toContain("h-[22px]"); + + rerender( {}} items={items} size="sm" />); + segments = container.querySelectorAll('[data-slot="pill-group-item"]'); + expect(segments[0]?.className).toContain("h-[20px]"); + }); }); diff --git a/packages/ui/src/components/pill-group.tsx b/packages/ui/src/components/pill-group.tsx new file mode 100644 index 000000000..def7616fc --- /dev/null +++ b/packages/ui/src/components/pill-group.tsx @@ -0,0 +1,101 @@ +"use client"; + +import { cva, type VariantProps } from "class-variance-authority"; +import * as React from "react"; + +import { cn } from "../lib/utils"; + +const pillGroupSegmentVariants = cva( + "inline-flex cursor-pointer items-center justify-center gap-1.5 whitespace-nowrap rounded-[5px] font-mono text-[10px] font-semibold uppercase tracking-[0.08em] transition-colors duration-150 ease-out focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-(--color-accent) focus-visible:ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50", + { + variants: { + active: { + true: "bg-(--color-surface-elevated) text-(--color-text-primary)", + false: "bg-transparent text-(--color-text-tertiary) hover:text-(--color-text-secondary)", + }, + size: { + sm: "h-[20px] px-2", + md: "h-[22px] px-2.5", + }, + }, + defaultVariants: { + active: false, + size: "md", + }, + } +); + +export type PillGroupSize = NonNullable["size"]>; + +export interface PillGroupItem { + value: V; + label: React.ReactNode; + /** Optional unread / count badge rendered inside the segment. */ + badge?: number; + disabled?: boolean; + testId?: string; +} + +export interface PillGroupProps extends Omit< + React.ComponentProps<"div">, + "onChange" +> { + items: ReadonlyArray>; + value: V; + onChange: (next: V) => void; + size?: PillGroupSize; +} + +function PillGroup({ + items, + value, + onChange, + size = "md", + className, + ...props +}: PillGroupProps) { + return ( +
+ {items.map(item => { + const isActive = item.value === value; + return ( + + ); + })} +
+ ); +} + +export { PillGroup, pillGroupSegmentVariants }; diff --git a/packages/ui/src/components/pill.test.tsx b/packages/ui/src/components/pill.test.tsx new file mode 100644 index 000000000..446a1c2b4 --- /dev/null +++ b/packages/ui/src/components/pill.test.tsx @@ -0,0 +1,217 @@ +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { MotionConfig } from "motion/react"; +import type { ReactNode } from "react"; +import { describe, expect, it, vi } from "vitest"; + +import { Pill, type PillTone } from "./pill"; + +function WithMotion({ + reducedMotion, + children, +}: { + reducedMotion: "always" | "never"; + children: ReactNode; +}) { + return {children}; +} + +describe("Pill", () => { + it("Should render a neutral span at sm size by default", () => { + render(label); + const pill = screen.getByText("label"); + expect(pill.tagName).toBe("SPAN"); + expect(pill).toHaveAttribute("data-slot", "pill"); + expect(pill).toHaveAttribute("data-tone", "neutral"); + expect(pill).toHaveAttribute("data-size", "sm"); + expect(pill.className).toContain("bg-(--color-neutral-tint)"); + expect(pill.className).toContain("text-(--color-text-secondary)"); + }); + + it.each<{ tone: PillTone; bg: string; text: string }>([ + { tone: "accent", bg: "bg-(--color-accent-tint)", text: "text-(--color-accent)" }, + { tone: "success", bg: "bg-(--color-success-tint)", text: "text-(--color-success)" }, + { tone: "warning", bg: "bg-(--color-warning-tint)", text: "text-(--color-warning)" }, + { tone: "danger", bg: "bg-(--color-danger-tint)", text: "text-(--color-danger)" }, + { tone: "info", bg: "bg-(--color-info-tint)", text: "text-(--color-info)" }, + ])("Should apply the $tone tint formula", ({ tone, bg, text }) => { + render(x); + const pill = screen.getByText("x"); + expect(pill).toHaveAttribute("data-tone", tone); + expect(pill.className).toContain(bg); + expect(pill.className).toContain(text); + }); + + it("Should switch to solid background and ink text when solid is true", () => { + render( + + NEW + + ); + const pill = screen.getByText("NEW"); + expect(pill).toHaveAttribute("data-solid", "true"); + expect(pill.className).toContain("bg-(--color-accent)"); + expect(pill.className).toContain("text-(--color-accent-ink)"); + }); + + it("Should adopt mono typography and uppercase when mono is true", () => { + render(token); + const pill = screen.getByText("token"); + expect(pill).toHaveAttribute("data-mono", "true"); + expect(pill.className).toContain("font-mono"); + expect(pill.className).toContain("uppercase"); + }); + + it("Should respect uppercase={false} explicit override", () => { + render( + + v1.2.3 + + ); + const pill = screen.getByText("v1.2.3"); + expect(pill.className).toContain("normal-case"); + expect(pill.className).not.toMatch(/(^| )uppercase( |$)/); + }); + + it("Should default xs size to non-uppercase chip chrome", () => { + render(capability-id); + const pill = screen.getByText("capability-id"); + expect(pill).toHaveAttribute("data-size", "xs"); + expect(pill.className).toContain("rounded-(--radius-chip)"); + expect(pill.className).toContain("normal-case"); + }); + + it("Should apply md filter-pill chrome when size='md'", () => { + render(FILTER); + const pill = screen.getByText("FILTER"); + expect(pill).toHaveAttribute("data-size", "md"); + expect(pill.className).toContain("h-8"); + expect(pill.className).toContain("font-semibold"); + expect(pill.className).toContain("uppercase"); + }); + + it("Should render as a button when render={ - ); - })} -
- ); -} - -export { Pill, Pills, pillVariants, pillToggleVariants }; diff --git a/packages/ui/src/components/status-dot.test.tsx b/packages/ui/src/components/status-dot.test.tsx deleted file mode 100644 index 2bb0bc385..000000000 --- a/packages/ui/src/components/status-dot.test.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import { render } from "@testing-library/react"; -import { MotionConfig } from "motion/react"; -import type { ReactNode } from "react"; -import { describe, expect, it } from "vitest"; - -import { StatusDot, type StatusDotTone } from "./status-dot"; - -function WithMotion({ - reducedMotion, - children, -}: { - reducedMotion: "always" | "never"; - children: ReactNode; -}) { - return {children}; -} - -const TONE_TO_CSS_COLOR: Record = { - success: "var(--color-success)", - warning: "var(--color-warning)", - danger: "var(--color-danger)", - info: "var(--color-info)", - accent: "var(--color-accent)", - neutral: "var(--color-text-tertiary)", -}; - -describe("StatusDot", () => { - it("Should render a neutral dot by default", () => { - const { container } = render(); - const dot = container.querySelector('[data-slot="status-dot"]'); - expect(dot).not.toBeNull(); - expect(dot?.getAttribute("data-tone")).toBe("neutral"); - expect(dot?.getAttribute("aria-hidden")).toBe("true"); - expect((dot as HTMLElement).style.backgroundColor).toBe(TONE_TO_CSS_COLOR.neutral); - }); - - it.each(["success", "warning", "danger", "info", "accent", "neutral"])( - "Should map tone %s to the semantic color token", - tone => { - const { container } = render( - - - - ); - const dot = container.querySelector('[data-slot="status-dot"]'); - expect(dot?.getAttribute("data-tone")).toBe(tone); - expect(dot?.style.backgroundColor).toBe(TONE_TO_CSS_COLOR[tone]); - } - ); - - it("Should apply the pulse animation class when pulse is true and reduced motion is off", () => { - const { container } = render( - - - - ); - const dot = container.querySelector('[data-slot="status-dot"]'); - expect(dot?.className).toContain("animate-pulse"); - expect(dot?.getAttribute("data-pulse")).toBe("true"); - }); - - it("Should not animate when pulse is false", () => { - const { container } = render( - - - - ); - const dot = container.querySelector('[data-slot="status-dot"]'); - expect(dot?.className).not.toContain("animate-pulse"); - expect(dot?.getAttribute("data-pulse")).toBeNull(); - }); - - it("Should suppress pulse animation when prefers-reduced-motion is reduce", () => { - const { container } = render( - - - - ); - const dot = container.querySelector('[data-slot="status-dot"]'); - expect(dot?.className).not.toContain("animate-pulse"); - expect(dot?.getAttribute("data-pulse")).toBeNull(); - }); - - it("Should render the compact size variant", () => { - const { container } = render(); - const dot = container.querySelector('[data-slot="status-dot"]'); - expect(dot?.getAttribute("data-size")).toBe("sm"); - expect(dot?.className).toContain("size-1.5"); - }); -}); diff --git a/packages/ui/src/components/status-dot.tsx b/packages/ui/src/components/status-dot.tsx deleted file mode 100644 index 6b8bfd7b4..000000000 --- a/packages/ui/src/components/status-dot.tsx +++ /dev/null @@ -1,66 +0,0 @@ -"use client"; - -import { useReducedMotionConfig } from "motion/react"; -import * as React from "react"; - -import { cn } from "../lib/utils"; - -export type StatusDotTone = "success" | "warning" | "danger" | "info" | "accent" | "neutral"; - -export type StatusDotSize = "sm" | "md"; - -export interface StatusDotProps extends Omit, "color"> { - tone?: StatusDotTone; - pulse?: boolean; - size?: StatusDotSize; -} - -const TONE_COLOR: Record = { - success: "var(--color-success)", - warning: "var(--color-warning)", - danger: "var(--color-danger)", - info: "var(--color-info)", - accent: "var(--color-accent)", - neutral: "var(--color-text-tertiary)", -}; - -const SIZE_CLASS: Record = { - sm: "size-1.5", - md: "size-2", -}; - -/** - * Tinted signal dot — `tone` maps to a semantic color, optional `pulse` drives a - * subtle opacity loop. Respects `prefers-reduced-motion` via `useReducedMotion`. - * Mirrors `.dot` in `docs/design/web-inspiration/styles/app.css` and DESIGN.md §4. - */ -function StatusDot({ - tone = "neutral", - pulse = false, - size = "md", - className, - style, - ...props -}: StatusDotProps) { - const reduced = useReducedMotionConfig(); - const shouldAnimate = pulse && !reduced; - return ( -
), ], - tags: ["autodocs"], }; export default meta; diff --git a/packages/ui/src/components/stories/avatar.stories.tsx b/packages/ui/src/components/stories/avatar.stories.tsx index 9ad913509..2477d0105 100644 --- a/packages/ui/src/components/stories/avatar.stories.tsx +++ b/packages/ui/src/components/stories/avatar.stories.tsx @@ -15,7 +15,6 @@ const meta: Meta = { }, }, }, - tags: ["autodocs"], }; export default meta; diff --git a/packages/ui/src/components/stories/badge.stories.tsx b/packages/ui/src/components/stories/badge.stories.tsx index 482c66207..2dc8f605d 100644 --- a/packages/ui/src/components/stories/badge.stories.tsx +++ b/packages/ui/src/components/stories/badge.stories.tsx @@ -13,7 +13,6 @@ const meta: Meta = { }, }, }, - tags: ["autodocs"], }; export default meta; diff --git a/packages/ui/src/components/stories/breadcrumb.stories.tsx b/packages/ui/src/components/stories/breadcrumb.stories.tsx index 3ce75d1d5..ec4fbc9b0 100644 --- a/packages/ui/src/components/stories/breadcrumb.stories.tsx +++ b/packages/ui/src/components/stories/breadcrumb.stories.tsx @@ -22,7 +22,6 @@ const meta: Meta = { }, }, }, - tags: ["autodocs"], }; export default meta; diff --git a/packages/ui/src/components/stories/button-group.stories.tsx b/packages/ui/src/components/stories/button-group.stories.tsx index 1c238cf36..8cfd67cef 100644 --- a/packages/ui/src/components/stories/button-group.stories.tsx +++ b/packages/ui/src/components/stories/button-group.stories.tsx @@ -18,7 +18,6 @@ const meta: Meta = { }, }, }, - tags: ["autodocs"], }; export default meta; diff --git a/packages/ui/src/components/stories/button.stories.tsx b/packages/ui/src/components/stories/button.stories.tsx index b7bf71d82..fe4cc1c0d 100644 --- a/packages/ui/src/components/stories/button.stories.tsx +++ b/packages/ui/src/components/stories/button.stories.tsx @@ -14,7 +14,6 @@ const meta: Meta = { }, }, }, - tags: ["autodocs"], }; export default meta; diff --git a/packages/ui/src/components/stories/card.stories.tsx b/packages/ui/src/components/stories/card.stories.tsx index b7288e188..1da10a4c2 100644 --- a/packages/ui/src/components/stories/card.stories.tsx +++ b/packages/ui/src/components/stories/card.stories.tsx @@ -29,7 +29,6 @@ const meta: Meta = {
), ], - tags: ["autodocs"], }; export default meta; diff --git a/packages/ui/src/components/stories/chat-message-bubble.stories.tsx b/packages/ui/src/components/stories/chat-message-bubble.stories.tsx index 31dc46ac5..5d000fa39 100644 --- a/packages/ui/src/components/stories/chat-message-bubble.stories.tsx +++ b/packages/ui/src/components/stories/chat-message-bubble.stories.tsx @@ -2,7 +2,7 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; import { expect, within } from "storybook/test"; import { ChatMessageBubble, type ChatMessageRole } from "../chat-message-bubble"; -import { StatusDot } from "../status-dot"; +import { Pill } from "../pill"; import { ToolCallCard, type ToolCallStatus } from "../tool-call-card"; const meta: Meta = { @@ -17,7 +17,6 @@ const meta: Meta = { }, }, }, - tags: ["autodocs"], }; export default meta; @@ -36,7 +35,7 @@ const ROLE_ALIGN: Record = { function AgentMeta() { return ( <> - + claude · 12:03 diff --git a/packages/ui/src/components/stories/code-block.stories.tsx b/packages/ui/src/components/stories/code-block.stories.tsx index b061ab011..11ee7b862 100644 --- a/packages/ui/src/components/stories/code-block.stories.tsx +++ b/packages/ui/src/components/stories/code-block.stories.tsx @@ -15,7 +15,6 @@ const meta: Meta = { }, }, }, - tags: ["autodocs"], }; export default meta; diff --git a/packages/ui/src/components/stories/collapsible.stories.tsx b/packages/ui/src/components/stories/collapsible.stories.tsx index 1a720e00e..618733b9b 100644 --- a/packages/ui/src/components/stories/collapsible.stories.tsx +++ b/packages/ui/src/components/stories/collapsible.stories.tsx @@ -17,7 +17,6 @@ const meta: Meta = { }, }, }, - tags: ["autodocs"], }; export default meta; diff --git a/packages/ui/src/components/stories/combobox.stories.tsx b/packages/ui/src/components/stories/combobox.stories.tsx index 0fd4eacaf..970e1bfcc 100644 --- a/packages/ui/src/components/stories/combobox.stories.tsx +++ b/packages/ui/src/components/stories/combobox.stories.tsx @@ -27,7 +27,6 @@ const meta: Meta = { }, }, }, - tags: ["autodocs"], }; export default meta; diff --git a/packages/ui/src/components/stories/command.stories.tsx b/packages/ui/src/components/stories/command.stories.tsx index 286793a2d..cd1926db8 100644 --- a/packages/ui/src/components/stories/command.stories.tsx +++ b/packages/ui/src/components/stories/command.stories.tsx @@ -33,7 +33,6 @@ const meta: Meta = { }, }, }, - tags: ["autodocs"], }; export default meta; diff --git a/packages/ui/src/components/stories/connection-indicator.stories.tsx b/packages/ui/src/components/stories/connection-indicator.stories.tsx deleted file mode 100644 index d6924deb7..000000000 --- a/packages/ui/src/components/stories/connection-indicator.stories.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react-vite"; - -import { ConnectionIndicator, type ConnectionStatus } from "../connection-indicator"; - -const meta: Meta = { - title: "ui/ConnectionIndicator", - component: ConnectionIndicator, - parameters: { - layout: "padded", - docs: { - description: { - component: - "StatusDot + mono label composite for daemon / socket connection state. `reconnecting` pulses the dot unless the user prefers reduced motion.", - }, - }, - }, - tags: ["autodocs"], -}; - -export default meta; -type Story = StoryObj; - -const STATES: ConnectionStatus[] = ["connected", "disconnected", "reconnecting"]; - -export const Connected: Story = { - args: { status: "connected" }, -}; - -export const Disconnected: Story = { - args: { status: "disconnected" }, -}; - -export const Reconnecting: Story = { - args: { status: "reconnecting" }, -}; - -export const AllStates: Story = { - render: () => ( -
- {STATES.map(state => ( - - ))} -
- ), -}; - -export const CustomLabel: Story = { - args: { - status: "connected", - label: "Live", - }, -}; diff --git a/packages/ui/src/components/stories/dialog.stories.tsx b/packages/ui/src/components/stories/dialog.stories.tsx index 30d2baa30..a3d5c4446 100644 --- a/packages/ui/src/components/stories/dialog.stories.tsx +++ b/packages/ui/src/components/stories/dialog.stories.tsx @@ -28,7 +28,6 @@ const meta: Meta = { }, }, }, - tags: ["autodocs"], }; export default meta; diff --git a/packages/ui/src/components/stories/direction.stories.tsx b/packages/ui/src/components/stories/direction.stories.tsx index 5d7c6bbc1..cdbac8802 100644 --- a/packages/ui/src/components/stories/direction.stories.tsx +++ b/packages/ui/src/components/stories/direction.stories.tsx @@ -14,7 +14,6 @@ const meta: Meta = { }, }, }, - tags: ["autodocs"], }; export default meta; diff --git a/packages/ui/src/components/stories/dropdown-menu.stories.tsx b/packages/ui/src/components/stories/dropdown-menu.stories.tsx index 112f4dad2..2def6d7dc 100644 --- a/packages/ui/src/components/stories/dropdown-menu.stories.tsx +++ b/packages/ui/src/components/stories/dropdown-menu.stories.tsx @@ -31,7 +31,6 @@ const meta: Meta = { }, }, }, - tags: ["autodocs"], }; export default meta; diff --git a/packages/ui/src/components/stories/empty.stories.tsx b/packages/ui/src/components/stories/empty.stories.tsx index 3cc96676c..6caf8f341 100644 --- a/packages/ui/src/components/stories/empty.stories.tsx +++ b/packages/ui/src/components/stories/empty.stories.tsx @@ -16,7 +16,6 @@ const meta: Meta = { }, }, }, - tags: ["autodocs"], }; export default meta; diff --git a/packages/ui/src/components/stories/field.stories.tsx b/packages/ui/src/components/stories/field.stories.tsx index 7aa54309b..790e86889 100644 --- a/packages/ui/src/components/stories/field.stories.tsx +++ b/packages/ui/src/components/stories/field.stories.tsx @@ -23,7 +23,6 @@ const meta: Meta = { }, }, }, - tags: ["autodocs"], }; export default meta; diff --git a/packages/ui/src/components/stories/input-group.stories.tsx b/packages/ui/src/components/stories/input-group.stories.tsx index 1bd6524f0..7b464b95c 100644 --- a/packages/ui/src/components/stories/input-group.stories.tsx +++ b/packages/ui/src/components/stories/input-group.stories.tsx @@ -25,7 +25,6 @@ const meta: Meta = { }, }, }, - tags: ["autodocs"], }; export default meta; diff --git a/packages/ui/src/components/stories/input.stories.tsx b/packages/ui/src/components/stories/input.stories.tsx index 3cdcc18b6..0cd9f8a95 100644 --- a/packages/ui/src/components/stories/input.stories.tsx +++ b/packages/ui/src/components/stories/input.stories.tsx @@ -20,7 +20,6 @@ const meta: Meta = { ), ], - tags: ["autodocs"], }; export default meta; diff --git a/packages/ui/src/components/stories/item.stories.tsx b/packages/ui/src/components/stories/item.stories.tsx index 0ae82a4af..8daf28a61 100644 --- a/packages/ui/src/components/stories/item.stories.tsx +++ b/packages/ui/src/components/stories/item.stories.tsx @@ -27,7 +27,6 @@ const meta: Meta = { }, }, }, - tags: ["autodocs"], }; export default meta; diff --git a/packages/ui/src/components/stories/kbd.stories.tsx b/packages/ui/src/components/stories/kbd.stories.tsx index 21f411dae..3c9ad04f2 100644 --- a/packages/ui/src/components/stories/kbd.stories.tsx +++ b/packages/ui/src/components/stories/kbd.stories.tsx @@ -20,7 +20,6 @@ const meta: Meta = { ), ], - tags: ["autodocs"], }; export default meta; diff --git a/packages/ui/src/components/stories/kind-chip.stories.tsx b/packages/ui/src/components/stories/kind-chip.stories.tsx deleted file mode 100644 index e6b048511..000000000 --- a/packages/ui/src/components/stories/kind-chip.stories.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react-vite"; - -import { KindChip } from "../kind-chip"; - -const meta: Meta = { - title: "ui/KindChip", - component: KindChip, - parameters: { - layout: "padded", - docs: { - description: { - component: - "Protocol kind marker (`say`, `greet`, `direct`, `receipt`, `recipe`, `whois`, `trace`). Uppercase mono, transparent surface with neutral border + colored 7px wire-dot — mirrors `.intent-badge` + `.wire-dot` in `docs/design/web-inspiration/styles/app.css`.", - }, - }, - }, - tags: ["autodocs"], -}; - -export default meta; -type Story = StoryObj; - -const KINDS = ["say", "greet", "direct", "receipt", "recipe", "trace", "whois"] as const; - -export const Default: Story = { - args: { - kind: "greet", - }, -}; - -export const AllProtocolKinds: Story = { - render: () => ( -
- {KINDS.map(kind => ( - - ))} -
- ), -}; - -export const InlineWithCopy: Story = { - render: () => ( -

- Messages of kind are forwarded by the router to any peer subscribed to - the channel. -

- ), -}; diff --git a/packages/ui/src/components/stories/label.stories.tsx b/packages/ui/src/components/stories/label.stories.tsx index 8912235d4..a971db6d3 100644 --- a/packages/ui/src/components/stories/label.stories.tsx +++ b/packages/ui/src/components/stories/label.stories.tsx @@ -22,7 +22,6 @@ const meta: Meta = { ), ], - tags: ["autodocs"], }; export default meta; diff --git a/packages/ui/src/components/stories/logo.stories.tsx b/packages/ui/src/components/stories/logo.stories.tsx index 8664b052e..3d4696246 100644 --- a/packages/ui/src/components/stories/logo.stories.tsx +++ b/packages/ui/src/components/stories/logo.stories.tsx @@ -14,7 +14,6 @@ const meta: Meta = { }, }, }, - tags: ["autodocs"], argTypes: { variant: { control: "select", diff --git a/packages/ui/src/components/stories/metric.stories.tsx b/packages/ui/src/components/stories/metric.stories.tsx index bff92ccfc..2baf7fc69 100644 --- a/packages/ui/src/components/stories/metric.stories.tsx +++ b/packages/ui/src/components/stories/metric.stories.tsx @@ -14,7 +14,6 @@ const meta: Meta = { }, }, }, - tags: ["autodocs"], }; export default meta; diff --git a/packages/ui/src/components/stories/mono-badge.stories.tsx b/packages/ui/src/components/stories/mono-badge.stories.tsx deleted file mode 100644 index 7f7c8cadd..000000000 --- a/packages/ui/src/components/stories/mono-badge.stories.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react-vite"; - -import { MonoBadge, type MonoBadgeTone } from "../mono-badge"; - -const meta: Meta = { - title: "ui/MonoBadge", - component: MonoBadge, - parameters: { - layout: "padded", - docs: { - description: { - component: - "Inline mono pill for identifiers (agent IDs, versions, protocol names) and tinted status badges. 6px radius, JetBrains Mono 11px/500 at 0.06em tracking.", - }, - }, - }, - tags: ["autodocs"], -}; - -export default meta; -type Story = StoryObj; - -const TONES: MonoBadgeTone[] = [ - "default", - "neutral", - "accent", - "success", - "warning", - "danger", - "info", -]; - -export const Default: Story = { - args: { - children: "agent-42", - }, -}; - -export const AllTones: Story = { - render: () => ( -
- {TONES.map(tone => ( - - {tone} - - ))} -
- ), -}; - -export const LowercaseIdentifier: Story = { - args: { - uppercase: false, - children: "agh-network/v0", - }, -}; - -export const BesideLabel: Story = { - render: () => ( -
- Running - v0.2.1 -
- ), -}; diff --git a/packages/ui/src/components/stories/mono-chip.stories.tsx b/packages/ui/src/components/stories/mono-chip.stories.tsx deleted file mode 100644 index 78f365a6c..000000000 --- a/packages/ui/src/components/stories/mono-chip.stories.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react-vite"; - -import { MonoChip } from "@agh/ui"; - -const meta: Meta = { - title: "ui/MonoChip", - component: MonoChip, - parameters: { - layout: "padded", - docs: { - description: { - component: - "Neutral inline chip — mirrors `.mono-chip` (default tone) in `docs/design/web-inspiration/styles/app.css`. Use for capability descriptors and tag rows. For tinted semantic variants use `MonoBadge`.", - }, - }, - }, - tags: ["autodocs"], -}; - -export default meta; -type Story = StoryObj; - -export const Default: Story = { - args: { children: "code" }, -}; - -export const Row: Story = { - render: () => ( -
- {["code", "shell", "file.read", "file.write", "plan.delegate"].map(label => ( - {label} - ))} -
- ), -}; diff --git a/packages/ui/src/components/stories/native-select.stories.tsx b/packages/ui/src/components/stories/native-select.stories.tsx index 93a03ad1d..3da587feb 100644 --- a/packages/ui/src/components/stories/native-select.stories.tsx +++ b/packages/ui/src/components/stories/native-select.stories.tsx @@ -15,7 +15,6 @@ const meta: Meta = { }, }, }, - tags: ["autodocs"], }; export default meta; diff --git a/packages/ui/src/components/stories/page-header.stories.tsx b/packages/ui/src/components/stories/page-header.stories.tsx index 8558e3fb2..fba79bf37 100644 --- a/packages/ui/src/components/stories/page-header.stories.tsx +++ b/packages/ui/src/components/stories/page-header.stories.tsx @@ -4,7 +4,7 @@ import { ListChecksIcon, PlusIcon, SparklesIcon } from "lucide-react"; import { Button } from "../button"; import { PageHeader } from "../page-header"; -import { Pills } from "../pills"; +import { PillGroup } from "../pill-group"; const meta: Meta = { title: "ui/PageHeader", @@ -14,11 +14,10 @@ const meta: Meta = { docs: { description: { component: - "Top-of-page header — icon + title + count badge on the left, segmented `Pills` controls in the middle, meta/actions on the right.", + "Top-of-page header — icon + title + count badge on the left, segmented `PillGroup` controls in the middle, meta/actions on the right.", }, }, }, - tags: ["autodocs"], }; export default meta; @@ -42,7 +41,7 @@ export const WithControlsAndMeta: Story = { icon={ListChecksIcon} count={42} controls={ - = { + title: "ui/PillGroup", + component: PillGroup, + parameters: { + layout: "padded", + docs: { + description: { + component: + "Segmented toggle track. Controlled via `items` + `value` + `onChange`. Renamed from the legacy `PillGroup`.", + }, + }, + }, +}; + +export default meta; +type Story = StoryObj; + +function PillGroupHarness() { + const [value, setValue] = useState<"list" | "kanban" | "dashboard" | "inbox">("list"); + return ( + + ); +} + +export const Default: Story = { + render: () => , +}; + +export const Selection: Story = { + render: () => , + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const list = await canvas.findByTestId("mode-list"); + const kanban = await canvas.findByTestId("mode-kanban"); + + await expect(list).toHaveAttribute("aria-pressed", "true"); + await expect(kanban).toHaveAttribute("aria-pressed", "false"); + + await userEvent.click(kanban); + await waitFor(() => expect(kanban).toHaveAttribute("aria-pressed", "true")); + await expect(list).toHaveAttribute("aria-pressed", "false"); + }, +}; + +export const SizeSm: Story = { + render: () => { + const [value, setValue] = useState<"a" | "b" | "c">("a"); + return ( + + ); + }, +}; + +export const DisabledItem: Story = { + render: () => ( + {}} + items={[ + { value: "list", label: "List" }, + { value: "kanban", label: "Kanban", disabled: true }, + ]} + /> + ), +}; diff --git a/packages/ui/src/components/stories/pill.stories.tsx b/packages/ui/src/components/stories/pill.stories.tsx new file mode 100644 index 000000000..6a4d00648 --- /dev/null +++ b/packages/ui/src/components/stories/pill.stories.tsx @@ -0,0 +1,268 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { expect, within } from "storybook/test"; + +import { Pill, type PillTone } from "../pill"; + +const meta: Meta = { + title: "ui/Pill", + component: Pill, + parameters: { + layout: "padded", + docs: { + description: { + component: + "Unified semantic pill — replaces `Pill`, `Pill`, `KindChip`, `Pill.Dot`, `WireChip`, and the legacy `Pill`. Compose with `Pill.Dot` for leading status dots.", + }, + }, + }, +}; + +export default meta; +type Story = StoryObj; + +const TONES: PillTone[] = ["neutral", "accent", "success", "warning", "danger", "info"]; + +const KIND_DOT_COLORS: Record = { + say: "#8E8E93", + greet: "#5BA6FF", + direct: "var(--color-accent)", + receipt: "var(--color-success)", + recipe: "var(--color-warning)", + trace: "#B892FF", + whois: "#4FD1C5", +}; + +export const Default: Story = { + args: { children: "label" }, +}; + +export const Tones: Story = { + render: () => ( +
+ {TONES.map(tone => ( + + {tone} + + ))} +
+ ), +}; + +export const TonesSans: Story = { + render: () => ( +
+ {TONES.map(tone => ( + + {tone} + + ))} +
+ ), +}; + +export const SolidEmphasis: Story = { + render: () => ( +
+ {TONES.map(tone => ( + + {tone} + + ))} +
+ ), + parameters: { + docs: { + description: { + story: "`solid` swaps the 15% tinted bg for a fully filled accent + ink-text formula.", + }, + }, + }, +}; + +export const Sizes: Story = { + render: () => ( +
+ + capability-id + + + v0.2.1 + + + FILTER + +
+ ), + parameters: { + docs: { + description: { + story: + "`xs` = chip (5px radius). `sm` = badge (22px tall, 6px radius). `md` = filter (32px, 20px radius).", + }, + }, + }, +}; + +export const MonoLowercaseIdentifier: Story = { + render: () => ( + + agh-network/v0 + + ), + parameters: { + docs: { + description: { + story: "Override the auto-uppercase default for protocol strings.", + }, + }, + }, +}; + +export const WithDot: Story = { + render: () => ( +
+ + + Connected + + + + Reconnecting + + + + Disconnected + +
+ ), +}; + +export const KindChipReplacement: Story = { + render: () => ( +
+ {Object.keys(KIND_DOT_COLORS).map(kind => ( + + + {kind} + + ))} +
+ ), + parameters: { + docs: { + description: { + story: "Protocol kind markers — leading dot keyed off the kind, label preserved.", + }, + }, + }, +}; + +export const ToggleInteractive: Story = { + render: () => ( +
+ }> + ALL + + }> + SAY + + }> + DIRECT + +
+ ), + parameters: { + docs: { + description: { + story: + "Pass `render={ diff --git a/packages/ui/src/components/stories/select.stories.tsx b/packages/ui/src/components/stories/select.stories.tsx index ecf929859..000453619 100644 --- a/packages/ui/src/components/stories/select.stories.tsx +++ b/packages/ui/src/components/stories/select.stories.tsx @@ -25,7 +25,6 @@ const meta: Meta = { }, }, }, - tags: ["autodocs"], }; export default meta; diff --git a/packages/ui/src/components/stories/separator.stories.tsx b/packages/ui/src/components/stories/separator.stories.tsx index 29fe2f12d..f8ab6e152 100644 --- a/packages/ui/src/components/stories/separator.stories.tsx +++ b/packages/ui/src/components/stories/separator.stories.tsx @@ -20,7 +20,6 @@ const meta: Meta = { ), ], - tags: ["autodocs"], }; export default meta; diff --git a/packages/ui/src/components/stories/sheet.stories.tsx b/packages/ui/src/components/stories/sheet.stories.tsx index 5c673d284..0a8c6ad62 100644 --- a/packages/ui/src/components/stories/sheet.stories.tsx +++ b/packages/ui/src/components/stories/sheet.stories.tsx @@ -28,7 +28,6 @@ const meta: Meta = { }, }, }, - tags: ["autodocs"], }; export default meta; diff --git a/packages/ui/src/components/stories/sidebar.stories.tsx b/packages/ui/src/components/stories/sidebar.stories.tsx index e0258dc7a..b70017eb9 100644 --- a/packages/ui/src/components/stories/sidebar.stories.tsx +++ b/packages/ui/src/components/stories/sidebar.stories.tsx @@ -28,7 +28,6 @@ const meta: Meta = { }, }, }, - tags: ["autodocs"], }; export default meta; diff --git a/packages/ui/src/components/stories/skeleton.stories.tsx b/packages/ui/src/components/stories/skeleton.stories.tsx index 3da35b955..5ce7e6faa 100644 --- a/packages/ui/src/components/stories/skeleton.stories.tsx +++ b/packages/ui/src/components/stories/skeleton.stories.tsx @@ -20,7 +20,6 @@ const meta: Meta = { ), ], - tags: ["autodocs"], }; export default meta; diff --git a/packages/ui/src/components/stories/sonner.stories.tsx b/packages/ui/src/components/stories/sonner.stories.tsx index c600fe10c..a3942eb79 100644 --- a/packages/ui/src/components/stories/sonner.stories.tsx +++ b/packages/ui/src/components/stories/sonner.stories.tsx @@ -16,7 +16,6 @@ const meta: Meta = { }, }, }, - tags: ["autodocs"], }; export default meta; diff --git a/packages/ui/src/components/stories/spinner.stories.tsx b/packages/ui/src/components/stories/spinner.stories.tsx index f95286cae..6bc8dd930 100644 --- a/packages/ui/src/components/stories/spinner.stories.tsx +++ b/packages/ui/src/components/stories/spinner.stories.tsx @@ -20,7 +20,6 @@ const meta: Meta = { ), ], - tags: ["autodocs"], }; export default meta; diff --git a/packages/ui/src/components/stories/split-pane.stories.tsx b/packages/ui/src/components/stories/split-pane.stories.tsx index 6824bdbb5..5ebcec8dc 100644 --- a/packages/ui/src/components/stories/split-pane.stories.tsx +++ b/packages/ui/src/components/stories/split-pane.stories.tsx @@ -18,7 +18,6 @@ const meta: Meta = { }, }, }, - tags: ["autodocs"], }; export default meta; diff --git a/packages/ui/src/components/stories/status-dot.stories.tsx b/packages/ui/src/components/stories/status-dot.stories.tsx deleted file mode 100644 index 3c629ae6d..000000000 --- a/packages/ui/src/components/stories/status-dot.stories.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react-vite"; -import { expect, within } from "storybook/test"; - -import { StatusDot, type StatusDotTone } from "../status-dot"; - -const meta: Meta = { - title: "ui/StatusDot", - component: StatusDot, - parameters: { - layout: "centered", - docs: { - description: { - component: - "Tinted signal dot — semantic tone + optional `pulse` loop. Mirrors DESIGN.md §4 status indicators.", - }, - }, - }, - tags: ["autodocs"], -}; - -export default meta; -type Story = StoryObj; - -const TONES: StatusDotTone[] = ["success", "warning", "danger", "info", "accent", "neutral"]; - -const TONE_TO_COLOR: Record = { - success: "var(--color-success)", - warning: "var(--color-warning)", - danger: "var(--color-danger)", - info: "var(--color-info)", - accent: "var(--color-accent)", - neutral: "var(--color-text-tertiary)", -}; - -export const Default: Story = { - args: { - tone: "success", - }, -}; - -export const Tones: Story = { - render: () => ( -
- {TONES.map(tone => ( -
- - - {tone} - -
- ))} -
- ), -}; - -export const ToneCycleInteraction: Story = { - render: () => ( -
- {TONES.map(tone => ( -
- -
- ))} -
- ), - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - for (const tone of TONES) { - const wrapper = await canvas.findByTestId(`tone-${tone}`); - const dot = wrapper.querySelector('[data-slot="status-dot"]') as HTMLElement; - await expect(dot).toBeInTheDocument(); - await expect(dot.getAttribute("data-tone")).toBe(tone); - await expect(dot.style.backgroundColor).toBe(TONE_TO_COLOR[tone]); - } - }, -}; - -export const PulseSuccess: Story = { - args: { - tone: "success", - pulse: true, - }, -}; - -export const SizeVariants: Story = { - render: () => ( -
-
- - - sm · 6px - -
-
- - - md · 8px - -
-
- ), -}; diff --git a/packages/ui/src/components/stories/switch.stories.tsx b/packages/ui/src/components/stories/switch.stories.tsx index c5130e3c4..0518f6e14 100644 --- a/packages/ui/src/components/stories/switch.stories.tsx +++ b/packages/ui/src/components/stories/switch.stories.tsx @@ -16,7 +16,6 @@ const meta: Meta = { }, }, }, - tags: ["autodocs"], }; export default meta; diff --git a/packages/ui/src/components/stories/table.stories.tsx b/packages/ui/src/components/stories/table.stories.tsx index 304fd7561..1bac91936 100644 --- a/packages/ui/src/components/stories/table.stories.tsx +++ b/packages/ui/src/components/stories/table.stories.tsx @@ -29,7 +29,6 @@ const meta: Meta = { ), ], - tags: ["autodocs"], }; export default meta; diff --git a/packages/ui/src/components/stories/tabs.stories.tsx b/packages/ui/src/components/stories/tabs.stories.tsx index f7d7a8ff7..b47ae3107 100644 --- a/packages/ui/src/components/stories/tabs.stories.tsx +++ b/packages/ui/src/components/stories/tabs.stories.tsx @@ -15,7 +15,6 @@ const meta: Meta = { }, }, }, - tags: ["autodocs"], }; export default meta; diff --git a/packages/ui/src/components/stories/textarea.stories.tsx b/packages/ui/src/components/stories/textarea.stories.tsx index 60c76e8fa..1b14488bc 100644 --- a/packages/ui/src/components/stories/textarea.stories.tsx +++ b/packages/ui/src/components/stories/textarea.stories.tsx @@ -15,7 +15,6 @@ const meta: Meta = { }, }, }, - tags: ["autodocs"], }; export default meta; diff --git a/packages/ui/src/components/stories/toggle-group.stories.tsx b/packages/ui/src/components/stories/toggle-group.stories.tsx index 28c378296..cba689b51 100644 --- a/packages/ui/src/components/stories/toggle-group.stories.tsx +++ b/packages/ui/src/components/stories/toggle-group.stories.tsx @@ -23,7 +23,6 @@ const meta: Meta = { }, }, }, - tags: ["autodocs"], }; export default meta; diff --git a/packages/ui/src/components/stories/toggle.stories.tsx b/packages/ui/src/components/stories/toggle.stories.tsx index fa09d7fc5..8252b1e2a 100644 --- a/packages/ui/src/components/stories/toggle.stories.tsx +++ b/packages/ui/src/components/stories/toggle.stories.tsx @@ -16,7 +16,6 @@ const meta: Meta = { }, }, }, - tags: ["autodocs"], }; export default meta; diff --git a/packages/ui/src/components/stories/tool-call-card.stories.tsx b/packages/ui/src/components/stories/tool-call-card.stories.tsx index 00d581aaf..6450baa6b 100644 --- a/packages/ui/src/components/stories/tool-call-card.stories.tsx +++ b/packages/ui/src/components/stories/tool-call-card.stories.tsx @@ -15,7 +15,6 @@ const meta: Meta = { }, }, }, - tags: ["autodocs"], }; export default meta; diff --git a/packages/ui/src/components/stories/toolbar.stories.tsx b/packages/ui/src/components/stories/toolbar.stories.tsx index 98590dfd5..24524e80f 100644 --- a/packages/ui/src/components/stories/toolbar.stories.tsx +++ b/packages/ui/src/components/stories/toolbar.stories.tsx @@ -3,7 +3,7 @@ import { useState } from "react"; import { PlusIcon } from "lucide-react"; import { Button } from "../button"; -import { Pills } from "../pills"; +import { PillGroup } from "../pill-group"; import { SearchInput } from "../search-input"; import { Toolbar } from "../toolbar"; @@ -15,11 +15,10 @@ const meta: Meta = { docs: { description: { component: - "Composition-first toolbar shell — pass `SearchInput`, `Pills`, `Button` children directly. Wraps on narrow viewports.", + "Composition-first toolbar shell — pass `SearchInput`, `PillGroup`, `Button` children directly. Wraps on narrow viewports.", }, }, }, - tags: ["autodocs"], }; export default meta; @@ -30,7 +29,7 @@ function Harness() { const [search, setSearch] = useState(""); return ( - (
- {}} items={[ diff --git a/packages/ui/src/components/stories/tooltip.stories.tsx b/packages/ui/src/components/stories/tooltip.stories.tsx index 980857e59..e21fb9ea2 100644 --- a/packages/ui/src/components/stories/tooltip.stories.tsx +++ b/packages/ui/src/components/stories/tooltip.stories.tsx @@ -18,7 +18,6 @@ const meta: Meta = { }, }, }, - tags: ["autodocs"], }; export default meta; diff --git a/packages/ui/src/components/stories/typing-dots.stories.tsx b/packages/ui/src/components/stories/typing-dots.stories.tsx index 3bb84da98..696d8662b 100644 --- a/packages/ui/src/components/stories/typing-dots.stories.tsx +++ b/packages/ui/src/components/stories/typing-dots.stories.tsx @@ -14,7 +14,6 @@ const meta: Meta = { }, }, }, - tags: ["autodocs"], }; export default meta; diff --git a/packages/ui/src/components/stories/ui-provider.stories.tsx b/packages/ui/src/components/stories/ui-provider.stories.tsx index 5403632ec..8249fa3be 100644 --- a/packages/ui/src/components/stories/ui-provider.stories.tsx +++ b/packages/ui/src/components/stories/ui-provider.stories.tsx @@ -28,7 +28,6 @@ const meta: Meta = { }, }, }, - tags: ["autodocs"], }; export default meta; diff --git a/packages/ui/src/components/stories/wire-card.stories.tsx b/packages/ui/src/components/stories/wire-card.stories.tsx index dcd4e8bd7..eb83ba277 100644 --- a/packages/ui/src/components/stories/wire-card.stories.tsx +++ b/packages/ui/src/components/stories/wire-card.stories.tsx @@ -15,7 +15,6 @@ const meta: Meta = { }, }, }, - tags: ["autodocs"], }; export default meta; diff --git a/packages/ui/src/components/stories/wire-chip.stories.tsx b/packages/ui/src/components/stories/wire-chip.stories.tsx deleted file mode 100644 index 0bfc27baf..000000000 --- a/packages/ui/src/components/stories/wire-chip.stories.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { useState } from "react"; -import type { Meta, StoryObj } from "@storybook/react-vite"; - -import { KIND_DOT_COLORS, WireChip } from "@agh/ui"; - -const meta: Meta = { - title: "ui/WireChip", - component: WireChip, - parameters: { - layout: "padded", - docs: { - description: { - component: - "Free-floating filter chip — mirrors `.wire-chip` in `docs/design/web-inspiration/styles/app.css`. For a contained segmented toggle, use `Pills` instead.", - }, - }, - }, - tags: ["autodocs"], -}; - -export default meta; -type Story = StoryObj; - -const KINDS = ["say", "greet", "direct", "receipt", "recipe", "trace", "whois"] as const; - -export const Default: Story = { - args: { children: "say" }, -}; - -export const KindFilterRow: Story = { - render: () => { - const [active, setActive] = useState("all"); - return ( -
- setActive("all")}> - all - - {KINDS.map(kind => ( - setActive(kind)} - > - {kind} - - ))} -
- ); - }, -}; diff --git a/packages/ui/src/components/tool-call-card.tsx b/packages/ui/src/components/tool-call-card.tsx index d8b981d3e..1845ac3c2 100644 --- a/packages/ui/src/components/tool-call-card.tsx +++ b/packages/ui/src/components/tool-call-card.tsx @@ -4,7 +4,7 @@ import { TerminalIcon } from "lucide-react"; import * as React from "react"; import { cn } from "../lib/utils"; -import { MonoBadge, type MonoBadgeTone } from "./mono-badge"; +import { Pill, type PillTone } from "./pill"; export type ToolCallStatus = "running" | "done" | "error"; @@ -24,7 +24,7 @@ function isIconComponent(value: unknown): value is ToolCallIconComponent { return false; } -const STATUS_TONE: Record = { +const STATUS_TONE: Record = { running: "accent", done: "success", error: "danger", @@ -96,13 +96,13 @@ function ToolCallCard({ {filePath} ) : null} - {STATUS_LABEL[status]} - +
{children ? (
{ /** * Horizontal toolbar shell — flex row with wrap on narrow viewports. - * Composition-first: host decides which children (SearchInput, Pills, Button, etc.) go inside. + * Composition-first: host decides which children (SearchInput, PillGroup, Button, etc.) go inside. */ function Toolbar({ className, sticky, ...props }: ToolbarProps) { return ( diff --git a/packages/ui/src/components/wire-chip.test.tsx b/packages/ui/src/components/wire-chip.test.tsx deleted file mode 100644 index a10990298..000000000 --- a/packages/ui/src/components/wire-chip.test.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { describe, expect, it, vi } from "vitest"; - -import { WireChip } from "./wire-chip"; - -describe("WireChip", () => { - it("Should render as a button with neutral chrome by default", () => { - const { container } = render(say); - const chip = container.querySelector('[data-slot="wire-chip"]'); - expect(chip).not.toBeNull(); - expect(chip?.tagName).toBe("BUTTON"); - expect(chip?.className).toContain("bg-[color:var(--color-surface)]"); - expect(chip?.className).toContain("border-[color:var(--color-divider)]"); - expect(chip?.getAttribute("aria-pressed")).toBe("false"); - }); - - it("Should reflect the active state via aria-pressed and elevated surface", () => { - const { container } = render(direct); - const chip = container.querySelector('[data-slot="wire-chip"]'); - expect(chip?.getAttribute("aria-pressed")).toBe("true"); - expect(chip?.getAttribute("data-active")).toBe("true"); - expect(chip?.className).toContain("bg-[color:var(--color-surface-elevated)]"); - }); - - it("Should render a colored leading dot when dotColor is provided", () => { - const { container } = render(direct); - const dot = container.querySelector('[data-slot="wire-chip-dot"]'); - expect(dot).not.toBeNull(); - expect(dot?.style.background).toBe("var(--color-accent)"); - }); - - it("Should fire onClick when activated", async () => { - const user = userEvent.setup(); - const handle = vi.fn(); - render(say); - - await user.click(screen.getByRole("button", { name: /say/i })); - - expect(handle).toHaveBeenCalledTimes(1); - }); -}); diff --git a/packages/ui/src/components/wire-chip.tsx b/packages/ui/src/components/wire-chip.tsx deleted file mode 100644 index ff6f01a13..000000000 --- a/packages/ui/src/components/wire-chip.tsx +++ /dev/null @@ -1,57 +0,0 @@ -"use client"; - -import * as React from "react"; - -import { cn } from "../lib/utils"; - -export interface WireChipProps extends Omit, "children"> { - active?: boolean; - /** Optional CSS color or var() for the leading 7px wire-dot. */ - dotColor?: string; - children: React.ReactNode; -} - -/** - * Free-floating filter chip — mirrors `.wire-chip` in - * `docs/design/web-inspiration/styles/app.css`. Used in stand-alone filter - * rows (e.g. the network channel header `ALL · SAY · DIRECT · …`). For a - * contained segmented toggle, use {@link Pills}. - */ -function WireChip({ - active = false, - dotColor, - children, - className, - type = "button", - ...props -}: WireChipProps) { - return ( - - ); -} - -export { WireChip }; diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index 57a06d718..8e415294c 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -160,33 +160,24 @@ export { SplitPane, SPLIT_LIST_WIDTH_DEFAULT, type SplitPaneProps } from "./comp export { PageHeader, type PageHeaderProps } from "./components/page-header"; export { Pill, - Pills, + PillDot, pillVariants, - pillToggleVariants, type PillProps, - type PillsProps, - type PillsItem, - type PillVariant, + type PillDotProps, + type PillTone, type PillSize, -} from "./components/pills"; +} from "./components/pill"; +export { + PillGroup, + pillGroupSegmentVariants, + type PillGroupProps, + type PillGroupItem, + type PillGroupSize, +} from "./components/pill-group"; export { SearchInput, type SearchInputProps } from "./components/search-input"; export { Empty, type EmptyProps } from "./components/empty"; export { Section, type SectionProps } from "./components/section"; export { Toolbar, type ToolbarProps } from "./components/toolbar"; -export { - StatusDot, - type StatusDotProps, - type StatusDotTone, - type StatusDotSize, -} from "./components/status-dot"; -export { - MonoBadge, - monoBadgeVariants, - type MonoBadgeProps, - type MonoBadgeTone, -} from "./components/mono-badge"; -export { KindChip, KIND_DOT_COLORS, type KindChipProps } from "./components/kind-chip"; -export { WireChip, type WireChipProps } from "./components/wire-chip"; export { WireCard, WireCardHead, @@ -194,7 +185,6 @@ export { WireCardFoot, type WireCardProps, } from "./components/wire-card"; -export { MonoChip, type MonoChipProps } from "./components/mono-chip"; export { TypingDots, type TypingDotsProps } from "./components/typing-dots"; export { CodeBlock, type CodeBlockProps } from "./components/code-block"; export { @@ -209,11 +199,6 @@ export { type ToolCallStatus, } from "./components/tool-call-card"; export { Metric, type MetricProps, type MetricTone } from "./components/metric"; -export { - ConnectionIndicator, - type ConnectionIndicatorProps, - type ConnectionStatus, -} from "./components/connection-indicator"; export { Avatar, AvatarBadge, diff --git a/skills-lock.json b/skills-lock.json index 1184c10fd..b65d62614 100644 --- a/skills-lock.json +++ b/skills-lock.json @@ -696,11 +696,6 @@ "sourceType": "github", "computedHash": "1f9dd6946d6bb66bd76c5d49d607e5becf7c93ad643070fc4e352d17e426b69d" }, - "shadcn-ui": { - "source": "pedronauck/skills", - "sourceType": "github", - "computedHash": "4c3e54ac47ddcc2c948995c85f6740b78f2f556c769d800e92ddff035d5c7e59" - }, "shape": { "source": "pbakaus/impeccable", "sourceType": "github", diff --git a/web/AGENTS.md b/web/AGENTS.md index 38b130610..ac5edea4c 100644 --- a/web/AGENTS.md +++ b/web/AGENTS.md @@ -22,6 +22,7 @@ No production users exist. Never sacrifice code quality for backward compatibili - **`make web-lint` and `make web-typecheck` MUST pass** before completing ANY web task. Zero warnings, zero errors. - **Oxlint has zero tolerance** — any warning is a blocking failure - **Follow shadcn kebab-case naming** for all files in `web/` +- **Native DOM wrappers** — if a component’s root is a single native element (`button`, `input`, `a`, …), its props MUST extend that element’s intrinsic type (`React.ComponentProps<"…">`), merge `className`, and spread `{...props}` onto the node (use `forwardRef` when refs apply). CVA + `VariantProps`: follow the `shadcn` skill. Canonical rule: `.agents/skills/react/SKILL.md` → _Extend native element props_. - **Never add JS dependencies by hand in `package.json`** — always use `bun add` - **Check dependent package APIs** before writing integration code or tests - **Local QA against an isolated daemon MUST read `AGH_WEB_API_PROXY_TARGET` from the active bootstrap manifest/env** — never hardcode `http://localhost:2123` when `agh-qa-bootstrap` or another isolated QA envelope is in use. @@ -30,25 +31,25 @@ No production users exist. Never sacrifice code quality for backward compatibili Activate skills **before** writing code. Match task domain → activate all required skills: -| Domain | Required Skills | Conditional Skills | -| ----------------------------- | ---------------------------------------------------------------- | -------------------------------------------------- | -| React / Web UI | `react` + `tailwindcss` + `vercel-react-best-practices` | `shadcn` + `shadcn-ui` | -| Routing | `tanstack-router-best-practices` | `tanstack` | -| Data fetching | `tanstack-query-best-practices` + `app-renderer-systems` | | -| State management | `zustand` | | -| Schema / Validation | `zod` | `typescript-advanced` | -| Web testing | `vitest` + `react` + `testing-anti-patterns` | | -| TypeScript (types) | `typescript-advanced` | `context7` | -| UI / UX Design (generic) | `frontend-design` + `design-taste-frontend` | `interface-design` + `shadcn-ui` + `minimalist-ui` | -| **AGH UI / Redesign tasks** | `agh-design` + `design-taste-frontend` + `minimalist-ui` | `frontend-design` + `interface-design` | -| Storybook / component stories | `storybook-stories` | `shadcn-ui` | -| Animation / motion | `motion-react` | `motion` | -| Component patterns | `vercel-composition-patterns` + `vercel-react-best-practices` | | -| AI / Streaming | `ai-sdk` | `tanstack-query-best-practices` | -| Bug fix | `systematic-debugging` + `no-workarounds` | `testing-anti-patterns` | -| Design polish passes | `impeccable:polish` + `impeccable:layout` + `impeccable:typeset` | `impeccable:delight` + `impeccable:critique` | -| External docs lookup | `context7` + `find-docs` | `exa-web-search-free` | -| Task completion | `cy-final-verify` | | +| Domain | Required Skills | Conditional Skills | +| ----------------------------- | ---------------------------------------------------------------- | ----------------------------------------------- | +| React / Web UI | `react` + `tailwindcss` + `vercel-react-best-practices` | `shadcn` | +| Routing | `tanstack-router-best-practices` | `tanstack` | +| Data fetching | `tanstack-query-best-practices` + `app-renderer-systems` | | +| State management | `zustand` | | +| Schema / Validation | `zod` | `typescript-advanced` | +| Web testing | `vitest` + `react` + `testing-anti-patterns` | | +| TypeScript (types) | `typescript-advanced` | `context7` | +| UI / UX Design (generic) | `frontend-design` + `design-taste-frontend` | `interface-design` + `shadcn` + `minimalist-ui` | +| **AGH UI / Redesign tasks** | `agh-design` + `design-taste-frontend` + `minimalist-ui` | `frontend-design` + `interface-design` | +| Storybook / component stories | `storybook-stories` | `shadcn` | +| Animation / motion | `motion-react` | `motion` | +| Component patterns | `vercel-composition-patterns` + `vercel-react-best-practices` | | +| AI / Streaming | `ai-sdk` | `tanstack-query-best-practices` | +| Bug fix | `systematic-debugging` + `no-workarounds` | `testing-anti-patterns` | +| Design polish passes | `impeccable:polish` + `impeccable:layout` + `impeccable:typeset` | `impeccable:delight` + `impeccable:critique` | +| External docs lookup | `context7` + `find-docs` | `exa-web-search-free` | +| Task completion | `cy-final-verify` | | **Redesign tasks (`.compozy/tasks/redesign/*`)**: you MUST run the `designer` agent in execution mode (not plan mode) AND activate `agh-design` + `design-taste-frontend` + `minimalist-ui` before touching any component. `DESIGN.md` tokens win over anything informal already in the codebase. diff --git a/web/CLAUDE.md b/web/CLAUDE.md index 38b130610..ac5edea4c 100644 --- a/web/CLAUDE.md +++ b/web/CLAUDE.md @@ -22,6 +22,7 @@ No production users exist. Never sacrifice code quality for backward compatibili - **`make web-lint` and `make web-typecheck` MUST pass** before completing ANY web task. Zero warnings, zero errors. - **Oxlint has zero tolerance** — any warning is a blocking failure - **Follow shadcn kebab-case naming** for all files in `web/` +- **Native DOM wrappers** — if a component’s root is a single native element (`button`, `input`, `a`, …), its props MUST extend that element’s intrinsic type (`React.ComponentProps<"…">`), merge `className`, and spread `{...props}` onto the node (use `forwardRef` when refs apply). CVA + `VariantProps`: follow the `shadcn` skill. Canonical rule: `.agents/skills/react/SKILL.md` → _Extend native element props_. - **Never add JS dependencies by hand in `package.json`** — always use `bun add` - **Check dependent package APIs** before writing integration code or tests - **Local QA against an isolated daemon MUST read `AGH_WEB_API_PROXY_TARGET` from the active bootstrap manifest/env** — never hardcode `http://localhost:2123` when `agh-qa-bootstrap` or another isolated QA envelope is in use. @@ -30,25 +31,25 @@ No production users exist. Never sacrifice code quality for backward compatibili Activate skills **before** writing code. Match task domain → activate all required skills: -| Domain | Required Skills | Conditional Skills | -| ----------------------------- | ---------------------------------------------------------------- | -------------------------------------------------- | -| React / Web UI | `react` + `tailwindcss` + `vercel-react-best-practices` | `shadcn` + `shadcn-ui` | -| Routing | `tanstack-router-best-practices` | `tanstack` | -| Data fetching | `tanstack-query-best-practices` + `app-renderer-systems` | | -| State management | `zustand` | | -| Schema / Validation | `zod` | `typescript-advanced` | -| Web testing | `vitest` + `react` + `testing-anti-patterns` | | -| TypeScript (types) | `typescript-advanced` | `context7` | -| UI / UX Design (generic) | `frontend-design` + `design-taste-frontend` | `interface-design` + `shadcn-ui` + `minimalist-ui` | -| **AGH UI / Redesign tasks** | `agh-design` + `design-taste-frontend` + `minimalist-ui` | `frontend-design` + `interface-design` | -| Storybook / component stories | `storybook-stories` | `shadcn-ui` | -| Animation / motion | `motion-react` | `motion` | -| Component patterns | `vercel-composition-patterns` + `vercel-react-best-practices` | | -| AI / Streaming | `ai-sdk` | `tanstack-query-best-practices` | -| Bug fix | `systematic-debugging` + `no-workarounds` | `testing-anti-patterns` | -| Design polish passes | `impeccable:polish` + `impeccable:layout` + `impeccable:typeset` | `impeccable:delight` + `impeccable:critique` | -| External docs lookup | `context7` + `find-docs` | `exa-web-search-free` | -| Task completion | `cy-final-verify` | | +| Domain | Required Skills | Conditional Skills | +| ----------------------------- | ---------------------------------------------------------------- | ----------------------------------------------- | +| React / Web UI | `react` + `tailwindcss` + `vercel-react-best-practices` | `shadcn` | +| Routing | `tanstack-router-best-practices` | `tanstack` | +| Data fetching | `tanstack-query-best-practices` + `app-renderer-systems` | | +| State management | `zustand` | | +| Schema / Validation | `zod` | `typescript-advanced` | +| Web testing | `vitest` + `react` + `testing-anti-patterns` | | +| TypeScript (types) | `typescript-advanced` | `context7` | +| UI / UX Design (generic) | `frontend-design` + `design-taste-frontend` | `interface-design` + `shadcn` + `minimalist-ui` | +| **AGH UI / Redesign tasks** | `agh-design` + `design-taste-frontend` + `minimalist-ui` | `frontend-design` + `interface-design` | +| Storybook / component stories | `storybook-stories` | `shadcn` | +| Animation / motion | `motion-react` | `motion` | +| Component patterns | `vercel-composition-patterns` + `vercel-react-best-practices` | | +| AI / Streaming | `ai-sdk` | `tanstack-query-best-practices` | +| Bug fix | `systematic-debugging` + `no-workarounds` | `testing-anti-patterns` | +| Design polish passes | `impeccable:polish` + `impeccable:layout` + `impeccable:typeset` | `impeccable:delight` + `impeccable:critique` | +| External docs lookup | `context7` + `find-docs` | `exa-web-search-free` | +| Task completion | `cy-final-verify` | | **Redesign tasks (`.compozy/tasks/redesign/*`)**: you MUST run the `designer` agent in execution mode (not plan mode) AND activate `agh-design` + `design-taste-frontend` + `minimalist-ui` before touching any component. `DESIGN.md` tokens win over anything informal already in the codebase. diff --git a/web/src/components/app-sidebar.test.tsx b/web/src/components/app-sidebar.test.tsx index 7d02b6fc8..d22049085 100644 --- a/web/src/components/app-sidebar.test.tsx +++ b/web/src/components/app-sidebar.test.tsx @@ -85,7 +85,6 @@ function makeProps(overrides: Partial = {}): AppSidebarProps { collapsed: false, onCollapseChange, workspaces, - activeWorkspace: workspaces[0], activeWorkspaceId: "ws_alpha", onSelectWorkspace, onAddWorkspace, @@ -109,21 +108,10 @@ describe("AppSidebar", () => { }); describe("Header", () => { - it("surfaces the active workspace name", () => { + it("does not render a sidebar header slot — workspace identity lives in the rail", () => { renderSidebar(makeProps()); - expect(screen.getByTestId("sidebar-workspace-name")).toHaveTextContent("alpha"); - }); - - it("removes the non-functional sidebar search affordances", () => { - renderSidebar(makeProps()); - expect(screen.queryByRole("button", { name: "Search" })).not.toBeInTheDocument(); - expect(screen.queryByText("Search…")).not.toBeInTheDocument(); - }); - - it("no longer carries the wordmark (now owned by the global app shell)", () => { - renderSidebar(makeProps()); - expect(screen.queryByTestId("sidebar-wordmark")).not.toBeInTheDocument(); - expect(screen.queryByTestId("sidebar-alpha-chip")).not.toBeInTheDocument(); + expect(screen.queryByTestId("sidebar-workspace-name")).not.toBeInTheDocument(); + expect(document.querySelector('[data-slot="sidebar-header"]')).toBeNull(); }); }); @@ -184,9 +172,7 @@ describe("AppSidebar", () => { }); it("still renders the + affordance when there are no workspaces", () => { - renderSidebar( - makeProps({ workspaces: [], activeWorkspace: undefined, activeWorkspaceId: null }) - ); + renderSidebar(makeProps({ workspaces: [], activeWorkspaceId: null })); expect(screen.getByTestId("add-workspace-btn")).toBeInTheDocument(); expect(screen.queryByTestId(/^workspace-avatar-/)).not.toBeInTheDocument(); }); diff --git a/web/src/components/app-sidebar.tsx b/web/src/components/app-sidebar.tsx index 27800a8ce..fbb31093d 100644 --- a/web/src/components/app-sidebar.tsx +++ b/web/src/components/app-sidebar.tsx @@ -15,16 +15,9 @@ import { type LucideIcon, } from "lucide-react"; -import { - cn, - ConnectionIndicator, - Logo, - type ConnectionStatus, - Sidebar, - SidebarSectionLabel, - StatusDot, -} from "@agh/ui"; +import { cn, Logo, Sidebar, SidebarSectionLabel, Pill } from "@agh/ui"; +import { ConnectionIndicator, type ConnectionStatus } from "@/components/connection-indicator"; import { AgentIcon, type AgentPayload } from "@/systems/agent"; import type { SessionPayload } from "@/systems/session"; import type { WorkspacePayload } from "@/systems/workspace"; @@ -88,21 +81,6 @@ function RailSlot({ ); } -interface HeaderSlotProps { - activeWorkspace: WorkspacePayload | undefined; -} - -function HeaderSlot({ activeWorkspace }: HeaderSlotProps) { - return ( - - {activeWorkspace?.name ?? ""} - - ); -} - const NAV_ROW_CLASS = "relative flex items-center gap-2 rounded-[6px] px-2 py-1.5 text-[13px] text-[color:var(--color-text-secondary)] transition-colors hover:bg-[color:var(--color-hover)] hover:text-[color:var(--color-text-primary)]"; const ACTIVE_NAV_ROW_CLASS = @@ -174,7 +152,7 @@ function AgentItem({ agent, hasActiveSession }: AgentItemProps) { /> {agent.name} {hasActiveSession ? ( - void; workspaces: WorkspacePayload[] | undefined; - activeWorkspace: WorkspacePayload | undefined; activeWorkspaceId: string | null; onSelectWorkspace: (id: string) => void; onAddWorkspace: () => void; @@ -355,7 +332,6 @@ function AppSidebar({ collapsed, onCollapseChange, workspaces, - activeWorkspace, activeWorkspaceId, onSelectWorkspace, onAddWorkspace, @@ -380,7 +356,6 @@ function AppSidebar({ onAddWorkspace={onAddWorkspace} /> } - header={} nav={ { @@ -14,7 +10,7 @@ export interface ConnectionIndicatorProps extends React.ComponentProps<"div"> { } interface StatusConfig { - tone: StatusDotTone; + tone: PillTone; label: string; pulse: boolean; } @@ -26,10 +22,16 @@ const STATUS_CONFIG: Record = { }; /** - * StatusDot + mono label composite — one canonical shape for daemon / socket - * connection state across the operator UI. Mirrors DESIGN.md §4 "Status Indicators". + * `Pill.Dot` + monospace label composite — canonical chrome for daemon / + * socket connection state across the operator UI. Wraps the dot in an + * `aria-live=polite` region so screen readers announce reconnects. */ -function ConnectionIndicator({ status, label, className, ...props }: ConnectionIndicatorProps) { +export function ConnectionIndicator({ + status, + label, + className, + ...props +}: ConnectionIndicatorProps) { const config = STATUS_CONFIG[status]; return (
- + {label ?? config.label}
); } - -export { ConnectionIndicator }; diff --git a/web/src/components/design-system-showcase.test.tsx b/web/src/components/design-system-showcase.test.tsx index 171531b59..ee2f3e4f7 100644 --- a/web/src/components/design-system-showcase.test.tsx +++ b/web/src/components/design-system-showcase.test.tsx @@ -187,7 +187,7 @@ describe("DesignSystemShowcase", () => { }); describe("file content contract", () => { - it("imports only from @agh/ui + lucide-react + react (no local UI primitives)", () => { + it("imports only from @agh/ui + lucide-react + react + the local helpers that compose @agh/ui Pill primitives", () => { const specifierRegex = /from\s+["']([^"']+)["']/g; const sources = new Set(); for (const match of SHOWCASE_SOURCE.matchAll(specifierRegex)) { @@ -196,7 +196,13 @@ describe("DesignSystemShowcase", () => { expect(sources.has("@agh/ui")).toBe(true); expect(sources.has("lucide-react")).toBe(true); expect(sources.has("react")).toBe(true); - const allowed = new Set(["@agh/ui", "lucide-react", "react"]); + const allowed = new Set([ + "@agh/ui", + "lucide-react", + "react", + "@/components/connection-indicator", + "@/systems/network/components/kind-chip", + ]); const forbidden = [...sources].filter(specifier => { if (allowed.has(specifier)) return false; return true; diff --git a/web/src/components/design-system-showcase.tsx b/web/src/components/design-system-showcase.tsx index df9b97ec3..3cd6b768d 100644 --- a/web/src/components/design-system-showcase.tsx +++ b/web/src/components/design-system-showcase.tsx @@ -45,7 +45,6 @@ import { Collapsible, CollapsibleContent, CollapsibleTrigger, - ConnectionIndicator, Dialog, DialogClose, DialogContent, @@ -75,15 +74,13 @@ import { ItemTitle, Kbd, KbdGroup, - KindChip, Label, Metric, - MonoBadge, + Pill, NativeSelect, NativeSelectOption, PageHeader, - Pill, - Pills, + PillGroup, Popover, PopoverContent, PopoverDescription, @@ -114,7 +111,6 @@ import { Skeleton, SplitPane, Spinner, - StatusDot, Switch, Table, TableBody, @@ -139,6 +135,9 @@ import { TooltipTrigger, } from "@agh/ui"; +import { ConnectionIndicator } from "@/components/connection-indicator"; +import { KindChip } from "@/systems/network/components/kind-chip"; + const DESIGN_MD_BASE = "https://github.com/compozy/agh/blob/main/DESIGN.md"; type SwatchKind = "color" | "radius" | "duration" | "easing" | "tracking"; @@ -442,7 +441,7 @@ function DesignSystemShowcase() { /> - setFilter(next)} items={FILTERS.map(item => ({ label: item.label, value: item.value }))} @@ -486,7 +485,7 @@ function FoundationsTokenSection() { id="foundations" data-testid="section-foundations" label={Foundations — Tokens} - right={tokens.css} + right={tokens.css} >
{TOKEN_GROUPS.map(group => ( @@ -588,7 +587,7 @@ function TypographySection() { id="typography" data-testid="section-typography" label={Foundations — Typography} - right={Inter · JetBrains Mono · NuixyberNext} + right={Inter · JetBrains Mono · NuixyberNext} >
@@ -623,7 +622,7 @@ function TypographySection() { agh - + Alpha
@@ -641,7 +640,11 @@ function ButtonsAndPillsSection() { id="buttons" data-testid="section-buttons" label={Buttons & Pills} - right={action} + right={ + + action + + } >
@@ -677,14 +680,14 @@ function ButtonsAndPillsSection() { Outline
- Neutral - Action - Stable - Pending - Error - Info + Neutral + Action + Stable + Pending + Error + Info
- Inputs & Search} - right={form primitives} + right={form primitives} >
@@ -797,7 +800,11 @@ function StatusAndMetricSection() { id="status" data-testid="section-status" label={Status, Metric, MonoBadge, KindChip} - right={signal} + right={ + + signal + + } >
@@ -811,15 +818,15 @@ function StatusAndMetricSection() {
- + Connected
- + Reconnecting
- + Disconnected
@@ -827,13 +834,27 @@ function StatusAndMetricSection() {
- id_01HQ… - idle - RUNNING - DONE - PARTIAL - ERROR - INFO + + id_01HQ… + + + idle + + + RUNNING + + + DONE + + + PARTIAL + + + ERROR + + + INFO +
{KINDS.map(kind => ( @@ -864,7 +885,11 @@ function FeedbackSection() { id="feedback" data-testid="section-feedback" label={Feedback (Alert, Empty, Toaster)} - right={state} + right={ + + state + + } >
@@ -914,7 +939,11 @@ function OverlaysSection() { id="overlays" data-testid="section-overlays" label={Dialog · Sheet · Popover · Tooltip} - right={motion} + right={ + + motion + + } >
@@ -1066,7 +1095,11 @@ agh session list --active`; id="code-chat" data-testid="section-code-chat" label={Code & Chat} - right={session shells} + right={ + + session shells + + } >
@@ -1078,7 +1111,7 @@ agh session list --active`; role="agent" meta={ <> - + CLAUDE · 10:42 } @@ -1105,7 +1138,7 @@ function LayoutSection() { id="layout" data-testid="section-layout" label={Sidebar & SplitPane} - right={layout} + right={layout} >
@@ -1206,7 +1239,9 @@ function LayoutSection() { Version - v0.4.2 + + v0.4.2 + diff --git a/web/src/components/stories/app-sidebar.stories.tsx b/web/src/components/stories/app-sidebar.stories.tsx index 3cff848c3..1116ab71b 100644 --- a/web/src/components/stories/app-sidebar.stories.tsx +++ b/web/src/components/stories/app-sidebar.stories.tsx @@ -31,21 +31,17 @@ function AppSidebarHarness({ defaultCollapsed = false, defaultWorkspaceId, activeWorkspaceId, - activeWorkspace, ...rest }: StoryArgs) { const [collapsed, setCollapsed] = useState(defaultCollapsed); const [workspaceId, setWorkspaceId] = useState( defaultWorkspaceId ?? activeWorkspaceId ?? null ); - const resolvedActive = - rest.workspaces?.find(ws => ws.id === workspaceId) ?? activeWorkspace ?? undefined; return ( = { docs: { description: { component: - "Thin composition over `@agh/ui` `Sidebar`. The rail owns the workspace switcher, the header surfaces the active workspace name + search, the nav owns the agent tree and workspace nav, and the footer owns the connection indicator + settings link. The global `agh` wordmark lives in the app-shell header one level up.", + "Thin composition over `@agh/ui` `Sidebar`. The rail owns the workspace switcher, the nav owns the agent tree and workspace nav, and the footer owns the connection indicator + settings link. The global `agh` wordmark lives in the app-shell header one level up.", }, }, }, args: { workspaces: workspaceFixtures, activeWorkspaceId: workspaceFixtures[1].id, - activeWorkspace: workspaceFixtures[1], onAddWorkspace: () => undefined, health: { version: "0.4.1" }, connectionStatus: "connected", @@ -104,7 +99,6 @@ export const Collapsed: Story = { export const NoWorkspaces: Story = { args: { workspaces: [], - activeWorkspace: undefined, activeWorkspaceId: null, defaultWorkspaceId: null, agents: [], diff --git a/web/src/hooks/routes/use-home-page.ts b/web/src/hooks/routes/use-home-page.ts index c60b24c8b..6344f85ca 100644 --- a/web/src/hooks/routes/use-home-page.ts +++ b/web/src/hooks/routes/use-home-page.ts @@ -1,6 +1,8 @@ import { useMemo } from "react"; -import type { ConnectionStatus, StatusDotTone } from "@agh/ui"; +import type { PillTone } from "@agh/ui"; + +import type { ConnectionStatus } from "@/components/connection-indicator"; import { useAgents } from "@/systems/agent"; import { useDaemonHealth } from "@/systems/daemon"; @@ -12,7 +14,7 @@ export type DaemonStatusKey = "healthy" | "degraded" | "disconnected" | "unknown interface DaemonStatusDescriptor { key: DaemonStatusKey; - tone: StatusDotTone; + tone: PillTone; label: string; description: string; } diff --git a/web/src/lib/kind-colors.ts b/web/src/lib/kind-colors.ts new file mode 100644 index 000000000..5d3726f68 --- /dev/null +++ b/web/src/lib/kind-colors.ts @@ -0,0 +1,19 @@ +/** + * Wire-protocol kind → leading-dot color map. + * Each protocol kind (`say`, `greet`, `direct`, …) is identified visually by + * a 7px colored dot rendered ahead of the kind label. Unknown kinds (platform + * names, event ids) render without a dot. + */ +export const KIND_COLORS: Record = { + say: "#8E8E93", + greet: "#5BA6FF", + direct: "var(--color-accent)", + receipt: "var(--color-success)", + recipe: "var(--color-warning)", + trace: "#B892FF", + whois: "#4FD1C5", +}; + +export function kindColorFor(kind: string): string | undefined { + return KIND_COLORS[kind.toLowerCase()]; +} diff --git a/web/src/lib/pill-variant.ts b/web/src/lib/pill-variant.ts index 6e7f6f473..4c6a09df1 100644 --- a/web/src/lib/pill-variant.ts +++ b/web/src/lib/pill-variant.ts @@ -1,9 +1,9 @@ -import type { PillVariant } from "@agh/ui"; +import type { PillTone } from "@agh/ui"; /** * Legacy tone strings emitted by `*-formatters.ts` helpers across domain systems. - * Maps the historical `design-system/pill` tone palette onto the new `@agh/ui` - * `Pill`/`Pills` semantic variant system. + * Maps the historical `design-system/pill` tone palette onto the unified `@agh/ui` + * `Pill` `tone` system. */ export type LegacyPillTone = | "neutral" @@ -14,7 +14,7 @@ export type LegacyPillTone = | "accent" | "warning"; -export function pillVariantFromTone(tone: LegacyPillTone | null | undefined): PillVariant { +export function pillVariantFromTone(tone: LegacyPillTone | null | undefined): PillTone { switch (tone) { case "amber": case "accent": @@ -29,6 +29,6 @@ export function pillVariantFromTone(tone: LegacyPillTone | null | undefined): Pi return "warning"; case "neutral": default: - return "default"; + return "neutral"; } } diff --git a/web/src/routes/_app.tsx b/web/src/routes/_app.tsx index 5ee8e8257..9de93590b 100644 --- a/web/src/routes/_app.tsx +++ b/web/src/routes/_app.tsx @@ -43,7 +43,6 @@ function AppLayout() { collapsed={page.collapsed} onCollapseChange={page.setCollapsed} workspaces={page.areWorkspacesLoading || page.workspacesError ? undefined : page.workspaces} - activeWorkspace={page.activeWorkspace} activeWorkspaceId={page.activeWorkspaceId} onSelectWorkspace={page.setActiveWorkspaceId} onAddWorkspace={page.openWorkspaceSetup} diff --git a/web/src/routes/_app/-tasks.test.tsx b/web/src/routes/_app/-tasks.test.tsx index dfb7c8e0c..13b318041 100644 --- a/web/src/routes/_app/-tasks.test.tsx +++ b/web/src/routes/_app/-tasks.test.tsx @@ -196,7 +196,7 @@ describe("TasksRoute", () => { expect(await screen.findByTestId("tasks-inbox-view")).toBeInTheDocument(); expect(screen.getByTestId("tasks-open-create")).toBeInTheDocument(); const inboxTab = screen.getByTestId("tasks-mode-inbox"); - expect(inboxTab.querySelector('[data-slot="pills-badge"]')).toHaveTextContent("1"); + expect(inboxTab.querySelector('[data-slot="pill-group-badge"]')).toHaveTextContent("1"); expect(screen.getByTestId("tasks-inbox-group-approvals")).toBeInTheDocument(); fireEvent.click(screen.getByTestId("tasks-inbox-item-approve-task_apr")); diff --git a/web/src/routes/_app/bridges.tsx b/web/src/routes/_app/bridges.tsx index 15c6a112e..39870b1b0 100644 --- a/web/src/routes/_app/bridges.tsx +++ b/web/src/routes/_app/bridges.tsx @@ -1,7 +1,7 @@ import { AlertCircle, Loader2, Plus, Waypoints } from "lucide-react"; import { createFileRoute } from "@tanstack/react-router"; -import { Button, Empty, PageHeader, Pills, SplitPane } from "@agh/ui"; +import { Button, Empty, PageHeader, PillGroup, SplitPane } from "@agh/ui"; import { BridgeCreateDialog, BridgeDetailPanel, @@ -64,7 +64,7 @@ function BridgesPage() { ); const controls = ( - + v{page.daemonVersion} - + ) : null } > @@ -103,7 +95,7 @@ function DaemonStatusSection({ page }: { page: HomePageView }) { data-status={page.daemonStatus.key} >
-
- {profile.backend} + + {profile.backend} + {backendLabel(profile.backend)} diff --git a/web/src/routes/_app/settings/-mcp-servers.test.tsx b/web/src/routes/_app/settings/-mcp-servers.test.tsx index 0ac761d5b..5f8e99b39 100644 --- a/web/src/routes/_app/settings/-mcp-servers.test.tsx +++ b/web/src/routes/_app/settings/-mcp-servers.test.tsx @@ -304,7 +304,7 @@ describe("MCPServersSettingsPage", () => { it("renders each row with a success StatusDot and name", () => { render(); const dot = screen.getByTestId("settings-page-mcp-servers-row-filesystem-status"); - expect(dot).toHaveAttribute("data-slot", "status-dot"); + expect(dot).toHaveAttribute("data-slot", "pill-dot"); expect(dot).toHaveAttribute("data-tone", "configured"); }); diff --git a/web/src/routes/_app/settings/general.tsx b/web/src/routes/_app/settings/general.tsx index dda8b25e9..8f65d325c 100644 --- a/web/src/routes/_app/settings/general.tsx +++ b/web/src/routes/_app/settings/general.tsx @@ -2,7 +2,7 @@ import { AlertCircle, Loader2 } from "lucide-react"; import { createFileRoute } from "@tanstack/react-router"; import { useCallback, useMemo, useState, type Dispatch, type SetStateAction } from "react"; -import { Button, Input, Pills } from "@agh/ui"; +import { Button, Input, PillGroup } from "@agh/ui"; import { useSettingsGeneralPage } from "@/hooks/routes/use-settings-general-page"; import type { SettingsGeneralSection } from "@/systems/settings"; import { @@ -244,7 +244,7 @@ function DefaultsSection({ draft, setDraft }: DraftSectionProps) { function PermissionsSection({ draft, setDraft }: DraftSectionProps) { return ( - - {declaration.event} + + {declaration.event} + {mode} @@ -436,16 +437,28 @@ function ExtensionRow({ data-testid={`settings-page-hooks-extensions-extensions-item-${entry.name}`} >
- +
{entry.name} {entry.state || (entry.enabled ? "running" : "stopped")} - {entry.version ? v{entry.version} : null} - {entry.health ? {entry.health} : null} - {missingEnv.length > 0 ? env missing : null} + {entry.version ? ( + + v{entry.version} + + ) : null} + {entry.health ? ( + + {entry.health} + + ) : null} + {missingEnv.length > 0 ? ( + + env missing + + ) : null} {entry.last_error ? ( onToggle(kind)} data-testid={`settings-page-hooks-extensions-policy-allowed-kinds-${kind}`} data-active={active ? "true" : "false"} - className={cn(pillToggleVariants({ active, size: "sm" }))} + className={cn(pillGroupSegmentVariants({ active, size: "sm" }))} > {kind} diff --git a/web/src/routes/_app/settings/mcp-servers.tsx b/web/src/routes/_app/settings/mcp-servers.tsx index 6a3e8bf53..5607228dd 100644 --- a/web/src/routes/_app/settings/mcp-servers.tsx +++ b/web/src/routes/_app/settings/mcp-servers.tsx @@ -9,11 +9,10 @@ import { Button, Empty, Input, - MonoBadge, + Pill, NativeSelect, NativeSelectOption, - Pills, - StatusDot, + PillGroup, Table, TableBody, TableCell, @@ -230,7 +229,7 @@ function ScopeSelector({ className="flex flex-wrap items-center gap-2" data-testid="settings-page-mcp-servers-scope-row" > - + items={items} value={currentValue} size="sm" @@ -338,7 +337,7 @@ function MCPServerRow({
- allowed: {entry.source_metadata.available_targets.map(available => ( - + {targetWriteLabel(available)} - + ))}
) : null} diff --git a/web/src/routes/_app/settings/observability.tsx b/web/src/routes/_app/settings/observability.tsx index 3267ac8da..4c3180867 100644 --- a/web/src/routes/_app/settings/observability.tsx +++ b/web/src/routes/_app/settings/observability.tsx @@ -2,7 +2,7 @@ import { AlertCircle, ExternalLink, Loader2 } from "lucide-react"; import { createFileRoute } from "@tanstack/react-router"; import { useCallback, useMemo, useState, type Dispatch, type SetStateAction } from "react"; -import { Button, MonoBadge, Switch } from "@agh/ui"; +import { Button, Pill, Switch } from "@agh/ui"; import { useSettingsObservabilityPage } from "@/hooks/routes/use-settings-observability-page"; import type { SettingsObservabilitySection } from "@/systems/settings"; import { @@ -222,12 +222,13 @@ function CaptureSection({ eyebrow="Capture" note="events, transcripts, logs" headerAction={ - 85 ? "warning" : "neutral"} data-testid="settings-page-observability-cap-percent" > {capPercent}% of cap - + } > { it("scopes Storybook to packages/ui stories with the expected addons and framework", () => { @@ -26,7 +28,8 @@ describe("packages/ui Storybook config", () => { }); it("imports shared tokens without pulling in web styling or data-layer providers", () => { - expect(packagesUiPreviewSource).toContain('import "@agh/ui/tokens.css";'); + expect(packagesUiPreviewSource).toContain('import "./preview.css";'); + expect(packagesUiPreviewCssSource).toContain('@import "@agh/ui/tokens.css";'); expect(packagesUiPreviewSource).not.toContain("web/src/styles.css"); expect(packagesUiPreviewSource).not.toContain("msw"); expect(packagesUiPreviewSource).not.toContain("QueryClient"); diff --git a/web/src/systems/agent/components/agent-info-panel.tsx b/web/src/systems/agent/components/agent-info-panel.tsx index 9148528b0..65ff64a33 100644 --- a/web/src/systems/agent/components/agent-info-panel.tsx +++ b/web/src/systems/agent/components/agent-info-panel.tsx @@ -1,4 +1,4 @@ -import { ScrollArea, Section, Empty, MonoBadge, cn } from "@agh/ui"; +import { ScrollArea, Section, Empty, Pill, cn } from "@agh/ui"; import { Plug } from "lucide-react"; import type { AgentPayload } from "../types"; @@ -53,9 +53,9 @@ export function AgentInfoPanel({ agent, className }: AgentInfoPanelProps) {
) : null}
- + {transport} - + ); })} diff --git a/web/src/systems/agent/components/agent-page-header.tsx b/web/src/systems/agent/components/agent-page-header.tsx index c7ec9fbbf..e2ef04e74 100644 --- a/web/src/systems/agent/components/agent-page-header.tsx +++ b/web/src/systems/agent/components/agent-page-header.tsx @@ -1,6 +1,6 @@ import { Plus, RefreshCw, Settings2 } from "lucide-react"; -import { Button, MonoBadge, PageHeader, Toolbar, cn } from "@agh/ui"; +import { Button, Pill, PageHeader, cn } from "@agh/ui"; import { AgentIcon } from "./agent-icon"; import type { AgentPayload } from "../types"; @@ -32,74 +32,64 @@ export function AgentPageHeader({ activeCount > 0 ? { label: "ACTIVE", tone: "success" as const } : { label: "IDLE", tone: "neutral" as const }; - const meta = ( -
- {agent.provider} - - - {sessions.length} {sessions.length === 1 ? "session" : "sessions"} - -
- ); return ( -
- - ( + + )} + title={ + + {agent.name} + + {status.label} + + + } + count={sessions.length} + meta={ +
+ - - - -
+ className={cn("size-3.5", isRefreshing && "animate-spin")} + /> + + + +
+ } + /> ); } diff --git a/web/src/systems/agent/components/agent-sessions-list.tsx b/web/src/systems/agent/components/agent-sessions-list.tsx index 673f04b49..3d45904ec 100644 --- a/web/src/systems/agent/components/agent-sessions-list.tsx +++ b/web/src/systems/agent/components/agent-sessions-list.tsx @@ -3,7 +3,7 @@ import { MessageSquare } from "lucide-react"; import { Empty, - MonoBadge, + Pill, Skeleton, Table, TableBody, @@ -110,9 +110,9 @@ function AgentSessionRow({ agentName, session, now }: AgentSessionRowProps) { - + {status.label} - + {formatDuration(session.activity?.elapsed_seconds)} diff --git a/web/src/systems/agent/components/stories/agent-page-header.stories.tsx b/web/src/systems/agent/components/stories/agent-page-header.stories.tsx index 3ecd15d0e..5d080a7c7 100644 --- a/web/src/systems/agent/components/stories/agent-page-header.stories.tsx +++ b/web/src/systems/agent/components/stories/agent-page-header.stories.tsx @@ -56,7 +56,8 @@ export const Default: Story = { }; /** - * No live sessions — IDLE chip + meta line still shows the historical session count. + * No live sessions — IDLE chip on the title; the count badge after the name still + * surfaces the historical session count. */ export const Idle: Story = { args: { sessions: idleSessions }, diff --git a/web/src/systems/agent/lib/session-status.ts b/web/src/systems/agent/lib/session-status.ts index 7d1ebc747..f1ede054e 100644 --- a/web/src/systems/agent/lib/session-status.ts +++ b/web/src/systems/agent/lib/session-status.ts @@ -1,4 +1,4 @@ -import type { MonoBadgeTone } from "@agh/ui"; +import type { PillTone } from "@agh/ui"; import type { SessionPayload } from "@/systems/session"; @@ -7,7 +7,7 @@ export type AgentSessionStatusKind = "active" | "starting" | "stopping" | "faile export interface AgentSessionStatus { kind: AgentSessionStatusKind; label: string; - tone: MonoBadgeTone; + tone: PillTone; } const ACTIVE_STATUS: AgentSessionStatus = { kind: "active", label: "ACTIVE", tone: "success" }; diff --git a/web/src/systems/automation/components/automation-detail-panel.tsx b/web/src/systems/automation/components/automation-detail-panel.tsx index eb095a916..771d8bb6b 100644 --- a/web/src/systems/automation/components/automation-detail-panel.tsx +++ b/web/src/systems/automation/components/automation-detail-panel.tsx @@ -11,18 +11,9 @@ import { Zap, } from "lucide-react"; -import { - Button, - CodeBlock, - Empty, - KindChip, - Metric, - MonoBadge, - Section, - StatusDot, - type MetricTone, -} from "@agh/ui"; +import { Button, CodeBlock, Empty, Metric, Pill, Section, type MetricTone } from "@agh/ui"; +import { KindChip } from "@/systems/network/components/kind-chip"; import { AutomationRunHistory } from "./automation-run-history"; import { automationScopeLabel, @@ -155,9 +146,9 @@ function JobScheduleSection({ job }: { job: AutomationJob }) {
+ {mode} - + } >
@@ -223,7 +214,9 @@ function JobSchedulerSection({ job }: { job: AutomationJob }) {
{scheduler.registered ? "REGISTERED" : "IDLE"} + + {scheduler.registered ? "REGISTERED" : "IDLE"} + } >
Event - + {trigger.event} - +
@@ -300,11 +293,12 @@ function TriggerHookSection({ trigger }: { trigger: AutomationTrigger }) { ) : (
{filters.map(([key, value]) => ( - {`${key}=${value}`} + >{`${key}=${value}`} ))}
)} @@ -334,9 +328,9 @@ function TriggerHookSection({ trigger }: { trigger: AutomationTrigger }) { Dispatches to - + {trigger.agent_name} - +
@@ -349,9 +343,9 @@ function PromptSection({ isTrigger, prompt }: { isTrigger: boolean; prompt: stri label={isTrigger ? "Prompt template" : "Prompt"} right={ isTrigger ? ( - + GO TEMPLATE - + ) : undefined } > @@ -467,14 +461,16 @@ export function AutomationDetailPanel({
- +

{item.name}

- {item.enabled ? "ENABLED" : "DISABLED"} - + + {item.enabled ? "ENABLED" : "DISABLED"} + + {automationSourceLabel(item.source)} - + {item.source === "config" ? (
- + {memory.type} - - + + {knowledgeScopeLabel(scopeForTone)} - +
- + Active Updated {formatKnowledgeRelativeTime(memory.mod_time)} @@ -185,7 +185,7 @@ function KnowledgeDetailPanel({
{row.tone === "mono" ? ( - {row.value} + {row.value} ) : ( {row.value} diff --git a/web/src/systems/knowledge/components/knowledge-list-panel.tsx b/web/src/systems/knowledge/components/knowledge-list-panel.tsx index 74968e8ce..6e53272d4 100644 --- a/web/src/systems/knowledge/components/knowledge-list-panel.tsx +++ b/web/src/systems/knowledge/components/knowledge-list-panel.tsx @@ -1,7 +1,7 @@ import { AlertCircle, BookOpen, Loader2 } from "lucide-react"; import { useMemo } from "react"; -import { Empty, MonoBadge, SearchInput } from "@agh/ui"; +import { Empty, Pill, SearchInput } from "@agh/ui"; import { cn } from "@/lib/utils"; import { @@ -67,12 +67,12 @@ function KnowledgeListItem({ memory, isSelected, onSelect }: KnowledgeListItemPr ) : null}
- + {memory.type} - - + + {scope === "workspace" ? "WS" : "GLOBAL"} - +
); @@ -156,7 +156,7 @@ function KnowledgeListPanel({ {group.label} - {group.memories.length} + {group.memories.length}
{group.memories.map(memory => ( = { +const TYPE_TONE: Record = { user: "accent", feedback: "accent", project: "success", reference: "info", }; -export function memoryTypeTone(type: MemoryType): MonoBadgeTone { +export function memoryTypeTone(type: MemoryType): PillTone { return TYPE_TONE[type] ?? "accent"; } -export function memoryScopeTone(scope: KnowledgeScope): MonoBadgeTone { +export function memoryScopeTone(scope: KnowledgeScope): PillTone { return scope === "workspace" ? "info" : "neutral"; } diff --git a/web/src/systems/network/components/kind-chip.tsx b/web/src/systems/network/components/kind-chip.tsx new file mode 100644 index 000000000..b41947de5 --- /dev/null +++ b/web/src/systems/network/components/kind-chip.tsx @@ -0,0 +1,35 @@ +import { Pill } from "@agh/ui"; +import * as React from "react"; + +import { kindColorFor } from "@/lib/kind-colors"; + +export interface KindChipProps extends Omit, "children"> { + kind: string; + /** Optional explicit label; defaults to `kind`. */ + label?: React.ReactNode; +} + +/** + * Protocol kind marker — transparent surface, neutral border + tertiary + * label, leading 7px colored dot keyed off the protocol kind. Unknown kinds + * (platform names, event ids) render without a dot. Composes `Pill` + `Pill.Dot`. + */ +export function KindChip({ kind, label, className, ...props }: KindChipProps) { + const dotColor = kindColorFor(kind); + + return ( + + {dotColor ? : null} + {label ?? kind} + + ); +} diff --git a/web/src/systems/network/components/network-create-channel-dialog.tsx b/web/src/systems/network/components/network-create-channel-dialog.tsx index 1d12a34b5..370450c5f 100644 --- a/web/src/systems/network/components/network-create-channel-dialog.tsx +++ b/web/src/systems/network/components/network-create-channel-dialog.tsx @@ -13,7 +13,7 @@ import { FieldDescription, FieldLabel, Input, - MonoBadge, + Pill, Section, Textarea, } from "@agh/ui"; @@ -107,9 +107,9 @@ export function NetworkCreateChannelDialog({
+ {draft.selectedAgentNames.length} selected - + } > {agents.length === 0 ? ( @@ -154,9 +154,9 @@ export function NetworkCreateChannelDialog({ {agent.name} - + {agent.provider} - + ); })} diff --git a/web/src/systems/network/components/network-workspace-shell.test.tsx b/web/src/systems/network/components/network-workspace-shell.test.tsx index e6fa8bcd7..a097ffd23 100644 --- a/web/src/systems/network/components/network-workspace-shell.test.tsx +++ b/web/src/systems/network/components/network-workspace-shell.test.tsx @@ -59,7 +59,7 @@ describe("NetworkWorkspaceShell", () => { ] as const)("maps %s status to a %s header status dot", (status, tone) => { const { container } = renderWorkspaceShell({ status }); - const statusDot = container.querySelector('[data-slot="status-dot"]'); + const statusDot = container.querySelector('[data-slot="pill-dot"]'); expect(statusDot).not.toBeNull(); expect(statusDot).toHaveAttribute("data-tone", tone); diff --git a/web/src/systems/network/components/network-workspace-shell.tsx b/web/src/systems/network/components/network-workspace-shell.tsx index 527478e25..fb2680081 100644 --- a/web/src/systems/network/components/network-workspace-shell.tsx +++ b/web/src/systems/network/components/network-workspace-shell.tsx @@ -14,19 +14,17 @@ import { import { Button, Empty, - KIND_DOT_COLORS, - KindChip, - MonoBadge, - MonoChip, - Pills, + Pill, + PillGroup, SearchInput, SidebarSectionLabel, - StatusDot, Textarea, WireCard, - WireChip, } from "@agh/ui"; + import { cn } from "@/lib/utils"; +import { KIND_COLORS } from "@/lib/kind-colors"; +import { KindChip } from "./kind-chip"; import { NETWORK_KIND_FILTERS, @@ -106,7 +104,7 @@ function fieldToneToBadgeTone(field?: NetworkRoomField["tone"]) { case "warning": return field; default: - return "default"; + return "neutral"; } } @@ -212,7 +210,7 @@ function NetworkSidebarRow({ )} /> ) : ( - + )} {unread ? ( - + {item.unreadCount} - + ) : null} {isChannel ? (
@@ -502,9 +536,9 @@ function NetworkDetailFieldList({ fields }: { fields: NetworkRoomField[] }) { {field.label}
{field.mono ? ( - + {field.value} - + ) : (

{field.value} @@ -585,7 +619,7 @@ export function NetworkWorkspaceShell({

- @@ -759,33 +793,45 @@ export function NetworkWorkspaceShell({ Filter by kind - onSelectKind("all")}> + } + onClick={() => onSelectKind("all")} + > all - + {NETWORK_KIND_FILTERS.map(kind => { const count = activeRoom?.kindCounts.find(metric => metric.kind === kind)?.count ?? 0; return ( - } onClick={() => onSelectKind(kind)} > + {formatNetworkKindLabel(kind).toLowerCase()} {count > 0 ? ` ${count}` : ""} - + ); })} - } onClick={onTogglePresence} > + presence {(activeRoom?.presenceCount ?? 0) > 0 ? ` ${activeRoom?.presenceCount ?? 0}` : ""} - +
@@ -795,7 +841,11 @@ export function NetworkWorkspaceShell({
- {activeRoom?.purpose ? {activeRoom.purpose} : null} + {activeRoom?.purpose ? ( + + {activeRoom.purpose} + + ) : null}

{activeRoom?.introTitle ?? "Loading room"} @@ -869,7 +919,7 @@ export function NetworkWorkspaceShell({

- + aria-label="Room detail tabs" items={[ { value: "about", label: "About" }, @@ -909,9 +959,13 @@ export function NetworkWorkspaceShell({ {activeRoom.capabilities.map(capability => (
- {capability.id} + + {capability.id} + {capability.detail?.version ? ( - {capability.detail.version} + + {capability.detail.version} + ) : null}

@@ -933,7 +987,7 @@ export function NetworkWorkspaceShell({ activeRoom.members.map(member => (

- +

{member.title} @@ -942,7 +996,9 @@ export function NetworkWorkspaceShell({ {member.subtitle}

- {member.local ? "local" : "remote"} + + {member.local ? "local" : "remote"} +
{member.lastSeen ? (

diff --git a/web/src/systems/session/components/chat-header.test.tsx b/web/src/systems/session/components/chat-header.test.tsx index e0e97fd3a..1185f4490 100644 --- a/web/src/systems/session/components/chat-header.test.tsx +++ b/web/src/systems/session/components/chat-header.test.tsx @@ -50,7 +50,7 @@ describe("ChatHeader", () => { ); const dot = screen.getByTestId("agent-status-dot"); - expect(dot.getAttribute("data-slot")).toBe("status-dot"); + expect(dot.getAttribute("data-slot")).toBe("pill-dot"); expect(dot.getAttribute("data-tone")).toBe("success"); expect(dot.getAttribute("data-size")).toBe("md"); }); @@ -85,7 +85,7 @@ describe("ChatHeader", () => { const badge = screen.getByTestId("session-workspace-badge"); expect(badge).toHaveTextContent("alpha"); - expect(badge.getAttribute("data-slot")).toBe("mono-badge"); + expect(badge.getAttribute("data-slot")).toBe("pill"); }); it("shows current runtime activity when the session is supervised", () => { diff --git a/web/src/systems/session/components/chat-header.tsx b/web/src/systems/session/components/chat-header.tsx index 3fe09b6fd..4fdb6d431 100644 --- a/web/src/systems/session/components/chat-header.tsx +++ b/web/src/systems/session/components/chat-header.tsx @@ -9,9 +9,8 @@ import { DialogFooter, DialogHeader, DialogTitle, - MonoBadge, - StatusDot, - type StatusDotTone, + Pill, + type PillTone, } from "@agh/ui"; import { cn } from "@/lib/utils"; @@ -30,7 +29,7 @@ export interface ChatHeaderProps { } interface StateSignal { - tone: StatusDotTone; + tone: PillTone; pulse?: boolean; } @@ -76,7 +75,7 @@ export function ChatHeader({ className="flex min-w-0 items-center gap-2 overflow-hidden" data-testid="chat-breadcrumb" > -

{ expect(badge).not.toBeNull(); expect(badge?.textContent).toBe("RUNNING"); expect(badge?.getAttribute("data-tone")).toBe("accent"); - expect(badge?.className).toMatch(/bg-\[color:var\(--color-accent-tint\)\]/); + expect(badge?.className).toMatch(/bg-\(--color-accent-tint\)/); expect(queryPrimitiveRoot()?.getAttribute("data-status")).toBe("running"); }); @@ -99,7 +99,7 @@ describe("ToolCallCard", () => { const badge = queryStatusBadge(); expect(badge?.textContent).toBe("DONE"); expect(badge?.getAttribute("data-tone")).toBe("success"); - expect(badge?.className).toMatch(/bg-\[color:var\(--color-success-tint\)\]/); + expect(badge?.className).toMatch(/bg-\(--color-success-tint\)/); expect(queryPrimitiveRoot()?.getAttribute("data-status")).toBe("done"); }); @@ -115,7 +115,7 @@ describe("ToolCallCard", () => { const badge = queryStatusBadge(); expect(badge?.textContent).toBe("ERROR"); expect(badge?.getAttribute("data-tone")).toBe("danger"); - expect(badge?.className).toMatch(/bg-\[color:var\(--color-danger-tint\)\]/); + expect(badge?.className).toMatch(/bg-\(--color-danger-tint\)/); const root = queryPrimitiveRoot(); expect(root?.getAttribute("data-status")).toBe("error"); expect(root?.className).toContain("data-[status=error]:border-[color:var(--color-danger)]/40"); diff --git a/web/src/systems/settings/components/provider-card.tsx b/web/src/systems/settings/components/provider-card.tsx index d9fe46261..dccca01fe 100644 --- a/web/src/systems/settings/components/provider-card.tsx +++ b/web/src/systems/settings/components/provider-card.tsx @@ -6,10 +6,8 @@ import { CardFooter, CardHeader, CardTitle, - MonoBadge, Pill, - StatusDot, - type StatusDotTone, + type PillTone, } from "@agh/ui"; import type { ReactNode } from "react"; @@ -56,7 +54,7 @@ export function ProviderCard({ provider, onEdit, onDelete }: ProviderCardProps)

{provider.default ? ( - DEFAULT + DEFAULT ) : null} @@ -72,12 +70,13 @@ export function ProviderCard({ provider, onEdit, onDelete }: ProviderCardProps) {provider.settings.api_key_env ? ( {provider.settings.api_key_env} - {provider.api_key_env_present ? "SET" : "MISSING"} - + ) : ( @@ -94,7 +93,7 @@ export function ProviderCard({ provider, onEdit, onDelete }: ProviderCardProps) - { = { "workspace-mcp-sidecar": "WS-MCP.JSON", }; -function badgeTone(kind: SettingsSourceKind): MonoBadgeTone { +function badgeTone(kind: SettingsSourceKind): PillTone { switch (kind) { case "builtin-provider": return "neutral"; @@ -52,12 +52,13 @@ function SettingsSourceBadge({ }: SettingsSourceBadgeProps) { return (
- {sourceLabel(source)} - + {shadowed && shadowed.length > 0 ? ( shadows {shadowed.map((entry, index) => ( - {sourceLabel(entry)} - + ))} ) : null} diff --git a/web/src/systems/settings/components/settings-status-line.tsx b/web/src/systems/settings/components/settings-status-line.tsx index c17db2e91..60555b5cc 100644 --- a/web/src/systems/settings/components/settings-status-line.tsx +++ b/web/src/systems/settings/components/settings-status-line.tsx @@ -1,5 +1,5 @@ import type { ReactNode } from "react"; -import { StatusDot } from "@agh/ui"; +import { Pill } from "@agh/ui"; interface SettingsStatusLineProps { daemonAvailable: boolean; @@ -18,7 +18,7 @@ function SettingsStatusLine({ return (
- + {label} {items.map((item, index) => ( diff --git a/web/src/systems/skill/components/marketplace-view.tsx b/web/src/systems/skill/components/marketplace-view.tsx index 2d8f112ee..bca02da83 100644 --- a/web/src/systems/skill/components/marketplace-view.tsx +++ b/web/src/systems/skill/components/marketplace-view.tsx @@ -11,8 +11,8 @@ import { CardFooter, CardHeader, Empty, - MonoBadge, - Pills, + Pill, + PillGroup, SearchInput, } from "@agh/ui"; @@ -82,23 +82,24 @@ function MarketplaceCard({ skill, isInstalled, onInstall, isInstalling }: Market {tags.length > 0 ? (
{tags.map(tag => ( - {tag} - + ))}
) : null} {isInstalled ? ( - + INSTALLED - + ) : onInstall ? (
)} @@ -168,7 +169,7 @@ function MarketplaceView({ } value={search} /> - ({ diff --git a/web/src/systems/skill/components/skill-detail-panel.tsx b/web/src/systems/skill/components/skill-detail-panel.tsx index c8b6d0d69..9ee22192a 100644 --- a/web/src/systems/skill/components/skill-detail-panel.tsx +++ b/web/src/systems/skill/components/skill-detail-panel.tsx @@ -3,10 +3,9 @@ import { AlertCircle, Loader2, Wrench } from "lucide-react"; import { Button, Empty, - MonoBadge, + Pill, PageHeader, Section, - StatusDot, Switch, Table, TableBody, @@ -44,12 +43,12 @@ function SkillDetailMeta({ skill }: { skill: SkillPayload }) { return (
{skill.version ? ( - {`v${skill.version}`} + {`v${skill.version}`} ) : null} - {author ? {`@${author}`} : null} - + {author ? {`@${author}`} : null} + {skill.source} - +
); } @@ -157,13 +156,14 @@ function SkillCapabilitiesSection({ skill }: { skill: SkillPayload }) { ) : (
{capabilities.map(capability => ( - {capability} - + ))}
)} @@ -204,7 +204,7 @@ function SkillRecentCallsSection({ skill }: { skill: SkillPayload }) { key={`${call.label}-${index}`} > - ) : null}
- @@ -170,7 +170,7 @@ function SkillListPanel({ {group.label} - {group.skills.length} + {group.skills.length}
{group.skills.map(skill => ( = { additional: 4, }; -const SOURCE_TONE: Record = { +const SOURCE_TONE: Record = { bundled: "success", workspace: "info", marketplace: "accent", @@ -34,7 +34,7 @@ export function compareSkillSource(left: string, right: string): number { return (SOURCE_ORDER[left] ?? 99) - (SOURCE_ORDER[right] ?? 99); } -export function skillSourceTone(source: string): MonoBadgeTone { +export function skillSourceTone(source: string): PillTone { return SOURCE_TONE[source] ?? "neutral"; } diff --git a/web/src/systems/tasks/components/task-card.test.tsx b/web/src/systems/tasks/components/task-card.test.tsx index a0a4fcf97..179b47f1d 100644 --- a/web/src/systems/tasks/components/task-card.test.tsx +++ b/web/src/systems/tasks/components/task-card.test.tsx @@ -43,7 +43,7 @@ describe("TaskCard", () => { expect(screen.getByTestId("task-card-children-task_001")).toHaveTextContent("2 children"); expect(screen.getByTestId("task-card-deps-task_001")).toHaveTextContent("1 dep"); // Status is rendered as a pulsing accent dot for in_progress tasks. - const dot = container.querySelector('[data-slot="status-dot"]'); + const dot = container.querySelector('[data-slot="pill-dot"]'); expect(dot).not.toBeNull(); expect(dot).toHaveAttribute("data-tone", "accent"); expect(dot).toHaveAttribute("data-pulse", "true"); diff --git a/web/src/systems/tasks/components/task-card.tsx b/web/src/systems/tasks/components/task-card.tsx index b7352add7..4c2cf6629 100644 --- a/web/src/systems/tasks/components/task-card.tsx +++ b/web/src/systems/tasks/components/task-card.tsx @@ -1,6 +1,6 @@ import { AlertCircle } from "lucide-react"; -import { MonoBadge, Pill } from "@agh/ui"; +import { Pill } from "@agh/ui"; import { pillVariantFromTone } from "@/lib/pill-variant"; import { @@ -92,12 +92,12 @@ export function TaskCard({
{task.priority ? ( - + {taskPriorityLabel(task.priority)} ) : null} {showApproval ? ( - {taskApprovalStateLabel(task.approval_state)} + {taskApprovalStateLabel(task.approval_state)} ) : null} {isDraft && onPublish ? (