@intelligo-dev/cli 1.0.0-beta.1 → 1.0.0-beta.14

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.
Files changed (223) hide show
  1. package/NOTICE +6 -0
  2. package/README.md +102 -0
  3. package/dist/bin.js +122 -37
  4. package/dist/bin.js.map +1 -1
  5. package/dist/commands/add.d.ts +26 -1
  6. package/dist/commands/add.d.ts.map +1 -1
  7. package/dist/commands/add.js +70 -4
  8. package/dist/commands/add.js.map +1 -1
  9. package/dist/commands/create-flow.d.ts +37 -0
  10. package/dist/commands/create-flow.d.ts.map +1 -0
  11. package/dist/commands/create-flow.js +212 -0
  12. package/dist/commands/create-flow.js.map +1 -0
  13. package/dist/commands/create.d.ts +27 -6
  14. package/dist/commands/create.d.ts.map +1 -1
  15. package/dist/commands/create.js +53 -13
  16. package/dist/commands/create.js.map +1 -1
  17. package/dist/commands/doctor.d.ts +14 -3
  18. package/dist/commands/doctor.d.ts.map +1 -1
  19. package/dist/commands/doctor.js +295 -37
  20. package/dist/commands/doctor.js.map +1 -1
  21. package/dist/commands/migrate-check.d.ts +73 -1
  22. package/dist/commands/migrate-check.d.ts.map +1 -1
  23. package/dist/commands/migrate-check.js +130 -5
  24. package/dist/commands/migrate-check.js.map +1 -1
  25. package/dist/commands/migrate.d.ts +97 -0
  26. package/dist/commands/migrate.d.ts.map +1 -0
  27. package/dist/commands/migrate.js +141 -0
  28. package/dist/commands/migrate.js.map +1 -0
  29. package/dist/commands/sync-messages.d.ts +37 -0
  30. package/dist/commands/sync-messages.d.ts.map +1 -0
  31. package/dist/commands/sync-messages.js +88 -0
  32. package/dist/commands/sync-messages.js.map +1 -0
  33. package/dist/commands/sync-scaffold.d.ts +36 -0
  34. package/dist/commands/sync-scaffold.d.ts.map +1 -0
  35. package/dist/commands/sync-scaffold.js +53 -0
  36. package/dist/commands/sync-scaffold.js.map +1 -0
  37. package/dist/commands/sync.d.ts +103 -0
  38. package/dist/commands/sync.d.ts.map +1 -0
  39. package/dist/commands/sync.js +361 -0
  40. package/dist/commands/sync.js.map +1 -0
  41. package/dist/commands/upgrade-check.d.ts +5 -5
  42. package/dist/commands/upgrade-check.d.ts.map +1 -1
  43. package/dist/commands/upgrade-check.js +22 -6
  44. package/dist/commands/upgrade-check.js.map +1 -1
  45. package/dist/env-files.d.ts +12 -0
  46. package/dist/env-files.d.ts.map +1 -0
  47. package/dist/env-files.js +29 -0
  48. package/dist/env-files.js.map +1 -0
  49. package/dist/index.d.ts +9 -0
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +9 -0
  52. package/dist/index.js.map +1 -1
  53. package/dist/manifest.d.ts +36 -15
  54. package/dist/manifest.d.ts.map +1 -1
  55. package/dist/manifest.js +32 -16
  56. package/dist/manifest.js.map +1 -1
  57. package/dist/migrations.d.ts +3 -6
  58. package/dist/migrations.d.ts.map +1 -1
  59. package/dist/migrations.js +3 -6
  60. package/dist/migrations.js.map +1 -1
  61. package/dist/model-catalogue.d.ts +14 -0
  62. package/dist/model-catalogue.d.ts.map +1 -0
  63. package/dist/model-catalogue.js +34 -0
  64. package/dist/model-catalogue.js.map +1 -0
  65. package/dist/module-exports.d.ts +63 -0
  66. package/dist/module-exports.d.ts.map +1 -0
  67. package/dist/module-exports.js +231 -0
  68. package/dist/module-exports.js.map +1 -0
  69. package/dist/registry-bundle.d.ts +43 -0
  70. package/dist/registry-bundle.d.ts.map +1 -0
  71. package/dist/registry-bundle.js +74 -0
  72. package/dist/registry-bundle.js.map +1 -0
  73. package/dist/registry-items.d.ts +84 -0
  74. package/dist/registry-items.d.ts.map +1 -0
  75. package/dist/registry-items.js +187 -0
  76. package/dist/registry-items.js.map +1 -0
  77. package/package.json +36 -10
  78. package/src/bin.ts +316 -0
  79. package/src/commands/add.ts +255 -0
  80. package/src/commands/create-flow.ts +284 -0
  81. package/src/commands/create.ts +149 -0
  82. package/src/commands/doctor.ts +487 -0
  83. package/src/commands/migrate-check.ts +298 -0
  84. package/src/commands/migrate.ts +228 -0
  85. package/src/commands/sync-messages.ts +114 -0
  86. package/src/commands/sync-scaffold.ts +83 -0
  87. package/src/commands/sync.ts +534 -0
  88. package/src/commands/upgrade-check.ts +160 -0
  89. package/src/env-files.ts +31 -0
  90. package/src/index.ts +39 -0
  91. package/src/manifest.ts +147 -0
  92. package/src/migrations-dir.ts +25 -0
  93. package/src/migrations.ts +133 -0
  94. package/src/model-catalogue.ts +35 -0
  95. package/src/module-exports.ts +266 -0
  96. package/src/registry-bundle.ts +103 -0
  97. package/src/registry-items.ts +237 -0
  98. package/templates/admin-page/admin-page.tsx.tpl +2 -3
  99. package/templates/app-scaffold/assistant-route.ts.tpl +2 -2
  100. package/templates/app-scaffold/auth-route.ts.tpl +10 -0
  101. package/templates/app-scaffold/components.json.tpl +6 -2
  102. package/templates/app-scaffold/db-schema.ts.tpl +29 -0
  103. package/templates/app-scaffold/drizzle-journal.json.tpl +5 -0
  104. package/templates/app-scaffold/drizzle.config.ts.tpl +45 -0
  105. package/templates/app-scaffold/env.example.tpl +65 -1
  106. package/templates/app-scaffold/globals.css.tpl +172 -281
  107. package/templates/app-scaffold/i18n-request.ts.tpl +25 -0
  108. package/templates/app-scaffold/instrumentation.ts.tpl +21 -0
  109. package/templates/app-scaffold/intelligo.ts.tpl +95 -41
  110. package/templates/app-scaffold/layout.tsx.tpl +18 -1
  111. package/templates/app-scaffold/lib-utils.ts.tpl +1 -6
  112. package/templates/app-scaffold/next.config.mjs.tpl +2 -0
  113. package/templates/app-scaffold/package.json.tpl +31 -17
  114. package/templates/app-scaffold/page.tsx.tpl +16 -0
  115. package/templates/app-scaffold/plans.ts.tpl +32 -13
  116. package/templates/app-scaffold/{middleware.ts.tpl → proxy.ts.tpl} +6 -0
  117. package/templates/app-scaffold/sonner.tsx.tpl +42 -20
  118. package/templates/app-scaffold/stripe-webhook-route.ts.tpl +24 -0
  119. package/templates/app-scaffold/theme-provider.tsx.tpl +6 -4
  120. package/templates/app-scaffold/tsconfig.json.tpl +20 -4
  121. package/templates/app-scaffold/use-mobile.ts.tpl +19 -0
  122. package/templates/app-scaffold/workspace-bootstrap.ts.tpl +30 -0
  123. package/templates/maintenance/maintenance-route.ts.tpl +119 -0
  124. package/templates/manifest.json +75 -17
  125. package/templates/registry/ai-agent-activity.json +23 -0
  126. package/templates/registry/ai-agent-progress.json +21 -0
  127. package/templates/registry/ai-approval-card.json +27 -0
  128. package/templates/registry/ai-artifact.json +22 -0
  129. package/templates/registry/ai-branch.json +21 -0
  130. package/templates/registry/ai-citations.json +24 -0
  131. package/templates/registry/ai-code-block.json +23 -0
  132. package/templates/registry/ai-composer-menu.json +19 -0
  133. package/templates/registry/ai-file-diff.json +25 -0
  134. package/templates/registry/ai-image-generation.json +23 -0
  135. package/templates/registry/ai-markdown.json +23 -0
  136. package/templates/registry/ai-message-bubble.json +23 -0
  137. package/templates/registry/ai-message-scroller.json +22 -0
  138. package/templates/registry/ai-message.json +21 -0
  139. package/templates/registry/ai-motion.json +19 -0
  140. package/templates/registry/ai-prompt-input.json +28 -0
  141. package/templates/registry/ai-reasoning-text.json +22 -0
  142. package/templates/registry/ai-reasoning.json +22 -0
  143. package/templates/registry/ai-shimmer-text.json +19 -0
  144. package/templates/registry/ai-sidebar.json +25 -0
  145. package/templates/registry/ai-speech-input.json +21 -0
  146. package/templates/registry/ai-streaming-response.json +24 -0
  147. package/templates/registry/ai-suggestion.json +19 -0
  148. package/templates/registry/ai-todo-list.json +22 -0
  149. package/templates/registry/ai-tool-approval.json +26 -0
  150. package/templates/registry/ai-tool-result.json +25 -0
  151. package/templates/registry/alert-dialog.json +23 -0
  152. package/templates/registry/animated-list.json +21 -0
  153. package/templates/registry/app-shell.json +93 -0
  154. package/templates/registry/artifacts.json +82 -0
  155. package/templates/registry/attachment.json +22 -0
  156. package/templates/registry/auth-email-verification.json +39 -0
  157. package/templates/registry/auth-login.json +72 -0
  158. package/templates/registry/auth-password-reset.json +53 -0
  159. package/templates/registry/auth-signup.json +41 -0
  160. package/templates/registry/billing-settings.json +60 -0
  161. package/templates/registry/button.json +22 -0
  162. package/templates/registry/chat-eve.json +19 -0
  163. package/templates/registry/chat-panel.json +30 -0
  164. package/templates/registry/chat-share.json +42 -0
  165. package/templates/registry/chat-widget.json +34 -0
  166. package/templates/registry/chat.json +326 -0
  167. package/templates/registry/checkbox.json +22 -0
  168. package/templates/registry/checkout.json +40 -0
  169. package/templates/registry/collapsible.json +19 -0
  170. package/templates/registry/command.json +23 -0
  171. package/templates/registry/copy-button.json +21 -0
  172. package/templates/registry/dashboard.json +69 -0
  173. package/templates/registry/dialog.json +24 -0
  174. package/templates/registry/document-viewer.json +22 -0
  175. package/templates/registry/dropdown-menu.json +23 -0
  176. package/templates/registry/expandable-tabs.json +22 -0
  177. package/templates/registry/feature-gating.json +73 -0
  178. package/templates/registry/file-upload.json +24 -0
  179. package/templates/registry/hold-action-button.json +22 -0
  180. package/templates/registry/input-group.json +23 -0
  181. package/templates/registry/input.json +19 -0
  182. package/templates/registry/intelligo.json +187 -0
  183. package/templates/registry/invitation-accept.json +64 -0
  184. package/templates/registry/language-switcher.json +29 -0
  185. package/templates/registry/morphing-modal.json +24 -0
  186. package/templates/registry/notification-stack.json +24 -0
  187. package/templates/registry/notifications.json +87 -0
  188. package/templates/registry/onboarding.json +83 -0
  189. package/templates/registry/otp-input.json +22 -0
  190. package/templates/registry/page-header.json +15 -0
  191. package/templates/registry/payment-poll.json +52 -0
  192. package/templates/registry/popover-morph.json +22 -0
  193. package/templates/registry/popover.json +22 -0
  194. package/templates/registry/pricing.json +111 -0
  195. package/templates/registry/privacy-settings.json +65 -0
  196. package/templates/registry/profile-settings.json +68 -0
  197. package/templates/registry/progress.json +19 -0
  198. package/templates/registry/radio-group.json +22 -0
  199. package/templates/registry/registry.json +2945 -0
  200. package/templates/registry/route-error.json +65 -0
  201. package/templates/registry/select-morph.json +23 -0
  202. package/templates/registry/select.json +23 -0
  203. package/templates/registry/settings-shell.json +47 -0
  204. package/templates/registry/sheet.json +24 -0
  205. package/templates/registry/sidebar.json +28 -0
  206. package/templates/registry/smoke.json +34 -0
  207. package/templates/registry/spinner.json +16 -0
  208. package/templates/registry/stat-card.json +18 -0
  209. package/templates/registry/status-badge.json +18 -0
  210. package/templates/registry/switch.json +20 -0
  211. package/templates/registry/tabs.json +23 -0
  212. package/templates/registry/team-settings.json +97 -0
  213. package/templates/registry/textarea.json +16 -0
  214. package/templates/registry/tooltip.json +22 -0
  215. package/templates/registry/trial-banner.json +43 -0
  216. package/templates/registry/usage.json +84 -0
  217. package/templates/registry/workspace-settings.json +71 -0
  218. package/templates/registry-items.json +116 -0
  219. package/templates/registry-requires.json +180 -0
  220. package/templates/usage-page/usage-page.tsx.tpl +23 -8
  221. package/templates/vitest/server-only.ts.tpl +7 -0
  222. package/templates/vitest/vitest.config.ts.tpl +37 -0
  223. package/templates/billing-page/billing-page.tsx.tpl +0 -72
