@usetheo/ui 0.6.3-next.0 → 0.8.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/package.json CHANGED
@@ -1,10 +1,12 @@
1
1
  {
2
2
  "name": "@usetheo/ui",
3
- "version": "0.6.3-next.0",
3
+ "version": "0.8.0-next.0",
4
4
  "description": "Theo UI — framework-agnostic React component library with the Violet Forge design system. Focused on AI-agent interfaces, cloud dashboards, and developer-tooling surfaces.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
7
- "sideEffects": ["**/*.css"],
7
+ "sideEffects": [
8
+ "**/*.css"
9
+ ],
8
10
  "exports": {
9
11
  ".": {
10
12
  "types": "./dist/index.d.ts",
@@ -21,6 +23,10 @@
21
23
  "./fonts-cdn.css": "./dist/fonts-cdn.css",
22
24
  "./slide/themes/default.css": "./dist/slide/themes/default.css",
23
25
  "./slide/themes/violet-forge.css": "./dist/slide/themes/violet-forge.css",
26
+ "./account-menu": {
27
+ "types": "./dist/index.d.ts",
28
+ "import": "./dist/index.js"
29
+ },
24
30
  "./agent-composer": {
25
31
  "types": "./dist/index.d.ts",
26
32
  "import": "./dist/index.js"
@@ -125,10 +131,18 @@
125
131
  "types": "./dist/index.d.ts",
126
132
  "import": "./dist/index.js"
127
133
  },
134
+ "./code-block": {
135
+ "types": "./dist/index.d.ts",
136
+ "import": "./dist/index.js"
137
+ },
128
138
  "./command-palette": {
129
139
  "types": "./dist/index.d.ts",
130
140
  "import": "./dist/index.js"
131
141
  },
142
+ "./confirm-dialog": {
143
+ "types": "./dist/index.d.ts",
144
+ "import": "./dist/index.js"
145
+ },
132
146
  "./context-card": {
133
147
  "types": "./dist/index.d.ts",
134
148
  "import": "./dist/index.js"
@@ -137,6 +151,10 @@
137
151
  "types": "./dist/index.d.ts",
138
152
  "import": "./dist/index.js"
139
153
  },
154
+ "./copy-button": {
155
+ "types": "./dist/index.d.ts",
156
+ "import": "./dist/index.js"
157
+ },
140
158
  "./cost-meter": {
141
159
  "types": "./dist/index.d.ts",
142
160
  "import": "./dist/index.js"
@@ -153,6 +171,10 @@
153
171
  "types": "./dist/index.d.ts",
154
172
  "import": "./dist/index.js"
155
173
  },
174
+ "./danger-zone": {
175
+ "types": "./dist/index.d.ts",
176
+ "import": "./dist/index.js"
177
+ },
156
178
  "./deployment-row": {
157
179
  "types": "./dist/index.d.ts",
158
180
  "import": "./dist/index.js"
@@ -253,6 +275,10 @@
253
275
  "types": "./dist/index.d.ts",
254
276
  "import": "./dist/index.js"
255
277
  },
278
+ "./plan-badge": {
279
+ "types": "./dist/index.d.ts",
280
+ "import": "./dist/index.js"
281
+ },
256
282
  "./preview-env-card": {
257
283
  "types": "./dist/index.d.ts",
258
284
  "import": "./dist/index.js"
@@ -261,6 +287,10 @@
261
287
  "types": "./dist/index.d.ts",
262
288
  "import": "./dist/index.js"
263
289
  },
290
+ "./progress": {
291
+ "types": "./dist/index.d.ts",
292
+ "import": "./dist/index.js"
293
+ },
264
294
  "./progress-checklist": {
265
295
  "types": "./dist/index.d.ts",
266
296
  "import": "./dist/index.js"
@@ -349,6 +379,14 @@
349
379
  "types": "./dist/index.d.ts",
350
380
  "import": "./dist/index.js"
351
381
  },
382
+ "./stat-tile": {
383
+ "types": "./dist/index.d.ts",
384
+ "import": "./dist/index.js"
385
+ },
386
+ "./status-dot": {
387
+ "types": "./dist/index.d.ts",
388
+ "import": "./dist/index.js"
389
+ },
352
390
  "./steps-rail": {
353
391
  "types": "./dist/index.d.ts",
354
392
  "import": "./dist/index.js"
@@ -365,6 +403,10 @@
365
403
  "types": "./dist/index.d.ts",
366
404
  "import": "./dist/index.js"
367
405
  },
406
+ "./table": {
407
+ "types": "./dist/index.d.ts",
408
+ "import": "./dist/index.js"
409
+ },
368
410
  "./tabs": {
369
411
  "types": "./dist/index.d.ts",
370
412
  "import": "./dist/index.js"
@@ -385,6 +427,10 @@
385
427
  "types": "./dist/index.d.ts",
386
428
  "import": "./dist/index.js"
387
429
  },
430
+ "./timestamp": {
431
+ "types": "./dist/index.d.ts",
432
+ "import": "./dist/index.js"
433
+ },
388
434
  "./toast": {
389
435
  "types": "./dist/index.d.ts",
390
436
  "import": "./dist/index.js"
@@ -417,6 +463,10 @@
417
463
  "types": "./dist/index.d.ts",
418
464
  "import": "./dist/index.js"
419
465
  },
466
+ "./usage-meter": {
467
+ "types": "./dist/index.d.ts",
468
+ "import": "./dist/index.js"
469
+ },
420
470
  "./whiteboard": {
421
471
  "types": "./dist/whiteboard/index.d.ts",
422
472
  "import": "./dist/whiteboard/index.js"
@@ -454,44 +504,14 @@
454
504
  "import": "./dist/preset-v3-legacy.js"
455
505
  }
456
506
  },
457
- "files": ["dist", "registry/r", "registry/index.json", "LICENSE", "NOTICE", "CHANGELOG.md"],
458
- "scripts": {
459
- "build": "tsup",
460
- "dev": "ladle serve",
461
- "ladle:build": "ladle build",
462
- "ladle:preview": "ladle preview",
463
- "playground": "vite --config playground/vite.config.ts",
464
- "playground:build": "vite build --config playground/vite.config.ts",
465
- "playground:preview": "vite preview --config playground/vite.config.ts",
466
- "typecheck": "tsc --noEmit",
467
- "lint": "biome check src",
468
- "lint:ci": "biome ci src scripts .ladle playground",
469
- "lint:fix": "biome check --write src",
470
- "format": "biome format --write src scripts .ladle package.json tsconfig.json tailwind.config.ts vitest.config.ts tsup.config.ts biome.json",
471
- "format:check": "biome format src scripts .ladle package.json tsconfig.json tailwind.config.ts vitest.config.ts tsup.config.ts biome.json",
472
- "test": "vitest run",
473
- "test:watch": "vitest",
474
- "test:ui": "vitest --ui",
475
- "registry:build": "tsx scripts/build-registry.ts",
476
- "registry:validate": "tsx scripts/validate-registry.ts",
477
- "sync:readme": "tsx scripts/sync-readme.ts",
478
- "sync:exports": "tsx scripts/sync-exports.ts",
479
- "test:registry": "tsx scripts/test-registry-install.ts",
480
- "test:coverage": "vitest run --coverage",
481
- "quality:structure": "tsx scripts/validate-quality-gates.ts",
482
- "quality:bundle": "tsx scripts/validate-bundle-size.ts",
483
- "quality:bundle:update": "tsx scripts/validate-bundle-size.ts --update",
484
- "quality:a11y": "vitest run src/test/ladle-axe.test.tsx",
485
- "dogfood:whiteboard": "tsx scripts/dogfood-whiteboard.ts",
486
- "dogfood:slide": "tsx scripts/dogfood-slide.ts",
487
- "dogfood:slide-deck": "tsx scripts/dogfood-slide-deck.ts",
488
- "dogfood:slide-rich": "tsx scripts/dogfood-slide-rich.ts",
489
- "dogfood:v4-zero-config": "tsx scripts/dogfood-v4-zero-config.ts",
490
- "dogfood:v4-real-build": "bash scripts/dogfood-v4-real-build.sh",
491
- "dogfood:precompiled-utilities": "tsx scripts/dogfood-precompiled-utilities.ts",
492
- "quality:gates": "pnpm format:check && pnpm lint:ci && pnpm typecheck && pnpm test && pnpm build && pnpm registry:build && pnpm registry:validate && pnpm quality:structure && pnpm quality:bundle && pnpm quality:a11y && pnpm ladle:build && pnpm dogfood:whiteboard && pnpm dogfood:slide && pnpm dogfood:slide-deck && pnpm dogfood:slide-rich && pnpm dogfood:v4-zero-config && pnpm dogfood:precompiled-utilities",
493
- "quality:gates:fast": "pnpm format:check && pnpm lint:ci && pnpm typecheck && pnpm registry:build && pnpm registry:validate && pnpm quality:structure"
494
- },
507
+ "files": [
508
+ "dist",
509
+ "registry/r",
510
+ "registry/index.json",
511
+ "LICENSE",
512
+ "NOTICE",
513
+ "CHANGELOG.md"
514
+ ],
495
515
  "peerDependencies": {
496
516
  "@tailwindcss/vite": "^4.0.0",
497
517
  "hast-util-from-html": "^2.0.0",
@@ -644,12 +664,6 @@
644
664
  "engines": {
645
665
  "node": ">=20"
646
666
  },
647
- "pnpm": {
648
- "onlyBuiltDependencies": ["@biomejs/biome", "@swc/core", "esbuild"],
649
- "overrides": {
650
- "postcss": ">=8.5.10"
651
- }
652
- },
653
667
  "keywords": [
654
668
  "react",
655
669
  "components",
@@ -665,5 +679,41 @@
665
679
  "publishConfig": {
666
680
  "access": "public"
667
681
  },
668
- "packageManager": "pnpm@10.32.1+sha512.a706938f0e89ac1456b6563eab4edf1d1faf3368d1191fc5c59790e96dc918e4456ab2e67d613de1043d2e8c81f87303e6b40d4ffeca9df15ef1ad567348f2be"
669
- }
682
+ "scripts": {
683
+ "build": "tsup",
684
+ "dev": "ladle serve",
685
+ "ladle:build": "ladle build",
686
+ "ladle:preview": "ladle preview",
687
+ "playground": "vite --config playground/vite.config.ts",
688
+ "playground:build": "vite build --config playground/vite.config.ts",
689
+ "playground:preview": "vite preview --config playground/vite.config.ts",
690
+ "typecheck": "tsc --noEmit",
691
+ "lint": "biome check src",
692
+ "lint:ci": "biome ci src scripts .ladle playground",
693
+ "lint:fix": "biome check --write src",
694
+ "format": "biome format --write src scripts .ladle package.json tsconfig.json tailwind.config.ts vitest.config.ts tsup.config.ts biome.json",
695
+ "format:check": "biome format src scripts .ladle package.json tsconfig.json tailwind.config.ts vitest.config.ts tsup.config.ts biome.json",
696
+ "test": "vitest run",
697
+ "test:watch": "vitest",
698
+ "test:ui": "vitest --ui",
699
+ "registry:build": "tsx scripts/build-registry.ts",
700
+ "registry:validate": "tsx scripts/validate-registry.ts",
701
+ "sync:readme": "tsx scripts/sync-readme.ts",
702
+ "sync:exports": "tsx scripts/sync-exports.ts",
703
+ "test:registry": "tsx scripts/test-registry-install.ts",
704
+ "test:coverage": "vitest run --coverage",
705
+ "quality:structure": "tsx scripts/validate-quality-gates.ts",
706
+ "quality:bundle": "tsx scripts/validate-bundle-size.ts",
707
+ "quality:bundle:update": "tsx scripts/validate-bundle-size.ts --update",
708
+ "quality:a11y": "vitest run src/test/ladle-axe.test.tsx",
709
+ "dogfood:whiteboard": "tsx scripts/dogfood-whiteboard.ts",
710
+ "dogfood:slide": "tsx scripts/dogfood-slide.ts",
711
+ "dogfood:slide-deck": "tsx scripts/dogfood-slide-deck.ts",
712
+ "dogfood:slide-rich": "tsx scripts/dogfood-slide-rich.ts",
713
+ "dogfood:v4-zero-config": "tsx scripts/dogfood-v4-zero-config.ts",
714
+ "dogfood:v4-real-build": "bash scripts/dogfood-v4-real-build.sh",
715
+ "dogfood:precompiled-utilities": "tsx scripts/dogfood-precompiled-utilities.ts",
716
+ "quality:gates": "pnpm format:check && pnpm lint:ci && pnpm typecheck && pnpm test && pnpm build && pnpm registry:build && pnpm registry:validate && pnpm quality:structure && pnpm quality:bundle && pnpm quality:a11y && pnpm ladle:build && pnpm dogfood:whiteboard && pnpm dogfood:slide && pnpm dogfood:slide-deck && pnpm dogfood:slide-rich && pnpm dogfood:v4-zero-config && pnpm dogfood:precompiled-utilities",
717
+ "quality:gates:fast": "pnpm format:check && pnpm lint:ci && pnpm typecheck && pnpm registry:build && pnpm registry:validate && pnpm quality:structure"
718
+ }
719
+ }
@@ -12,6 +12,12 @@
12
12
  }
13
13
  },
