@usetheo/ui 0.1.0-next.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +227 -0
- package/LICENSE +201 -0
- package/README.md +347 -0
- package/dist/fonts/LICENSE-GEIST.txt +92 -0
- package/dist/fonts/geist-400.woff2 +0 -0
- package/dist/fonts/geist-500.woff2 +0 -0
- package/dist/fonts/geist-600.woff2 +0 -0
- package/dist/fonts/geist-mono-400.woff2 +0 -0
- package/dist/fonts/geist-mono-500.woff2 +0 -0
- package/dist/fonts/geist-mono-600.woff2 +0 -0
- package/dist/fonts-cdn.css +28 -0
- package/dist/fonts.css +75 -0
- package/dist/index.d.ts +3063 -0
- package/dist/index.js +7746 -0
- package/dist/index.js.map +1 -0
- package/dist/styles.css +88 -0
- package/dist/tokens.css +230 -0
- package/package.json +520 -0
- package/registry/index.json +700 -0
- package/registry/r/agent-composer.json +22 -0
- package/registry/r/agent-editor.json +27 -0
- package/registry/r/agent-error-card.json +22 -0
- package/registry/r/agent-event.json +24 -0
- package/registry/r/agent-handoff.json +22 -0
- package/registry/r/agent-profile.json +23 -0
- package/registry/r/agent-starting-state.json +22 -0
- package/registry/r/agent-stream.json +27 -0
- package/registry/r/agent-streaming.json +22 -0
- package/registry/r/agent-timeline.json +22 -0
- package/registry/r/agent-types.json +15 -0
- package/registry/r/approval-card.json +25 -0
- package/registry/r/artifact-preview.json +22 -0
- package/registry/r/attachment-chip.json +24 -0
- package/registry/r/audit-log-entry.json +23 -0
- package/registry/r/auto-compact-notice.json +22 -0
- package/registry/r/avatar.json +23 -0
- package/registry/r/badge.json +22 -0
- package/registry/r/browser-controls.json +22 -0
- package/registry/r/build-log-stream.json +19 -0
- package/registry/r/button.json +23 -0
- package/registry/r/capability-indicator.json +23 -0
- package/registry/r/card.json +22 -0
- package/registry/r/chat-composer.json +23 -0
- package/registry/r/chat-message.json +21 -0
- package/registry/r/chat-thread.json +20 -0
- package/registry/r/chat-types.json +15 -0
- package/registry/r/checkbox.json +23 -0
- package/registry/r/cn.json +19 -0
- package/registry/r/command-palette.json +25 -0
- package/registry/r/context-card.json +23 -0
- package/registry/r/context-window-bar.json +20 -0
- package/registry/r/cost-meter.json +22 -0
- package/registry/r/created-files-card.json +23 -0
- package/registry/r/cron-job-card.json +22 -0
- package/registry/r/cron-jobs-list.json +23 -0
- package/registry/r/deployment-row.json +23 -0
- package/registry/r/dialog.json +23 -0
- package/registry/r/diff-viewer.json +20 -0
- package/registry/r/domain-config.json +25 -0
- package/registry/r/empty-state.json +20 -0
- package/registry/r/env-var-editor.json +25 -0
- package/registry/r/folder-context-card.json +23 -0
- package/registry/r/folder-selector.json +22 -0
- package/registry/r/form-field.json +23 -0
- package/registry/r/hook-config.json +22 -0
- package/registry/r/hook-event-log.json +22 -0
- package/registry/r/input.json +19 -0
- package/registry/r/intent-selector.json +24 -0
- package/registry/r/label.json +22 -0
- package/registry/r/lane-board.json +20 -0
- package/registry/r/live-region-context.json +16 -0
- package/registry/r/login-split.json +20 -0
- package/registry/r/mcp-server-card.json +22 -0
- package/registry/r/mcp-server-list.json +23 -0
- package/registry/r/memory-editor.json +23 -0
- package/registry/r/mention-menu.json +23 -0
- package/registry/r/metrics-panel.json +22 -0
- package/registry/r/mode-types.json +15 -0
- package/registry/r/model-card.json +23 -0
- package/registry/r/model-selector.json +23 -0
- package/registry/r/permission-matrix.json +22 -0
- package/registry/r/permission-modal.json +24 -0
- package/registry/r/permission-types.json +15 -0
- package/registry/r/preview-env-card.json +25 -0
- package/registry/r/preview-panel.json +21 -0
- package/registry/r/progress-checklist.json +23 -0
- package/registry/r/project-card.json +25 -0
- package/registry/r/project-switcher.json +22 -0
- package/registry/r/quick-action-chips.json +21 -0
- package/registry/r/radio-group.json +23 -0
- package/registry/r/recent-folders-list.json +22 -0
- package/registry/r/rollback-ui.json +24 -0
- package/registry/r/rule-card.json +23 -0
- package/registry/r/rule-editor.json +28 -0
- package/registry/r/rule-types.json +18 -0
- package/registry/r/run-stats.json +22 -0
- package/registry/r/running-tasks-panel.json +22 -0
- package/registry/r/safe-href.json +16 -0
- package/registry/r/scroll-area.json +22 -0
- package/registry/r/select.json +23 -0
- package/registry/r/session-list-item.json +20 -0
- package/registry/r/session-timeline.json +22 -0
- package/registry/r/sheet.json +24 -0
- package/registry/r/sidebar.json +19 -0
- package/registry/r/skeleton.json +19 -0
- package/registry/r/skill-card.json +24 -0
- package/registry/r/skill-editor.json +28 -0
- package/registry/r/skills-list.json +23 -0
- package/registry/r/social-auth-row.json +21 -0
- package/registry/r/steps-rail.json +20 -0
- package/registry/r/sub-agent-dispatch.json +22 -0
- package/registry/r/switch.json +22 -0
- package/registry/r/system-prompt-editor.json +22 -0
- package/registry/r/tabs.json +22 -0
- package/registry/r/tailwind-preset.json +19 -0
- package/registry/r/task-header.json +24 -0
- package/registry/r/task-plan.json +22 -0
- package/registry/r/task-types.json +15 -0
- package/registry/r/terminal-panel.json +22 -0
- package/registry/r/textarea.json +19 -0
- package/registry/r/theme-provider.json +59 -0
- package/registry/r/theme-script.json +18 -0
- package/registry/r/theo-ui-provider.json +20 -0
- package/registry/r/toast.json +30 -0
- package/registry/r/token-usage-chart.json +20 -0
- package/registry/r/tokens.json +21 -0
- package/registry/r/tool-call-card.json +23 -0
- package/registry/r/tool-call.json +22 -0
- package/registry/r/tool-result.json +20 -0
- package/registry/r/tools-list.json +23 -0
- package/registry/r/tooltip.json +22 -0
- package/registry/r/topnav.json +22 -0
- package/registry/r/types.json +15 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "tokens",
|
|
4
|
+
"type": "registry:lib",
|
|
5
|
+
"title": "Violet Forge tokens",
|
|
6
|
+
"description": "Design system CSS variables (light + dark) for the Theo UI 'Violet Forge' theme. Mirrors src/styles/tokens.css verbatim — the embedded files content is the single source of truth.",
|
|
7
|
+
"files": [
|
|
8
|
+
{
|
|
9
|
+
"path": "styles/tokens.css",
|
|
10
|
+
"type": "registry:lib",
|
|
11
|
+
"target": "styles/tokens.css",
|
|
12
|
+
"content": "/* Theo UI — Design System \"Violet Forge\" tokens.\n *\n * Tokens normativos. Não edite valores aqui sem revisão do design system.\n * Referência: docs/design-system.md\n *\n * Estratégia:\n * - CSS custom properties para todas as cores, scales e durations.\n * - Default é light. Dark é ativado via [data-theme=\"dark\"] ou .dark.\n * - HSL split (h s l) em vez de hex permite alpha via color-mix() ou hsl(var(--x) / .5).\n *\n * Roxo Theo: #7C3AED → hsl(262 83% 58%)\n * Burnt sienna: #C96442 → hsl(15 54% 53%)\n */\n\n/* `:root` and `[data-theme=\"dark\"]` declarations are intentionally NOT wrapped\n * in `@layer base` so they apply at the document level regardless of whether\n * the consumer's CSS pipeline runs Tailwind's preflight. The texture utilities\n * below remain in `@layer utilities` because that layer affects ordering only,\n * not the cascade root.\n */\n:root {\n /* Base palette ------------------------------------------------------- *\n * Neutrals are 100% desaturated (Vercel-style). Color comes from the\n * primary (violet) + accent (burnt sienna) tokens below — never from\n * surface tints.\n */\n --background: 0 0% 100%; /* #FFFFFF */\n --foreground: 0 0% 4%; /* #0A0A0A */\n\n --card: 0 0% 100%; /* #FFFFFF */\n --card-foreground: 0 0% 4%;\n\n --popover: 0 0% 100%;\n --popover-foreground: 0 0% 4%;\n\n /* Primary — Theo violet (equity) ------------------------------------- */\n --primary: 262 83% 58%; /* #7C3AED */\n --primary-deep: 263 70% 42%; /* #5B21B6 — pressed */\n --primary-glow: 263 90% 76%; /* #A78BFA — hover halo */\n --primary-foreground: 0 0% 100%;\n\n /* Secondary — neutral muted ----------------------------------------- */\n --secondary: 0 0% 96%; /* #F5F5F5 */\n --secondary-foreground: 0 0% 4%;\n\n /* Accent — burnt sienna --------------------------------------------- */\n --accent: 15 54% 53%; /* #C96442 */\n --accent-deep: 15 55% 40%; /* #9C4A2E */\n --accent-foreground: 0 0% 100%;\n\n /* Muted ------------------------------------------------------------- */\n --muted: 0 0% 96%;\n --muted-foreground: 0 0% 45%; /* #737373 */\n\n /* Surfaces, borders, inputs ----------------------------------------- */\n --border: 0 0% 91%; /* #E8E8E8 — Vercel-style hairline */\n --input: 0 0% 91%;\n --ring: 262 83% 58%; /* matches primary */\n\n /* Semantics --------------------------------------------------------- */\n --success: 142 71% 36%; /* #16A34A */\n --success-foreground: 0 0% 100%;\n --warning: 33 92% 44%; /* #D97706 */\n --warning-foreground: 0 0% 100%;\n --destructive: 0 72% 51%; /* #DC2626 */\n --destructive-foreground: 0 0% 100%;\n --info: 217 91% 60%; /* #3B82F6 */\n --info-foreground: 0 0% 100%;\n\n /* Radii ------------------------------------------------------------- */\n --radius-none: 0px;\n --radius-sm: 4px;\n --radius-md: 6px;\n --radius-lg: 10px;\n --radius-xl: 14px;\n --radius-2xl: 20px;\n --radius-full: 9999px;\n /* shadcn compat */\n --radius: 14px;\n\n /* Spacing scale (4px base) ------------------------------------------ */\n --space-1: 4px;\n --space-2: 8px;\n --space-3: 12px;\n --space-4: 16px;\n --space-5: 20px;\n --space-6: 24px;\n --space-8: 32px;\n --space-10: 40px;\n --space-12: 48px;\n --space-16: 64px;\n --space-20: 80px;\n --space-24: 96px;\n --space-32: 128px;\n\n /* Elevation — theme-aware (uses --foreground for shadow ink,\n * --primary for the signature glow, so swapping themes recolors them).\n */\n --shadow-sm: 0 1px 2px 0 hsl(var(--foreground) / 0.06);\n --shadow-md: 0 2px 8px -2px hsl(var(--foreground) / 0.08), 0 1px 3px hsl(var(--foreground) / 0.06);\n --shadow-lg: 0 12px 32px -8px hsl(var(--foreground) / 0.12), 0 4px 12px\n hsl(var(--foreground) / 0.08);\n --shadow-glow: 0 0 24px hsl(var(--primary) / 0.25);\n --shadow-glow-strong: 0 0 32px hsl(var(--primary) / 0.4);\n\n /* Motion ------------------------------------------------------------ */\n --ease-out-soft: cubic-bezier(0.22, 1, 0.36, 1);\n --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);\n --ease-snap: cubic-bezier(0.85, 0, 0.15, 1);\n --duration-fast: 120ms;\n --duration-base: 200ms;\n --duration-slow: 360ms;\n --stagger: 60ms;\n\n /* Typography (Geist family — Vercel-inspired Violet Forge) ---------- */\n --font-display: \"Geist\", -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;\n --font-body: \"Geist\", -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;\n --font-mono: \"Geist Mono\", ui-monospace, SFMono-Regular, Menlo, monospace;\n}\n\n/* Dark mode — dominante --------------------------------------------- *\n * Vercel-aligned grayscale: pure neutrals (0% saturation) for every\n * surface and text layer. Color only in primary (violet), accent (burnt\n * sienna), and semantic tokens (success/warning/destructive/info).\n *\n * Activated exclusively via the `.dark` class on `<html>` (set by\n * `ThemeProvider`, `ThemeScript`, or consumers manually). The previous\n * `[data-theme=\"dark\"]` companion selector was dead — `data-theme` holds\n * the theme NAME, never the literal `\"dark\"`.\n */\n.dark {\n --background: 0 0% 4%; /* #0A0A0A */\n --foreground: 0 0% 96%; /* #F5F5F5 */\n\n --card: 0 0% 7%; /* #121212 */\n --card-foreground: 0 0% 96%;\n\n --popover: 0 0% 9%; /* #171717 */\n --popover-foreground: 0 0% 96%;\n\n --primary: 262 83% 58%;\n --primary-deep: 263 70% 42%;\n --primary-glow: 263 90% 76%;\n --primary-foreground: 0 0% 100%;\n\n --secondary: 0 0% 11%; /* #1C1C1C */\n --secondary-foreground: 0 0% 96%;\n\n --accent: 15 54% 53%;\n --accent-deep: 15 55% 40%;\n --accent-foreground: 0 0% 100%;\n\n --muted: 0 0% 11%;\n --muted-foreground: 0 0% 60%; /* #999 — Vercel gray-500 */\n\n --border: 0 0% 16%; /* #292929 */\n --input: 0 0% 11%;\n --ring: 262 83% 58%;\n\n --success: 152 79% 52%; /* #22E58C */\n --success-foreground: 0 0% 4%;\n --warning: 38 92% 50%; /* #F59E0B */\n --warning-foreground: 0 0% 4%;\n --destructive: 350 100% 65%; /* #FF4F6D */\n --destructive-foreground: 0 0% 4%;\n --info: 213 100% 70%; /* #5FB3FF */\n --info-foreground: 0 0% 4%;\n\n /* In dark mode, shadows are heavier ink (against the dark surface) and\n * the glow brightens. Both still derive from theme tokens.\n */\n --shadow-sm: 0 1px 2px 0 hsl(0 0% 0% / 0.4);\n --shadow-md: 0 2px 8px -2px hsl(0 0% 0% / 0.5), 0 1px 3px hsl(0 0% 0% / 0.4);\n --shadow-lg: 0 12px 32px -8px hsl(0 0% 0% / 0.6), 0 4px 12px hsl(0 0% 0% / 0.4);\n --shadow-glow: 0 0 24px hsl(var(--primary-glow) / 0.4);\n --shadow-glow-strong: 0 0 36px hsl(var(--primary-glow) / 0.6);\n}\n\n/* Reduced motion — neutralizes durations and transitions for users who\n * request prefers-reduced-motion: reduce. Components that use animation as\n * semantic state (e.g. a spinner on a running step) can opt back in with\n * Tailwind's `motion-safe:` prefix.\n *\n * Reference: WCAG 2.3.3 Animation from Interactions (Level AAA).\n */\n@media (prefers-reduced-motion: reduce) {\n :root {\n --duration-fast: 0ms;\n --duration-base: 0ms;\n --duration-slow: 0ms;\n --stagger: 0ms;\n }\n *,\n *::before,\n *::after {\n animation-duration: 0.001ms !important;\n animation-iteration-count: 1 !important;\n transition-duration: 0.001ms !important;\n scroll-behavior: auto !important;\n }\n}\n\n/* Texture utilities (signature) ---------------------------------------- */\n@layer utilities {\n .bg-dotted-violet {\n background-image: radial-gradient(hsl(var(--primary) / 0.08) 1px, transparent 1px);\n background-size: 20px 20px;\n }\n\n .bg-dotted-violet-strong {\n background-image: radial-gradient(hsl(var(--primary) / 0.16) 1px, transparent 1px);\n background-size: 20px 20px;\n }\n\n .bg-hero-glow {\n background-image: radial-gradient(\n ellipse 60% 50% at 70% 0%,\n hsl(var(--primary) / 0.18) 0%,\n transparent 60%\n );\n }\n\n .bg-paper-grain {\n background-image: url(\"data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2'/%3E%3CfeColorMatrix values='0 0 0 0 0.06 0 0 0 0 0.04 0 0 0 0 0.08 0 0 0 0.18 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.5'/%3E%3C/svg%3E\");\n }\n\n .text-balance {\n text-wrap: balance;\n }\n}\n"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"path": "styles/fonts.css",
|
|
16
|
+
"type": "registry:lib",
|
|
17
|
+
"target": "styles/fonts.css",
|
|
18
|
+
"content": "/* Theo UI — font loading (self-hosted by default).\n *\n * Default theme (Violet Forge): Geist Sans + Geist Mono.\n * Vercel-inspired typographic vocabulary — tight letter-spacing on display,\n * 3 strict weights (400 body / 500 UI / 600 display), OpenType \"liga\"\n * enabled globally. Geist is Vercel's open-source typeface, optimized for\n * product UIs and code surfaces. Licensed under SIL Open Font License\n * (LICENSE-GEIST.txt sits next to the woff2 files in `dist/fonts/`).\n *\n * ── Why self-hosted by default (HIGH-002 / D6) ────────────────────────────\n * The previous default `@import url(\"https://fonts.googleapis.com/...\")`\n * triggered a third-party network request on every page load — friction for\n * CSP-strict deployments, GDPR jurisdictions, and offline-first products.\n * The fonts ship as woff2 next to this CSS; consumers who installed\n * `@usetheo/ui/styles.css` get Geist with zero external dependencies.\n *\n * Asset budget: 6 woff2 files (~290 KB total). Three weights per family\n * cover the entire Violet Forge typescale (display-2xl through code-sm).\n *\n * ── Opt-in CDN ────────────────────────────────────────────────────────────\n * Consumers who prefer Google Fonts CDN (e.g. don't want to host static\n * assets) can use the parallel entrypoint instead:\n *\n * @import \"@usetheo/ui/fonts-cdn.css\";\n *\n * That file keeps the legacy @import to fonts.googleapis.com.\n */\n\n@font-face {\n font-family: \"Geist\";\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"./fonts/geist-400.woff2\") format(\"woff2\");\n}\n\n@font-face {\n font-family: \"Geist\";\n font-style: normal;\n font-weight: 500;\n font-display: swap;\n src: url(\"./fonts/geist-500.woff2\") format(\"woff2\");\n}\n\n@font-face {\n font-family: \"Geist\";\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"./fonts/geist-600.woff2\") format(\"woff2\");\n}\n\n@font-face {\n font-family: \"Geist Mono\";\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"./fonts/geist-mono-400.woff2\") format(\"woff2\");\n}\n\n@font-face {\n font-family: \"Geist Mono\";\n font-style: normal;\n font-weight: 500;\n font-display: swap;\n src: url(\"./fonts/geist-mono-500.woff2\") format(\"woff2\");\n}\n\n@font-face {\n font-family: \"Geist Mono\";\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"./fonts/geist-mono-600.woff2\") format(\"woff2\");\n}\n"
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "tool-call-card",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "ToolCallCard",
|
|
6
|
+
"description": "Single agent tool invocation rendered inside the stream.",
|
|
7
|
+
"dependencies": [
|
|
8
|
+
"lucide-react"
|
|
9
|
+
],
|
|
10
|
+
"registryDependencies": [
|
|
11
|
+
"cn",
|
|
12
|
+
"tailwind-preset",
|
|
13
|
+
"types"
|
|
14
|
+
],
|
|
15
|
+
"files": [
|
|
16
|
+
{
|
|
17
|
+
"path": "components/primitives/tool-call-card/tool-call-card.tsx",
|
|
18
|
+
"type": "registry:ui",
|
|
19
|
+
"target": "components/ui/tool-call-card.tsx",
|
|
20
|
+
"content": "import { Check, ChevronRight, Loader2, X } from \"lucide-react\";\nimport { useState } from \"react\";\nimport type { HTMLAttributes, ReactNode } from \"react\";\nimport { cn } from \"@/lib/cn\";\nimport type { IconComponent } from \"@/lib/types\";\n\n/**\n * ToolCallCard — single agent tool invocation rendered inside the stream.\n *\n * Visual: row with tool icon + tool name + target/command (mono) + status +\n * optional chevron. Expandable: when `output` is provided the row becomes a\n * `<details>` whose body renders the stdout/stderr/result block.\n *\n * Distinct from `AgentEvent` in the existing AgentTimeline by being a\n * stand-alone card that lives inside an AgentStream alongside chat messages.\n */\n\nexport type ToolCallStatus = \"running\" | \"success\" | \"failed\" | \"queued\" | \"skipped\";\n\nconst STATUS_ICON: Record<ToolCallStatus, ReactNode> = {\n running: <Loader2 className=\"size-3.5 animate-spin text-primary\" aria-hidden=\"true\" />,\n success: <Check className=\"size-3.5 text-success\" aria-hidden=\"true\" />,\n failed: <X className=\"size-3.5 text-destructive\" aria-hidden=\"true\" />,\n queued: <span className=\"size-2 rounded-full bg-warning\" aria-hidden=\"true\" />,\n skipped: <span className=\"size-2 rounded-full bg-muted-foreground\" aria-hidden=\"true\" />,\n};\n\nconst STATUS_LABEL: Record<ToolCallStatus, string> = {\n running: \"Running\",\n success: \"Completed\",\n failed: \"Failed\",\n queued: \"Queued\",\n skipped: \"Skipped\",\n};\n\ninterface ToolCallCardProps extends HTMLAttributes<HTMLElement> {\n /** Tool name (matches Theo Code / Claude Code tool registry: Bash, Edit, Read, …). */\n tool: ReactNode;\n /** Optional icon for the tool. */\n icon?: IconComponent;\n /** Target / command shown in monospace next to the tool name. */\n target?: ReactNode;\n status: ToolCallStatus;\n /** Optional stdout/stderr/result body. When present, the card becomes expandable. */\n output?: ReactNode;\n /** Default expanded state. Default: false (collapsed). */\n defaultExpanded?: boolean;\n /** Timestamp shown on the right. */\n timestamp?: ReactNode;\n}\n\nexport function ToolCallCard({\n className,\n tool,\n icon: Icon,\n target,\n status,\n output,\n defaultExpanded = false,\n timestamp,\n ...props\n}: ToolCallCardProps) {\n const [open, setOpen] = useState(defaultExpanded);\n const expandable = !!output;\n\n return (\n <article\n className={cn(\n \"overflow-hidden rounded-lg border border-border/40 bg-card/40 text-card-foreground\",\n className,\n )}\n {...props}\n >\n {/* T5.4: <header role=\"button\"> previously failed axe's\n * aria-prohibited-attr + semantic-landmark guidance. Replaced by\n * <div> for the layout container and a separate <button> for the\n * expand affordance (when expandable). Status icon span now carries\n * role=\"img\" to make aria-label valid. */}\n <div className={cn(\"flex items-center gap-2 px-3 py-2\")}>\n {expandable ? (\n <button\n type=\"button\"\n onClick={() => setOpen((v) => !v)}\n aria-expanded={open}\n aria-label={open ? `Collapse ${tool} details` : `Expand ${tool} details`}\n className=\"-m-1 inline-flex size-6 shrink-0 items-center justify-center rounded-md p-1 text-muted-foreground hover:bg-muted/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n >\n <ChevronRight\n className={cn(\"size-3.5 transition-transform duration-base\", open && \"rotate-90\")}\n aria-hidden=\"true\"\n />\n </button>\n ) : null}\n {Icon ? (\n <Icon className=\"size-4 shrink-0 text-muted-foreground\" aria-hidden=\"true\" />\n ) : null}\n <span className=\"shrink-0 font-medium font-mono text-code-sm text-foreground\">{tool}</span>\n {target ? (\n <span className=\"truncate font-mono text-code-sm text-muted-foreground\">{target}</span>\n ) : null}\n <span\n role=\"img\"\n aria-label={STATUS_LABEL[status]}\n className=\"ml-auto inline-flex shrink-0 items-center gap-1.5\"\n >\n {STATUS_ICON[status]}\n </span>\n {timestamp ? (\n <span className=\"shrink-0 font-mono text-label text-muted-foreground tabular-nums\">\n {timestamp}\n </span>\n ) : null}\n </div>\n {expandable && open ? (\n <div className=\"border-border/40 border-t bg-muted/20 px-3 py-2 font-mono text-code-sm\">\n {output}\n </div>\n ) : null}\n </article>\n );\n}\n"
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "tool-call",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "ToolCall",
|
|
6
|
+
"description": "Collapsible row representing an agent tool invocation.",
|
|
7
|
+
"dependencies": [
|
|
8
|
+
"lucide-react"
|
|
9
|
+
],
|
|
10
|
+
"registryDependencies": [
|
|
11
|
+
"cn",
|
|
12
|
+
"tailwind-preset"
|
|
13
|
+
],
|
|
14
|
+
"files": [
|
|
15
|
+
{
|
|
16
|
+
"path": "components/primitives/tool-call/tool-call.tsx",
|
|
17
|
+
"type": "registry:ui",
|
|
18
|
+
"target": "components/ui/tool-call.tsx",
|
|
19
|
+
"content": "import { ChevronRight, Wrench } from \"lucide-react\";\nimport { forwardRef, useState } from \"react\";\nimport type { HTMLAttributes, ReactNode } from \"react\";\nimport { cn } from \"@/lib/cn\";\n\ninterface ToolCallProps extends HTMLAttributes<HTMLDivElement> {\n /** Tool name e.g. \"bash\", \"read_file\", \"edit_file\". */\n name?: string;\n /**\n * Summary label e.g. \"Ran 2 commands\", \"Read 18 files\".\n */\n summary: ReactNode;\n /**\n * Collapsible payload (e.g. command, stdout, file list).\n */\n detail?: ReactNode;\n defaultOpen?: boolean;\n /**\n * If true, hides the wrench icon (useful when grouping by name elsewhere).\n */\n hideIcon?: boolean;\n}\n\n/**\n * ToolCall — collapsible row representing an agent tool invocation.\n *\n * Visual: subtle muted container, wrench icon, summary in body text,\n * chevron rotates on expand. Pairs with `<ToolResult>` for the rendered output.\n *\n * Typical usage inside a ChatMessage assistant body:\n *\n * <ToolCall summary=\"Read 18 files\" detail={<ToolResult>{output}</ToolResult>} />\n */\nconst ToolCall = forwardRef<HTMLDivElement, ToolCallProps>(\n ({ className, name, summary, detail, defaultOpen, hideIcon, ...props }, ref) => {\n const [open, setOpen] = useState(defaultOpen ?? false);\n const expandable = detail !== undefined;\n return (\n <div\n ref={ref}\n className={cn(\"rounded-md border border-border/40 bg-muted/30\", className)}\n {...props}\n >\n <button\n type=\"button\"\n onClick={() => expandable && setOpen((v) => !v)}\n aria-expanded={expandable ? open : undefined}\n disabled={!expandable}\n className={cn(\n \"flex w-full items-center gap-2 px-3 py-2 text-left\",\n \"font-sans text-body-sm text-foreground\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background\",\n expandable && \"cursor-pointer hover:bg-muted/60\",\n )}\n >\n {!hideIcon ? (\n <Wrench className=\"size-3.5 shrink-0 text-primary\" aria-hidden=\"true\" />\n ) : null}\n {name ? (\n <span className=\"font-mono text-code-sm text-muted-foreground\">{name}</span>\n ) : null}\n <span className=\"flex-1 truncate\">{summary}</span>\n {expandable ? (\n <ChevronRight\n className={cn(\n \"size-3.5 text-muted-foreground transition-transform\",\n open && \"rotate-90\",\n )}\n aria-hidden=\"true\"\n />\n ) : null}\n </button>\n {expandable && open ? (\n <div className=\"border-border/40 border-t bg-card px-3 py-2\">{detail}</div>\n ) : null}\n </div>\n );\n },\n);\nToolCall.displayName = \"ToolCall\";\n\nexport { ToolCall };\n"
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "tool-result",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "ToolResult",
|
|
6
|
+
"description": "Formatted output of a tool invocation.",
|
|
7
|
+
"dependencies": [],
|
|
8
|
+
"registryDependencies": [
|
|
9
|
+
"cn",
|
|
10
|
+
"tailwind-preset"
|
|
11
|
+
],
|
|
12
|
+
"files": [
|
|
13
|
+
{
|
|
14
|
+
"path": "components/primitives/tool-result/tool-result.tsx",
|
|
15
|
+
"type": "registry:ui",
|
|
16
|
+
"target": "components/ui/tool-result.tsx",
|
|
17
|
+
"content": "import { forwardRef } from \"react\";\nimport type { HTMLAttributes, ReactNode } from \"react\";\nimport { cn } from \"@/lib/cn\";\n\ntype Variant = \"text\" | \"code\" | \"json\";\n\ninterface ToolResultProps extends HTMLAttributes<HTMLDivElement> {\n variant?: Variant;\n /**\n * Pre-formatted content. For `code`/`json`, the component uses mono font\n * and preserves whitespace. For `text`, normal body font.\n */\n children: ReactNode;\n}\n\n/**\n * ToolResult — formatted output of a tool invocation.\n *\n * Three quick variants: plain text, code (monospace), json (monospace, tinted).\n * Always rendered as a `<div>` for predictable prop typing; code/json variants\n * wrap children in `<pre>` internally.\n */\nconst ToolResult = forwardRef<HTMLDivElement, ToolResultProps>(\n ({ className, variant = \"text\", children, ...props }, ref) => {\n if (variant === \"text\") {\n return (\n <div ref={ref} className={cn(\"text-body-sm text-muted-foreground\", className)} {...props}>\n {children}\n </div>\n );\n }\n return (\n <div ref={ref} className={className} {...props}>\n <pre\n className={cn(\n \"overflow-x-auto whitespace-pre-wrap font-mono text-code-sm\",\n variant === \"json\" ? \"text-primary-glow\" : \"text-foreground\",\n )}\n >\n {children}\n </pre>\n </div>\n );\n },\n);\nToolResult.displayName = \"ToolResult\";\n\nexport { ToolResult };\n"
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "tools-list",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "ToolsList",
|
|
6
|
+
"description": "Surface every tool the agent could call, with its enablement",
|
|
7
|
+
"dependencies": [
|
|
8
|
+
"lucide-react"
|
|
9
|
+
],
|
|
10
|
+
"registryDependencies": [
|
|
11
|
+
"cn",
|
|
12
|
+
"tailwind-preset",
|
|
13
|
+
"types"
|
|
14
|
+
],
|
|
15
|
+
"files": [
|
|
16
|
+
{
|
|
17
|
+
"path": "components/primitives/tools-list/tools-list.tsx",
|
|
18
|
+
"type": "registry:ui",
|
|
19
|
+
"target": "components/ui/tools-list.tsx",
|
|
20
|
+
"content": "import { Eye, Lock, Settings2 } from \"lucide-react\";\nimport { forwardRef } from \"react\";\nimport type { HTMLAttributes, ReactNode } from \"react\";\nimport { cn } from \"@/lib/cn\";\nimport type { IconComponent } from \"@/lib/types\";\n\nexport type ToolEnablement = \"enabled\" | \"ask\" | \"denied\";\n\nexport interface ToolEntry {\n id: string;\n name: string;\n description?: ReactNode;\n icon?: IconComponent;\n enablement?: ToolEnablement;\n /**\n * Source of the tool: built-in, plugin/skill, or MCP server name.\n */\n source?: string;\n /** Optional badge text (e.g. \"destructive\", \"experimental\"). */\n badge?: ReactNode;\n}\n\ninterface ToolsListProps extends Omit<HTMLAttributes<HTMLDivElement>, \"title\" | \"onChange\"> {\n tools: ToolEntry[];\n /** Title above the list. */\n title?: ReactNode;\n /**\n * Fires when the consumer toggles a tool's enablement state.\n * Cycle: enabled → ask → denied → enabled.\n */\n onEnablementChange?: (id: string, next: ToolEnablement) => void;\n}\n\nconst ENABLEMENT_LABEL: Record<ToolEnablement, string> = {\n enabled: \"Allowed\",\n ask: \"Ask before use\",\n denied: \"Denied\",\n};\n\nconst ENABLEMENT_CLASS: Record<ToolEnablement, string> = {\n enabled: \"bg-success/15 text-success border-success/40\",\n ask: \"bg-warning/15 text-warning border-warning/40\",\n denied: \"bg-destructive/15 text-destructive border-destructive/40\",\n};\n\nconst cycle = (cur: ToolEnablement): ToolEnablement =>\n cur === \"enabled\" ? \"ask\" : cur === \"ask\" ? \"denied\" : \"enabled\";\n\n/**\n * ToolsList — surface every tool the agent could call, with its enablement\n * state. Click the chip to cycle: Allowed → Ask → Denied.\n */\nconst ToolsList = forwardRef<HTMLDivElement, ToolsListProps>(\n ({ className, tools, title = \"Tools\", onEnablementChange, ...props }, ref) => (\n <section ref={ref} className={cn(\"rounded-xl border bg-card\", className)} {...props}>\n {title ? (\n <header className=\"flex items-center justify-between border-border/40 border-b px-4 py-3\">\n <h3 className=\"font-display text-title-md tracking-tight\">{title}</h3>\n <span className=\"font-mono text-label text-muted-foreground\">\n {tools.length} {tools.length === 1 ? \"tool\" : \"tools\"}\n </span>\n </header>\n ) : null}\n <ul className=\"divide-y divide-border/30\">\n {tools.map((tool) => {\n const Icon = tool.icon ?? Settings2;\n const state = tool.enablement ?? \"enabled\";\n return (\n <li\n key={tool.id}\n className=\"grid grid-cols-[auto_1fr_auto] items-start gap-3 px-4 py-3\"\n >\n <span className=\"mt-0.5 grid size-8 place-items-center rounded-md bg-muted text-muted-foreground\">\n <Icon className=\"size-4\" aria-hidden=\"true\" />\n </span>\n <div className=\"min-w-0\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <span className=\"font-medium font-mono text-body-sm text-foreground\">\n {tool.name}\n </span>\n {tool.source ? (\n <span className=\"font-mono text-label text-muted-foreground uppercase tracking-wider\">\n {tool.source}\n </span>\n ) : null}\n {tool.badge ? (\n <span className=\"inline-flex items-center gap-1 rounded-full bg-accent/15 px-2 py-0.5 font-mono text-accent text-label uppercase\">\n {tool.badge}\n </span>\n ) : null}\n </div>\n {tool.description ? (\n <p className=\"mt-0.5 text-body-sm text-muted-foreground\">{tool.description}</p>\n ) : null}\n </div>\n <button\n type=\"button\"\n onClick={() => onEnablementChange?.(tool.id, cycle(state))}\n className={cn(\n \"inline-flex items-center gap-1.5 rounded-full border px-2.5 py-1\",\n \"font-mono text-label uppercase tracking-wider transition-colors\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n ENABLEMENT_CLASS[state],\n !onEnablementChange && \"pointer-events-none\",\n )}\n aria-label={`Cycle enablement for ${tool.name}`}\n >\n {state === \"enabled\" ? (\n <Eye className=\"size-3\" aria-hidden=\"true\" />\n ) : state === \"ask\" ? (\n <Settings2 className=\"size-3\" aria-hidden=\"true\" />\n ) : (\n <Lock className=\"size-3\" aria-hidden=\"true\" />\n )}\n {ENABLEMENT_LABEL[state]}\n </button>\n </li>\n );\n })}\n </ul>\n </section>\n ),\n);\nToolsList.displayName = \"ToolsList\";\n\nexport { ToolsList };\n"
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "tooltip",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "Tooltip",
|
|
6
|
+
"description": "Built on Radix Tooltip — accessible hover / focus tooltip with delay and side / align controls.",
|
|
7
|
+
"dependencies": [
|
|
8
|
+
"@radix-ui/react-tooltip"
|
|
9
|
+
],
|
|
10
|
+
"registryDependencies": [
|
|
11
|
+
"cn",
|
|
12
|
+
"tailwind-preset"
|
|
13
|
+
],
|
|
14
|
+
"files": [
|
|
15
|
+
{
|
|
16
|
+
"path": "components/primitives/tooltip/tooltip.tsx",
|
|
17
|
+
"type": "registry:ui",
|
|
18
|
+
"target": "components/ui/tooltip.tsx",
|
|
19
|
+
"content": "import * as TooltipPrimitive from \"@radix-ui/react-tooltip\";\nimport { forwardRef } from \"react\";\nimport type { ComponentPropsWithoutRef, ElementRef, ReactNode } from \"react\";\nimport { cn } from \"@/lib/cn\";\n\n/**\n * Tooltip — built on Radix Tooltip.\n *\n * Visual: dark surface in light mode (and inverse in dark) with rounded-md,\n * shadow-md, text-body-sm. 8px delay-show default.\n *\n * Wrap your app in <Tooltip.Provider> once (or use the default delayDuration here).\n */\n\nconst Provider = TooltipPrimitive.Provider;\nconst Root = TooltipPrimitive.Root;\nconst Trigger = TooltipPrimitive.Trigger;\n\nconst Content = forwardRef<\n ElementRef<typeof TooltipPrimitive.Content>,\n ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>\n>(({ className, sideOffset = 6, ...props }, ref) => (\n <TooltipPrimitive.Portal>\n <TooltipPrimitive.Content\n ref={ref}\n sideOffset={sideOffset}\n className={cn(\n \"z-50 overflow-hidden rounded-md border border-border/40 bg-foreground px-2.5 py-1.5\",\n \"text-background text-body-sm shadow-md\",\n \"data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-[state=delayed-open]:animate-in\",\n \"data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=closed]:animate-out\",\n \"data-[side=top]:slide-in-from-bottom-1 data-[side=bottom]:slide-in-from-top-1\",\n \"data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1\",\n className,\n )}\n {...props}\n />\n </TooltipPrimitive.Portal>\n));\nContent.displayName = \"Tooltip.Content\";\n\ninterface TooltipProps extends ComponentPropsWithoutRef<typeof TooltipPrimitive.Root> {\n label: ReactNode;\n side?: \"top\" | \"right\" | \"bottom\" | \"left\";\n align?: \"start\" | \"center\" | \"end\";\n children: ReactNode;\n}\n\n/**\n * Shorthand: <Tooltip label=\"…\"><Button>…</Button></Tooltip>\n * Wraps Provider + Root + Trigger asChild + Content for the common case.\n * For advanced usage (controlled state, custom content), use Tooltip.Root etc. directly.\n */\nconst Tooltip = ({\n label,\n side = \"top\",\n align = \"center\",\n delayDuration = 200,\n children,\n ...rootProps\n}: TooltipProps) =>\n (\n <Provider delayDuration={delayDuration}>\n <Root {...rootProps}>\n <Trigger asChild>{children}</Trigger>\n <Content side={side} align={align}>\n {label}\n </Content>\n </Root>\n </Provider>\n ) as ReturnType<typeof Provider>;\n\nconst TooltipWithStatics = Tooltip as typeof Tooltip & {\n Provider: typeof Provider;\n Root: typeof Root;\n Trigger: typeof Trigger;\n Content: typeof Content;\n};\nTooltipWithStatics.Provider = Provider;\nTooltipWithStatics.Root = Root;\nTooltipWithStatics.Trigger = Trigger;\nTooltipWithStatics.Content = Content;\n\nexport { TooltipWithStatics as Tooltip };\n"
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "topnav",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "TopNav",
|
|
6
|
+
"description": "Horizontal app bar (64px) with breadcrumbs, mode switcher (radiogroup), and action slots.",
|
|
7
|
+
"dependencies": [
|
|
8
|
+
"lucide-react"
|
|
9
|
+
],
|
|
10
|
+
"registryDependencies": [
|
|
11
|
+
"cn",
|
|
12
|
+
"tailwind-preset"
|
|
13
|
+
],
|
|
14
|
+
"files": [
|
|
15
|
+
{
|
|
16
|
+
"path": "components/primitives/topnav/topnav.tsx",
|
|
17
|
+
"type": "registry:ui",
|
|
18
|
+
"target": "components/ui/topnav.tsx",
|
|
19
|
+
"content": "import { ChevronRight } from \"lucide-react\";\nimport { Fragment, forwardRef } from \"react\";\nimport type { HTMLAttributes, KeyboardEvent, ReactNode } from \"react\";\nimport { cn } from \"@/lib/cn\";\n\n/**\n * TopNav — horizontal app bar (64px).\n *\n * Composition:\n * <TopNav>\n * <TopNav.Left>\n * <TopNav.Breadcrumbs items={[{label: \"acme\"}, {label: \"api\"}]} />\n * </TopNav.Left>\n * <TopNav.Center>…segmented switcher…</TopNav.Center>\n * <TopNav.Right>…actions…</TopNav.Right>\n * </TopNav>\n *\n * Variant — hairline bottom border. No glass/blur (anti-glass guideline).\n */\n\nconst Root = forwardRef<HTMLElement, HTMLAttributes<HTMLElement>>(\n ({ className, ...props }, ref) => (\n <header\n ref={ref}\n className={cn(\n \"flex h-16 items-center justify-between gap-4 border-border/40 border-b bg-card px-6\",\n className,\n )}\n {...props}\n />\n ),\n);\nRoot.displayName = \"TopNav\";\n\nconst Left = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(\n ({ className, ...props }, ref) => (\n <div ref={ref} className={cn(\"flex flex-1 items-center gap-3\", className)} {...props} />\n ),\n);\nLeft.displayName = \"TopNav.Left\";\n\nconst Center = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(\n ({ className, ...props }, ref) => (\n <div ref={ref} className={cn(\"hidden flex-1 justify-center md:flex\", className)} {...props} />\n ),\n);\nCenter.displayName = \"TopNav.Center\";\n\nconst Right = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(\n ({ className, ...props }, ref) => (\n <div\n ref={ref}\n className={cn(\"flex flex-1 items-center justify-end gap-2\", className)}\n {...props}\n />\n ),\n);\nRight.displayName = \"TopNav.Right\";\n\ninterface BreadcrumbItem {\n label: ReactNode;\n href?: string;\n}\n\ninterface BreadcrumbsProps extends HTMLAttributes<HTMLElement> {\n items: BreadcrumbItem[];\n}\n\nconst Breadcrumbs = forwardRef<HTMLElement, BreadcrumbsProps>(\n ({ className, items, ...props }, ref) => (\n <nav\n ref={ref}\n aria-label=\"Breadcrumb\"\n className={cn(\"flex items-center gap-1.5 text-body-sm\", className)}\n {...props}\n >\n {items.map((item, idx) => {\n const isLast = idx === items.length - 1;\n const key = typeof item.label === \"string\" ? item.label : idx;\n return (\n <Fragment key={key}>\n {item.href && !isLast ? (\n <a\n href={item.href}\n className=\"font-sans text-muted-foreground transition-colors hover:text-foreground\"\n >\n {item.label}\n </a>\n ) : (\n <span\n className={cn(\n \"font-sans\",\n isLast ? \"font-medium text-foreground\" : \"text-muted-foreground\",\n )}\n aria-current={isLast ? \"page\" : undefined}\n >\n {item.label}\n </span>\n )}\n {!isLast ? (\n <ChevronRight className=\"size-3.5 text-muted-foreground\" aria-hidden=\"true\" />\n ) : null}\n </Fragment>\n );\n })}\n </nav>\n ),\n);\nBreadcrumbs.displayName = \"TopNav.Breadcrumbs\";\n\ninterface ModeSwitcherOption {\n value: string;\n label: ReactNode;\n}\n\ninterface ModeSwitcherProps extends Omit<HTMLAttributes<HTMLDivElement>, \"onChange\"> {\n value: string;\n options: ModeSwitcherOption[];\n onChange?: (value: string) => void;\n /**\n * Accessible label for the radiogroup. Defaults to \"Mode\".\n */\n ariaLabel?: string;\n}\n\n/**\n * TopNav.ModeSwitcher — segmented control (Chat / Code / Infra).\n *\n * ARIA semantics: `role=\"radiogroup\"` + `role=\"radio\"` per option, with roving\n * tabindex and full keyboard navigation (Arrow keys + Home/End). Per WAI-ARIA\n * radiogroup pattern, exactly one option has `tabIndex=0` (the active one) and\n * the rest have `tabIndex=-1`, so Tab moves in and Tab moves out.\n *\n * Stateless: pass `value` + `onChange`.\n */\nconst ModeSwitcher = forwardRef<HTMLDivElement, ModeSwitcherProps>(\n ({ className, value, options, onChange, ariaLabel = \"Mode\", ...props }, ref) => {\n const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {\n if (!onChange || options.length === 0) return;\n const idx = options.findIndex((o) => o.value === value);\n const current = idx >= 0 ? idx : 0;\n let nextIdx: number | null = null;\n if (e.key === \"ArrowRight\" || e.key === \"ArrowDown\") {\n nextIdx = (current + 1) % options.length;\n } else if (e.key === \"ArrowLeft\" || e.key === \"ArrowUp\") {\n nextIdx = (current - 1 + options.length) % options.length;\n } else if (e.key === \"Home\") {\n nextIdx = 0;\n } else if (e.key === \"End\") {\n nextIdx = options.length - 1;\n }\n if (nextIdx === null) return;\n e.preventDefault();\n const target = options[nextIdx];\n if (target) onChange(target.value);\n };\n\n return (\n <div\n ref={ref}\n role=\"radiogroup\"\n aria-label={ariaLabel}\n onKeyDown={handleKeyDown}\n className={cn(\n \"inline-flex items-center rounded-lg border border-border/60 bg-muted p-1\",\n className,\n )}\n {...props}\n >\n {options.map((opt) => {\n const isActive = opt.value === value;\n return (\n <button\n key={opt.value}\n type=\"button\"\n // biome-ignore lint/a11y/useSemanticElements: WAI-ARIA radiogroup pattern requires role=\"radio\" on buttons for segmented controls\n role=\"radio\"\n aria-checked={isActive}\n tabIndex={isActive ? 0 : -1}\n onClick={() => onChange?.(opt.value)}\n className={cn(\n \"rounded-md px-3 py-1.5 font-medium font-sans text-body-sm\",\n \"transition-all duration-base ease-out-soft\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n isActive\n ? \"bg-card text-foreground shadow-sm\"\n : \"text-muted-foreground hover:text-foreground\",\n )}\n >\n {opt.label}\n </button>\n );\n })}\n </div>\n );\n },\n);\nModeSwitcher.displayName = \"TopNav.ModeSwitcher\";\n\nconst TopNav = /*#__PURE__*/ Object.assign(Root, {\n Left,\n Center,\n Right,\n Breadcrumbs,\n ModeSwitcher,\n});\n\nexport { TopNav };\n"
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "types",
|
|
4
|
+
"type": "registry:lib",
|
|
5
|
+
"title": "Theo UI shared types",
|
|
6
|
+
"description": "Shared TypeScript helper types (IconComponent, etc.) used across Theo UI.",
|
|
7
|
+
"files": [
|
|
8
|
+
{
|
|
9
|
+
"path": "lib/types.ts",
|
|
10
|
+
"type": "registry:lib",
|
|
11
|
+
"target": "lib/types.ts",
|
|
12
|
+
"content": "import type { ComponentType, SVGProps } from \"react\";\n\n/**\n * IconComponent — shape compatible with both lucide-react icons and custom React SVG components.\n *\n * Centralizing this here avoids TS2322 noise from `exactOptionalPropertyTypes` when mapping\n * icons through Record<...> structures.\n */\nexport type IconComponent = ComponentType<SVGProps<SVGSVGElement> & { className?: string }>;\n"
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
}
|