@@ -0,0 +1,25 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "ai-tool-result",
4
+ "title": "AI Tool Result",
5
+ "description": "The result of a tool the agent ran: a summary line that opens onto highlighted output, status rolling from running to done, copy and run-again actions.",
6
+ "dependencies": [
7
+ "motion",
8
+ "lucide-react"
9
+ ],
10
+ "registryDependencies": [
11
+ "@intelligo/ai-code-block",
12
+ "@intelligo/ai-motion",
13
+ "@intelligo/button",
14
+ "@intelligo/spinner"
15
+ ],
16
+ "files": [
17
+ {
18
+ "path": "base/ui/ai-tool-result/ai-tool-result.tsx",
19
+ "content": "\"use client\";\n\n/*\n * The result of a tool the agent ran — a one-line summary that opens onto\n * the output, with the status rolling from running to done, a copy and a\n * run-again action, and the viewport following the output while it\n * streams.\n */\n\nimport * as React from \"react\";\nimport {\n BanIcon,\n BracesIcon,\n CheckIcon,\n ChevronDownIcon,\n CircleCheckIcon,\n CircleXIcon,\n CopyIcon,\n RotateCcwIcon,\n SquareTerminalIcon,\n WrenchIcon,\n} from \"lucide-react\";\nimport { motion, useReducedMotion } from \"motion/react\";\n\nimport {\n type CodeLanguage,\n HighlightedCode,\n} from \"@/components/ui/ai-code-block\";\nimport { Disclosure, SPRING_SWAP, SwapText } from \"@/components/ui/ai-motion\";\nimport { Button } from \"@/components/ui/button\";\nimport { Spinner } from \"@/components/ui/spinner\";\nimport { cn } from \"@/lib/utils\";\n\nexport type ToolResultStatus = \"running\" | \"success\" | \"error\" | \"cancelled\";\nexport type ToolResultKind = \"terminal\" | \"request\" | \"custom\";\n\nconst STATUS_LABELS: Record<ToolResultStatus, string> = {\n running: \"Running\",\n success: \"Completed\",\n error: \"Failed\",\n cancelled: \"Cancelled\",\n};\n\nconst STATUS_CLASS: Record<ToolResultStatus, string> = {\n running: \"text-info\",\n success: \"text-success\",\n error: \"text-destructive\",\n cancelled: \"text-muted-foreground\",\n};\n\nfunction getSwapKey(value: React.ReactNode, fallback: string) {\n return typeof value === \"string\" || typeof value === \"number\"\n ? String(value)\n : fallback;\n}\n\nfunction KindIcon({ kind }: { kind: ToolResultKind }) {\n if (kind === \"terminal\") return <SquareTerminalIcon className=\"size-4\" />;\n if (kind === \"request\") return <BracesIcon className=\"size-4\" />;\n return <WrenchIcon className=\"size-4\" />;\n}\n\nfunction StatusIcon({\n status,\n label,\n}: {\n status: ToolResultStatus;\n label: string;\n}) {\n if (status === \"running\") return <Spinner className=\"size-3\" aria-label={label} />;\n if (status === \"success\") return <CircleCheckIcon className=\"size-3\" />;\n if (status === \"error\") return <CircleXIcon className=\"size-3\" />;\n return <BanIcon className=\"size-3\" />;\n}\n\nfunction ToolResultAction({\n label,\n onClick,\n children,\n}: {\n label: string;\n onClick: () => void;\n children: React.ReactNode;\n}) {\n return (\n <Button\n data-slot=\"tool-result-action\"\n variant=\"ghost\"\n size=\"icon-sm\"\n aria-label={label}\n title={label}\n onClick={onClick}\n className=\"text-muted-foreground\"\n >\n {children}\n </Button>\n );\n}\n\n/* ----------------------------------------------------------------------------\n * ToolResultOutput: the tool's text, highlighted, wrapping in the pane.\n * ------------------------------------------------------------------------- */\n\nexport interface ToolResultOutputProps\n extends Omit<React.ComponentProps<typeof HighlightedCode>, \"code\" | \"children\"> {\n children: string;\n language?: CodeLanguage;\n}\n\nfunction ToolResultOutput({\n children,\n language = \"bash\",\n className,\n ...props\n}: ToolResultOutputProps) {\n return (\n <HighlightedCode\n data-slot=\"tool-result-output\"\n code={children}\n language={language}\n className={cn(\"break-words whitespace-pre-wrap text-foreground/80\", className)}\n {...props}\n />\n );\n}\n\n/* ----------------------------------------------------------------------------\n * ToolResult\n * ------------------------------------------------------------------------- */\n\nexport interface ToolResultProps\n extends Omit<React.ComponentProps<\"div\">, \"title\" | \"children\"> {\n /** The tool's name, in monospace. */\n tool: React.ReactNode;\n /** What the call did, in words. */\n title: React.ReactNode;\n children: React.ReactNode;\n status?: ToolResultStatus;\n kind?: ToolResultKind;\n /** A small detail next to the title — a duration, a count. */\n meta?: React.ReactNode;\n /** Replaces the kind icon. */\n icon?: React.ReactNode;\n open?: boolean;\n defaultOpen?: boolean;\n onOpenChange?: (open: boolean) => void;\n /** Close the output once the run ends; a new run opens it again. */\n collapseOnComplete?: boolean;\n /** Pixel height the output scrolls within. */\n maxHeight?: number;\n /** Text the copy action writes; `onCopy` takes over when given. */\n copyText?: string;\n onCopy?: () => void | Promise<void>;\n onRetry?: () => void;\n contentClassName?: string;\n statusLabels?: Partial<Record<ToolResultStatus, string>>;\n copyLabel?: string;\n copiedLabel?: string;\n retryLabel?: string;\n}\n\nfunction ToolResult({\n tool,\n title,\n children,\n status = \"running\",\n kind = \"custom\",\n meta,\n icon,\n open,\n defaultOpen = true,\n onOpenChange,\n collapseOnComplete = true,\n maxHeight = 220,\n copyText,\n onCopy,\n onRetry,\n className,\n contentClassName,\n statusLabels,\n copyLabel = \"Copy result\",\n copiedLabel = \"Copied\",\n retryLabel = \"Run again\",\n ...props\n}: ToolResultProps) {\n const reduce = useReducedMotion() ?? false;\n const baseId = React.useId();\n const triggerId = `${baseId}-trigger`;\n const contentId = `${baseId}-content`;\n const viewportRef = React.useRef<HTMLDivElement>(null);\n const previousStatus = React.useRef(status);\n const copyTimer = React.useRef<number | undefined>(undefined);\n const [copied, setCopied] = React.useState(false);\n const [internalOpen, setInternalOpen] = React.useState(defaultOpen);\n const currentOpen = open ?? internalOpen;\n const running = status === \"running\";\n const canCopy = Boolean(copyText || onCopy);\n const labels = { ...STATUS_LABELS, ...statusLabels };\n const statusLabel = labels[status];\n const titleKey = getSwapKey(title, status);\n const metaKey = getSwapKey(meta, `${status}-meta`);\n const toolKey = getSwapKey(tool, `${status}-tool`);\n\n const setOpen = React.useCallback(\n (next: boolean) => {\n if (open === undefined) setInternalOpen(next);\n onOpenChange?.(next);\n },\n [onOpenChange, open]\n );\n\n React.useEffect(() => {\n if (previousStatus.current !== \"running\" && status === \"running\") {\n setOpen(true);\n }\n if (\n previousStatus.current === \"running\" &&\n status !== \"running\" &&\n collapseOnComplete\n ) {\n setOpen(false);\n }\n previousStatus.current = status;\n }, [collapseOnComplete, setOpen, status]);\n\n React.useEffect(\n () => () => {\n if (copyTimer.current) window.clearTimeout(copyTimer.current);\n },\n []\n );\n\n // While the tool runs the viewport follows the newest output.\n React.useLayoutEffect(() => {\n const viewport = viewportRef.current;\n if (!viewport || !currentOpen || !running) return;\n\n const frame = requestAnimationFrame(() => {\n if (typeof viewport.scrollTo === \"function\") {\n viewport.scrollTo({\n top: viewport.scrollHeight,\n behavior: reduce ? \"auto\" : \"smooth\",\n });\n } else {\n viewport.scrollTop = viewport.scrollHeight;\n }\n });\n return () => cancelAnimationFrame(frame);\n });\n\n const handleCopy = React.useCallback(async () => {\n if (onCopy) await onCopy();\n else if (copyText) await navigator.clipboard?.writeText(copyText);\n\n setCopied(true);\n if (copyTimer.current) window.clearTimeout(copyTimer.current);\n copyTimer.current = window.setTimeout(() => setCopied(false), 1600);\n }, [copyText, onCopy]);\n\n return (\n <div\n data-slot=\"tool-result\"\n data-state={status}\n data-kind={kind}\n aria-busy={running || undefined}\n className={cn(\"w-full text-sm\", className)}\n {...props}\n >\n <button\n id={triggerId}\n type=\"button\"\n data-slot=\"tool-result-trigger\"\n aria-expanded={currentOpen}\n aria-controls={contentId}\n onClick={() => setOpen(!currentOpen)}\n className=\"group/tool-result flex min-h-9 w-full items-center gap-2 rounded-md py-1 text-left outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background\"\n >\n <span\n aria-hidden=\"true\"\n className=\"grid size-4 shrink-0 place-items-center text-muted-foreground\"\n >\n {icon ?? <KindIcon kind={kind} />}\n </span>\n <span className=\"flex min-w-0 flex-1 items-baseline gap-2\">\n <span className=\"min-w-0 truncate font-medium text-foreground/90\">\n <SwapText value={titleKey}>{title}</SwapText>\n </span>\n {meta ? (\n <span className=\"shrink-0 text-xs text-muted-foreground/60\">\n <SwapText value={metaKey}>{meta}</SwapText>\n </span>\n ) : null}\n <span className=\"min-w-0 truncate font-mono text-xs text-muted-foreground/55\">\n <SwapText value={toolKey}>{tool}</SwapText>\n </span>\n </span>\n <span\n data-slot=\"tool-result-status\"\n className={cn(\n \"inline-flex shrink-0 items-center gap-1 text-xs font-medium\",\n STATUS_CLASS[status]\n )}\n >\n <StatusIcon status={status} label={statusLabel} />\n <SwapText value={status}>{statusLabel}</SwapText>\n </span>\n <motion.span\n aria-hidden=\"true\"\n animate={{ rotate: currentOpen ? 180 : 0 }}\n transition={reduce ? { duration: 0 } : SPRING_SWAP}\n className=\"shrink-0 text-muted-foreground/50 transition-colors group-hover/tool-result:text-muted-foreground\"\n >\n <ChevronDownIcon className=\"size-3.5\" />\n </motion.span>\n </button>\n\n <Disclosure\n id={contentId}\n role=\"region\"\n aria-labelledby={triggerId}\n open={currentOpen}\n >\n <div className=\"pt-1.5 pl-6\">\n <div\n data-slot=\"tool-result-panel\"\n className=\"overflow-hidden rounded-xl bg-muted/80\"\n >\n <div\n ref={viewportRef}\n data-slot=\"tool-result-viewport\"\n role=\"log\"\n aria-live=\"polite\"\n className=\"overflow-y-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden\"\n style={{ maxHeight }}\n >\n <div className={cn(\"p-3\", contentClassName)}>{children}</div>\n </div>\n\n {canCopy || onRetry ? (\n <div\n data-slot=\"tool-result-actions\"\n className=\"flex items-center gap-0.5 px-2 pb-1.5\"\n >\n {canCopy ? (\n <ToolResultAction\n label={copied ? copiedLabel : copyLabel}\n onClick={handleCopy}\n >\n {copied ? (\n <CheckIcon className=\"size-3.5\" />\n ) : (\n <CopyIcon className=\"size-3.5\" />\n )}\n </ToolResultAction>\n ) : null}\n {onRetry ? (\n <ToolResultAction label={retryLabel} onClick={onRetry}>\n <RotateCcwIcon className=\"size-3.5\" />\n </ToolResultAction>\n ) : null}\n <span className=\"ml-auto text-xs text-muted-foreground/55\">\n <SwapText value={status}>{statusLabel}</SwapText>\n </span>\n </div>\n ) : null}\n </div>\n </div>\n </Disclosure>\n </div>\n );\n}\n\nexport { ToolResult, ToolResultOutput };\n",
20
+ "type": "registry:ui",
21
+ "target": "components/ui/ai-tool-result.tsx"
22
+ }
23
+ ],
24
+ "type": "registry:ui"
25
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "alert-dialog",
4
+ "title": "Alert Dialog",
5
+ "description": "A modal that confirms a destructive or irreversible action, with the dialog's backdrop and spring.",
6
+ "dependencies": [
7
+ "@base-ui/react",
8
+ "motion"
9
+ ],
10
+ "registryDependencies": [
11
+ "@intelligo/button",
12
+ "@intelligo/ai-motion"
13
+ ],
14
+ "files": [
15
+ {
16
+ "path": "base/ui/alert-dialog/alert-dialog.tsx",
17
+ "content": "\"use client\";\n\n/*\n * The alert dialog: the dialog's backdrop and spring, for a decision the\n * reader must make. Backdrop and panel animate with motion; the root is\n * kept controlled so the exit plays.\n */\n\nimport * as React from \"react\";\nimport { AlertDialog as AlertDialogPrimitive } from \"@base-ui/react/alert-dialog\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\n\nimport {\n backdropMotion,\n popupMotion,\n useOpenState,\n} from \"@/components/ui/ai-motion\";\nimport { Button } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\n\nconst AlertDialogOpenContext = React.createContext<boolean | null>(null);\n\nconst OVERLAY =\n \"fixed inset-0 isolate z-overlay bg-background/60 supports-backdrop-filter:backdrop-blur-sm\";\n\nfunction AlertDialog({\n open: openProp,\n defaultOpen,\n onOpenChange,\n ...props\n}: AlertDialogPrimitive.Root.Props) {\n const [open, setOpen] = useOpenState(openProp, defaultOpen, onOpenChange);\n return (\n <AlertDialogOpenContext.Provider value={open}>\n <AlertDialogPrimitive.Root\n data-slot=\"alert-dialog\"\n open={open}\n onOpenChange={setOpen}\n {...props}\n />\n </AlertDialogOpenContext.Provider>\n );\n}\n\nfunction AlertDialogTrigger({ ...props }: AlertDialogPrimitive.Trigger.Props) {\n return (\n <AlertDialogPrimitive.Trigger data-slot=\"alert-dialog-trigger\" {...props} />\n );\n}\n\nfunction AlertDialogPortal({ ...props }: AlertDialogPrimitive.Portal.Props) {\n return (\n <AlertDialogPrimitive.Portal data-slot=\"alert-dialog-portal\" {...props} />\n );\n}\n\nfunction AlertDialogOverlay({\n className,\n ...props\n}: AlertDialogPrimitive.Backdrop.Props) {\n return (\n <AlertDialogPrimitive.Backdrop\n data-slot=\"alert-dialog-overlay\"\n className={cn(\n OVERLAY,\n \"transition-[opacity,backdrop-filter] duration-normal ease-standard data-starting-style:opacity-0 data-ending-style:opacity-0 data-ending-style:duration-fast\",\n className\n )}\n {...props}\n />\n );\n}\n\nfunction AlertDialogContent({\n className,\n size = \"default\",\n ...props\n}: AlertDialogPrimitive.Popup.Props & {\n size?: \"default\" | \"sm\";\n}) {\n const open = React.useContext(AlertDialogOpenContext) ?? true;\n const reduced = useReducedMotion() ?? false;\n\n return (\n <AnimatePresence>\n {open && (\n <AlertDialogPrimitive.Portal data-slot=\"alert-dialog-portal\" keepMounted>\n <AlertDialogPrimitive.Backdrop\n data-slot=\"alert-dialog-overlay\"\n render={<motion.div {...backdropMotion(reduced)} />}\n className={OVERLAY}\n />\n <AlertDialogPrimitive.Popup\n data-slot=\"alert-dialog-content\"\n data-size={size}\n render={<motion.div {...popupMotion(reduced, 0.94)} />}\n className={cn(\n \"group/alert-dialog-content fixed top-1/2 left-1/2 z-modal grid w-full -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl border border-border bg-popover p-5 text-popover-foreground shadow-2xl outline-none data-[size=default]:max-w-xs data-[size=sm]:max-w-xs data-[size=default]:sm:max-w-sm\",\n className\n )}\n {...props}\n />\n </AlertDialogPrimitive.Portal>\n )}\n </AnimatePresence>\n );\n}\n\nfunction AlertDialogHeader({\n className,\n ...props\n}: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"alert-dialog-header\"\n className={cn(\n \"grid grid-rows-[auto_1fr] place-items-center gap-1.5 text-center has-data-[slot=alert-dialog-media]:grid-rows-[auto_auto_1fr] has-data-[slot=alert-dialog-media]:gap-x-4 sm:group-data-[size=default]/alert-dialog-content:place-items-start sm:group-data-[size=default]/alert-dialog-content:text-left sm:group-data-[size=default]/alert-dialog-content:has-data-[slot=alert-dialog-media]:grid-rows-[auto_1fr]\",\n className\n )}\n {...props}\n />\n );\n}\n\nfunction AlertDialogFooter({\n className,\n ...props\n}: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"alert-dialog-footer\"\n className={cn(\n \"-mx-5 -mb-5 flex flex-col-reverse gap-2 rounded-b-xl border-t bg-muted/60 px-5 py-4 group-data-[size=sm]/alert-dialog-content:grid group-data-[size=sm]/alert-dialog-content:grid-cols-2 sm:flex-row sm:justify-end\",\n className\n )}\n {...props}\n />\n );\n}\n\nfunction AlertDialogMedia({\n className,\n ...props\n}: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"alert-dialog-media\"\n className={cn(\n \"mb-2 inline-flex size-10 items-center justify-center rounded-lg bg-muted sm:group-data-[size=default]/alert-dialog-content:row-span-2 *:[svg:not([class*='size-'])]:size-6\",\n className\n )}\n {...props}\n />\n );\n}\n\nfunction AlertDialogTitle({\n className,\n ...props\n}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {\n return (\n <AlertDialogPrimitive.Title\n data-slot=\"alert-dialog-title\"\n className={cn(\n \"font-heading text-base font-medium sm:group-data-[size=default]/alert-dialog-content:group-has-data-[slot=alert-dialog-media]/alert-dialog-content:col-start-2\",\n className\n )}\n {...props}\n />\n );\n}\n\nfunction AlertDialogDescription({\n className,\n ...props\n}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {\n return (\n <AlertDialogPrimitive.Description\n data-slot=\"alert-dialog-description\"\n className={cn(\n \"text-sm text-balance text-muted-foreground md:text-pretty *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground\",\n className\n )}\n {...props}\n />\n );\n}\n\nfunction AlertDialogAction({\n className,\n ...props\n}: React.ComponentProps<typeof Button>) {\n return (\n <Button\n data-slot=\"alert-dialog-action\"\n className={cn(className)}\n {...props}\n />\n );\n}\n\nfunction AlertDialogCancel({\n className,\n variant = \"outline\",\n size = \"default\",\n ...props\n}: AlertDialogPrimitive.Close.Props &\n Pick<React.ComponentProps<typeof Button>, \"variant\" | \"size\">) {\n return (\n <AlertDialogPrimitive.Close\n data-slot=\"alert-dialog-cancel\"\n className={cn(className)}\n render={<Button variant={variant} size={size} />}\n {...props}\n />\n );\n}\n\nexport {\n AlertDialog,\n AlertDialogAction,\n AlertDialogCancel,\n AlertDialogContent,\n AlertDialogDescription,\n AlertDialogFooter,\n AlertDialogHeader,\n AlertDialogMedia,\n AlertDialogOverlay,\n AlertDialogPortal,\n AlertDialogTitle,\n AlertDialogTrigger,\n};\n",
18
+ "type": "registry:ui",
19
+ "target": "components/ui/alert-dialog.tsx"
20
+ }
21
+ ],
22
+ "type": "registry:ui"
23
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "animated-list",
4
+ "title": "Animated List",
5
+ "description": "A list container that staggers its rows in on mount, rows that rise in, fall out and glide on reorder (ul, ol, div or tbody/tr), and a block that reveals once as it scrolls into view.",
6
+ "dependencies": [
7
+ "motion"
8
+ ],
9
+ "registryDependencies": [
10
+ "@intelligo/ai-motion"
11
+ ],
12
+ "files": [
13
+ {
14
+ "path": "base/ui/animated-list/animated-list.tsx",
15
+ "content": "\"use client\";\n\n/*\n * Lists that arrive: a container that staggers its rows in on mount, rows\n * that rise in, fall out under AnimatePresence and glide when reordered,\n * and a block that reveals once as it scrolls into view.\n */\n\nimport * as React from \"react\";\nimport {\n motion,\n useInView,\n useReducedMotion,\n type HTMLMotionProps,\n type Variants,\n} from \"motion/react\";\n\nimport {\n EASE_OUT,\n listItem,\n listStagger,\n SPRING_LAYOUT,\n} from \"@/components/ui/ai-motion\";\nimport { cn } from \"@/lib/utils\";\n\n/* ----------------------------------------------------------------------------\n * AnimatedList: the container. Its children stagger in on mount; rows added\n * later rise in on their own. Render it as the element the markup needs —\n * a `tbody` for table rows keeps the table valid.\n * ------------------------------------------------------------------------- */\n\nexport type AnimatedListElement = \"ul\" | \"ol\" | \"div\" | \"tbody\";\n\nexport type AnimatedListProps<T extends AnimatedListElement = \"ul\"> = Omit<\n HTMLMotionProps<T>,\n \"variants\" | \"initial\" | \"animate\"\n> & {\n /** The element to render; `tbody` for table rows. */\n as?: T;\n /** Seconds between one row and the next. */\n stagger?: number;\n /** Seconds before the first row starts. */\n delay?: number;\n /** Play the stagger on mount (default). False shows the rows at rest. */\n animateOnMount?: boolean;\n};\n\nconst REDUCED_ITEM: Variants = {\n hidden: { opacity: 0 },\n shown: { opacity: 1, transition: { duration: 0 } },\n exit: { opacity: 0, transition: { duration: 0 } },\n};\n\nfunction AnimatedList<T extends AnimatedListElement = \"ul\">({\n as,\n stagger,\n delay,\n animateOnMount = true,\n className,\n ...props\n}: AnimatedListProps<T>) {\n const reduced = useReducedMotion() ?? false;\n const Component = motion[as ?? \"ul\"] as React.ElementType;\n\n const variants = React.useMemo<Variants>(() => {\n if (reduced) return { hidden: {}, shown: {} };\n if (stagger === undefined && delay === undefined) return listStagger;\n return {\n hidden: {},\n shown: {\n transition: {\n staggerChildren: stagger ?? 0.04,\n delayChildren: delay ?? 0.02,\n },\n },\n };\n }, [reduced, stagger, delay]);\n\n return (\n <Component\n data-slot=\"animated-list\"\n variants={variants}\n initial={animateOnMount ? \"hidden\" : false}\n animate=\"shown\"\n className={className}\n {...props}\n />\n );\n}\n\n/* ----------------------------------------------------------------------------\n * AnimatedListItem: a row. It takes its entrance from the list's stagger,\n * leaves with its exit when wrapped in AnimatePresence, and glides to its\n * new place on a reorder. Use `tr` inside a `tbody` list. Keep\n * AnimatePresence in its default mode for table rows — `popLayout` lifts\n * the leaving row out of the table.\n * ------------------------------------------------------------------------- */\n\nexport type AnimatedListItemElement = \"li\" | \"div\" | \"tr\";\n\nexport type AnimatedListItemProps<T extends AnimatedListItemElement = \"li\"> =\n Omit<HTMLMotionProps<T>, \"variants\"> & {\n /** The element to render; `tr` for table rows. */\n as?: T;\n };\n\nfunction AnimatedListItem<T extends AnimatedListItemElement = \"li\">({\n as,\n layout,\n transition,\n className,\n ...props\n}: AnimatedListItemProps<T>) {\n const reduced = useReducedMotion() ?? false;\n const Component = motion[as ?? \"li\"] as React.ElementType;\n\n return (\n <Component\n data-slot=\"animated-list-item\"\n variants={reduced ? REDUCED_ITEM : listItem}\n exit=\"exit\"\n layout={reduced ? false : (layout ?? \"position\")}\n transition={\n transition ?? (reduced ? { duration: 0 } : { layout: SPRING_LAYOUT })\n }\n className={className}\n {...props}\n />\n );\n}\n\n/* ----------------------------------------------------------------------------\n * ScrollReveal: a block that rises and clears the first time enough of it\n * scrolls into view.\n * ------------------------------------------------------------------------- */\n\nexport interface ScrollRevealProps\n extends Omit<HTMLMotionProps<\"div\">, \"initial\" | \"animate\"> {\n /** Distance in px the block rises from. */\n y?: number;\n /** Blur in px it clears from; kept at 10 or under. */\n blur?: number;\n /** Seconds the reveal takes. */\n duration?: number;\n /** Seconds before the reveal starts once in view. */\n delay?: number;\n /** Reveal only the first time (default), or every time it enters. */\n once?: boolean;\n /** How much of the block must be visible to reveal it. */\n amount?: \"some\" | \"all\" | number;\n /** The scroll container, for blocks inside a scroll area. Defaults to the viewport. */\n root?: React.RefObject<Element | null>;\n}\n\nfunction ScrollReveal({\n y = 16,\n blur = 8,\n duration = 0.6,\n delay = 0,\n once = true,\n amount = 0.3,\n root,\n transition,\n className,\n children,\n ...props\n}: ScrollRevealProps) {\n const reduced = useReducedMotion() ?? false;\n const ref = React.useRef<HTMLDivElement>(null);\n const inView = useInView(ref, { root, once, amount });\n const blurPx = Math.min(Math.max(blur, 0), 10);\n\n const hidden = reduced\n ? { opacity: 0 }\n : { opacity: 0, y, filter: `blur(${blurPx}px)` };\n const shown = reduced\n ? { opacity: 1 }\n : { opacity: 1, y: 0, filter: \"blur(0px)\" };\n\n return (\n <motion.div\n data-slot=\"scroll-reveal\"\n {...props}\n ref={ref}\n initial={hidden}\n animate={inView ? shown : hidden}\n transition={\n transition ??\n (reduced ? { duration: 0 } : { duration, delay, ease: EASE_OUT })\n }\n className={cn(className)}\n >\n {children}\n </motion.div>\n );\n}\n\nexport { AnimatedList, AnimatedListItem, ScrollReveal };\n",
16
+ "type": "registry:ui",
17
+ "target": "components/ui/animated-list.tsx"
18
+ }
19
+ ],
20
+ "type": "registry:ui"
21
+ }
@@ -0,0 +1,93 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "app-shell",
4
+ "title": "App Shell",
5
+ "description": "Authenticated application shell: session and onboarding gate, workspace bootstrap, a sidebar that morphs into an icon rail with a gliding active pill, workspace switcher, configurable nav and user menu, and a page region that lifts each section in.",
6
+ "dependencies": [
7
+ "@intelligo-dev/auth",
8
+ "@intelligo-dev/core",
9
+ "next-themes",
10
+ "drizzle-orm",
11
+ "lucide-react",
12
+ "sonner",
13
+ "motion",
14
+ "server-only",
15
+ "next-intl"
16
+ ],
17
+ "registryDependencies": [
18
+ "avatar",
19
+ "@intelligo/dropdown-menu",
20
+ "separator",
21
+ "@intelligo/ai-sidebar",
22
+ "@intelligo/ai-motion"
23
+ ],
24
+ "files": [
25
+ {
26
+ "path": "base/app-shell/layout.tsx",
27
+ "content": "import { getLocale, getTranslations } from \"next-intl/server\";\n/**\n * The layout for every route under `(app)`. Checks, in order:\n *\n * 1. A session, checked here because middleware only reads the cookie.\n * None → `/login`.\n * 2. Completed onboarding. Incomplete → `/onboarding`. The onboarding\n * route is a sibling of `(app)`, so this cannot redirect into itself;\n * if you nest onboarding under `(app)`, skip this check on that route.\n * 3. An active workspace. `ensureUserWorkspace` creates one if none\n * exists; what a new workspace starts with is the handler\n * `lib/intelligo.ts` sets with `setWorkspaceCreatedHandler`.\n */\n\nimport { eq } from \"drizzle-orm\";\nimport { headers } from \"next/headers\";\nimport type { ReactNode } from \"react\";\n\nimport {\n auth,\n ensureUserWorkspace,\n getAuthSession,\n getWorkspaceContextById,\n} from \"@intelligo-dev/auth\";\nimport { db } from \"@intelligo-dev/core/db\";\nimport { users } from \"@intelligo-dev/core/db/schema\";\n\nimport { AppSidebar } from \"@/components/shell/app-sidebar\";\nimport { PageTransition } from \"@/components/shell/page-transition\";\nimport { ShellHeader } from \"@/components/shell/shell-header\";\nimport { TimeZoneCookie } from \"@/components/shell/time-zone-cookie\";\nimport { AISidebarInset, AISidebarProvider } from \"@/components/ui/ai-sidebar\";\nimport { redirect } from \"@/i18n/navigation\";\nimport { shellConfig } from \"@/lib/shell-config\";\n\nconst SKIP_LINK_CLASS =\n \"sr-only focus:not-sr-only focus:fixed focus:top-3 focus:left-3 focus:z-50 focus:rounded-lg focus:border focus:border-ring focus:bg-background focus:px-3 focus:py-2 focus:text-sm focus:font-medium focus:text-foreground focus:shadow-md focus:outline-none focus:ring-3 focus:ring-ring/40\";\n\nexport default async function AppLayout({ children }: { children: ReactNode }) {\n const session = await getAuthSession();\n if (!session) {\n redirect({ href: \"/login\", locale: await getLocale() });\n return null;\n }\n\n const [userRecord] = await db\n .select({ onboardingCompleted: users.onboardingCompleted })\n .from(users)\n .where(eq(users.id, session.user.id))\n .limit(1);\n\n if (userRecord && !userRecord.onboardingCompleted) {\n redirect({ href: \"/onboarding\", locale: await getLocale() });\n return null;\n }\n\n const hdrs = await headers();\n const activeWorkspaceId = await ensureUserWorkspace(session.user, hdrs);\n\n const [workspaces, workspaceContext] = await Promise.all([\n auth.api.listOrganizations({ headers: hdrs }).then((orgs) => orgs ?? []),\n getWorkspaceContextById(activeWorkspaceId),\n ]);\n\n if (!workspaceContext) {\n redirect({ href: \"/login\", locale: await getLocale() });\n return null;\n }\n\n const workspaceList = workspaces.map((org) => ({\n id: org.id,\n name: org.name,\n slug: org.slug,\n logo: org.logo ?? null,\n }));\n\n // The shell is exactly one screen tall and the page region scrolls\n // inside it. A shell that grows with its page scrolls the window\n // instead, and a page that pins something to the bottom — the chat\n // composer — or scrolls its own region — the transcript — has no\n // height to work in.\n const SidebarContent = shellConfig.sidebarContent;\n const t = await getTranslations(\"app-shell\");\n return (\n <AISidebarProvider className=\"h-svh min-h-0 overflow-hidden\">\n {/* The first focusable element: keyboard users jump past the sidebar. */}\n <a href=\"#main-content\" className={SKIP_LINK_CLASS}>\n {t(\"skipToContent\")}\n </a>\n {/* Renders nothing; tells the server which day it is here. */}\n <TimeZoneCookie />\n <AppSidebar\n workspace={workspaceContext.workspace}\n workspaces={workspaceList}\n user={{\n name: session.user.name,\n email: session.user.email,\n image: session.user.image,\n }}\n >\n {SidebarContent ? <SidebarContent /> : null}\n </AppSidebar>\n <AISidebarInset\n id=\"main-content\"\n tabIndex={-1}\n className=\"min-h-0 overflow-hidden outline-none\"\n >\n <ShellHeader>\n {shellConfig.headerRight && (\n <div className=\"ml-auto flex items-center gap-2\">\n <shellConfig.headerRight />\n </div>\n )}\n </ShellHeader>\n {shellConfig.bannerTop && <shellConfig.bannerTop />}\n <PageTransition className=\"min-h-0 flex-1 overflow-y-auto\">\n {children}\n </PageTransition>\n </AISidebarInset>\n </AISidebarProvider>\n );\n}\n",
28
+ "type": "registry:page",
29
+ "target": "app/[locale]/(app)/layout.tsx"
30
+ },
31
+ {
32
+ "path": "base/app-shell/components/app-sidebar.tsx",
33
+ "content": "\"use client\";\n\n/**\n * The shell's sidebar. `items` replaces the nav from `@/lib/nav-config`\n * entirely. It collapses to an icon rail on desktop (⌘/Ctrl-B, or the\n * header's trigger) and slides in as a sheet on mobile.\n */\n\nimport * as React from \"react\";\nimport { useTranslations } from \"next-intl\";\nimport type { LucideIcon } from \"lucide-react\";\n\nimport { Link, usePathname } from \"@/i18n/navigation\";\nimport {\n AISidebar,\n AISidebarContent,\n AISidebarFooter,\n AISidebarHeader,\n AISidebarItem,\n AISidebarMenu,\n AISidebarMenuItem,\n AISidebarRail,\n AISidebarSection,\n} from \"@/components/ui/ai-sidebar\";\nimport { UserMenu } from \"./user-menu\";\nimport { WorkspaceSwitcher, type Workspace } from \"./workspace-switcher\";\nimport { navItems as configuredNavItems } from \"@/lib/nav-config\";\n\nexport interface NavItem {\n /**\n * A message key in the `app-shell` namespace (e.g.\n * `\"sidebar.nav.dashboard\"`), not a literal title.\n */\n titleKey: string;\n href: string;\n icon?: LucideIcon;\n}\n\ninterface AppSidebarProps {\n workspace: Workspace | null;\n workspaces: Workspace[];\n user: {\n name: string | null;\n email: string;\n image?: string | null;\n };\n items?: NavItem[];\n /** Rendered under the navigation — `shellConfig.sidebarContent`. */\n children?: React.ReactNode;\n}\n\nexport function AppSidebar({\n workspace,\n workspaces,\n user,\n items = configuredNavItems,\n children,\n}: AppSidebarProps) {\n const pathname = usePathname();\n const t = useTranslations(\"app-shell\");\n\n return (\n <AISidebar\n collapsible=\"icon\"\n label={t(\"sidebar.label\")}\n closeLabel={t(\"sidebar.close\")}\n >\n <AISidebarHeader>\n <WorkspaceSwitcher\n currentWorkspace={workspace}\n workspaces={workspaces}\n />\n </AISidebarHeader>\n\n <AISidebarContent>\n <AISidebarSection>\n <AISidebarMenu>\n {items.map((item) => {\n const isActive =\n pathname === item.href || pathname?.startsWith(`${item.href}/`);\n return (\n <AISidebarMenuItem key={item.href}>\n <AISidebarItem\n isActive={isActive}\n icon={item.icon ? <item.icon /> : undefined}\n tooltip={t(item.titleKey)}\n render={<Link href={item.href} />}\n >\n {t(item.titleKey)}\n </AISidebarItem>\n </AISidebarMenuItem>\n );\n })}\n </AISidebarMenu>\n </AISidebarSection>\n {children}\n </AISidebarContent>\n\n <AISidebarFooter>\n <UserMenu user={user} />\n </AISidebarFooter>\n <AISidebarRail label={t(\"sidebar.toggle\")} />\n </AISidebar>\n );\n}\n",
34
+ "type": "registry:component",
35
+ "target": "components/shell/app-sidebar.tsx"
36
+ },
37
+ {
38
+ "path": "base/app-shell/components/workspace-switcher.tsx",
39
+ "content": "\"use client\";\n\n/**\n * Lists the caller's workspaces (fetched by the layout) and activates one\n * through Better-Auth's organization plugin. It has no create-workspace\n * action; build one on `authClient.organization.create` if you need it.\n */\n\nimport * as React from \"react\";\nimport { toast } from \"sonner\";\nimport { useTranslations } from \"next-intl\";\nimport { Check, ChevronsUpDown } from \"lucide-react\";\n\nimport { authClient } from \"@intelligo-dev/auth/client\";\n\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuGroup,\n DropdownMenuItem,\n DropdownMenuLabel,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\";\nimport { useAISidebar, useAISidebarPanel } from \"@/components/ui/ai-sidebar\";\nimport { cn } from \"@/lib/utils\";\nimport { useRouter } from \"@/i18n/navigation\";\n\nconst TRIGGER =\n \"flex w-full min-w-0 items-center gap-2 rounded-xl p-1.5 text-left text-sidebar-foreground outline-none transition-colors hover:bg-sidebar-accent focus-visible:ring-2 focus-visible:ring-sidebar-ring disabled:opacity-50 data-popup-open:bg-sidebar-accent data-popup-open:text-sidebar-accent-foreground\";\n\nexport interface Workspace {\n id: string;\n name: string;\n slug: string;\n logo?: string | null;\n}\n\ninterface WorkspaceSwitcherProps {\n currentWorkspace: Workspace | null;\n workspaces: Workspace[];\n}\n\nexport function WorkspaceSwitcher({\n currentWorkspace,\n workspaces,\n}: WorkspaceSwitcherProps) {\n const { isMobile } = useAISidebar();\n const { collapsed } = useAISidebarPanel();\n const router = useRouter();\n const [isSwitching, setIsSwitching] = React.useState(false);\n const t = useTranslations(\"app-shell\");\n\n if (!currentWorkspace) return null;\n\n async function handleSwitch(workspaceId: string) {\n if (!currentWorkspace || workspaceId === currentWorkspace.id) return;\n setIsSwitching(true);\n try {\n // Better-Auth answers a refusal as `{ error }` rather than throwing.\n const { error } = await authClient.organization.setActive({\n organizationId: workspaceId,\n });\n if (error) {\n toast.error(t(\"workspaceSwitcher.switchFailed\"));\n return;\n }\n router.refresh();\n } catch {\n toast.error(t(\"workspaceSwitcher.switchFailed\"));\n } finally {\n setIsSwitching(false);\n }\n }\n\n return (\n <DropdownMenu>\n <DropdownMenuTrigger\n render={\n <button\n type=\"button\"\n disabled={isSwitching}\n className={cn(TRIGGER, collapsed && \"justify-center\")}\n />\n }\n >\n <div className=\"flex size-8 shrink-0 items-center justify-center rounded-lg bg-sidebar-primary text-xs font-semibold text-sidebar-primary-foreground\">\n {currentWorkspace.name[0]?.toUpperCase()}\n </div>\n {collapsed ? null : (\n <>\n <div className=\"grid min-w-0 flex-1 text-left leading-tight\">\n <span className=\"truncate text-sm font-medium\">\n {currentWorkspace.name}\n </span>\n <span className=\"truncate text-xs text-muted-foreground\">\n {currentWorkspace.slug}\n </span>\n </div>\n {workspaces.length > 1 && (\n <ChevronsUpDown className=\"ml-auto size-4 opacity-50\" />\n )}\n </>\n )}\n </DropdownMenuTrigger>\n {workspaces.length > 1 && (\n <DropdownMenuContent\n className=\"w-(--anchor-width) min-w-56\"\n align=\"start\"\n side={isMobile ? \"bottom\" : \"right\"}\n sideOffset={4}\n >\n <DropdownMenuGroup>\n <DropdownMenuLabel className=\"text-xs text-muted-foreground\">\n {t(\"workspaceSwitcher.workspacesLabel\")}\n </DropdownMenuLabel>\n <DropdownMenuSeparator />\n {workspaces.map((ws) => (\n <DropdownMenuItem\n key={ws.id}\n onClick={() => handleSwitch(ws.id)}\n disabled={isSwitching}\n className=\"gap-2\"\n >\n <div className=\"flex size-6 shrink-0 items-center justify-center rounded-md bg-sidebar-primary text-xs font-semibold text-sidebar-primary-foreground\">\n {ws.name[0]?.toUpperCase()}\n </div>\n <div className=\"grid flex-1 leading-tight\">\n <span className=\"truncate text-sm\">{ws.name}</span>\n <span className=\"truncate text-xs text-muted-foreground\">\n {ws.slug}\n </span>\n </div>\n {ws.id === currentWorkspace.id && (\n <Check className=\"ml-auto size-4\" />\n )}\n </DropdownMenuItem>\n ))}\n </DropdownMenuGroup>\n </DropdownMenuContent>\n )}\n </DropdownMenu>\n );\n}\n",
40
+ "type": "registry:component",
41
+ "target": "components/shell/workspace-switcher.tsx"
42
+ },
43
+ {
44
+ "path": "base/app-shell/components/user-menu.tsx",
45
+ "content": "\"use client\";\n\n/**\n * Avatar dropdown: account pages (`accountItems` from `@/lib/nav-config`),\n * settings, a theme submenu and sign out.\n *\n * The theme submenu only switches themes once a `ThemeProvider` is\n * mounted; without one `useTheme()` is a no-op. The `mounted` check\n * avoids a hydration mismatch between the server render (no theme) and\n * the client's resolved theme.\n */\n\nimport * as React from \"react\";\nimport { useTranslations } from \"next-intl\";\nimport {\n ChevronsUpDown,\n LogOut,\n Monitor,\n Moon,\n Settings,\n Sun,\n User,\n} from \"lucide-react\";\nimport { useTheme } from \"next-themes\";\n\nimport { authClient } from \"@intelligo-dev/auth/client\";\n\nimport { Avatar, AvatarFallback, AvatarImage } from \"@/components/ui/avatar\";\nimport { Link, useRouter } from \"@/i18n/navigation\";\nimport { accountItems } from \"@/lib/nav-config\";\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuGroup,\n DropdownMenuItem,\n DropdownMenuLabel,\n DropdownMenuSeparator,\n DropdownMenuSub,\n DropdownMenuSubContent,\n DropdownMenuSubTrigger,\n DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\";\nimport { useAISidebar, useAISidebarPanel } from \"@/components/ui/ai-sidebar\";\nimport { cn } from \"@/lib/utils\";\n\nconst TRIGGER =\n \"flex w-full min-w-0 items-center gap-2 rounded-xl p-1.5 text-left text-sidebar-foreground outline-none transition-colors hover:bg-sidebar-accent focus-visible:ring-2 focus-visible:ring-sidebar-ring disabled:opacity-50 data-popup-open:bg-sidebar-accent data-popup-open:text-sidebar-accent-foreground\";\n\ninterface UserMenuProps {\n user: {\n name: string | null;\n email: string;\n image?: string | null;\n };\n}\n\nexport function UserMenu({ user }: UserMenuProps) {\n const { isMobile } = useAISidebar();\n const { collapsed } = useAISidebarPanel();\n const { theme, setTheme } = useTheme();\n const [mounted, setMounted] = React.useState(false);\n const router = useRouter();\n const t = useTranslations(\"app-shell\");\n\n React.useEffect(() => {\n setMounted(true);\n }, []);\n\n const initials = user.name\n ? user.name\n .split(\" \")\n .map((part) => part[0])\n .join(\"\")\n .toUpperCase()\n .slice(0, 2)\n : user.email[0]?.toUpperCase();\n\n async function handleSignOut() {\n await authClient.signOut();\n router.push(\"/login\");\n }\n\n return (\n <DropdownMenu>\n <DropdownMenuTrigger\n render={\n <button\n type=\"button\"\n className={cn(TRIGGER, collapsed && \"justify-center\")}\n />\n }\n >\n <Avatar className=\"size-8 rounded-lg\">\n {user.image && <AvatarImage src={user.image} alt={user.name ?? \"\"} />}\n <AvatarFallback className=\"rounded-lg\">{initials}</AvatarFallback>\n </Avatar>\n {collapsed ? null : (\n <>\n <div className=\"grid min-w-0 flex-1 text-left leading-tight\">\n <span className=\"truncate text-sm font-medium\">\n {user.name || user.email.split(\"@\")[0]}\n </span>\n <span className=\"truncate text-xs text-muted-foreground\">\n {user.email}\n </span>\n </div>\n <ChevronsUpDown className=\"ml-auto size-4 opacity-50\" />\n </>\n )}\n </DropdownMenuTrigger>\n <DropdownMenuContent\n className=\"w-(--anchor-width) min-w-56\"\n side={isMobile ? \"bottom\" : collapsed ? \"right\" : \"top\"}\n align=\"end\"\n sideOffset={4}\n >\n <DropdownMenuGroup>\n <DropdownMenuLabel className=\"p-0 font-normal\">\n <div className=\"flex items-center gap-2 px-2 py-1.5\">\n <Avatar className=\"size-8 rounded-lg\">\n {user.image && (\n <AvatarImage src={user.image} alt={user.name ?? \"\"} />\n )}\n <AvatarFallback className=\"rounded-lg\">\n {initials}\n </AvatarFallback>\n </Avatar>\n <div className=\"grid flex-1 leading-tight\">\n <span className=\"truncate text-sm font-medium\">\n {user.name || user.email.split(\"@\")[0]}\n </span>\n <span className=\"truncate text-xs text-muted-foreground\">\n {user.email}\n </span>\n </div>\n </div>\n </DropdownMenuLabel>\n </DropdownMenuGroup>\n <DropdownMenuSeparator />\n\n <DropdownMenuGroup>\n <DropdownMenuItem render={<Link href=\"/settings\" />}>\n <User />\n {t(\"userMenu.profile\")}\n </DropdownMenuItem>\n <DropdownMenuItem render={<Link href=\"/settings/workspace\" />}>\n <Settings />\n {t(\"userMenu.workspaceSettings\")}\n </DropdownMenuItem>\n </DropdownMenuGroup>\n <DropdownMenuSeparator />\n\n {accountItems.length > 0 ? (\n <>\n <DropdownMenuGroup>\n {accountItems.map((item) => {\n const Icon = item.icon;\n return (\n <DropdownMenuItem\n key={item.href}\n render={<Link href={item.href} />}\n >\n {Icon ? <Icon /> : null}\n {t(item.titleKey)}\n </DropdownMenuItem>\n );\n })}\n </DropdownMenuGroup>\n <DropdownMenuSeparator />\n </>\n ) : null}\n\n <DropdownMenuSub>\n <DropdownMenuSubTrigger>\n {mounted && theme === \"dark\" ? <Moon /> : <Sun />}\n {t(\"userMenu.theme\")}\n </DropdownMenuSubTrigger>\n <DropdownMenuSubContent>\n <DropdownMenuItem onClick={() => setTheme(\"light\")}>\n <Sun />\n {t(\"userMenu.themeLight\")}\n </DropdownMenuItem>\n <DropdownMenuItem onClick={() => setTheme(\"dark\")}>\n <Moon />\n {t(\"userMenu.themeDark\")}\n </DropdownMenuItem>\n <DropdownMenuItem onClick={() => setTheme(\"system\")}>\n <Monitor />\n {t(\"userMenu.themeSystem\")}\n </DropdownMenuItem>\n </DropdownMenuSubContent>\n </DropdownMenuSub>\n\n <DropdownMenuSeparator />\n <DropdownMenuItem\n onClick={handleSignOut}\n className=\"text-destructive focus:text-destructive\"\n >\n <LogOut />\n {t(\"userMenu.signOut\")}\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n );\n}\n",
46
+ "type": "registry:component",
47
+ "target": "components/shell/user-menu.tsx"
48
+ },
49
+ {
50
+ "path": "base/app-shell/components/shell-header.tsx",
51
+ "content": "/**\n * Shell top bar. It must render `AISidebarTrigger`: on mobile the sidebar\n * is an off-canvas sheet with no other way to open it. `children` adds\n * breadcrumbs or actions. A page portals its own bar into the empty\n * `shell-header-slot` (the chat's title and actions do) rather than\n * stacking a second header under this one.\n */\n\nimport type { ReactNode } from \"react\";\nimport { getTranslations } from \"next-intl/server\";\n\nimport { AISidebarTrigger } from \"@/components/ui/ai-sidebar\";\nimport { Separator } from \"@/components/ui/separator\";\n\ninterface ShellHeaderProps {\n children?: ReactNode;\n}\n\nexport async function ShellHeader({ children }: ShellHeaderProps) {\n const t = await getTranslations(\"app-shell\");\n\n return (\n <header className=\"flex h-14 shrink-0 items-center gap-2 border-b px-4\">\n <AISidebarTrigger\n size=\"icon-sm\"\n label={t(\"sidebar.toggle\")}\n className=\"-ml-1\"\n />\n <div className=\"mr-2 flex h-4 items-center\">\n <Separator orientation=\"vertical\" />\n </div>\n <div\n data-slot=\"shell-header-slot\"\n className=\"flex min-w-0 flex-1 items-center\"\n />\n {children}\n </header>\n );\n}\n",
52
+ "type": "registry:component",
53
+ "target": "components/shell/shell-header.tsx"
54
+ },
55
+ {
56
+ "path": "base/app-shell/components/theme-provider.tsx",
57
+ "content": "\"use client\";\n\n/**\n * The user menu's Light/Dark/System submenu drives `next-themes`, which\n * is inert until this provider is mounted above it. The root layout is\n * yours, so mounting it is one manual step:\n *\n * // app/[locale]/layout.tsx\n * import { ThemeProvider } from \"@/components/shell/theme-provider\";\n * <html lang={locale} suppressHydrationWarning>\n * <body>\n * <ThemeProvider>{children}</ThemeProvider>\n * </body>\n * </html>\n *\n * `suppressHydrationWarning` is required: next-themes sets the class\n * attribute before hydration.\n *\n * Defaults: class strategy (Tailwind's `.dark` tokens), system preference\n * honoured, no transition on switch. Props pass through to override them.\n */\n\nimport { ThemeProvider as NextThemesProvider } from \"next-themes\";\nimport type { ThemeProviderProps } from \"next-themes\";\n\nexport function ThemeProvider({ children, ...props }: ThemeProviderProps) {\n return (\n <NextThemesProvider\n attribute=\"class\"\n defaultTheme=\"system\"\n enableSystem\n disableTransitionOnChange\n {...props}\n >\n {children}\n </NextThemesProvider>\n );\n}\n",
58
+ "type": "registry:component",
59
+ "target": "components/shell/theme-provider.tsx"
60
+ },
61
+ {
62
+ "path": "base/app-shell/components/page-transition.tsx",
63
+ "content": "\"use client\";\n\n/**\n * The page region of the shell. Moving between sections — dashboard to\n * chat, chat to artifacts — lifts the new page in; moving inside one\n * (a conversation to the next, a settings tab to another) does not,\n * because the section's own layout owns that motion.\n *\n * It animates in place rather than keying on the route, so nothing under\n * it remounts: a layout's state survives the navigation.\n */\n\nimport * as React from \"react\";\nimport { useAnimate, useReducedMotion } from \"motion/react\";\n\nimport { EASE_OUT } from \"@/components/ui/ai-motion\";\nimport { usePathname } from \"@/i18n/navigation\";\nimport { cn } from \"@/lib/utils\";\n\nfunction sectionOf(pathname: string | null) {\n return pathname?.split(\"/\").filter(Boolean)[0] ?? \"\";\n}\n\nexport function PageTransition({\n className,\n children,\n}: {\n className?: string;\n children: React.ReactNode;\n}) {\n const pathname = usePathname();\n const reduced = useReducedMotion() ?? false;\n const [scope, animate] = useAnimate<HTMLDivElement>();\n const section = sectionOf(pathname);\n const previous = React.useRef(section);\n\n React.useEffect(() => {\n if (previous.current === section) return;\n previous.current = section;\n scope.current?.scrollTo({ top: 0 });\n if (reduced) return;\n const element = scope.current;\n // Cleared after: a leftover transform would make this region the\n // containing block of every fixed-position element inside the page.\n void animate(\n element,\n { opacity: [0, 1], y: [8, 0] },\n { duration: 0.36, ease: EASE_OUT }\n ).then(() => {\n element.style.transform = \"\";\n element.style.opacity = \"\";\n });\n }, [animate, reduced, scope, section]);\n\n return (\n <div ref={scope} data-slot=\"page\" className={cn(className)}>\n {children}\n </div>\n );\n}\n",
64
+ "type": "registry:component",
65
+ "target": "components/shell/page-transition.tsx"
66
+ },
67
+ {
68
+ "path": "base/app-shell/components/time-zone-cookie.tsx",
69
+ "content": "\"use client\";\n\n/**\n * Writes the browser's time zone to a cookie so per-day figures bucket\n * in the reader's day rather than UTC. The server reads it through\n * `@intelligo-dev/core/request-context`; a browser's time zone reaches the\n * server no other way. The first render of a new session has no cookie\n * yet and buckets in UTC.\n */\n\nimport { useEffect } from \"react\";\n\nconst COOKIE = \"tz\";\nconst ONE_YEAR_SECONDS = 60 * 60 * 24 * 365;\n\nexport function TimeZoneCookie() {\n useEffect(() => {\n const zone = Intl.DateTimeFormat().resolvedOptions().timeZone;\n if (!zone) return;\n\n const current = document.cookie.match(/(?:^|;\\s*)tz=([^;]*)/)?.[1];\n if (current && decodeURIComponent(current) === zone) return;\n\n // Lax: this rides same-site navigation, and it is a display\n // preference, not a credential.\n document.cookie = `${COOKIE}=${encodeURIComponent(zone)}; path=/; max-age=${ONE_YEAR_SECONDS}; samesite=lax`;\n }, []);\n\n return null;\n}\n",
70
+ "type": "registry:component",
71
+ "target": "components/shell/time-zone-cookie.tsx"
72
+ },
73
+ {
74
+ "path": "base/app-shell/lib/shell-config.tsx",
75
+ "content": "/**\n * What your product adds around the app shell, without editing\n * `layout.tsx` or `components/shell/*`. Every slot is optional and takes\n * a component with no props; one that needs data fetches it itself.\n *\n * - `bannerTop`: above the page content on every authenticated page\n * (a trial banner, an incident notice).\n * - `headerRight`: the right end of the header (a notification bell,\n * a language switcher).\n * - `sidebarContent`: the sidebar under the navigation (conversation\n * history). May be an async server component; it refreshes with the\n * page. Hiding itself when the sidebar collapses is its own call.\n *\n * For example:\n *\n * import { TrialBanner } from \"@/components/trial/trial-banner\";\n *\n * export const shellConfig: ShellConfig = {\n * bannerTop: TrialBanner,\n * };\n */\n\nimport type { ComponentType } from \"react\";\n\nexport interface ShellConfig {\n /** Rendered inside `SidebarInset`, above `main`. Takes no props. */\n bannerTop?: ComponentType;\n /**\n * Rendered at the right end of the shell header — e.g. a\n * notification bell, a language switcher, or both. Takes no props.\n */\n headerRight?: ComponentType;\n /**\n * Rendered in the sidebar under the navigation — e.g. conversation\n * history. Takes no props; may be an async server component.\n */\n sidebarContent?: ComponentType;\n}\n\nexport const shellConfig: ShellConfig = {};\n",
76
+ "type": "registry:file",
77
+ "target": "lib/shell-config.tsx"
78
+ },
79
+ {
80
+ "path": "base/app-shell/messages/en.json",
81
+ "content": "{\n \"skipToContent\": \"Skip to content\",\n \"sidebar\": {\n \"label\": \"Main navigation\",\n \"toggle\": \"Toggle sidebar\",\n \"close\": \"Close sidebar\",\n \"nav\": {\n \"dashboard\": \"Dashboard\",\n \"chat\": \"Chat\",\n \"artifacts\": \"Artifacts\"\n }\n },\n \"userMenu\": {\n \"profile\": \"Profile\",\n \"workspaceSettings\": \"Workspace settings\",\n \"usage\": \"Usage\",\n \"pricing\": \"Pricing\",\n \"theme\": \"Theme\",\n \"themeLight\": \"Light\",\n \"themeDark\": \"Dark\",\n \"themeSystem\": \"System\",\n \"signOut\": \"Sign out\"\n },\n \"workspaceSwitcher\": {\n \"workspacesLabel\": \"Workspaces\",\n \"switchFailed\": \"Couldn't switch workspace. Please try again.\"\n }\n}\n",
82
+ "type": "registry:file",
83
+ "target": "messages/en/app-shell.json"
84
+ },
85
+ {
86
+ "path": "base/app-shell/lib/nav-config.ts",
87
+ "content": "/**\n * Shell navigation. Imported directly by client components, never passed\n * from a Server Component: icons are components and cannot cross the\n * server/client boundary.\n *\n * `navItems` fills the sidebar; `accountItems` fills the user menu.\n * `titleKey`s are keys in `messages/<locale>/app-shell.json`. Remove the\n * entries whose routes you did not install; they would 404.\n */\n\nimport {\n BarChart3,\n CreditCard,\n FileText,\n LayoutDashboard,\n MessageSquare,\n} from \"lucide-react\";\n\nimport type { NavItem } from \"@/components/shell/app-sidebar\";\n\n/** The sidebar: the product's own surfaces. */\nexport const navItems: NavItem[] = [\n {\n titleKey: \"sidebar.nav.dashboard\",\n href: \"/dashboard\",\n icon: LayoutDashboard,\n },\n { titleKey: \"sidebar.nav.chat\", href: \"/chat\", icon: MessageSquare },\n { titleKey: \"sidebar.nav.artifacts\", href: \"/artifacts\", icon: FileText },\n];\n\n/** The user menu: the account's own pages. */\nexport const accountItems: NavItem[] = [\n { titleKey: \"userMenu.usage\", href: \"/usage\", icon: BarChart3 },\n { titleKey: \"userMenu.pricing\", href: \"/pricing\", icon: CreditCard },\n];\n",
88
+ "type": "registry:file",
89
+ "target": "lib/nav-config.ts"
90
+ }
91
+ ],
92
+ "type": "registry:block"
93
+ }
@@ -0,0 +1,82 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "artifacts",
4
+ "title": "Artifacts",
5
+ "description": "Browse and manage AI-generated document artifacts: filterable list, full-content preview, copy-to-clipboard, and version delete, backed by the framework's document persistence service.",
6
+ "dependencies": [
7
+ "@intelligo-dev/auth",
8
+ "@intelligo-dev/core",
9
+ "@intelligo-dev/next",
10
+ "lucide-react",
11
+ "sonner",
12
+ "server-only",
13
+ "next-intl"
14
+ ],
15
+ "registryDependencies": [
16
+ "alert",
17
+ "badge",
18
+ "@intelligo/button",
19
+ "card",
20
+ "@intelligo/dialog",
21
+ "scroll-area",
22
+ "skeleton",
23
+ "@intelligo/spinner",
24
+ "@intelligo/page-header",
25
+ "@intelligo/document-viewer",
26
+ "@intelligo/animated-list"
27
+ ],
28
+ "files": [
29
+ {
30
+ "path": "base/artifacts/page.tsx",
31
+ "content": "import type { Metadata } from \"next\";\nimport { getTranslations } from \"next-intl/server\";\n\nimport {\n Card,\n CardDescription,\n CardHeader,\n CardTitle,\n} from \"@/components/ui/card\";\n\nimport { listDocuments } from \"@/actions/documents\";\nimport { DocumentList } from \"@/components/artifacts/document-list\";\nimport {\n PageHeader,\n PageHeaderContent,\n PageHeaderDescription,\n PageHeaderTitle,\n} from \"@/components/ui/page-header\";\n\nexport async function generateMetadata(): Promise<Metadata> {\n const t = await getTranslations(\"artifacts\");\n return { title: t(\"page.title\") };\n}\n\n/**\n * Every artifact the user created in the active workspace, fetched once;\n * filtering and the preview dialog are client-side.\n *\n * `force-dynamic` because `listDocuments()` reads the session, so a\n * static prerender would fail at build time.\n */\nexport const dynamic = \"force-dynamic\";\n\nexport default async function ArtifactsPage() {\n const [t, result] = await Promise.all([\n getTranslations(\"artifacts\"),\n listDocuments(),\n ]);\n\n return (\n <div className=\"container mx-auto space-y-8 px-4 py-8\">\n <PageHeader>\n <PageHeaderContent>\n <PageHeaderTitle>{t(\"page.heading\")}</PageHeaderTitle>\n <PageHeaderDescription>{t(\"page.description\")}</PageHeaderDescription>\n </PageHeaderContent>\n </PageHeader>\n\n {result.success ? (\n <DocumentList documents={result.data} />\n ) : (\n <Card>\n <CardHeader>\n <CardTitle>{t(\"page.unavailableTitle\")}</CardTitle>\n <CardDescription>{result.error}</CardDescription>\n </CardHeader>\n </Card>\n )}\n </div>\n );\n}\n",
32
+ "type": "registry:page",
33
+ "target": "app/[locale]/(app)/artifacts/page.tsx"
34
+ },
35
+ {
36
+ "path": "base/artifacts/loading.tsx",
37
+ "content": "import { Card } from \"@/components/ui/card\";\nimport { Skeleton } from \"@/components/ui/skeleton\";\n\nexport default function ArtifactsLoading() {\n return (\n <div className=\"container mx-auto space-y-8 px-4 py-8\">\n <div className=\"space-y-2\">\n <Skeleton className=\"h-7 w-32\" />\n <Skeleton className=\"h-4 w-80\" />\n </div>\n\n <div className=\"flex flex-wrap gap-2\">\n {Array.from({ length: 3 }).map((_, index) => (\n <Skeleton key={index} className=\"h-8 w-20 rounded-full\" />\n ))}\n </div>\n\n <div className=\"grid gap-4 md:grid-cols-2 lg:grid-cols-3\">\n {Array.from({ length: 6 }).map((_, index) => (\n <Card key={index} className=\"p-5\">\n <div className=\"mb-3 flex items-start gap-3\">\n <Skeleton className=\"size-4 rounded\" />\n <div className=\"min-w-0 flex-1 space-y-2\">\n <Skeleton className=\"h-5 w-48\" />\n <Skeleton className=\"h-4 w-16 rounded-full\" />\n </div>\n </div>\n <div className=\"flex items-center justify-between\">\n <Skeleton className=\"h-4 w-28\" />\n <Skeleton className=\"h-7 w-16 rounded\" />\n </div>\n </Card>\n ))}\n </div>\n </div>\n );\n}\n",
38
+ "type": "registry:page",
39
+ "target": "app/[locale]/(app)/artifacts/loading.tsx"
40
+ },
41
+ {
42
+ "path": "base/artifacts/error.tsx",
43
+ "content": "\"use client\";\n\n/** Error boundary for the artifacts route. */\n\nimport { useEffect } from \"react\";\nimport { useTranslations } from \"next-intl\";\n\nimport { Alert, AlertDescription } from \"@/components/ui/alert\";\nimport { Button } from \"@/components/ui/button\";\nimport { Link } from \"@/i18n/navigation\";\n\nexport default function ArtifactsError({\n error,\n reset,\n}: {\n error: Error & { digest?: string };\n reset: () => void;\n}) {\n const t = useTranslations(\"artifacts\");\n\n useEffect(() => {\n console.error(\"[artifacts] error boundary caught:\", error);\n }, [error]);\n\n return (\n <div className=\"flex min-h-96 items-center justify-center p-4\">\n <div className=\"w-full max-w-md space-y-4\">\n <Alert variant=\"destructive\">\n <AlertDescription>{t(\"error.description\")}</AlertDescription>\n </Alert>\n\n <div className=\"flex justify-center gap-3\">\n <Button onClick={reset}>{t(\"error.retry\")}</Button>\n <Button\n variant=\"outline\"\n render={<Link href=\"/\" />}\n nativeButton={false}\n >\n {t(\"error.goHome\")}\n </Button>\n </div>\n </div>\n </div>\n );\n}\n",
44
+ "type": "registry:page",
45
+ "target": "app/[locale]/(app)/artifacts/error.tsx"
46
+ },
47
+ {
48
+ "path": "base/artifacts/actions.ts",
49
+ "content": "\"use server\";\n\n/**\n * Artifact library actions over `@intelligo-dev/core/documents`: resolve\n * the caller with `requireWorkspace()`, call the service, map a\n * `DocumentServiceError` to a translated message.\n *\n * Deleting uses `deleteDocumentVersions(actor, id, timestamp)`, which\n * removes every version created strictly after `timestamp`, within the\n * last 30 days. It is a revert, not a hard delete: `deleteLatestVersion`\n * passes one millisecond before the version this page shows, so an\n * artifact with older versions reverts to the previous one, and one\n * with a single version disappears.\n */\n\nimport { getTranslations } from \"next-intl/server\";\n\nimport { requireWorkspace } from \"@intelligo-dev/auth\";\nimport {\n deleteDocumentVersions,\n getUserDocuments,\n isDocumentServiceError,\n isProductDocument,\n type DocumentListItem,\n} from \"@intelligo-dev/core/documents\";\nimport type { ActionResult as BaseActionResult } from \"@intelligo-dev/next\";\n\nexport type ActionResult<T> = BaseActionResult<T>;\n\n/**\n * A document list item plus whether its title matches a pattern\n * registered through `@/lib/document-patterns.ts` — drives the\n * \"Reports\" filter tab.\n */\nexport type ArtifactListItem = DocumentListItem & { isReport: boolean };\n\ntype Translator = Awaited<ReturnType<typeof getTranslations>>;\n\nfunction friendlyMessageKey(code: string): string | undefined {\n switch (code) {\n case \"not_found\":\n return \"actions.notFound\";\n case \"forbidden\":\n return \"actions.forbidden\";\n case \"invalid_input\":\n return \"actions.invalidInput\";\n case \"database_error\":\n return \"actions.databaseError\";\n default:\n return undefined;\n }\n}\n\nfunction friendlyError(t: Translator, error: unknown): string {\n // Unknown errors deliberately map to the generic key — a raw\n // `Error#message` can carry internals (SQL, hostnames) to the UI.\n if (isDocumentServiceError(error)) {\n const key = friendlyMessageKey(error.code);\n return key ? t(key) : t(\"actions.genericError\");\n }\n return t(\"actions.genericError\");\n}\n\n/**\n * Every artifact the caller owns in the active workspace, newest first.\n * Filtering happens client-side.\n */\nexport async function listDocuments(): Promise<\n ActionResult<ArtifactListItem[]>\n> {\n try {\n const { workspace, user } = await requireWorkspace();\n const documents = await getUserDocuments({\n workspaceId: workspace.id,\n userId: user.id,\n });\n\n return {\n success: true,\n data: documents.map((doc) => ({\n ...doc,\n isReport: isProductDocument(doc.title),\n })),\n };\n } catch (error) {\n return {\n success: false,\n error: friendlyError(await getTranslations(\"artifacts\"), error),\n };\n }\n}\n\n/**\n * Deletes the version of the artifact this page shows (see the module\n * comment). `latestCreatedAt` is that version's `createdAt` as an ISO\n * string, as `listDocuments` returns it.\n */\nexport async function deleteLatestVersion(\n id: string,\n latestCreatedAt: string\n): Promise<ActionResult<undefined>> {\n const latest = new Date(latestCreatedAt);\n if (Number.isNaN(latest.getTime())) {\n const t = await getTranslations(\"artifacts\");\n return { success: false, error: t(\"actions.invalidTimestamp\") };\n }\n\n try {\n const { workspace, user } = await requireWorkspace();\n const cutoff = new Date(latest.getTime() - 1).toISOString();\n\n await deleteDocumentVersions(\n { workspaceId: workspace.id, userId: user.id },\n id,\n cutoff\n );\n\n return { success: true, data: undefined };\n } catch (error) {\n return {\n success: false,\n error: friendlyError(await getTranslations(\"artifacts\"), error),\n };\n }\n}\n",
50
+ "type": "registry:file",
51
+ "target": "actions/documents.ts"
52
+ },
53
+ {
54
+ "path": "base/artifacts/lib/document-patterns.ts",
55
+ "content": "import \"server-only\";\n\n/**\n * Title patterns that map a document to an agent label (shown on its\n * card) and to the \"Reports\" tab. With none registered, every document\n * gets the generic \"AI Assistant\" label.\n *\n * Call this exactly once, from your composition root: calling it from a\n * request-scoped file (an action, a page) would push duplicate entries\n * onto the same in-memory registry.\n */\nexport function registerDefaultDocumentPatterns(): void {\n // Registers nothing by default.\n}\n\n// Example: label your agent's documents and put them under \"Reports\"\n// (replace the export above):\n//\n// import { registerDocumentPatterns } from \"@intelligo-dev/core/documents\";\n//\n// export function registerDefaultDocumentPatterns(): void {\n// registerDocumentPatterns({\n// productSlug: \"default\",\n// agentLabel: \"Assistant\",\n// patterns: [\"report\", \"summary\"],\n// });\n// }\n",
56
+ "type": "registry:file",
57
+ "target": "lib/document-patterns.ts"
58
+ },
59
+ {
60
+ "path": "base/artifacts/components/document-list.tsx",
61
+ "content": "\"use client\";\n\n/**\n * The artifact library: a grid of document cards and a reader dialog.\n * Documents render through `@/components/ui/document-viewer`, as in the\n * chat canvas.\n *\n * The \"Reports\" tab filters on `doc.isReport`, computed server-side from\n * the patterns in `@/lib/document-patterns`; the kind tabs come from the\n * kinds present, so a custom kind gets its own tab. A deleted item is\n * dropped from local state once the server action succeeds.\n */\n\nimport { useEffect, useMemo, useState } from \"react\";\nimport { useFormatter, useTranslations } from \"next-intl\";\nimport { Bot, ExternalLink, File } from \"lucide-react\";\n\nimport { Link } from \"@/i18n/navigation\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n Dialog,\n DialogContent,\n DialogHeader,\n DialogTitle,\n} from \"@/components/ui/dialog\";\nimport { ScrollArea } from \"@/components/ui/scroll-area\";\nimport {\n DocumentView,\n HtmlPreview,\n documentKindIcon,\n extensionOf,\n isPreviewableTitle,\n} from \"@/components/ui/document-viewer\";\nimport { AnimatedList, AnimatedListItem } from \"@/components/ui/animated-list\";\n\nimport type { ArtifactListItem } from \"@/actions/documents\";\nimport { DocumentActions } from \"./document-actions\";\n\n/** How much of a document a card shows before it fades out. */\nconst PREVIEW_LINES = 8;\n\nfunction head(text: string): string {\n return text.trimStart().split(\"\\n\").slice(0, PREVIEW_LINES).join(\"\\n\");\n}\n\n/**\n * An inline style rather than a class, because the design system keeps\n * gradients out of class names. It fades the bottom: a card shows a\n * document's opening lines.\n */\nconst PREVIEW_MASK = \"linear-gradient(to bottom, black 60%, transparent)\";\n\ninterface DocumentListProps {\n documents: ArtifactListItem[];\n}\n\n/**\n * Relative timestamp that can't cause a hydration mismatch: the server\n * (and the first client render) show a locale-formatted absolute date,\n * and an effect swaps in the relative form once mounted — `Date.now()`\n * during render would differ between server and client.\n */\nfunction RelativeTime({ iso }: { iso: string }) {\n const format = useFormatter();\n const [now, setNow] = useState<Date | null>(null);\n useEffect(() => setNow(new Date()), []);\n const date = new Date(iso);\n return (\n <>\n {now\n ? format.relativeTime(date, now)\n : format.dateTime(date, { dateStyle: \"medium\" })}\n </>\n );\n}\n\n/** The kind's glyph on the tile both the card and the reader use. */\nfunction KindTile({ kind, large }: { kind: string; large?: boolean }) {\n const Icon = documentKindIcon(kind);\n return (\n <span\n aria-hidden=\"true\"\n className={\n large\n ? \"grid size-10 shrink-0 place-items-center rounded-lg bg-muted text-muted-foreground\"\n : \"grid size-8 shrink-0 place-items-center rounded-lg bg-muted text-muted-foreground\"\n }\n >\n <Icon className={large ? \"size-5\" : \"size-4\"} />\n </span>\n );\n}\n\nexport function DocumentList({ documents }: DocumentListProps) {\n const t = useTranslations(\"artifacts\");\n const [items, setItems] = useState(documents);\n const [activeFilter, setActiveFilter] = useState<string>(\"all\");\n const [selectedId, setSelectedId] = useState<string | null>(null);\n const [view, setView] = useState<\"preview\" | \"source\">(\"preview\");\n\n // `/artifacts?document=<id>` — the chat canvas's \"Open in Artifacts\" —\n // lands on that document's preview.\n useEffect(() => {\n const id = new URLSearchParams(window.location.search).get(\"document\");\n if (id) setSelectedId(id);\n }, []);\n\n // Each document opens on its rendered form, not on whatever the last\n // one was left showing.\n useEffect(() => setView(\"preview\"), [selectedId]);\n\n /**\n * A known kind (text/code/sheet/image) resolves through this item's\n * own messages; a product-defined custom kind falls back to\n * capitalizing the raw value — `t.has` is the check next-intl exposes\n * for \"does this key exist\" without throwing.\n */\n function kindLabel(kind: string): string {\n const key = `filters.kind.${kind}`;\n return t.has(key) ? t(key) : kind.charAt(0).toUpperCase() + kind.slice(1);\n }\n\n /** \"Code · py\" — the same subtitle the chat's artifact card writes. */\n function subtitleOf(doc: ArtifactListItem): string {\n const extension = doc.kind === \"code\" ? extensionOf(doc.title) : undefined;\n return [kindLabel(doc.kind), extension].filter(Boolean).join(\" · \");\n }\n\n const kinds = useMemo(\n () => Array.from(new Set(items.map((doc) => doc.kind))).sort(),\n [items]\n );\n const hasReports = items.some((doc) => doc.isReport);\n const selected = items.find((doc) => doc.id === selectedId) ?? null;\n\n const filters: { type: string; label: string; count: number }[] = [\n { type: \"all\", label: t(\"filters.all\"), count: items.length },\n ...(hasReports\n ? [\n {\n type: \"report\",\n label: t(\"filters.reports\"),\n count: items.filter((doc) => doc.isReport).length,\n },\n ]\n : []),\n ...kinds.map((kind) => ({\n type: kind,\n label: kindLabel(kind),\n count: items.filter((doc) => doc.kind === kind).length,\n })),\n ];\n\n const filtered = items.filter((doc) => {\n if (activeFilter === \"all\") return true;\n if (activeFilter === \"report\") return doc.isReport;\n return doc.kind === activeFilter;\n });\n\n function handleDeleted(id: string) {\n setItems((prev) => prev.filter((doc) => doc.id !== id));\n setSelectedId((prev) => (prev === id ? null : prev));\n }\n\n if (items.length === 0) {\n return <EmptyState />;\n }\n\n const activeFilterLabel =\n filters.find((filter) => filter.type === activeFilter)?.label ??\n activeFilter;\n\n // An HTML page or an SVG has a rendered form worth offering; anything\n // else is only ever its source.\n const canPreview = selected ? isPreviewableTitle(selected.title) : false;\n\n return (\n <>\n {/* One segmented control, not a row of pill buttons: these are\n views of one list, and the counts say what is behind each. */}\n <div\n role=\"group\"\n aria-label={t(\"filters.all\")}\n className=\"flex w-fit max-w-full flex-wrap items-center gap-0.5 rounded-lg bg-muted p-0.5\"\n >\n {filters.map((filter) => {\n const active = activeFilter === filter.type;\n return (\n <button\n key={filter.type}\n type=\"button\"\n aria-pressed={active}\n onClick={() => setActiveFilter(filter.type)}\n className={`h-7 rounded-md px-2.5 text-xs font-medium outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring ${\n active\n ? \"bg-background text-foreground shadow-xs\"\n : \"text-muted-foreground hover:text-foreground\"\n }`}\n >\n {filter.label}\n <span className=\"ml-1.5 tabular-nums opacity-60\">\n {filter.count}\n </span>\n </button>\n );\n })}\n </div>\n\n {filtered.length === 0 ? (\n <FilteredEmptyState filterLabel={activeFilterLabel} />\n ) : (\n <AnimatedList\n as=\"div\"\n className=\"grid gap-4 sm:grid-cols-2 xl:grid-cols-3\"\n >\n {filtered.map((doc) => {\n const previewText = doc.content ? head(doc.content) : \"\";\n return (\n <AnimatedListItem\n as=\"div\"\n key={doc.id}\n className=\"group flex cursor-pointer flex-col overflow-hidden rounded-xl border bg-card transition-colors hover:border-foreground/20 focus-within:border-foreground/20\"\n onClick={() => setSelectedId(doc.id)}\n >\n {/* The document itself, clipped under a fade. Decorative:\n the title below is the accessible name. */}\n {doc.kind === \"image\" && doc.content ? (\n <div className=\"h-28 overflow-hidden border-b bg-muted/30\">\n <img\n src={doc.content}\n alt=\"\"\n aria-hidden=\"true\"\n className=\"size-full object-cover\"\n />\n </div>\n ) : previewText ? (\n <div\n aria-hidden=\"true\"\n className=\"h-28 overflow-hidden border-b bg-muted/30 px-4 pt-3 font-mono text-xs leading-4 whitespace-pre-wrap text-muted-foreground\"\n style={{\n maskImage: PREVIEW_MASK,\n WebkitMaskImage: PREVIEW_MASK,\n }}\n >\n {previewText}\n </div>\n ) : (\n <div className=\"grid h-28 place-items-center border-b bg-muted/30\">\n <File\n aria-hidden=\"true\"\n className=\"size-6 text-muted-foreground/40\"\n />\n </div>\n )}\n\n <div className=\"flex min-w-0 items-start gap-3 p-4\">\n <KindTile kind={doc.kind} />\n <div className=\"min-w-0 flex-1\">\n {/* A real button, so the card is reachable and\n named without nesting controls inside it. */}\n <button\n type=\"button\"\n onClick={() => setSelectedId(doc.id)}\n className=\"line-clamp-2 text-left text-sm leading-tight font-medium outline-none focus-visible:underline\"\n >\n {doc.title}\n </button>\n <p className=\"mt-1 truncate text-xs text-muted-foreground\">\n {subtitleOf(doc)}\n </p>\n <div className=\"mt-2 flex min-w-0 items-center gap-1.5 text-xs text-muted-foreground\">\n <Bot className=\"size-3 shrink-0\" />\n <span className=\"truncate\">{doc.agentLabel}</span>\n <span className=\"shrink-0\">·</span>\n <span className=\"shrink-0\">\n <RelativeTime iso={doc.createdAt} />\n </span>\n </div>\n </div>\n\n {doc.isReport ? (\n <Badge variant=\"secondary\">{t(\"badge.report\")}</Badge>\n ) : null}\n\n <DocumentActions\n documentId={doc.id}\n title={doc.title}\n kind={doc.kind}\n content={doc.content}\n createdAt={doc.createdAt}\n onDeleted={() => handleDeleted(doc.id)}\n buttonClassName=\"size-7 opacity-0 transition-opacity group-hover:opacity-100 focus-visible:opacity-100\"\n />\n </div>\n </AnimatedListItem>\n );\n })}\n </AnimatedList>\n )}\n\n <Dialog\n open={selected !== null}\n onOpenChange={(open) => !open && setSelectedId(null)}\n >\n {selected && (\n /* `sm:` matters: DialogContent's base classes include\n `sm:max-w-sm`, which tailwind-merge does not replace with an\n unprefixed `max-w-*`, so that would win above 640px. */\n <DialogContent className=\"flex max-h-[80vh] w-full flex-col sm:max-w-5xl\">\n <DialogHeader>\n <div className=\"flex items-start gap-3 pr-8\">\n <KindTile kind={selected.kind} large />\n <div className=\"min-w-0 flex-1\">\n <DialogTitle className=\"text-left leading-tight\">\n {selected.title}\n </DialogTitle>\n <p className=\"mt-1 truncate text-xs text-muted-foreground\">\n {subtitleOf(selected)} · {selected.agentLabel} ·{\" \"}\n <RelativeTime iso={selected.createdAt} />\n </p>\n </div>\n </div>\n\n {/* Second row, as in the chat canvas: how to look at the\n document, then what to do with it. */}\n <div className=\"flex min-w-0 flex-wrap items-center gap-2 pt-1\">\n {canPreview ? (\n <div\n role=\"group\"\n aria-label={t(\"preview.view\")}\n className=\"flex items-center rounded-lg bg-muted p-0.5\"\n >\n {([\"preview\", \"source\"] as const).map((option) => (\n <button\n key={option}\n type=\"button\"\n aria-pressed={view === option}\n onClick={() => setView(option)}\n className={`h-6 rounded-md px-2 text-xs font-medium outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring ${\n view === option\n ? \"bg-background text-foreground shadow-xs\"\n : \"text-muted-foreground hover:text-foreground\"\n }`}\n >\n {option === \"preview\"\n ? t(\"preview.rendered\")\n : t(\"preview.source\")}\n </button>\n ))}\n </div>\n ) : null}\n\n <div className=\"ml-auto flex items-center gap-1\">\n {selected.conversationId ? (\n <Button\n size=\"sm\"\n variant=\"outline\"\n render={\n <Link href={`/chat/${selected.conversationId}`} />\n }\n nativeButton={false}\n >\n <ExternalLink data-icon=\"inline-start\" />\n {t(\"preview.openConversation\")}\n </Button>\n ) : null}\n <DocumentActions\n documentId={selected.id}\n title={selected.title}\n kind={selected.kind}\n content={selected.content}\n createdAt={selected.createdAt}\n onDeleted={() => handleDeleted(selected.id)}\n />\n </div>\n </div>\n </DialogHeader>\n\n {/* `flex flex-col` is what makes this scroll: ScrollArea's\n viewport is `size-full`, and a percentage height against\n a shrunk flex item resolves to `auto`, so a long document\n would run past the dialog. A flex parent sizes it by\n layout instead. */}\n <ScrollArea className=\"mt-2 flex min-h-0 flex-1 flex-col\">\n {selected.content ? (\n canPreview && view === \"preview\" ? (\n <HtmlPreview\n content={selected.content}\n title={selected.title}\n />\n ) : (\n <DocumentView\n kind={selected.kind}\n title={selected.title}\n content={selected.content}\n />\n )\n ) : (\n <p className=\"text-sm italic text-muted-foreground\">\n {t(\"noContent\")}\n </p>\n )}\n </ScrollArea>\n </DialogContent>\n )}\n </Dialog>\n </>\n );\n}\n\nfunction EmptyState() {\n const t = useTranslations(\"artifacts\");\n return (\n <div className=\"flex flex-col items-center justify-center py-16 text-center\">\n <File className=\"mb-4 size-12 text-muted-foreground/40\" />\n <h3 className=\"mb-1 text-lg font-semibold\">{t(\"emptyState.title\")}</h3>\n <p className=\"max-w-sm text-sm text-muted-foreground\">\n {t(\"emptyState.description\")}\n </p>\n {/* An empty artifacts page is a dead end without this: nothing\n on it produces an artifact — the chat surface does. */}\n <Button\n size=\"sm\"\n className=\"mt-4\"\n render={<Link href=\"/chat\" />}\n nativeButton={false}\n >\n {t(\"emptyState.cta\")}\n </Button>\n </div>\n );\n}\n\nfunction FilteredEmptyState({ filterLabel }: { filterLabel: string }) {\n const t = useTranslations(\"artifacts\");\n const lowerFilterLabel = filterLabel.toLowerCase();\n return (\n <div className=\"flex flex-col items-center justify-center py-16 text-center\">\n <File className=\"mb-4 size-12 text-muted-foreground/40\" />\n <h3 className=\"mb-1 text-lg font-semibold\">\n {t(\"filteredEmptyState.title\", { filterLabel: lowerFilterLabel })}\n </h3>\n <p className=\"text-sm text-muted-foreground\">\n {t(\"filteredEmptyState.description\", { filterLabel })}\n </p>\n </div>\n );\n}\n",
62
+ "type": "registry:component",
63
+ "target": "components/artifacts/document-list.tsx"
64
+ },
65
+ {
66
+ "path": "base/artifacts/components/document-actions.tsx",
67
+ "content": "\"use client\";\n\n/**\n * Per-artifact actions: copy, download, and delete. Delete reverts to an\n * earlier saved version when one exists rather than removing the\n * artifact, and refuses artifacts older than 30 days; the confirm dialog\n * says so.\n */\n\nimport { useState, useTransition } from \"react\";\nimport { useTranslations } from \"next-intl\";\nimport { Check, Copy, Download, Trash2 } from \"lucide-react\";\nimport { toast } from \"sonner\";\n\nimport { useRouter } from \"@/i18n/navigation\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n} from \"@/components/ui/dialog\";\nimport {\n documentKindExtension,\n fileNameOf,\n} from \"@/components/ui/document-viewer\";\nimport { Spinner } from \"@/components/ui/spinner\";\n\nimport { deleteLatestVersion } from \"@/actions/documents\";\n\ninterface DocumentActionsProps {\n documentId: string;\n title: string;\n kind: string;\n content: string | null;\n createdAt: string;\n /** Called after a successful delete, e.g. to drop the item from local state. */\n onDeleted?: () => void;\n buttonClassName?: string;\n}\n\nexport function DocumentActions({\n documentId,\n title,\n kind,\n content,\n createdAt,\n onDeleted,\n buttonClassName,\n}: DocumentActionsProps) {\n const t = useTranslations(\"artifacts\");\n const router = useRouter();\n const [copied, setCopied] = useState(false);\n const [confirmOpen, setConfirmOpen] = useState(false);\n const [isDeleting, startDeleting] = useTransition();\n\n /**\n * Saved the way the canvas saves it: an image is already a URL, and\n * everything else becomes a blob typed by its kind, so a sheet lands\n * as a `.csv` a spreadsheet will open rather than as plain text.\n */\n function handleDownload() {\n if (!content) return;\n const link = document.createElement(\"a\");\n if (kind === \"image\") {\n link.href = content;\n } else {\n link.href = URL.createObjectURL(\n new Blob([content], {\n type: kind === \"sheet\" ? \"text/csv\" : \"text/plain\",\n })\n );\n }\n link.download = fileNameOf(title, documentKindExtension(kind));\n link.click();\n if (link.href.startsWith(\"blob:\")) URL.revokeObjectURL(link.href);\n }\n\n async function handleCopy() {\n if (!content) return;\n try {\n await navigator.clipboard.writeText(content);\n setCopied(true);\n toast.success(t(\"documentActions.copySuccess\"));\n setTimeout(() => setCopied(false), 2000);\n } catch {\n toast.error(t(\"documentActions.copyErrorTitle\"), {\n description: t(\"documentActions.copyErrorDescription\"),\n });\n }\n }\n\n function handleDelete() {\n startDeleting(async () => {\n const result = await deleteLatestVersion(documentId, createdAt);\n if (!result.success) {\n toast.error(result.error);\n return;\n }\n toast.success(t(\"documentActions.deleteSuccess\"));\n setConfirmOpen(false);\n onDeleted?.();\n router.refresh();\n });\n }\n\n return (\n <div className=\"flex items-center gap-1\">\n {content && (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon\"\n onClick={(event) => {\n event.stopPropagation();\n void handleCopy();\n }}\n className={buttonClassName}\n title={\n copied\n ? t(\"documentActions.copyTooltipCopied\")\n : t(\"documentActions.copyTooltipDefault\")\n }\n >\n {copied ? (\n <Check className=\"size-4 text-success\" />\n ) : (\n <Copy className=\"size-4\" />\n )}\n </Button>\n )}\n\n {content && (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon\"\n onClick={(event) => {\n event.stopPropagation();\n handleDownload();\n }}\n className={buttonClassName}\n title={t(\"documentActions.downloadTooltip\")}\n >\n <Download className=\"size-4\" />\n </Button>\n )}\n\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon\"\n onClick={(event) => {\n event.stopPropagation();\n setConfirmOpen(true);\n }}\n className={buttonClassName}\n title={t(\"documentActions.deleteTooltip\")}\n >\n <Trash2 className=\"size-4\" />\n </Button>\n\n <Dialog open={confirmOpen} onOpenChange={setConfirmOpen}>\n <DialogContent onClick={(event) => event.stopPropagation()}>\n <DialogHeader>\n <DialogTitle>{t(\"documentActions.deleteDialogTitle\")}</DialogTitle>\n <DialogDescription>\n {t(\"documentActions.deleteDialogDescription\")}\n </DialogDescription>\n </DialogHeader>\n <DialogFooter>\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={() => setConfirmOpen(false)}\n disabled={isDeleting}\n >\n {t(\"documentActions.cancel\")}\n </Button>\n <Button\n type=\"button\"\n variant=\"destructive\"\n onClick={handleDelete}\n disabled={isDeleting}\n >\n {isDeleting ? <Spinner /> : t(\"documentActions.delete\")}\n </Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n </div>\n );\n}\n",
68
+ "type": "registry:component",
69
+ "target": "components/artifacts/document-actions.tsx"
70
+ },
71
+ {
72
+ "path": "base/artifacts/messages/en.json",
73
+ "content": "{\n \"page\": {\n \"title\": \"Artifacts\",\n \"heading\": \"Artifacts\",\n \"description\": \"All the documents your AI agents have created for you, in one place.\",\n \"unavailableTitle\": \"Artifacts unavailable\"\n },\n \"actions\": {\n \"notFound\": \"This artifact no longer exists.\",\n \"forbidden\": \"You don't have permission to change this artifact.\",\n \"invalidInput\": \"This artifact is more than 30 days old and can't be deleted from here.\",\n \"databaseError\": \"Something went wrong. Please try again.\",\n \"genericError\": \"Something went wrong.\",\n \"invalidTimestamp\": \"Invalid artifact timestamp.\"\n },\n \"error\": {\n \"description\": \"Something went wrong while loading your artifacts. Please try again.\",\n \"retry\": \"Try again\",\n \"goHome\": \"Go home\"\n },\n \"filters\": {\n \"all\": \"All\",\n \"reports\": \"Reports\",\n \"kind\": {\n \"text\": \"Text\",\n \"code\": \"Code\",\n \"sheet\": \"Sheet\",\n \"image\": \"Image\"\n }\n },\n \"badge\": {\n \"report\": \"Report\"\n },\n \"emptyState\": {\n \"title\": \"No artifacts yet\",\n \"description\": \"Artifacts your AI agents create — reports, code, text, and more — will show up here.\",\n \"cta\": \"Start a chat\"\n },\n \"filteredEmptyState\": {\n \"title\": \"No {filterLabel} artifacts\",\n \"description\": \"No artifacts match the “{filterLabel}” filter. Try a different filter.\"\n },\n \"preview\": {\n \"view\": \"View\",\n \"rendered\": \"Preview\",\n \"source\": \"Source\",\n \"openConversation\": \"Open conversation\"\n },\n \"documentActions\": {\n \"copySuccess\": \"Copied to clipboard\",\n \"copyErrorTitle\": \"Failed to copy\",\n \"copyErrorDescription\": \"Could not copy to clipboard. Please try again.\",\n \"copyTooltipCopied\": \"Copied!\",\n \"copyTooltipDefault\": \"Copy to clipboard\",\n \"downloadTooltip\": \"Download\",\n \"deleteTooltip\": \"Delete\",\n \"deleteDialogTitle\": \"Delete artifact\",\n \"deleteDialogDescription\": \"This removes the version shown here. If this artifact has earlier saved versions, it reverts to the previous one instead of disappearing entirely. Artifacts older than 30 days can't be deleted from this page.\",\n \"cancel\": \"Cancel\",\n \"delete\": \"Delete\",\n \"deleteSuccess\": \"Artifact deleted\"\n },\n \"noContent\": \"No content available for this artifact.\"\n}\n",
74
+ "type": "registry:file",
75
+ "target": "messages/en/artifacts.json"
76
+ }
77
+ ],
78
+ "css": {
79
+ "@source \"../node_modules/streamdown/dist/*.js\"": {}
80
+ },
81
+ "type": "registry:block"
82
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "attachment",
4
+ "title": "Attachment",
5
+ "description": "A file attached to a message or a prompt.",
6
+ "dependencies": [
7
+ "@base-ui/react",
8
+ "class-variance-authority"
9
+ ],
10
+ "registryDependencies": [
11
+ "@intelligo/button"
12
+ ],
13
+ "files": [
14
+ {
15
+ "path": "base/ui/attachment/attachment.tsx",
16
+ "content": "import * as React from \"react\";\nimport { mergeProps } from \"@base-ui/react/merge-props\";\nimport { useRender } from \"@base-ui/react/use-render\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\n\nimport { Button } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\n\nconst attachmentVariants = cva(\n \"group/attachment relative flex w-fit max-w-full min-w-0 shrink-0 flex-wrap rounded-lg border bg-card text-card-foreground transition-colors focus-within:ring-1 focus-within:ring-ring/50 has-[>a,>button]:hover:bg-muted/50 data-[state=error]:border-destructive/30 data-[state=idle]:border-dashed\",\n {\n variants: {\n size: {\n default:\n \"gap-2 text-sm has-data-[slot=attachment-content]:px-2.5 has-data-[slot=attachment-content]:py-2 has-data-[slot=attachment-media]:p-2\",\n sm: \"gap-2.5 text-xs has-data-[slot=attachment-content]:px-2 has-data-[slot=attachment-content]:py-1.5 has-data-[slot=attachment-media]:p-1.5\",\n xs: \"gap-1.5 rounded-lg text-xs has-data-[slot=attachment-content]:px-1.5 has-data-[slot=attachment-content]:py-1 has-data-[slot=attachment-media]:p-1\",\n },\n orientation: {\n horizontal: \"min-w-40 items-center\",\n vertical: \"w-24 flex-col has-data-[slot=attachment-content]:w-30\",\n },\n },\n }\n);\n\nfunction Attachment({\n className,\n state = \"done\",\n size = \"default\",\n orientation = \"horizontal\",\n ...props\n}: React.ComponentProps<\"div\"> &\n VariantProps<typeof attachmentVariants> & {\n state?: \"idle\" | \"uploading\" | \"processing\" | \"error\" | \"done\";\n }) {\n return (\n <div\n data-slot=\"attachment\"\n data-state={state}\n data-size={size}\n data-orientation={orientation}\n className={cn(attachmentVariants({ size, orientation }), className)}\n {...props}\n />\n );\n}\n\nconst attachmentMediaVariants = cva(\n \"relative flex aspect-square w-10 shrink-0 items-center justify-center overflow-hidden rounded-lg bg-muted text-foreground group-data-[orientation=vertical]/attachment:w-full group-data-[size=sm]/attachment:w-8 group-data-[size=xs]/attachment:w-7 group-data-[size=xs]/attachment:rounded-md group-data-[state=error]/attachment:bg-destructive/10 group-data-[state=error]/attachment:text-destructive group-data-[orientation=vertical]/attachment:*:data-[slot=spinner]:size-6! [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 group-data-[orientation=vertical]/attachment:[&_svg:not([class*='size-'])]:size-6 group-data-[size=xs]/attachment:[&_svg:not([class*='size-'])]:size-3.5\",\n {\n variants: {\n variant: {\n icon: \"\",\n image:\n \"opacity-60 group-data-[state=done]/attachment:opacity-100 group-data-[state=idle]/attachment:opacity-100 *:[img]:aspect-square *:[img]:w-full *:[img]:object-cover\",\n },\n },\n defaultVariants: {\n variant: \"icon\",\n },\n }\n);\n\nfunction AttachmentMedia({\n className,\n variant = \"icon\",\n ...props\n}: React.ComponentProps<\"div\"> & VariantProps<typeof attachmentMediaVariants>) {\n return (\n <div\n data-slot=\"attachment-media\"\n data-variant={variant}\n className={cn(attachmentMediaVariants({ variant }), className)}\n {...props}\n />\n );\n}\n\nfunction AttachmentContent({\n className,\n ...props\n}: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"attachment-content\"\n className={cn(\n \"max-w-full min-w-0 flex-1 leading-tight group-data-[orientation=vertical]/attachment:px-1\",\n className\n )}\n {...props}\n />\n );\n}\n\nfunction AttachmentTitle({\n className,\n ...props\n}: React.ComponentProps<\"span\">) {\n return (\n <span\n data-slot=\"attachment-title\"\n className={cn(\n \"block max-w-full min-w-0 truncate font-medium group-data-[state=processing]/attachment:shimmer group-data-[state=uploading]/attachment:shimmer\",\n className\n )}\n {...props}\n />\n );\n}\n\nfunction AttachmentDescription({\n className,\n ...props\n}: React.ComponentProps<\"span\">) {\n return (\n <span\n data-slot=\"attachment-description\"\n className={cn(\n \"mt-0.5 block min-w-0 truncate text-xs text-muted-foreground group-data-[state=error]/attachment:text-destructive/80\",\n \"max-w-full\",\n className\n )}\n {...props}\n />\n );\n}\n\nfunction AttachmentActions({\n className,\n ...props\n}: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"attachment-actions\"\n className={cn(\n \"relative z-20 flex shrink-0 items-center group-data-[orientation=vertical]/attachment:absolute group-data-[orientation=vertical]/attachment:top-3 group-data-[orientation=vertical]/attachment:right-3 group-data-[orientation=vertical]/attachment:gap-1\",\n className\n )}\n {...props}\n />\n );\n}\n\nfunction AttachmentAction({\n className,\n variant,\n size = \"icon-xs\",\n ...props\n}: React.ComponentProps<typeof Button>) {\n return (\n <Button\n data-slot=\"attachment-action\"\n variant={variant ?? \"ghost\"}\n size={size}\n className={cn(className)}\n {...props}\n />\n );\n}\n\nfunction AttachmentTrigger({\n className,\n render,\n type,\n ...props\n}: useRender.ComponentProps<\"button\">) {\n return useRender({\n defaultTagName: \"button\",\n props: mergeProps<\"button\">(\n {\n type: render ? type : (type ?? \"button\"),\n className: cn(\"absolute inset-0 z-10 outline-none\", className),\n },\n props\n ),\n render,\n state: {\n slot: \"attachment-trigger\",\n },\n });\n}\n\nfunction AttachmentGroup({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"attachment-group\"\n className={cn(\n \"flex min-w-0 scroll-fade-x snap-x snap-mandatory scroll-px-1 scrollbar-none gap-3 overflow-x-auto overscroll-x-contain py-1 *:data-[slot=attachment]:flex-none *:data-[slot=attachment]:snap-start\",\n className\n )}\n {...props}\n />\n );\n}\n\nexport {\n Attachment,\n AttachmentGroup,\n AttachmentMedia,\n AttachmentContent,\n AttachmentTitle,\n AttachmentDescription,\n AttachmentActions,\n AttachmentAction,\n AttachmentTrigger,\n};\n",
17
+ "type": "registry:ui",
18
+ "target": "components/ui/attachment.tsx"
19
+ }
20
+ ],
21
+ "type": "registry:ui"
22
+ }
@@ -0,0 +1,39 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "auth-email-verification",
4
+ "title": "Auth: Email Verification",
5
+ "description": "Post-signup email verification status page with a resend control, backed by the Better-Auth client SDK's sendVerificationEmail action. Reads the ?error=/?email= search params Better-Auth's own /verify-email link handler redirects back with. Requires the auth-login item (provides the shared AuthCard shell); install it first.",
6
+ "dependencies": [
7
+ "@intelligo-dev/auth",
8
+ "lucide-react",
9
+ "next-intl"
10
+ ],
11
+ "registryDependencies": [
12
+ "alert",
13
+ "@intelligo/button",
14
+ "@intelligo/input",
15
+ "@intelligo/spinner",
16
+ "field"
17
+ ],
18
+ "files": [
19
+ {
20
+ "path": "base/auth-email-verification/page.tsx",
21
+ "content": "import type { Metadata } from \"next\";\nimport { getTranslations } from \"next-intl/server\";\n\nimport { getAuthSession } from \"@intelligo-dev/auth\";\n\nimport { Link } from \"@/i18n/navigation\";\nimport { AuthCard } from \"@/components/auth/auth-card\";\nimport { Alert, AlertDescription } from \"@/components/ui/alert\";\nimport { ResendVerificationButton } from \"@/components/auth/resend-verification-button\";\n\nexport async function generateMetadata(): Promise<Metadata> {\n const t = await getTranslations(\"auth-email-verification\");\n return {\n title: t(\"metadata.title\"),\n description: t(\"metadata.description\"),\n };\n}\n\ninterface Props {\n searchParams: Promise<{ error?: string; email?: string }>;\n}\n\n/**\n * Better-Auth's `/verify-email` link handler (clicked from the emailed\n * link, not called from this page) redirects back here with `?error=CODE`\n * on an invalid/expired/unknown token, or with no query param at all —\n * plus a fresh session cookie — on success. That gives this page three\n * honestly distinguishable states without guessing: verified (a session\n * exists), failed (an `error` code came back), or still pending (neither\n * — the visitor hasn't clicked the link yet).\n */\nfunction errorMessageFor(\n error: string | undefined,\n t: Awaited<ReturnType<typeof getTranslations>>\n): string | null {\n if (!error) return null;\n const knownCodes = [\"INVALID_TOKEN\", \"TOKEN_EXPIRED\", \"USER_NOT_FOUND\"];\n const key = knownCodes.includes(error) ? error : \"UNKNOWN\";\n return t(`errors.${key}`);\n}\n\nexport default async function VerifyEmailPage({ searchParams }: Props) {\n const { error, email } = await searchParams;\n const authSession = await getAuthSession();\n const t = await getTranslations(\"auth-email-verification\");\n\n if (authSession?.user.emailVerified) {\n return (\n <AuthCard\n title={t(\"verifiedState.title\")}\n description={t(\"verifiedState.description\")}\n >\n <div className=\"space-y-4\">\n <Alert>\n <AlertDescription>{t(\"verifiedState.message\")}</AlertDescription>\n </Alert>\n <Link\n href=\"/dashboard\"\n className=\"block text-center text-sm font-medium text-primary hover:text-primary/80\"\n >\n {t(\"verifiedState.dashboardLink\")}\n </Link>\n </div>\n </AuthCard>\n );\n }\n\n const errorMessage = errorMessageFor(error, t);\n\n return (\n <AuthCard\n title={errorMessage ? t(\"failedState.title\") : t(\"pendingState.title\")}\n description={errorMessage ?? t(\"pendingState.description\")}\n >\n <div className=\"space-y-4\">\n {errorMessage && (\n <Alert variant=\"destructive\">\n <AlertDescription>{errorMessage}</AlertDescription>\n </Alert>\n )}\n\n <ResendVerificationButton email={email} />\n\n <div className=\"text-center\">\n <Link\n href=\"/login\"\n className=\"text-sm text-muted-foreground hover:text-primary\"\n >\n {t(\"backToLogin\")}\n </Link>\n </div>\n </div>\n </AuthCard>\n );\n}\n",
22
+ "type": "registry:page",
23
+ "target": "app/[locale]/(auth)/verify-email/page.tsx"
24
+ },
25
+ {
26
+ "path": "base/auth-email-verification/components/resend-verification-button.tsx",
27
+ "content": "\"use client\";\n\n/**\n * Resends a verification email through Better-Auth's\n * `sendVerificationEmail`. The endpoint works with or without a session\n * and answers the same whether or not the address is registered. When\n * `email` is passed (e.g. from the signup redirect's `?email=`) this\n * renders just the button; otherwise it asks for the address first.\n */\n\nimport { useState } from \"react\";\nimport { useTranslations } from \"next-intl\";\n\nimport { authClient } from \"@intelligo-dev/auth/client\";\n\nimport { Alert, AlertDescription } from \"@/components/ui/alert\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { Field, FieldLabel } from \"@/components/ui/field\";\nimport { Spinner } from \"@/components/ui/spinner\";\n\ninterface ResendVerificationButtonProps {\n email?: string;\n}\n\nexport function ResendVerificationButton({\n email: knownEmail,\n}: ResendVerificationButtonProps) {\n const t = useTranslations(\"auth-email-verification\");\n const [email, setEmail] = useState(knownEmail ?? \"\");\n const [isResending, setIsResending] = useState(false);\n const [resendSuccess, setResendSuccess] = useState(false);\n const [resendError, setResendError] = useState<string | null>(null);\n\n async function handleResend() {\n if (!email) {\n setResendError(t(\"resend.errors.missingEmail\"));\n return;\n }\n\n try {\n setIsResending(true);\n setResendError(null);\n setResendSuccess(false);\n\n const result = await authClient.sendVerificationEmail({\n email,\n callbackURL: \"/verify-email\",\n });\n\n if (result.error) {\n setResendError(result.error.message ?? t(\"resend.errors.unexpected\"));\n return;\n }\n\n setResendSuccess(true);\n } catch (error) {\n console.error(\"Resend verification error:\", error);\n setResendError(t(\"resend.errors.unexpected\"));\n } finally {\n setIsResending(false);\n }\n }\n\n return (\n <div className=\"space-y-4\">\n {!knownEmail && (\n <Field>\n <FieldLabel htmlFor=\"resend-email\">\n {t(\"resend.emailLabel\")}\n </FieldLabel>\n <Input\n id=\"resend-email\"\n type=\"email\"\n autoComplete=\"email\"\n value={email}\n onChange={(event) => setEmail(event.target.value)}\n disabled={isResending || resendSuccess}\n aria-invalid={resendError ? true : undefined}\n aria-describedby={resendError ? \"resend-email-error\" : undefined}\n />\n </Field>\n )}\n\n {resendSuccess && (\n <Alert>\n <AlertDescription>{t(\"resend.success\")}</AlertDescription>\n </Alert>\n )}\n\n {resendError && (\n <Alert id=\"resend-email-error\" variant=\"destructive\">\n <AlertDescription>{resendError}</AlertDescription>\n </Alert>\n )}\n\n <Button\n type=\"button\"\n variant=\"outline\"\n className=\"w-full\"\n onClick={handleResend}\n disabled={isResending || resendSuccess}\n aria-busy={isResending || undefined}\n >\n {isResending ? (\n <>\n <Spinner data-icon=\"inline-start\" />\n {t(\"resend.sending\")}\n </>\n ) : resendSuccess ? (\n t(\"resend.sent\")\n ) : (\n t(\"resend.submit\")\n )}\n </Button>\n </div>\n );\n}\n",
28
+ "type": "registry:component",
29
+ "target": "components/auth/resend-verification-button.tsx"
30
+ },
31
+ {
32
+ "path": "base/auth-email-verification/messages/en.json",
33
+ "content": "{\n \"metadata\": {\n \"title\": \"Verify your email\",\n \"description\": \"Confirm your email address.\"\n },\n \"verifiedState\": {\n \"title\": \"Email verified\",\n \"description\": \"Your email address has been confirmed\",\n \"message\": \"You're all set — your account is fully active.\",\n \"dashboardLink\": \"Go to dashboard\"\n },\n \"pendingState\": {\n \"title\": \"Verify your email\",\n \"description\": \"We've sent a verification link to your email. Click it to activate your account.\"\n },\n \"failedState\": {\n \"title\": \"Verification failed\"\n },\n \"backToLogin\": \"Back to login\",\n \"errors\": {\n \"INVALID_TOKEN\": \"This verification link is invalid. Request a new one below.\",\n \"TOKEN_EXPIRED\": \"This verification link has expired. Request a new one below.\",\n \"USER_NOT_FOUND\": \"We couldn't find an account for this verification link.\",\n \"UNKNOWN\": \"This verification link could not be used. Request a new one below.\"\n },\n \"resend\": {\n \"emailLabel\": \"Email\",\n \"submit\": \"Resend verification email\",\n \"sending\": \"Sending…\",\n \"sent\": \"Verification email sent\",\n \"success\": \"Verification email sent. Check your inbox.\",\n \"errors\": {\n \"missingEmail\": \"Enter your email address first.\",\n \"unexpected\": \"Failed to send verification email.\"\n }\n }\n}\n",
34
+ "type": "registry:file",
35
+ "target": "messages/en/auth-email-verification.json"
36
+ }
37
+ ],
38
+ "type": "registry:block"
39
+ }