14
14
  "items": [
15
+ {
16
+ "name": "account-menu",
17
+ "type": "registry:ui",
18
+ "title": "AccountMenu",
19
+ "description": "Sidebar header for PaaS surfaces. Avatar + name + (optional) PlanBadge + (optional) secondary line, with dual mode: with onClick renders as a `button` with trailing chevron (account picker affordance); without, renders as a static `div`. PaaS-shape sibling of ProjectSwitcher."
20
+ },
15
21
  {
16
22
  "name": "agent-composer",
17
23
  "type": "registry:ui",
@@ -186,12 +192,24 @@
186
192
  "title": "cn (Tailwind class merger)",
187
193
  "description": "Merge Tailwind classes with conflict resolution."
188
194
  },
195
+ {
196
+ "name": "code-block",
197
+ "type": "registry:ui",
198
+ "title": "CodeBlock",
199
+ "description": "Terminal / code-snippet surface. Renders code inside a <pre> with optional 'terminal' prefix per line ('$ '), optional caption (file name), and optional inline CopyButton in top-right. Copy uses the raw code (without the visual prefix). language prop is forward-compat for future syntax highlighting."
200
+ },
189
201
  {
190
202
  "name": "command-palette",
191
203
  "type": "registry:ui",
192
204
  "title": "CommandPalette",
193
205
  "description": "Cmd+K-style global launcher with arrow-key navigation, fuzzy ranking, and Enter/Escape behavior — built on cmdk + Theo Dialog."
194
206
  },
207
+ {
208
+ "name": "confirm-dialog",
209
+ "type": "registry:ui",
210
+ "title": "ConfirmDialog",
211
+ "description": "Controlled confirmation modal built on Dialog. Auto-focuses Cancel on open (deliberate — NOT the destructive button). Optional intent=destructive switches the confirm button to destructive variant. Optional confirmationPhrase enables typed-confirmation guard (case-sensitive, empty string = no phrase). Async onConfirm shows Loader2 spinner; resolve closes the dialog; reject keeps it open so consumers can surface their own error. Enter in the phrase input triggers confirm when matched."
212
+ },
195
213
  {
196
214
  "name": "context-card",
197
215
  "type": "registry:ui",
@@ -204,6 +222,12 @@
204
222
  "title": "ContextWindowBar",
205
223
  "description": "Shows how much of the model's context window has been"
206
224
  },
225
+ {
226
+ "name": "copy-button",
227
+ "type": "registry:ui",
228
+ "title": "CopyButton",
229
+ "description": "Click-to-copy button primitive. Wraps navigator.clipboard.writeText with icon swap (Copy → Check on success, Copy → X on failure), aria-live announcement for screen readers, optional label, ghost/outline variants, and SSR-safe rendering. Auto-cleans the revert timer on unmount and debounces double-clicks."
230
+ },
207
231
  {
208
232
  "name": "cost-meter",
209
233
  "type": "registry:ui",
@@ -228,6 +252,12 @@
228
252
  "title": "CronJobsList",
229
253
  "description": "Grid of CronJobCards with a sticky \"new job\" action."
230
254
  },
255
+ {
256
+ "name": "danger-zone",
257
+ "type": "registry:ui",
258
+ "title": "DangerZone",
259
+ "description": "Destructive-actions section primitive with sub-component DangerZone.Action. Red-bordered container with title bar (default 'Danger Zone') and action rows. Each row carries title + description + consumer-provided action slot (typically a destructive Button). Rows separated by hairline dividers; last row drops the bottom border via last:border-b-0."
260
+ },
231
261
  {
232
262
  "name": "deployment-row",
233
263
  "type": "registry:block",
@@ -396,6 +426,12 @@
396
426
  "title": "Theo UI permission types",
397
427
  "description": "Shared TypeScript types for permission requests, scopes, and decisions."
398
428
  },
429
+ {
430
+ "name": "plan-badge",
431
+ "type": "registry:ui",
432
+ "title": "PlanBadge",
433
+ "description": "Semantic pricing-tier badge. Five canonical tiers (free, hobby, pro, team, enterprise) with distinct color tokens. Consumers self-document intent (plan=\"hobby\") instead of mapping generic Badge variants per app — future rebrand / dark-mode tweaks propagate automatically."
434
+ },
399
435
  {
400
436
  "name": "preview-env-card",
401
437
  "type": "registry:block",
@@ -414,6 +450,12 @@
414
450
  "title": "ProgressChecklist",
415
451
  "description": "Right-inspector checklist tracking subtask completion with success / running / pending tones."
416
452
  },
453
+ {
454
+ "name": "progress",
455
+ "type": "registry:ui",
456
+ "title": "Progress",
457
+ "description": "Accessible progress bar primitive with intent variants (default, success, warning, destructive), 4 heights (h-1 / h-1.5 / h-2 / h-3), and an indeterminate animated state. Built on role=\"progressbar\" + ARIA semantics."
458
+ },
417
459
  {
418
460
  "name": "project-card",
419
461
  "type": "registry:block",
@@ -588,6 +630,18 @@
588
630
  "title": "SocialAuthRow",
589
631
  "description": "Row of OAuth provider buttons."
590
632
  },
633
+ {
634
+ "name": "stat-tile",
635
+ "type": "registry:ui",
636
+ "title": "StatTile",
637
+ "description": "Big-number stat tile primitive for dashboard summaries. Renders value + label + optional icon + optional delta (trend up/down/flat with TrendingUp/TrendingDown/Minus icons and success/destructive/muted color). Dual mode: with onClick renders as button with hover state + trailing ArrowUpRight chevron; without, renders as static div. Value uses font-display tabular-nums whitespace-nowrap."
638
+ },
639
+ {
640
+ "name": "status-dot",
641
+ "type": "registry:ui",
642
+ "title": "StatusDot",
643
+ "description": "Semantic status indicator (small colored circle + optional label). Five status kinds: live (success), building (warning, auto-pulses), failed (destructive), idle (muted), warning (warning, static). Three sizes (xs 6px / sm 8px default / md 10px). When neither label nor aria-label is provided, auto-applies aria-label=status and emits a dev warning."
644
+ },
591
645
  {
592
646
  "name": "steps-rail",
593
647
  "type": "registry:ui",
@@ -612,6 +666,12 @@
612
666
  "title": "SystemPromptEditor",
613
667
  "description": "Surface the agent's system prompt with a clear"
614
668
  },
669
+ {
670
+ "name": "table",
671
+ "type": "registry:ui",
672
+ "title": "Table",
673
+ "description": "Semantic data-table primitive with sub-components (Table.Header, Table.Body, Table.Row, Table.Cell, Table.HeaderCell). Supports density (default | compact via Context), per-cell align (left | center | right), numeric cells (font-mono tabular-nums), and sortable header cells (onSort + sortDirection with ChevronUp/ChevronDown affordance + aria-sort)."
674
+ },
615
675
  {
616
676
  "name": "tabs",
617
677
  "type": "registry:ui",
@@ -672,6 +732,12 @@
672
732
  "title": "TheoUIProvider",
673
733
  "description": "Primary entry-point provider — composes ThemeProvider + Toaster with sensible defaults. Use as the single root wrapper in consumer apps."
674
734
  },
735
+ {
736
+ "name": "timestamp",
737
+ "type": "registry:ui",
738
+ "title": "Timestamp",
739
+ "description": "Accessible <time> primitive with relative/absolute/both formats, auto-refresh interval (default 60s, 0 disables), native title tooltip with absolute time, and aria-label that always carries the full date. Built on Intl.RelativeTimeFormat (zero deps). value accepts ISO string, Date, or Unix ms (NOT seconds). Invalid date renders empty element; invalid locale falls back to default with dev warning."
740
+ },
675
741
  {
676
742
  "name": "toast",
677
743
  "type": "registry:ui",
@@ -732,6 +798,12 @@
732
798
  "title": "Theo UI shared types",
733
799
  "description": "Shared TypeScript helper types (IconComponent, etc.) used across Theo UI."
734
800
  },
801
+ {
802
+ "name": "usage-meter",
803
+ "type": "registry:ui",
804
+ "title": "UsageMeter",
805
+ "description": "Multi-metric stacked usage card for PaaS dashboards. Renders N metrics (data transfer, requests, build minutes, seats, …) each with label + value/max + Progress bar. Supports custom per-metric formatter, over-quota warning, and a compact bars-only mode. PaaS-shape sibling of CostMeter."
806
+ },
735
807
  {
736
808
  "name": "whiteboard",
737
809
  "type": "registry:ui",
@@ -0,0 +1,24 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "account-menu",
4
+ "type": "registry:ui",
5
+ "title": "AccountMenu",
6
+ "description": "Sidebar header for PaaS surfaces. Avatar + name + (optional) PlanBadge + (optional) secondary line, with dual mode: with onClick renders as a `button` with trailing chevron (account picker affordance); without, renders as a static `div`. PaaS-shape sibling of ProjectSwitcher.",
7
+ "dependencies": [
8
+ "lucide-react"
9
+ ],
10
+ "registryDependencies": [
11
+ "https://usetheodev.github.io/theo-ui/r/cn.json",
12
+ "https://usetheodev.github.io/theo-ui/r/avatar.json",
13
+ "https://usetheodev.github.io/theo-ui/r/plan-badge.json",
14
+ "https://usetheodev.github.io/theo-ui/r/tailwind-preset.json"
15
+ ],
16
+ "files": [
17
+ {
18
+ "path": "components/composites/account-menu/account-menu.tsx",
19
+ "type": "registry:ui",
20
+ "target": "components/ui/account-menu.tsx",
21
+ "content": "import { ChevronsUpDown } from \"lucide-react\";\nimport { forwardRef } from \"react\";\nimport type { ButtonHTMLAttributes, HTMLAttributes, ReactNode } from \"react\";\nimport { cn } from \"@/lib/cn\";\nimport { Avatar } from \"@/components/ui/avatar\";\nimport { PlanBadge, type PlanTier } from \"@/components/ui/plan-badge\";\n\n/**\n * AccountMenu — sidebar header for PaaS surfaces.\n *\n * Renders avatar + name + (optional) plan badge + (optional) secondary line.\n * Dual mode: with `onClick`, renders as a `<button>` with a `ChevronsUpDown`\n * trailing icon (account picker affordance); without, renders as a static\n * `<div>` (read-only display, not focusable).\n *\n * Composition:\n *\n * <Sidebar.Header className=\"p-0\">\n * <AccountMenu\n * name=\"paulohenriquevn\"\n * avatar=\"https://avatars.githubusercontent.com/u/12345\"\n * plan=\"hobby\"\n * onClick={openAccountSwitcher}\n * />\n * </Sidebar.Header>\n *\n * Avatar handling:\n * - URL (`http(s)://` or `/`) → `<Avatar.Image>` with `<Avatar.Fallback>` initials\n * - Short string (≤2 chars) → treated as initials directly\n * - Undefined → initials derived from the first character of `name`\n *\n * PaaS-shape sibling of `<ProjectSwitcher>` (workspace + branch + agent-status).\n * Same dual-mode (interactive vs static) pattern; different semantics.\n */\n\nexport interface AccountMenuProps\n extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, \"type\" | \"children\" | \"name\"> {\n /** Display name (username, email, org name). */\n name: ReactNode;\n /** Avatar URL or 1-2-char initials. If undefined, derives initials from `name`. */\n avatar?: string;\n /** Plan tier — renders inline `<PlanBadge size=\"sm\">`. Omit for none. */\n plan?: PlanTier;\n /** Optional secondary line below name (e.g. email). */\n secondary?: ReactNode;\n /** Make the row interactive (button) with a trailing chevron. */\n onClick?: () => void;\n}\n\nconst URL_RE = /^(?:https?:\\/\\/|\\/)/;\n\nfunction deriveInitials(name: ReactNode, avatar: string | undefined): string {\n if (avatar && !URL_RE.test(avatar) && avatar.length <= 2) {\n return avatar.toUpperCase();\n }\n if (typeof name === \"string\" && name.length > 0) {\n return name.charAt(0).toUpperCase();\n }\n return \"?\";\n}\n\nconst AccountMenu = forwardRef<HTMLElement, AccountMenuProps>(\n ({ className, name, avatar, plan, secondary, onClick, ...props }, ref) => {\n const interactive = typeof onClick === \"function\";\n const initials = deriveInitials(name, avatar);\n const isUrlAvatar = avatar !== undefined && URL_RE.test(avatar);\n const altText = typeof name === \"string\" ? name : \"account\";\n\n const content = (\n <>\n <Avatar size=\"sm\">\n {isUrlAvatar ? <Avatar.Image src={avatar} alt={altText} /> : null}\n <Avatar.Fallback delayMs={0}>{initials}</Avatar.Fallback>\n </Avatar>\n\n <div className=\"flex min-w-0 flex-1 flex-col\">\n <div className=\"flex min-w-0 items-center gap-2\">\n <span className=\"truncate font-medium text-body-sm text-foreground\">{name}</span>\n {plan ? <PlanBadge plan={plan} size=\"sm\" /> : null}\n </div>\n {secondary ? (\n <span className=\"truncate text-label text-muted-foreground\">{secondary}</span>\n ) : null}\n </div>\n\n {interactive ? (\n <ChevronsUpDown className=\"size-3 shrink-0 text-muted-foreground\" aria-hidden=\"true\" />\n ) : null}\n </>\n );\n\n const baseClass = cn(\n \"flex w-full items-center gap-3 px-3 py-2\",\n interactive &&\n cn(\n \"rounded-md text-left transition-colors\",\n \"hover:bg-muted/40\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-card\",\n ),\n className,\n );\n\n if (interactive) {\n const { ...buttonProps } = props as ButtonHTMLAttributes<HTMLButtonElement>;\n return (\n <button\n ref={ref as React.Ref<HTMLButtonElement>}\n type=\"button\"\n className={baseClass}\n onClick={onClick}\n {...buttonProps}\n >\n {content}\n </button>\n );\n }\n\n return (\n <div\n ref={ref as React.Ref<HTMLDivElement>}\n className={baseClass}\n {...(props as HTMLAttributes<HTMLDivElement>)}\n >\n {content}\n </div>\n );\n },\n);\nAccountMenu.displayName = \"AccountMenu\";\n\nexport { AccountMenu };\n"
22
+ }
23
+ ]
24
+ }
@@ -17,7 +17,7 @@
17
17
  "path": "components/primitives/button/button.tsx",
18
18
  "type": "registry:ui",
19
19
  "target": "components/ui/button.tsx",
20
- "content": "import { Slot } from \"@radix-ui/react-slot\";\nimport { type VariantProps, cva } from \"class-variance-authority\";\nimport { forwardRef } from \"react\";\nimport type { ButtonHTMLAttributes } from \"react\";\nimport { cn } from \"@/lib/cn\";\n\n/**\n * Button — primitive action element in the Violet Forge design system.\n *\n * Variants:\n * - primary Theo violet fill, glow on hover (signature)\n * - secondary surface with hairline border\n * - accent burnt-sienna fill, celebratory actions\n * - ghost transparent, hover lifts surface\n * - link text-only, primary color, underline on hover\n * - destructive for irreversible actions\n *\n * Sizes: sm (32px) · md (40px, default) · lg (48px) · icon (square 40px)\n *\n * `asChild` swaps the root for the consumer's element (Radix Slot pattern).\n */\nconst buttonVariants = cva(\n [\n \"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-lg\",\n // NIT-004: `font-medium` (500) aligns with the design-system.md UI weight.\n // Previously `font-bold` (700) exceeded the normative 400/500/600 weight\n // range declared for Geist Sans in the Violet Forge identity.\n \"font-medium font-sans tracking-tight\",\n \"transition-[box-shadow,background-color,color,transform] duration-base ease-out-soft\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background\",\n \"disabled:pointer-events-none disabled:opacity-50\",\n \"[&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0\",\n ],\n {\n variants: {\n variant: {\n primary: [\n \"bg-primary text-primary-foreground\",\n \"hover:bg-primary hover:shadow-glow\",\n \"active:scale-[0.98] active:bg-primary-deep active:shadow-none\",\n ],\n secondary: [\n \"border border-border bg-secondary text-secondary-foreground\",\n \"hover:bg-muted\",\n \"active:scale-[0.98]\",\n ],\n accent: [\"bg-accent text-accent-foreground\", \"hover:bg-accent-deep\", \"active:scale-[0.98]\"],\n ghost: [\n \"bg-transparent text-foreground\",\n \"hover:bg-muted\",\n \"active:scale-[0.98] active:bg-secondary\",\n ],\n link: [\n \"bg-transparent text-primary underline-offset-4\",\n \"hover:text-primary-deep hover:underline\",\n \"h-auto p-0\",\n ],\n destructive: [\n \"bg-destructive text-destructive-foreground\",\n \"hover:bg-destructive/90\",\n \"active:scale-[0.98]\",\n ],\n },\n size: {\n sm: \"h-8 px-3 text-body-sm\",\n // md: tier ajustável via density (CSS var on :root). See D3 ADR of\n // faang-density-tightening plan. Default `comfortable` density makes\n // this 36px (--theo-control-h: 2.25rem). sm and lg stay hardcoded.\n md: \"h-[var(--theo-control-h,2.25rem)] px-[var(--theo-control-px,0.875rem)] text-body-sm\",\n lg: \"h-11 px-4 text-body-md\",\n icon: \"h-[var(--theo-control-h,2.25rem)] w-[var(--theo-control-h,2.25rem)] p-0\",\n },\n },\n defaultVariants: {\n variant: \"primary\",\n size: \"md\",\n },\n },\n);\n\nexport interface ButtonProps\n extends ButtonHTMLAttributes<HTMLButtonElement>,\n VariantProps<typeof buttonVariants> {\n asChild?: boolean;\n}\n\nconst Button = forwardRef<HTMLButtonElement, ButtonProps>(\n ({ className, variant, size, asChild = false, type, ...props }, ref) => {\n const Comp = asChild ? Slot : \"button\";\n return (\n <Comp\n ref={ref}\n type={asChild ? undefined : (type ?? \"button\")}\n className={cn(buttonVariants({ variant, size }), className)}\n {...props}\n />\n );\n },\n);\nButton.displayName = \"Button\";\n\nexport { Button, buttonVariants };\n"
20
+ "content": "import { Slot } from \"@radix-ui/react-slot\";\nimport { type VariantProps, cva } from \"class-variance-authority\";\nimport { forwardRef } from \"react\";\nimport type { ButtonHTMLAttributes } from \"react\";\nimport { cn } from \"@/lib/cn\";\n\n/**\n * Button — primitive action element in the Violet Forge design system.\n *\n * Variants:\n * - primary Theo violet fill, glow on hover (signature)\n * - secondary surface with hairline border\n * - accent burnt-sienna fill, celebratory actions\n * - ghost transparent, hover lifts surface\n * - link text-only, primary color, underline on hover\n * - destructive for irreversible actions\n *\n * Sizes: sm (32px) · md (40px, default) · lg (48px) · icon (square 40px)\n *\n * `asChild` swaps the root for the consumer's element (Radix Slot pattern).\n */\nconst buttonVariants = cva(\n [\n \"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-lg\",\n // NIT-004: `font-medium` (500) aligns with the design-system.md UI weight.\n // Previously `font-bold` (700) exceeded the normative 400/500/600 weight\n // range declared for Geist Sans in the Violet Forge identity.\n \"font-medium font-sans tracking-tight\",\n \"transition-[box-shadow,background-color,color,transform] duration-base ease-out-soft\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background\",\n // Tailwind v4 dropped the `button { cursor: pointer }` preflight rule\n // (https://tailwindcss.com/docs/upgrade-guide#default-button-cursor) so\n // every <button> now shows the default arrow cursor. Restore the\n // \"clickable hand\" explicitly for the Button primitive; the\n // `disabled:pointer-events-none` rule below short-circuits cursor\n // application for disabled state (no events → cursor is moot).\n // `aria-disabled:cursor-default` is a belt-and-suspenders override\n // for paths where pointer-events still flow.\n \"cursor-pointer disabled:cursor-default aria-disabled:cursor-default\",\n \"disabled:pointer-events-none disabled:opacity-50\",\n \"[&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0\",\n ],\n {\n variants: {\n variant: {\n primary: [\n \"bg-primary text-primary-foreground\",\n \"hover:bg-primary hover:shadow-glow\",\n \"active:scale-[0.98] active:bg-primary-deep active:shadow-none\",\n ],\n secondary: [\n \"border border-border bg-secondary text-secondary-foreground\",\n \"hover:bg-muted\",\n \"active:scale-[0.98]\",\n ],\n accent: [\"bg-accent text-accent-foreground\", \"hover:bg-accent-deep\", \"active:scale-[0.98]\"],\n ghost: [\n \"bg-transparent text-foreground\",\n \"hover:bg-muted\",\n \"active:scale-[0.98] active:bg-secondary\",\n ],\n link: [\n \"bg-transparent text-primary underline-offset-4\",\n \"hover:text-primary-deep hover:underline\",\n \"h-auto p-0\",\n ],\n destructive: [\n \"bg-destructive text-destructive-foreground\",\n \"hover:bg-destructive/90\",\n \"active:scale-[0.98]\",\n ],\n },\n size: {\n sm: \"h-8 px-3 text-body-sm\",\n // md: tier ajustável via density (CSS var on :root). See D3 ADR of\n // faang-density-tightening plan. Default `comfortable` density makes\n // this 36px (--theo-control-h: 2.25rem). sm and lg stay hardcoded.\n md: \"h-[var(--theo-control-h,2.25rem)] px-[var(--theo-control-px,0.875rem)] text-body-sm\",\n lg: \"h-11 px-4 text-body-md\",\n icon: \"h-[var(--theo-control-h,2.25rem)] w-[var(--theo-control-h,2.25rem)] p-0\",\n },\n },\n defaultVariants: {\n variant: \"primary\",\n size: \"md\",\n },\n },\n);\n\nexport interface ButtonProps\n extends ButtonHTMLAttributes<HTMLButtonElement>,\n VariantProps<typeof buttonVariants> {\n asChild?: boolean;\n}\n\nconst Button = forwardRef<HTMLButtonElement, ButtonProps>(\n ({ className, variant, size, asChild = false, type, ...props }, ref) => {\n const Comp = asChild ? Slot : \"button\";\n return (\n <Comp\n ref={ref}\n type={asChild ? undefined : (type ?? \"button\")}\n className={cn(buttonVariants({ variant, size }), className)}\n {...props}\n />\n );\n },\n);\nButton.displayName = \"Button\";\n\nexport { Button, buttonVariants };\n"
21
21
  }
22
22
  ]
23
23
  }
@@ -0,0 +1,21 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "code-block",
4
+ "type": "registry:ui",
5
+ "title": "CodeBlock",
6
+ "description": "Terminal / code-snippet surface. Renders code inside a <pre> with optional 'terminal' prefix per line ('$ '), optional caption (file name), and optional inline CopyButton in top-right. Copy uses the raw code (without the visual prefix). language prop is forward-compat for future syntax highlighting.",
7
+ "dependencies": [],
8
+ "registryDependencies": [
9
+ "https://usetheodev.github.io/theo-ui/r/cn.json",
10
+ "https://usetheodev.github.io/theo-ui/r/copy-button.json",
11
+ "https://usetheodev.github.io/theo-ui/r/tailwind-preset.json"
12
+ ],
13
+ "files": [
14
+ {
15
+ "path": "components/composites/code-block/code-block.tsx",
16
+ "type": "registry:ui",
17
+ "target": "components/ui/code-block.tsx",
18
+ "content": "import { forwardRef } from \"react\";\nimport type { HTMLAttributes, ReactNode } from \"react\";\nimport { cn } from \"@/lib/cn\";\nimport { CopyButton } from \"@/components/ui/copy-button\";\n\n/**\n * CodeBlock — terminal command / code snippet surface.\n *\n * Pre-rendered code block with optional terminal \"$ \" prefix per line,\n * optional caption (file name), and optional CopyButton positioned top-right.\n * The CopyButton receives the RAW `code` (without the visual \"$ \" prefix),\n * so consumers paste only the executable command.\n *\n * @example\n * <CodeBlock code=\"theo deploy\" terminal copyable />\n * <CodeBlock code={dotenv} caption=\".env.local\" copyable />\n *\n * `language` is reserved for future syntax highlighting (v1: ignored).\n */\nexport interface CodeBlockProps extends Omit<HTMLAttributes<HTMLDivElement>, \"children\"> {\n /** Code content. Can be multiline. */\n code: string;\n /** Language hint (forward-compat; v1 ignored). */\n language?: string;\n /** When true, prefix each line with \"$ \" for shell commands. */\n terminal?: boolean;\n /** Show inline CopyButton in top-right. */\n copyable?: boolean;\n /** Optional caption above block (e.g. \".env.local\"). */\n caption?: ReactNode;\n}\n\nconst CodeBlock = forwardRef<HTMLDivElement, CodeBlockProps>(\n ({ className, code, language: _language, terminal, copyable, caption, ...props }, ref) => {\n const lines = code.split(/\\r?\\n/);\n\n return (\n <div\n ref={ref}\n className={cn(\n \"relative rounded-lg border border-border/40 bg-muted/40 font-mono text-body-sm\",\n className,\n )}\n {...props}\n >\n {caption !== undefined ? (\n <div className=\"border-border/40 border-b px-3 py-1.5 font-sans text-label text-muted-foreground\">\n {caption}\n </div>\n ) : null}\n {copyable ? (\n <CopyButton value={code} aria-label=\"Copy code\" className=\"absolute top-2 right-2\" />\n ) : null}\n <pre className=\"overflow-x-auto p-3 text-foreground\">\n {terminal ? (\n <code>\n {lines.map((line, i) => (\n // biome-ignore lint/suspicious/noArrayIndexKey: code lines are positional; reorder requires consumer recompute.\n <span key={i} className=\"block whitespace-pre\">\n <span className=\"select-none text-muted-foreground\">$ </span>\n {line}\n </span>\n ))}\n </code>\n ) : (\n <code>{code}</code>\n )}\n </pre>\n </div>\n );\n },\n);\nCodeBlock.displayName = \"CodeBlock\";\n\nexport { CodeBlock };\n"
19
+ }
20
+ ]
21
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "confirm-dialog",
4
+ "type": "registry:ui",
5
+ "title": "ConfirmDialog",
6
+ "description": "Controlled confirmation modal built on Dialog. Auto-focuses Cancel on open (deliberate — NOT the destructive button). Optional intent=destructive switches the confirm button to destructive variant. Optional confirmationPhrase enables typed-confirmation guard (case-sensitive, empty string = no phrase). Async onConfirm shows Loader2 spinner; resolve closes the dialog; reject keeps it open so consumers can surface their own error. Enter in the phrase input triggers confirm when matched.",
7
+ "dependencies": [
8
+ "lucide-react"
9
+ ],
10
+ "registryDependencies": [
11
+ "https://usetheodev.github.io/theo-ui/r/cn.json",
12
+ "https://usetheodev.github.io/theo-ui/r/dialog.json",
13
+ "https://usetheodev.github.io/theo-ui/r/button.json",
14
+ "https://usetheodev.github.io/theo-ui/r/input.json",
15
+ "https://usetheodev.github.io/theo-ui/r/tailwind-preset.json"
16
+ ],
17
+ "files": [
18
+ {
19
+ "path": "components/composites/confirm-dialog/confirm-dialog.tsx",
20
+ "type": "registry:ui",
21
+ "target": "components/ui/confirm-dialog.tsx",
22
+ "content": "import { Loader2 } from \"lucide-react\";\nimport { forwardRef, useEffect, useRef, useState } from \"react\";\nimport type { KeyboardEvent, ReactNode } from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Dialog } from \"@/components/ui/dialog\";\nimport { Input } from \"@/components/ui/input\";\n\n/**\n * ConfirmDialog — controlled confirmation modal built on `Dialog`.\n *\n * Focuses Cancel on open (deliberate — NOT the destructive button).\n * `intent=\"destructive\"` styles the confirm button with the destructive\n * variant. `confirmationPhrase` enables typed-confirmation guard:\n * the confirm button is disabled until the input value matches the\n * phrase exactly (case-sensitive). An empty string phrase is treated\n * as \"no phrase required\" (`!!confirmationPhrase`). Pressing Enter in\n * the input triggers confirm when `canConfirm` is true.\n *\n * `onConfirm` can be async. While the returned promise is pending,\n * both buttons are disabled and a `Loader2` spinner appears. On\n * resolve, the dialog closes via `onOpenChange(false)`. On reject,\n * the dialog stays open so the consumer can show their own error.\n *\n * @example\n * <ConfirmDialog\n * open={open} onOpenChange={setOpen}\n * title=\"Delete project\"\n * description=\"This cannot be undone.\"\n * intent=\"destructive\"\n * confirmationPhrase=\"my-project\"\n * onConfirm={async () => api.deleteProject(id)}\n * />\n */\nexport interface ConfirmDialogProps {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n title: ReactNode;\n description: ReactNode;\n confirmLabel?: ReactNode;\n cancelLabel?: ReactNode;\n intent?: \"default\" | \"destructive\";\n confirmationPhrase?: string;\n onConfirm: () => void | Promise<void>;\n loading?: boolean;\n}\n\nconst ConfirmDialog = forwardRef<HTMLDivElement, ConfirmDialogProps>(\n (\n {\n open,\n onOpenChange,\n title,\n description,\n confirmLabel = \"Confirm\",\n cancelLabel = \"Cancel\",\n intent = \"default\",\n confirmationPhrase,\n onConfirm,\n loading: externalLoading,\n },\n ref,\n ) => {\n const [phraseInput, setPhraseInput] = useState(\"\");\n const [internalLoading, setInternalLoading] = useState(false);\n const cancelRef = useRef<HTMLButtonElement | null>(null);\n\n const phraseRequired = !!confirmationPhrase;\n const phraseMatched = phraseRequired ? phraseInput === confirmationPhrase : true;\n const showLoading = externalLoading === true || internalLoading;\n const canConfirm = phraseMatched && !showLoading;\n\n // Reset phrase input whenever the dialog closes.\n useEffect(() => {\n if (!open) setPhraseInput(\"\");\n }, [open]);\n\n // Auto-focus Cancel on open (NOT confirm — destructive safety).\n useEffect(() => {\n if (open) {\n const id = window.setTimeout(() => cancelRef.current?.focus(), 0);\n return () => window.clearTimeout(id);\n }\n }, [open]);\n\n async function handleConfirm() {\n if (!canConfirm) return;\n setInternalLoading(true);\n try {\n await onConfirm();\n onOpenChange(false);\n } catch {\n // Stay open; consumer surfaces error.\n } finally {\n setInternalLoading(false);\n }\n }\n\n function handleInputKeyDown(e: KeyboardEvent<HTMLInputElement>) {\n if (e.key === \"Enter\" && canConfirm) {\n e.preventDefault();\n void handleConfirm();\n }\n }\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <Dialog.Content ref={ref}>\n <Dialog.Header>\n <Dialog.Title>{title}</Dialog.Title>\n <Dialog.Description>{description}</Dialog.Description>\n </Dialog.Header>\n {phraseRequired ? (\n <Dialog.Body>\n <p className=\"mb-2 text-body-sm text-muted-foreground\">\n Type{\" \"}\n <code className=\"rounded bg-muted px-1 py-0.5 font-mono text-foreground\">\n {confirmationPhrase}\n </code>{\" \"}\n to confirm\n </p>\n <Input\n value={phraseInput}\n onChange={(e) => setPhraseInput(e.target.value)}\n onKeyDown={handleInputKeyDown}\n autoComplete=\"off\"\n aria-label=\"Confirmation phrase\"\n />\n </Dialog.Body>\n ) : null}\n <Dialog.Footer>\n <Button\n ref={cancelRef}\n variant=\"secondary\"\n onClick={() => onOpenChange(false)}\n disabled={showLoading}\n >\n {cancelLabel}\n </Button>\n <Button\n variant={intent === \"destructive\" ? \"destructive\" : \"primary\"}\n onClick={() => void handleConfirm()}\n disabled={!canConfirm}\n data-confirm\n >\n {showLoading ? <Loader2 aria-hidden=\"true\" className=\"size-4 animate-spin\" /> : null}\n {confirmLabel}\n </Button>\n </Dialog.Footer>\n </Dialog.Content>\n </Dialog>\n );\n },\n);\nConfirmDialog.displayName = \"ConfirmDialog\";\n\nexport { ConfirmDialog };\n"
23
+ }
24
+ ]
25
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "copy-button",
4
+ "type": "registry:ui",
5
+ "title": "CopyButton",
6
+ "description": "Click-to-copy button primitive. Wraps navigator.clipboard.writeText with icon swap (Copy → Check on success, Copy → X on failure), aria-live announcement for screen readers, optional label, ghost/outline variants, and SSR-safe rendering. Auto-cleans the revert timer on unmount and debounces double-clicks.",
7
+ "dependencies": [
8
+ "lucide-react"
9
+ ],
10
+ "registryDependencies": [
11
+ "https://usetheodev.github.io/theo-ui/r/cn.json",
12
+ "https://usetheodev.github.io/theo-ui/r/tailwind-preset.json"
13
+ ],
14
+ "files": [
15
+ {
16
+ "path": "components/primitives/copy-button/copy-button.tsx",
17
+ "type": "registry:ui",
18
+ "target": "components/ui/copy-button.tsx",
19
+ "content": "import { Check, Copy, X } from \"lucide-react\";\nimport { forwardRef, useCallback, useEffect, useRef, useState } from \"react\";\nimport type { ButtonHTMLAttributes, ReactNode } from \"react\";\nimport { cn } from \"@/lib/cn\";\n\n/**\n * CopyButton — click-to-copy primitive for PaaS surfaces.\n *\n * Wraps the Clipboard API behind a button that:\n * - Calls `navigator.clipboard.writeText(value)` on click\n * - Swaps the icon (Copy → Check on success, Copy → X on failure)\n * - Optionally swaps the visible `label` to \"Copied!\" / \"Failed\"\n * - Announces the state change via an `aria-live=\"polite\"` sr-only region\n * - Reverts to idle after `feedbackDuration` ms (default 1500)\n *\n * SSR-safe (guards `navigator?.clipboard?.writeText`). Debounces double-clicks\n * by ignoring clicks while not in the `idle` state. Cleans up the revert timer\n * on unmount so no `setState` happens on unmounted components.\n *\n * @example\n * <CopyButton value={envVar.value} /> // icon-only ghost\n * <CopyButton value={token} label=\"Copy token\" variant=\"outline\" />\n */\ntype CopyState = \"idle\" | \"copied\" | \"failed\";\n\nexport interface CopyButtonProps\n extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, \"type\" | \"onClick\" | \"children\"> {\n /** String to copy when clicked. */\n value: string;\n /** Optional button label. Default: just the icon. */\n label?: ReactNode;\n /** Visual style. */\n variant?: \"ghost\" | \"outline\";\n /** Size. */\n size?: \"sm\" | \"md\";\n /** Callback after successful copy (e.g. analytics). */\n onCopied?: (value: string) => void;\n /** Duration of the feedback state in ms. Default 1500. */\n feedbackDuration?: number;\n}\n\nconst VARIANT: Record<NonNullable<CopyButtonProps[\"variant\"]>, string> = {\n ghost: \"hover:bg-muted\",\n outline: \"border border-border/60 rounded-md\",\n};\n\nconst SIZE: Record<NonNullable<CopyButtonProps[\"size\"]>, string> = {\n sm: \"px-2 py-1 text-label\",\n md: \"px-2.5 py-1.5 text-body-sm\",\n};\n\nconst CopyButton = forwardRef<HTMLButtonElement, CopyButtonProps>(\n (\n {\n className,\n value,\n label,\n variant = \"ghost\",\n size = \"sm\",\n onCopied,\n feedbackDuration = 1500,\n ...props\n },\n ref,\n ) => {\n const [state, setState] = useState<CopyState>(\"idle\");\n const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n useEffect(() => {\n return () => {\n if (timerRef.current !== null) {\n clearTimeout(timerRef.current);\n }\n };\n }, []);\n\n const scheduleRevert = useCallback(() => {\n if (timerRef.current !== null) {\n clearTimeout(timerRef.current);\n }\n timerRef.current = setTimeout(() => {\n setState(\"idle\");\n timerRef.current = null;\n }, feedbackDuration);\n }, [feedbackDuration]);\n\n const handleClick = useCallback(() => {\n if (state !== \"idle\") return;\n\n if (typeof navigator === \"undefined\" || !navigator.clipboard?.writeText) {\n setState(\"failed\");\n scheduleRevert();\n return;\n }\n\n navigator.clipboard.writeText(value).then(\n () => {\n setState(\"copied\");\n onCopied?.(value);\n scheduleRevert();\n },\n () => {\n setState(\"failed\");\n scheduleRevert();\n },\n );\n }, [state, value, onCopied, scheduleRevert]);\n\n const Icon = state === \"copied\" ? Check : state === \"failed\" ? X : Copy;\n const liveMessage =\n state === \"copied\" ? \"Copied to clipboard\" : state === \"failed\" ? \"Copy failed\" : \"\";\n\n const labelText =\n label !== undefined\n ? state === \"copied\"\n ? \"Copied!\"\n : state === \"failed\"\n ? \"Failed\"\n : label\n : null;\n\n return (\n <button\n ref={ref}\n type=\"button\"\n onClick={handleClick}\n data-state={state}\n className={cn(\n \"inline-flex items-center gap-1.5\",\n \"font-sans transition-colors\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-card\",\n VARIANT[variant],\n SIZE[size],\n className,\n )}\n {...props}\n >\n <Icon\n aria-hidden=\"true\"\n className={cn(\n \"size-3.5 shrink-0 transition-opacity duration-200\",\n state === \"copied\" && \"text-success\",\n state === \"failed\" && \"text-destructive\",\n )}\n />\n {labelText !== null ? <span>{labelText}</span> : null}\n <span className=\"sr-only\" aria-live=\"polite\">\n {liveMessage}\n </span>\n </button>\n );\n },\n);\nCopyButton.displayName = \"CopyButton\";\n\nexport { CopyButton };\n"
20
+ }
21
+ ]
22
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "danger-zone",
4
+ "type": "registry:ui",
5
+ "title": "DangerZone",
6
+ "description": "Destructive-actions section primitive with sub-component DangerZone.Action. Red-bordered container with title bar (default 'Danger Zone') and action rows. Each row carries title + description + consumer-provided action slot (typically a destructive Button). Rows separated by hairline dividers; last row drops the bottom border via last:border-b-0.",
7
+ "dependencies": [],
8
+ "registryDependencies": [
9
+ "https://usetheodev.github.io/theo-ui/r/cn.json",
10
+ "https://usetheodev.github.io/theo-ui/r/tailwind-preset.json"
11
+ ],
12
+ "files": [
13
+ {
14
+ "path": "components/primitives/danger-zone/danger-zone.tsx",
15
+ "type": "registry:ui",
16
+ "target": "components/ui/danger-zone.tsx",
17
+ "content": "import { forwardRef } from \"react\";\nimport type { HTMLAttributes, ReactNode } from \"react\";\nimport { cn } from \"@/lib/cn\";\n\n/**\n * DangerZone — destructive-actions section primitive.\n *\n * Red-bordered container with a title bar and `DangerZone.Action` rows.\n * Each Action is laid out as title + description on the left, with a\n * consumer-provided action slot (typically a destructive Button) on\n * the right. Rows are separated by hairline dividers; the last row\n * has no bottom border via `last:border-b-0`.\n *\n * The consumer supplies the destructive button — this primitive never\n * imports `<Button>`, keeping it free of internal `@usetheo/ui` deps\n * (true primitive).\n *\n * @example\n * <DangerZone>\n * <DangerZone.Action\n * title=\"Delete project\"\n * description=\"Permanently delete this project.\"\n * action={<Button variant=\"destructive\">Delete</Button>}\n * />\n * </DangerZone>\n */\nexport interface DangerZoneProps extends Omit<HTMLAttributes<HTMLElement>, \"title\"> {\n /** Section title. Default \"Danger Zone\". */\n title?: ReactNode;\n}\n\nconst Root = forwardRef<HTMLElement, DangerZoneProps>(\n ({ className, title = \"Danger Zone\", children, ...props }, ref) => (\n <section\n ref={ref}\n aria-label={typeof title === \"string\" ? title : \"Danger Zone\"}\n className={cn(\"rounded-xl border border-destructive/30 bg-destructive/[0.02]\", className)}\n {...props}\n >\n <div className=\"border-destructive/20 border-b px-5 py-3 font-sans text-destructive text-label-caps uppercase tracking-wider\">\n {title}\n </div>\n {children}\n </section>\n ),\n);\nRoot.displayName = \"DangerZone\";\n\nexport interface DangerZoneActionProps extends Omit<HTMLAttributes<HTMLDivElement>, \"title\"> {\n title: ReactNode;\n description: ReactNode;\n /** Consumer-provided destructive button (or any ReactNode). */\n action: ReactNode;\n}\n\nconst Action = forwardRef<HTMLDivElement, DangerZoneActionProps>(\n ({ className, title, description, action, ...props }, ref) => (\n <div\n ref={ref}\n className={cn(\n \"flex items-center justify-between gap-4 border-destructive/10 border-b px-5 py-4 last:border-b-0\",\n className,\n )}\n {...props}\n >\n <div className=\"flex flex-col\">\n <span className=\"font-medium font-sans text-body-sm text-foreground\">{title}</span>\n <span className=\"mt-0.5 font-sans text-label text-muted-foreground\">{description}</span>\n </div>\n <div className=\"shrink-0\">{action}</div>\n </div>\n ),\n);\nAction.displayName = \"DangerZone.Action\";\n\ntype DangerZoneRoot = typeof Root & { Action: typeof Action };\nconst DangerZone: DangerZoneRoot = Object.assign(Root, { Action });\n\nexport { DangerZone };\n"
18
+ }
19
+ ]
20
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "plan-badge",
4
+ "type": "registry:ui",
5
+ "title": "PlanBadge",
6
+ "description": "Semantic pricing-tier badge. Five canonical tiers (free, hobby, pro, team, enterprise) with distinct color tokens. Consumers self-document intent (plan=\"hobby\") instead of mapping generic Badge variants per app — future rebrand / dark-mode tweaks propagate automatically.",
7
+ "dependencies": [],
8
+ "registryDependencies": [
9
+ "https://usetheodev.github.io/theo-ui/r/cn.json",
10
+ "https://usetheodev.github.io/theo-ui/r/tailwind-preset.json"
11
+ ],
12
+ "files": [
13
+ {
14
+ "path": "components/primitives/plan-badge/plan-badge.tsx",
15
+ "type": "registry:ui",
16
+ "target": "components/ui/plan-badge.tsx",
17
+ "content": "import { forwardRef } from \"react\";\nimport type { HTMLAttributes } from \"react\";\nimport { cn } from \"@/lib/cn\";\n\n/**\n * PlanBadge — semantic pricing-tier badge.\n *\n * Five canonical tiers (`free` / `hobby` / `pro` / `team` / `enterprise`) with\n * distinct color tokens. Consumers self-document intent with `plan=\"hobby\"`\n * instead of mapping a generic `<Badge variant=\"outline\">` to colors per app.\n * Future rebrand / dark-mode tweaks propagate automatically — no consumer\n * code change.\n *\n * Visual spec (per `theo/docs/handoff/2026-05-23-theo-ui-cloud-dashboard-gaps-brief.md`):\n *\n * | tier | bg | border | text |\n * |--------------|---------------------|--------------------------|-----------------------|\n * | free | bg-muted/40 | border-muted-foreground/20 | text-muted-foreground |\n * | hobby | bg-warning/10 | border-warning/30 | text-warning |\n * | pro | bg-primary/10 | border-primary/30 | text-primary |\n * | team | bg-success/10 | border-success/30 | text-success |\n * | enterprise | bg-foreground/5 | border-foreground/20 | text-foreground |\n *\n * Default label capitalizes the tier (`hobby → \"Hobby\"`, `enterprise → \"Enterprise\"`).\n *\n * Used by `<AccountMenu>` inline with the user name; usable standalone.\n */\n\nexport type PlanTier = \"free\" | \"hobby\" | \"pro\" | \"team\" | \"enterprise\";\n\nexport interface PlanBadgeProps extends HTMLAttributes<HTMLSpanElement> {\n /** Plan tier identifier. */\n plan: PlanTier;\n /** Override the display label. Defaults to the capitalized tier name. */\n label?: string;\n /** Size variant. */\n size?: \"sm\" | \"md\";\n}\n\nconst TIER_CLASS: Record<PlanTier, string> = {\n free: \"bg-muted/40 border-muted-foreground/20 text-muted-foreground\",\n hobby: \"bg-warning/10 border-warning/30 text-warning\",\n pro: \"bg-primary/10 border-primary/30 text-primary\",\n team: \"bg-success/10 border-success/30 text-success\",\n enterprise: \"bg-foreground/5 border-foreground/20 text-foreground\",\n};\n\nconst SIZE_CLASS = {\n sm: \"px-1.5 py-0 text-label-caps\",\n md: \"px-2 py-0.5 text-label\",\n} as const;\n\nfunction defaultLabel(plan: PlanTier): string {\n return plan.charAt(0).toUpperCase() + plan.slice(1);\n}\n\nconst PlanBadge = forwardRef<HTMLSpanElement, PlanBadgeProps>(\n ({ className, plan, label, size = \"md\", ...props }, ref) => {\n // Runtime fallback for unknown tier (TypeScript prevents this at compile\n // time; the guard handles consumers casting an arbitrary string).\n const tierClass = TIER_CLASS[plan] ?? TIER_CLASS.free;\n const displayLabel = label ?? defaultLabel(plan);\n return (\n <span\n ref={ref}\n className={cn(\n \"inline-flex items-center rounded-md border\",\n \"font-mono uppercase tabular-nums tracking-wider\",\n tierClass,\n SIZE_CLASS[size],\n className,\n )}\n data-plan={plan}\n {...props}\n >\n {displayLabel}\n </span>\n );\n },\n);\nPlanBadge.displayName = \"PlanBadge\";\n\nexport { PlanBadge };\n"
18
+ }
19
+ ]
20
+ }