@intelligo-dev/cli 1.0.0-beta.13 → 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 (171) hide show
  1. package/README.md +41 -2
  2. package/dist/bin.js +37 -0
  3. package/dist/bin.js.map +1 -1
  4. package/dist/commands/add.d.ts +4 -0
  5. package/dist/commands/add.d.ts.map +1 -1
  6. package/dist/commands/add.js +9 -0
  7. package/dist/commands/add.js.map +1 -1
  8. package/dist/commands/create-flow.d.ts +4 -1
  9. package/dist/commands/create-flow.d.ts.map +1 -1
  10. package/dist/commands/create-flow.js +63 -31
  11. package/dist/commands/create-flow.js.map +1 -1
  12. package/dist/commands/create.d.ts +8 -4
  13. package/dist/commands/create.d.ts.map +1 -1
  14. package/dist/commands/create.js +15 -7
  15. package/dist/commands/create.js.map +1 -1
  16. package/dist/commands/doctor.d.ts.map +1 -1
  17. package/dist/commands/doctor.js +21 -19
  18. package/dist/commands/doctor.js.map +1 -1
  19. package/dist/commands/migrate-check.d.ts +20 -1
  20. package/dist/commands/migrate-check.d.ts.map +1 -1
  21. package/dist/commands/migrate-check.js +35 -6
  22. package/dist/commands/migrate-check.js.map +1 -1
  23. package/dist/commands/migrate.d.ts.map +1 -1
  24. package/dist/commands/migrate.js +2 -8
  25. package/dist/commands/migrate.js.map +1 -1
  26. package/dist/commands/sync-messages.d.ts +37 -0
  27. package/dist/commands/sync-messages.d.ts.map +1 -0
  28. package/dist/commands/sync-messages.js +88 -0
  29. package/dist/commands/sync-messages.js.map +1 -0
  30. package/dist/commands/sync-scaffold.d.ts +36 -0
  31. package/dist/commands/sync-scaffold.d.ts.map +1 -0
  32. package/dist/commands/sync-scaffold.js +53 -0
  33. package/dist/commands/sync-scaffold.js.map +1 -0
  34. package/dist/commands/sync.d.ts +103 -0
  35. package/dist/commands/sync.d.ts.map +1 -0
  36. package/dist/commands/sync.js +361 -0
  37. package/dist/commands/sync.js.map +1 -0
  38. package/dist/commands/upgrade-check.d.ts.map +1 -1
  39. package/dist/commands/upgrade-check.js +4 -1
  40. package/dist/commands/upgrade-check.js.map +1 -1
  41. package/dist/manifest.d.ts +27 -0
  42. package/dist/manifest.d.ts.map +1 -1
  43. package/dist/manifest.js +30 -3
  44. package/dist/manifest.js.map +1 -1
  45. package/dist/module-exports.d.ts +63 -0
  46. package/dist/module-exports.d.ts.map +1 -0
  47. package/dist/module-exports.js +231 -0
  48. package/dist/module-exports.js.map +1 -0
  49. package/dist/registry-bundle.d.ts +43 -0
  50. package/dist/registry-bundle.d.ts.map +1 -0
  51. package/dist/registry-bundle.js +74 -0
  52. package/dist/registry-bundle.js.map +1 -0
  53. package/dist/registry-items.d.ts +41 -13
  54. package/dist/registry-items.d.ts.map +1 -1
  55. package/dist/registry-items.js +117 -33
  56. package/dist/registry-items.js.map +1 -1
  57. package/package.json +1 -1
  58. package/src/bin.ts +52 -0
  59. package/src/commands/add.ts +11 -0
  60. package/src/commands/create-flow.ts +82 -26
  61. package/src/commands/create.ts +17 -7
  62. package/src/commands/doctor.ts +27 -27
  63. package/src/commands/migrate-check.ts +51 -10
  64. package/src/commands/migrate.ts +2 -8
  65. package/src/commands/sync-messages.ts +114 -0
  66. package/src/commands/sync-scaffold.ts +83 -0
  67. package/src/commands/sync.ts +534 -0
  68. package/src/commands/upgrade-check.ts +4 -1
  69. package/src/manifest.ts +57 -3
  70. package/src/module-exports.ts +266 -0
  71. package/src/registry-bundle.ts +103 -0
  72. package/src/registry-items.ts +141 -36
  73. package/templates/app-scaffold/intelligo.ts.tpl +6 -47
  74. package/templates/manifest.json +19 -0
  75. package/templates/registry/ai-agent-activity.json +23 -0
  76. package/templates/registry/ai-agent-progress.json +21 -0
  77. package/templates/registry/ai-approval-card.json +27 -0
  78. package/templates/registry/ai-artifact.json +22 -0
  79. package/templates/registry/ai-branch.json +21 -0
  80. package/templates/registry/ai-citations.json +24 -0
  81. package/templates/registry/ai-code-block.json +23 -0
  82. package/templates/registry/ai-composer-menu.json +19 -0
  83. package/templates/registry/ai-file-diff.json +25 -0
  84. package/templates/registry/ai-image-generation.json +23 -0
  85. package/templates/registry/ai-markdown.json +23 -0
  86. package/templates/registry/ai-message-bubble.json +23 -0
  87. package/templates/registry/ai-message-scroller.json +22 -0
  88. package/templates/registry/ai-message.json +21 -0
  89. package/templates/registry/ai-motion.json +19 -0
  90. package/templates/registry/ai-prompt-input.json +28 -0
  91. package/templates/registry/ai-reasoning-text.json +22 -0
  92. package/templates/registry/ai-reasoning.json +22 -0
  93. package/templates/registry/ai-shimmer-text.json +19 -0
  94. package/templates/registry/ai-sidebar.json +25 -0
  95. package/templates/registry/ai-speech-input.json +21 -0
  96. package/templates/registry/ai-streaming-response.json +24 -0
  97. package/templates/registry/ai-suggestion.json +19 -0
  98. package/templates/registry/ai-todo-list.json +22 -0
  99. package/templates/registry/ai-tool-approval.json +26 -0
  100. package/templates/registry/ai-tool-result.json +25 -0
  101. package/templates/registry/alert-dialog.json +23 -0
  102. package/templates/registry/animated-list.json +21 -0
  103. package/templates/registry/app-shell.json +93 -0
  104. package/templates/registry/artifacts.json +82 -0
  105. package/templates/registry/attachment.json +22 -0
  106. package/templates/registry/auth-email-verification.json +39 -0
  107. package/templates/registry/auth-login.json +72 -0
  108. package/templates/registry/auth-password-reset.json +53 -0
  109. package/templates/registry/auth-signup.json +41 -0
  110. package/templates/registry/billing-settings.json +60 -0
  111. package/templates/registry/button.json +22 -0
  112. package/templates/registry/chat-eve.json +19 -0
  113. package/templates/registry/chat-panel.json +30 -0
  114. package/templates/registry/chat-share.json +42 -0
  115. package/templates/registry/chat-widget.json +34 -0
  116. package/templates/registry/chat.json +326 -0
  117. package/templates/registry/checkbox.json +22 -0
  118. package/templates/registry/checkout.json +40 -0
  119. package/templates/registry/collapsible.json +19 -0
  120. package/templates/registry/command.json +23 -0
  121. package/templates/registry/copy-button.json +21 -0
  122. package/templates/registry/dashboard.json +69 -0
  123. package/templates/registry/dialog.json +24 -0
  124. package/templates/registry/document-viewer.json +22 -0
  125. package/templates/registry/dropdown-menu.json +23 -0
  126. package/templates/registry/expandable-tabs.json +22 -0
  127. package/templates/registry/feature-gating.json +73 -0
  128. package/templates/registry/file-upload.json +24 -0
  129. package/templates/registry/hold-action-button.json +22 -0
  130. package/templates/registry/input-group.json +23 -0
  131. package/templates/registry/input.json +19 -0
  132. package/templates/registry/intelligo.json +187 -0
  133. package/templates/registry/invitation-accept.json +64 -0
  134. package/templates/registry/language-switcher.json +29 -0
  135. package/templates/registry/morphing-modal.json +24 -0
  136. package/templates/registry/notification-stack.json +24 -0
  137. package/templates/registry/notifications.json +87 -0
  138. package/templates/registry/onboarding.json +83 -0
  139. package/templates/registry/otp-input.json +22 -0
  140. package/templates/registry/page-header.json +15 -0
  141. package/templates/registry/payment-poll.json +52 -0
  142. package/templates/registry/popover-morph.json +22 -0
  143. package/templates/registry/popover.json +22 -0
  144. package/templates/registry/pricing.json +111 -0
  145. package/templates/registry/privacy-settings.json +65 -0
  146. package/templates/registry/profile-settings.json +68 -0
  147. package/templates/registry/progress.json +19 -0
  148. package/templates/registry/radio-group.json +22 -0
  149. package/templates/registry/registry.json +2945 -0
  150. package/templates/registry/route-error.json +65 -0
  151. package/templates/registry/select-morph.json +23 -0
  152. package/templates/registry/select.json +23 -0
  153. package/templates/registry/settings-shell.json +47 -0
  154. package/templates/registry/sheet.json +24 -0
  155. package/templates/registry/sidebar.json +28 -0
  156. package/templates/registry/smoke.json +34 -0
  157. package/templates/registry/spinner.json +16 -0
  158. package/templates/registry/stat-card.json +18 -0
  159. package/templates/registry/status-badge.json +18 -0
  160. package/templates/registry/switch.json +20 -0
  161. package/templates/registry/tabs.json +23 -0
  162. package/templates/registry/team-settings.json +97 -0
  163. package/templates/registry/textarea.json +16 -0
  164. package/templates/registry/tooltip.json +22 -0
  165. package/templates/registry/trial-banner.json +43 -0
  166. package/templates/registry/usage.json +84 -0
  167. package/templates/registry/workspace-settings.json +71 -0
  168. package/templates/registry-items.json +1 -1
  169. package/templates/registry-requires.json +26 -2
  170. package/templates/vitest/server-only.ts.tpl +7 -0
  171. package/templates/vitest/vitest.config.ts.tpl +37 -0
@@ -0,0 +1,111 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "pricing",
4
+ "title": "Pricing",
5
+ "description": "Plan comparison and upgrade page with a monthly/yearly toggle, backed by the framework's checkout service and the product's registered plan catalogue. Ships the lib/billing.ts product binding and actions/billing.ts checkout actions that the checkout and billing-settings items also depend on — install this item first.",
6
+ "dependencies": [
7
+ "@intelligo-dev/core",
8
+ "@intelligo-dev/auth",
9
+ "@intelligo-dev/billing",
10
+ "@intelligo-dev/next",
11
+ "zod@^4.6.5",
12
+ "lucide-react",
13
+ "server-only",
14
+ "next-intl"
15
+ ],
16
+ "registryDependencies": [
17
+ "alert",
18
+ "badge",
19
+ "@intelligo/button",
20
+ "card",
21
+ "skeleton",
22
+ "@intelligo/animated-list"
23
+ ],
24
+ "files": [
25
+ {
26
+ "path": "base/pricing/page.tsx",
27
+ "content": "import type { Metadata } from \"next\";\nimport { getTranslations } from \"next-intl/server\";\n\nimport { requireWorkspace } from \"@intelligo-dev/auth\";\nimport { getBillingOverview } from \"@intelligo-dev/billing\";\n\nimport { getPlans } from \"@/lib/billing\";\nimport { PricingContent } from \"@/components/billing/pricing-content\";\n\nexport async function generateMetadata(): Promise<Metadata> {\n const t = await getTranslations(\"pricing\");\n return { title: t(\"meta.title\") };\n}\n\nexport default async function PricingPage() {\n const t = await getTranslations(\"pricing\");\n const { workspace, membership } = await requireWorkspace();\n\n const [plans, overview] = await Promise.all([\n Promise.resolve(getPlans()),\n getBillingOverview({ workspaceId: workspace.id, role: membership.role }),\n ]);\n\n return (\n <div className=\"mx-auto w-full max-w-5xl space-y-8 p-4 md:p-10\">\n <header className=\"space-y-2 text-center\">\n <h1 className=\"text-3xl font-bold text-foreground md:text-4xl\">\n {t(\"page.title\")}\n </h1>\n <p className=\"text-base text-muted-foreground\">\n {t(\"page.description\")}\n </p>\n </header>\n\n <PricingContent\n plans={plans}\n currentPlanSlug={overview.planSlug}\n canCheckout={membership.role === \"owner\"}\n />\n </div>\n );\n}\n",
28
+ "type": "registry:page",
29
+ "target": "app/[locale]/(app)/pricing/page.tsx"
30
+ },
31
+ {
32
+ "path": "base/pricing/loading.tsx",
33
+ "content": "import { Card } from \"@/components/ui/card\";\nimport { Skeleton } from \"@/components/ui/skeleton\";\n\nexport default function PricingLoading() {\n return (\n <div className=\"mx-auto w-full max-w-5xl space-y-8 p-4 md:p-10\">\n <div className=\"space-y-2 text-center\">\n <Skeleton className=\"mx-auto h-8 w-48\" />\n <Skeleton className=\"mx-auto h-4 w-72\" />\n </div>\n\n <div className=\"flex justify-center\">\n <Skeleton className=\"h-10 w-64\" />\n </div>\n\n <div className=\"grid grid-cols-1 gap-6 md:grid-cols-3\">\n {[1, 2, 3].map((i) => (\n <Card key={i} className=\"p-6\">\n <div className=\"space-y-6\">\n <div className=\"space-y-2\">\n <Skeleton className=\"h-6 w-24\" />\n <Skeleton className=\"h-4 w-full\" />\n </div>\n <div className=\"space-y-2\">\n <Skeleton className=\"h-10 w-32\" />\n </div>\n <div className=\"space-y-3\">\n {[1, 2, 3, 4].map((j) => (\n <Skeleton key={j} className=\"h-4 w-full\" />\n ))}\n </div>\n <Skeleton className=\"h-10 w-full\" />\n </div>\n </Card>\n ))}\n </div>\n </div>\n );\n}\n",
34
+ "type": "registry:page",
35
+ "target": "app/[locale]/(app)/pricing/loading.tsx"
36
+ },
37
+ {
38
+ "path": "base/pricing/error.tsx",
39
+ "content": "\"use client\";\n\n/** Error boundary for the pricing route. */\n\nimport { useEffect } from \"react\";\nimport { useTranslations } from \"next-intl\";\n\nimport { Link } from \"@/i18n/navigation\";\nimport { Alert, AlertDescription } from \"@/components/ui/alert\";\nimport { Button } from \"@/components/ui/button\";\n\nexport default function PricingError({\n error,\n reset,\n}: {\n error: Error & { digest?: string };\n reset: () => void;\n}) {\n const t = useTranslations(\"pricing\");\n\n useEffect(() => {\n console.error(\"[pricing] 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.title\")}</AlertDescription>\n </Alert>\n\n <div className=\"flex justify-center gap-3\">\n <Button onClick={reset}>{t(\"error.tryAgain\")}</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",
40
+ "type": "registry:page",
41
+ "target": "app/[locale]/(app)/pricing/error.tsx"
42
+ },
43
+ {
44
+ "path": "base/pricing/components/pricing-content.tsx",
45
+ "content": "\"use client\";\n\n/**\n * Client wrapper around the plan grid: owns the monthly/yearly\n * interval selection and the \"checkout was canceled\" banner, both of\n * which need client state/searchParams the server-rendered page\n * doesn't have.\n */\n\nimport { useEffect, useState } from \"react\";\nimport { useSearchParams } from \"next/navigation\";\nimport { useTranslations } from \"next-intl\";\n\nimport { Alert, AlertDescription } from \"@/components/ui/alert\";\nimport { AnimatedList, AnimatedListItem } from \"@/components/ui/animated-list\";\nimport type { PlanConfig } from \"@intelligo-dev/billing/plans\";\n\nimport { IntervalToggle } from \"./interval-toggle\";\nimport { PlanCard } from \"./plan-card\";\n\ninterface PricingContentProps {\n /** The plan catalogue, resolved by the page through `getPlans`. */\n plans: Partial<Record<string, PlanConfig>>;\n currentPlanSlug: string;\n canCheckout: boolean;\n}\n\nexport function PricingContent({\n plans,\n currentPlanSlug,\n canCheckout,\n}: PricingContentProps) {\n const t = useTranslations(\"pricing\");\n const searchParams = useSearchParams();\n const [interval, setInterval] = useState<\"monthly\" | \"yearly\">(\"monthly\");\n const hasIntervalPricing = Object.values(plans).some(\n (plan) =>\n plan !== undefined &&\n (plan.priceMonthly !== undefined || plan.priceYearly !== undefined)\n );\n const [showCanceled, setShowCanceled] = useState(false);\n\n useEffect(() => {\n if (searchParams.get(\"canceled\") !== \"true\") return;\n setShowCanceled(true);\n const timeout = setTimeout(() => setShowCanceled(false), 5000);\n return () => clearTimeout(timeout);\n }, [searchParams]);\n\n const planEntries = Object.values(plans).filter((plan): plan is PlanConfig =>\n Boolean(plan)\n );\n\n return (\n <div className=\"space-y-8\">\n {showCanceled && (\n <Alert>\n <AlertDescription>{t(\"content.canceled\")}</AlertDescription>\n </Alert>\n )}\n\n {/* Hidden unless at least one plan is actually priced per period\n — a toggle that changes nothing is worse than no toggle. */}\n {hasIntervalPricing ? (\n <div className=\"flex justify-center\">\n <IntervalToggle interval={interval} onChange={setInterval} />\n </div>\n ) : null}\n\n {/* Centred wrap rather than a grid: the catalogue can hold any\n number of plans, and fixed-width cards stay the same size\n whether there are two or five. */}\n <AnimatedList as=\"div\" className=\"flex flex-wrap justify-center gap-6\">\n {planEntries.map((plan) => (\n <AnimatedListItem as=\"div\" key={plan.slug} className=\"w-full sm:w-80\">\n <PlanCard\n plan={plan}\n currentPlanSlug={currentPlanSlug}\n interval={interval}\n canCheckout={canCheckout}\n // No plan is highlighted by default; set `isRecommended`\n // for the one you want to single out.\n />\n </AnimatedListItem>\n ))}\n </AnimatedList>\n </div>\n );\n}\n",
46
+ "type": "registry:component",
47
+ "target": "components/billing/pricing-content.tsx"
48
+ },
49
+ {
50
+ "path": "base/pricing/components/plan-card.tsx",
51
+ "content": "\"use client\";\n\n/**\n * One plan's pricing card: name, price, feature list, and a checkout\n * CTA whose state depends on whether it's the workspace's current\n * plan, the free plan, or something the caller can afford to buy.\n *\n * The catalogue has no tier order, so every non-current, non-free plan\n * gets the same \"switch to this plan\" button. The plan's name,\n * description and features come from the deployment's `plans` messages\n * when it translates them, and from the catalogue otherwise\n * (`lib/plan-copy.ts`).\n */\n\nimport { Check } from \"lucide-react\";\nimport { useFormatter, useTranslations } from \"next-intl\";\n\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport { Card } from \"@/components/ui/card\";\n\nimport { CheckoutButton } from \"./checkout-button\";\nimport { CURRENCY } from \"@/lib/billing-config\";\nimport { planCopy } from \"@/lib/plan-copy\";\nimport type { PlanConfig } from \"@intelligo-dev/billing/plans\";\n\ninterface PlanCardProps {\n plan: PlanConfig;\n currentPlanSlug: string;\n interval: \"monthly\" | \"yearly\";\n /** Whether the signed-in caller is allowed to start checkout (owner-only). */\n canCheckout: boolean;\n isRecommended?: boolean;\n}\n\nexport function PlanCard({\n plan,\n currentPlanSlug,\n interval,\n canCheckout,\n isRecommended = false,\n}: PlanCardProps) {\n const t = useTranslations(\"pricing\");\n const tPlans = useTranslations(\"plans\");\n const format = useFormatter();\n const copy = planCopy(tPlans, plan);\n const isCurrent = plan.slug === currentPlanSlug;\n\n // A plan with interval prices is quoted per period; one without is a\n // single purchase, and the toggle above is hidden for it entirely\n // (see pricing-content.tsx) so the label can't contradict the price.\n const intervalPrice =\n interval === \"monthly\" ? plan.priceMonthly : plan.priceYearly;\n const price = intervalPrice ?? plan.priceOneTime;\n const isFree = price === 0;\n const periodLabel =\n intervalPrice === undefined\n ? t(\"planCard.oneTime\")\n : interval === \"monthly\"\n ? t(\"planCard.perMonth\")\n : t(\"planCard.perYear\");\n\n // Money is never rendered by a package helper: currency is a\n // deployment decision (`CURRENCY` in lib/billing-config.ts), and\n // \"free\" is copy, not a formatted zero.\n const priceLabel = isFree\n ? t(\"planCard.free\")\n : format.number(price, {\n style: \"currency\",\n currency: CURRENCY,\n maximumFractionDigits: 0,\n });\n\n return (\n <Card\n className={`relative flex h-full flex-col p-6 ${\n isRecommended\n ? \"border-2 border-foreground/30 shadow-lg dark:border-foreground/40\"\n : \"\"\n }`}\n >\n {isRecommended && (\n <Badge className=\"absolute -top-3 left-1/2 -translate-x-1/2 bg-foreground text-background\">\n {t(\"planCard.recommended\")}\n </Badge>\n )}\n\n <div className=\"flex flex-1 flex-col space-y-6\">\n <div className=\"space-y-2\">\n <div className=\"flex items-center justify-between\">\n <h3 className=\"text-xl font-bold text-foreground\">{copy.name}</h3>\n {isCurrent && (\n <Badge className=\"border border-border bg-accent text-muted-foreground\">\n {t(\"planCard.current\")}\n </Badge>\n )}\n </div>\n <p className=\"text-sm text-muted-foreground\">{copy.description}</p>\n </div>\n\n <div className=\"space-y-1\">\n <div className=\"flex items-baseline gap-1\">\n <span className=\"text-4xl font-bold text-foreground\">\n {priceLabel}\n </span>\n {!isFree && (\n <span className=\"text-sm text-muted-foreground\">\n {periodLabel}\n </span>\n )}\n </div>\n </div>\n\n <ul className=\"space-y-3\">\n {copy.features.map((feature) => (\n <li key={feature} className=\"flex items-start gap-2\">\n <Check className=\"mt-0.5 size-4 flex-shrink-0 text-muted-foreground\" />\n <span className=\"text-sm text-foreground\">{feature}</span>\n </li>\n ))}\n </ul>\n\n <div className=\"mt-auto pt-2\">\n {isCurrent ? (\n // Outline, not the default fill: the plan you are already\n // on is a statement of fact, and a solid disabled button\n // reads as the page's primary action greyed out.\n <Button disabled variant=\"outline\" className=\"w-full\">\n {t(\"planCard.currentPlanButton\")}\n </Button>\n ) : isFree ? (\n <div className=\"py-2 text-center\">\n <p className=\"text-sm text-muted-foreground\">\n {t(\"planCard.freeForever\")}\n </p>\n </div>\n ) : !canCheckout ? (\n <Button variant=\"outline\" disabled className=\"w-full\">\n {t(\"planCard.askOwner\")}\n </Button>\n ) : (\n <CheckoutButton\n planSlug={plan.slug}\n interval={interval}\n className=\"w-full\"\n >\n {t(\"planCard.switchTo\", { planName: copy.name })}\n </CheckoutButton>\n )}\n </div>\n </div>\n </Card>\n );\n}\n",
52
+ "type": "registry:component",
53
+ "target": "components/billing/plan-card.tsx"
54
+ },
55
+ {
56
+ "path": "base/pricing/components/interval-toggle.tsx",
57
+ "content": "\"use client\";\n\n/** Monthly/yearly switch; the selected interval is the parent's state. */\n\nimport { useTranslations } from \"next-intl\";\n\nimport { Button } from \"@/components/ui/button\";\n\ninterface IntervalToggleProps {\n interval: \"monthly\" | \"yearly\";\n onChange: (interval: \"monthly\" | \"yearly\") => void;\n}\n\nexport function IntervalToggle({ interval, onChange }: IntervalToggleProps) {\n const t = useTranslations(\"pricing\");\n\n return (\n // Sized to its two words, not to the page; the parent centres it.\n <div className=\"inline-flex w-fit items-center gap-1 rounded-lg bg-muted p-1\">\n <Button\n type=\"button\"\n variant={interval === \"monthly\" ? \"default\" : \"ghost\"}\n size=\"sm\"\n onClick={() => onChange(\"monthly\")}\n >\n {t(\"intervalToggle.monthly\")}\n </Button>\n <Button\n type=\"button\"\n variant={interval === \"yearly\" ? \"default\" : \"ghost\"}\n size=\"sm\"\n onClick={() => onChange(\"yearly\")}\n >\n {t(\"intervalToggle.yearly\")}\n </Button>\n </div>\n );\n}\n",
58
+ "type": "registry:component",
59
+ "target": "components/billing/interval-toggle.tsx"
60
+ },
61
+ {
62
+ "path": "base/pricing/components/checkout-button.tsx",
63
+ "content": "\"use client\";\n\n/**\n * Triggers a Stripe subscription checkout for one plan/interval and\n * redirects the browser to the returned hosted checkout URL.\n */\n\nimport { useState } from \"react\";\nimport { useTranslations } from \"next-intl\";\n\nimport { Alert, AlertDescription } from \"@/components/ui/alert\";\nimport { Button } from \"@/components/ui/button\";\n\nimport { createCheckoutSession } from \"@/actions/billing\";\n\ninterface CheckoutButtonProps {\n planSlug: string;\n interval: \"monthly\" | \"yearly\";\n children: React.ReactNode;\n disabled?: boolean;\n className?: string;\n}\n\nexport function CheckoutButton({\n planSlug,\n interval,\n children,\n disabled,\n className,\n}: CheckoutButtonProps) {\n const t = useTranslations(\"pricing\");\n const [isLoading, setIsLoading] = useState(false);\n const [error, setError] = useState<string | null>(null);\n\n const handleClick = async () => {\n setError(null);\n setIsLoading(true);\n\n const result = await createCheckoutSession({ planSlug, interval });\n\n if (!result.success) {\n setError(result.error);\n setIsLoading(false);\n return;\n }\n\n window.location.href = result.data.url;\n };\n\n return (\n <div className=\"space-y-2\">\n <Button\n onClick={handleClick}\n disabled={disabled || isLoading}\n className={className}\n >\n {isLoading ? t(\"checkoutButton.redirecting\") : children}\n </Button>\n\n {error && (\n <Alert variant=\"destructive\">\n <AlertDescription>{error}</AlertDescription>\n </Alert>\n )}\n </div>\n );\n}\n",
64
+ "type": "registry:component",
65
+ "target": "components/billing/checkout-button.tsx"
66
+ },
67
+ {
68
+ "path": "base/pricing/actions.ts",
69
+ "content": "\"use server\";\n\n/**\n * Checkout server actions over `@intelligo-dev/billing`'s checkout service:\n * authorize, call the service, and map a `BillingServiceError` code to a\n * translated message. The `billing-settings` item imports this file too.\n */\n\nimport { headers } from \"next/headers\";\nimport { getTranslations } from \"next-intl/server\";\nimport { z } from \"zod\";\n\nimport { requireRole } from \"@intelligo-dev/auth\";\nimport {\n createBillingPortal,\n createCreditCheckout,\n createSubscriptionCheckout,\n isBillingServiceError,\n} from \"@intelligo-dev/billing\";\nimport { getDefaultProductSlug } from \"@intelligo-dev/billing/plans\";\nimport type { ActionResult } from \"@intelligo-dev/next\";\n\nimport { getCreditBundle } from \"@/lib/billing\";\n\nexport type BillingActionResult<T = { url: string }> = ActionResult<T>;\n\ntype Translator = Awaited<ReturnType<typeof getTranslations<\"pricing\">>>;\n\nfunction friendlyMessage(t: Translator): Partial<Record<string, string>> {\n return {\n invalid_plan: t(\"errors.invalidPlan\"),\n checkout_unavailable: t(\"errors.checkoutUnavailable\"),\n invalid_bundle: t(\"errors.invalidBundle\"),\n no_billing_account: t(\"errors.noBillingAccount\"),\n provider_error: t(\"errors.providerError\"),\n };\n}\n\nfunction friendlyError(error: unknown, t: Translator): string {\n if (isBillingServiceError(error)) {\n return friendlyMessage(t)[error.code] ?? error.message;\n }\n // `Error#message` can carry internals (SQL, hostnames) to the UI.\n console.error(\"[pricing]\", error);\n return t(\"errors.genericFailure\");\n}\n\n/**\n * This deployment's origin, for Stripe's success/cancel redirect URLs.\n *\n * `NEXT_PUBLIC_APP_URL` first: the `Host` header is whatever the client\n * sent, and a forged one points the post-payment redirect — which\n * carries `{CHECKOUT_SESSION_ID}` — at someone else's domain. The\n * header stays as the fallback for a deployment that has not set the\n * variable.\n */\nasync function origin(): Promise<string> {\n const configured = process.env.NEXT_PUBLIC_APP_URL;\n if (configured) return configured.replace(/\\/+$/, \"\");\n\n const requestHeaders = await headers();\n const host = requestHeaders.get(\"host\");\n const protocol = process.env.NODE_ENV === \"production\" ? \"https\" : \"http\";\n return `${protocol}://${host}`;\n}\n\nconst checkoutSchema = z.object({\n planSlug: z.string().min(1),\n interval: z.enum([\"monthly\", \"yearly\"]).default(\"monthly\"),\n});\n\nexport type CheckoutActionInput = z.infer<typeof checkoutSchema>;\n\nexport async function createCheckoutSession(\n input: CheckoutActionInput\n): Promise<BillingActionResult> {\n const t = await getTranslations(\"pricing\");\n\n const parsed = checkoutSchema.safeParse(input);\n if (!parsed.success) {\n return {\n success: false,\n error: parsed.error.issues[0]?.message ?? t(\"errors.invalidInput\"),\n };\n }\n\n try {\n const { workspace, user } = await requireRole([\"owner\"]);\n const base = await origin();\n\n const { url } = await createSubscriptionCheckout({\n workspaceId: workspace.id,\n userId: user.id,\n planSlug: parsed.data.planSlug,\n interval: parsed.data.interval,\n productSlug: getDefaultProductSlug(),\n successUrl: `${base}/checkout/success?session_id={CHECKOUT_SESSION_ID}`,\n cancelUrl: `${base}/pricing?canceled=true`,\n });\n\n return { success: true, data: { url } };\n } catch (error) {\n return { success: false, error: friendlyError(error, t) };\n }\n}\n\nconst creditPurchaseSchema = z.object({\n bundleId: z.string().min(1),\n});\n\nexport type CreditPurchaseActionInput = z.infer<typeof creditPurchaseSchema>;\n\nexport async function createCreditPurchaseSession(\n input: CreditPurchaseActionInput\n): Promise<BillingActionResult> {\n const t = await getTranslations(\"pricing\");\n\n const parsed = creditPurchaseSchema.safeParse(input);\n if (!parsed.success) {\n return {\n success: false,\n error: parsed.error.issues[0]?.message ?? t(\"errors.invalidInput\"),\n };\n }\n\n const bundle = getCreditBundle(parsed.data.bundleId);\n if (!bundle) {\n return { success: false, error: t(\"errors.invalidBundle\") };\n }\n\n try {\n const { workspace, user } = await requireRole([\"owner\"]);\n const base = await origin();\n\n const { url } = await createCreditCheckout({\n workspaceId: workspace.id,\n userId: user.id,\n bundle,\n successUrl: `${base}/settings/billing?credits=success`,\n cancelUrl: `${base}/settings/billing?credits=cancelled`,\n });\n\n return { success: true, data: { url } };\n } catch (error) {\n return { success: false, error: friendlyError(error, t) };\n }\n}\n\nexport async function createPortalSession(): Promise<BillingActionResult> {\n const t = await getTranslations(\"pricing\");\n\n try {\n const { workspace } = await requireRole([\"owner\"]);\n const base = await origin();\n\n const { url } = await createBillingPortal({\n workspaceId: workspace.id,\n returnUrl: `${base}/settings/billing`,\n });\n\n return { success: true, data: { url } };\n } catch (error) {\n return { success: false, error: friendlyError(error, t) };\n }\n}\n",
70
+ "type": "registry:file",
71
+ "target": "actions/billing.ts"
72
+ },
73
+ {
74
+ "path": "base/pricing/lib/billing.ts",
75
+ "content": "import \"server-only\";\n\n/**\n * Server-side billing binding for the pricing, checkout and\n * billing-settings items. `getPlans`/`getPlan` read the catalogue your\n * composition root registered for the slug passed to\n * `setDefaultProductSlug`.\n *\n * To sell subscriptions only, delete `CreditBundles` and the\n * `createCreditPurchaseSession` action.\n */\n\nimport { getPlanBySlug, type PlanConfig } from \"@intelligo-dev/billing\";\nimport {\n getDefaultProductSlug,\n getProductPlans,\n} from \"@intelligo-dev/billing/plans\";\n\nexport { CREDIT_BUNDLES, getCreditBundle } from \"./billing-config\";\n\n/** The registered plan catalogue for this deployment's product. */\nexport function getPlans(): Partial<Record<string, PlanConfig>> {\n return getProductPlans(getDefaultProductSlug()) ?? {};\n}\n\nexport function getPlan(slug: string): PlanConfig | null {\n return getPlanBySlug(slug, getDefaultProductSlug());\n}\n",
76
+ "type": "registry:file",
77
+ "target": "lib/billing.ts"
78
+ },
79
+ {
80
+ "path": "base/pricing/lib/billing-config.ts",
81
+ "content": "/**\n * Client-safe billing configuration, kept apart from the `server-only`\n * `lib/billing.ts` so client components can read it without pulling\n * Stripe into the browser bundle. A bundle is validated server-side by\n * `createCreditCheckout`.\n *\n * `CREDIT_BUNDLES` is the one-time credit packs you sell; edit or empty\n * it. A bundle's `name` is rendered verbatim, not translated.\n */\n\nimport { fromMajor } from \"@intelligo-dev/core/money\";\nimport type { CreditBundle } from \"@intelligo-dev/billing\";\n\n/**\n * ISO 4217 code every money figure in the installed pages is formatted\n * in. It only affects display and converts nothing: it must match what\n * your payment provider charges in, and the `usdRateMicros` on the\n * billing settings row (editable in the admin console) must convert\n * model cost into it (1 for USD).\n */\nexport const CURRENCY = \"USD\";\n\n/**\n * The buyer is charged `price` through the payment provider; the\n * workspace receives `grant` in the billing currency. They need not be\n * the same currency.\n */\nexport const CREDIT_BUNDLES: CreditBundle[] = [\n {\n id: \"credits-small\",\n name: \"Small credit pack\",\n grant: fromMajor(5, CURRENCY),\n price: fromMajor(5, \"USD\"),\n },\n {\n id: \"credits-medium\",\n name: \"Medium credit pack\",\n grant: fromMajor(13, CURRENCY),\n price: fromMajor(12, \"USD\"),\n },\n {\n id: \"credits-large\",\n name: \"Large credit pack\",\n grant: fromMajor(40, CURRENCY),\n price: fromMajor(35, \"USD\"),\n },\n];\n\nexport function getCreditBundle(id: string): CreditBundle | undefined {\n return CREDIT_BUNDLES.find((bundle) => bundle.id === id);\n}\n",
82
+ "type": "registry:file",
83
+ "target": "lib/billing-config.ts"
84
+ },
85
+ {
86
+ "path": "base/pricing/lib/format-money.ts",
87
+ "content": "/**\n * Formats money for display. Amounts are micros (millionths of one major\n * unit) with their currency; next-intl supplies the symbol, its position\n * and the separators from the reader's locale.\n *\n * Takes the formatter rather than calling a hook, so it serves a server\n * component (`await getFormatter()`) and a client one (`useFormatter()`).\n */\n\nimport type { useFormatter } from \"next-intl\";\n\n/** next-intl's formatter, from `useFormatter()` or `getFormatter()`. */\ntype Formatter = ReturnType<typeof useFormatter>;\n\n/** An amount in micros and the currency it is denominated in. */\nexport type MoneyLike = { amount: number; currency: string };\n\nconst MICROS_PER_UNIT = 1_000_000;\n\n/** Two significant digits is enough to read a sub-cent amount by. */\nconst SIGNIFICANT_DIGITS = 2;\n\n/** What `Intl` says this currency's minor unit is: 2 for USD, 0 for MNT. */\nfunction currencyDigits(currency: string): number {\n return (\n new Intl.NumberFormat(\"en-US\", {\n style: \"currency\",\n currency,\n }).resolvedOptions().maximumFractionDigits ?? 2\n );\n}\n\n/**\n * How many decimals this amount needs to be legible: the currency's\n * own, except when that would render a real charge as nothing. One chat\n * turn costs a fraction of a cent, and a usage page that says every\n * request cost $0.00 is worse than one that says $0.0048.\n */\nexport function moneyFractionDigits(value: MoneyLike): number {\n const digits = currencyDigits(value.currency);\n const major = Math.abs(value.amount) / MICROS_PER_UNIT;\n if (major === 0 || major >= 10 ** -digits / 2) return digits;\n const leadingZeros = Math.floor(-Math.log10(major));\n return Math.min(6, leadingZeros + SIGNIFICANT_DIGITS);\n}\n\n/**\n * Formats `value` in its own currency. To change options for every call\n * site (compact totals, say), edit this function.\n */\nexport function formatMoney(format: Formatter, value: MoneyLike): string {\n const digits = moneyFractionDigits(value);\n return format.number(value.amount / MICROS_PER_UNIT, {\n style: \"currency\",\n currency: value.currency,\n minimumFractionDigits: digits,\n maximumFractionDigits: digits,\n });\n}\n",
88
+ "type": "registry:file",
89
+ "target": "lib/format-money.ts"
90
+ },
91
+ {
92
+ "path": "base/pricing/lib/plan-copy.ts",
93
+ "content": "/**\n * A plan's copy in the reader's language.\n *\n * The plan catalogue (`registerProductPlans`) holds one set of strings\n * per plan. A deployment translates them in its own `plans` message\n * namespace — `messages/<locale>/plans.json` — keyed by plan slug:\n *\n * ```json\n * {\n * \"pro\": {\n * \"name\": \"Pro\",\n * \"description\": \"For teams that ship every day.\",\n * \"features\": [\"Unlimited chats\", \"Priority support\"]\n * }\n * }\n * ```\n *\n * Every key is optional: a key the locale does not define falls back to\n * the catalogue's string, and an app with no `plans.json` at all renders\n * the catalogue verbatim. Pass the translator of that namespace —\n * `useTranslations(\"plans\")` in a client component,\n * `await getTranslations(\"plans\")` on the server.\n */\n\n/** The part of a next-intl translator this module reads. */\nexport interface PlanMessages {\n (key: string): string;\n has(key: string): boolean;\n raw(key: string): unknown;\n}\n\nexport interface PlanCopy {\n name: string;\n description: string;\n features: string[];\n}\n\nfunction text(t: PlanMessages, key: string, fallback: string): string {\n return t.has(key) ? t(key) : fallback;\n}\n\n/** The plan's display name for `slug`, or `fallback` when untranslated. */\nexport function planName(\n t: PlanMessages,\n slug: string,\n fallback: string\n): string {\n return text(t, `${slug}.name`, fallback);\n}\n\n/** The plan's name, description and features, each translated when the namespace has it. */\nexport function planCopy(\n t: PlanMessages,\n plan: { slug: string } & PlanCopy\n): PlanCopy {\n const key = `${plan.slug}.features`;\n const raw = t.has(key) ? t.raw(key) : undefined;\n const features =\n Array.isArray(raw) && raw.every((item) => typeof item === \"string\")\n ? (raw as string[])\n : plan.features;\n return {\n name: planName(t, plan.slug, plan.name),\n description: text(t, `${plan.slug}.description`, plan.description),\n features,\n };\n}\n",
94
+ "type": "registry:file",
95
+ "target": "lib/plan-copy.ts"
96
+ },
97
+ {
98
+ "path": "base/pricing/messages/en.json",
99
+ "content": "{\n \"meta\": {\n \"title\": \"Pricing\"\n },\n \"page\": {\n \"title\": \"Pricing\",\n \"description\": \"Start free, upgrade when you need more.\"\n },\n \"planCard\": {\n \"recommended\": \"Recommended\",\n \"current\": \"Current\",\n \"oneTime\": \"one-time\",\n \"currentPlanButton\": \"Current plan\",\n \"freeForever\": \"Free forever\",\n \"askOwner\": \"Ask an owner to change plans\",\n \"switchTo\": \"Switch to {planName}\",\n \"free\": \"Free\",\n \"perMonth\": \"per month\",\n \"perYear\": \"per year\"\n },\n \"intervalToggle\": {\n \"monthly\": \"Monthly\",\n \"yearly\": \"Yearly\"\n },\n \"checkoutButton\": {\n \"redirecting\": \"Redirecting…\"\n },\n \"content\": {\n \"canceled\": \"Checkout was canceled. You were not charged.\"\n },\n \"error\": {\n \"title\": \"Something went wrong while loading pricing. Please try again.\",\n \"tryAgain\": \"Try again\",\n \"goHome\": \"Go home\"\n },\n \"errors\": {\n \"invalidPlan\": \"That plan isn't available.\",\n \"checkoutUnavailable\": \"This plan isn't set up for checkout yet. Contact support.\",\n \"invalidBundle\": \"That credit bundle isn't available.\",\n \"noBillingAccount\": \"No billing account found. Subscribe to a plan or purchase credits first.\",\n \"providerError\": \"Something went wrong. Please try again.\",\n \"invalidInput\": \"Invalid input\",\n \"genericFailure\": \"Something went wrong.\"\n }\n}\n",
100
+ "type": "registry:file",
101
+ "target": "messages/en/pricing.json"
102
+ },
103
+ {
104
+ "path": "base/pricing/messages/plans/en.json",
105
+ "content": "{}\n",
106
+ "type": "registry:file",
107
+ "target": "messages/en/plans.json"
108
+ }
109
+ ],
110
+ "type": "registry:block"
111
+ }
@@ -0,0 +1,65 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "privacy-settings",
4
+ "title": "Privacy Settings",
5
+ "description": "GDPR-style data export, a read-only memory-audit trail, and per-category fact listing with delete, backed by the framework's identity service.",
6
+ "dependencies": [
7
+ "@intelligo-dev/auth",
8
+ "@intelligo-dev/core",
9
+ "@intelligo-dev/next",
10
+ "lucide-react",
11
+ "sonner",
12
+ "next-intl"
13
+ ],
14
+ "registryDependencies": [
15
+ "@intelligo/button",
16
+ "card",
17
+ "skeleton",
18
+ "@intelligo/page-header"
19
+ ],
20
+ "files": [
21
+ {
22
+ "path": "base/privacy-settings/page.tsx",
23
+ "content": "/**\n * Three cards: a GDPR-style data export, a per-category listing of the\n * caller's own facts with per-fact delete, and a read-only audit trail\n * of every mutation to their identity graph. Everything reads through\n * the framework's identity service (`@intelligo-dev/core/identity`) via\n * `actions/privacy.ts` — no direct database access here.\n */\n\nimport type { Metadata } from \"next\";\nimport { getFormatter, getTranslations } from \"next-intl/server\";\n\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from \"@/components/ui/card\";\nimport { ExportDataButton } from \"@/components/privacy/export-data-button\";\nimport { FactList } from \"@/components/privacy/fact-list\";\nimport { getAuditTrail, listFacts } from \"@/actions/privacy\";\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(\"privacy-settings\");\n return { title: t(\"page.title\") };\n}\n\nexport default async function PrivacySettingsPage() {\n const t = await getTranslations(\"privacy-settings\");\n const format = await getFormatter();\n const [facts, auditTrail] = await Promise.all([listFacts(), getAuditTrail()]);\n\n return (\n <div className=\"space-y-6\">\n <PageHeader>\n <PageHeaderContent>\n <PageHeaderTitle level={2}>{t(\"page.title\")}</PageHeaderTitle>\n <PageHeaderDescription>{t(\"page.description\")}</PageHeaderDescription>\n </PageHeaderContent>\n </PageHeader>\n\n <Card>\n <CardHeader>\n <CardTitle>{t(\"page.export.title\")}</CardTitle>\n <CardDescription>{t(\"page.export.description\")}</CardDescription>\n </CardHeader>\n <CardContent>\n <ExportDataButton />\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle>{t(\"page.yourData.title\")}</CardTitle>\n <CardDescription>{t(\"page.yourData.description\")}</CardDescription>\n </CardHeader>\n <CardContent>\n <FactList initialFacts={facts} />\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle>{t(\"page.auditTrail.title\")}</CardTitle>\n <CardDescription>\n {t(\"page.auditTrail.description\", { count: auditTrail.length })}\n </CardDescription>\n </CardHeader>\n <CardContent>\n {auditTrail.length === 0 ? (\n <p className=\"text-sm text-muted-foreground\">\n {t(\"page.auditTrail.empty\")}\n </p>\n ) : (\n <ul className=\"divide-y divide-border\">\n {auditTrail.map((row) => (\n <li key={row.id} className=\"py-3 text-sm\">\n <div className=\"flex items-center justify-between gap-4\">\n <span className=\"font-medium capitalize\">\n {row.action} · {row.targetKind}\n </span>\n <time\n dateTime={new Date(row.createdAt).toISOString()}\n className=\"shrink-0 text-xs text-muted-foreground\"\n >\n {format.dateTime(new Date(row.createdAt), {\n dateStyle: \"medium\",\n timeStyle: \"short\",\n })}\n </time>\n </div>\n {row.reason && (\n <p className=\"mt-1 text-xs text-muted-foreground\">\n {row.reason}\n </p>\n )}\n </li>\n ))}\n </ul>\n )}\n </CardContent>\n </Card>\n </div>\n );\n}\n",
24
+ "type": "registry:page",
25
+ "target": "app/[locale]/(app)/settings/privacy/page.tsx"
26
+ },
27
+ {
28
+ "path": "base/privacy-settings/loading.tsx",
29
+ "content": "import { Skeleton } from \"@/components/ui/skeleton\";\n\nexport default function PrivacyLoading() {\n return (\n <div className=\"container mx-auto max-w-2xl space-y-6 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 <Skeleton className=\"h-36 rounded-lg\" />\n <Skeleton className=\"h-64 rounded-lg\" />\n <Skeleton className=\"h-56 rounded-lg\" />\n </div>\n );\n}\n",
30
+ "type": "registry:page",
31
+ "target": "app/[locale]/(app)/settings/privacy/loading.tsx"
32
+ },
33
+ {
34
+ "path": "base/privacy-settings/actions.ts",
35
+ "content": "\"use server\";\n\n/**\n * Privacy settings server actions — thin transport over the\n * framework's identity service (`@intelligo-dev/core/identity`):\n * authenticate the caller, call the service, map any\n * `IdentityServiceError` to a friendly message, and revalidate. No\n * business rules here — fact ownership checks, the memory-audit write,\n * and data-export aggregation all live in the service.\n */\n\nimport { revalidatePath } from \"next/cache\";\nimport { getTranslations } from \"next-intl/server\";\n\nimport { requireWorkspace } from \"@intelligo-dev/auth\";\nimport {\n deleteFact as deleteFactService,\n exportIdentity as exportIdentityService,\n getAuditTrail as getAuditTrailService,\n isIdentityServiceError,\n listFacts as listFactsService,\n type IdentityExport,\n type UserFact,\n type UserMemoryAuditRow,\n} from \"@intelligo-dev/core/identity\";\nimport type { ActionResult } from \"@intelligo-dev/next\";\n\nexport type PrivacyActionResult<T = undefined> = ActionResult<T>;\n\ntype Translator = Awaited<\n ReturnType<typeof getTranslations<\"privacy-settings\">>\n>;\n\nfunction friendlyMessage(t: Translator): Partial<Record<string, string>> {\n return {\n not_found: t(\"errors.notFound\"),\n invalid_input: t(\"errors.invalidInput\"),\n database_error: t(\"errors.databaseError\"),\n };\n}\n\nfunction friendlyError(error: unknown, t: Translator): string {\n if (isIdentityServiceError(error)) {\n return friendlyMessage(t)[error.code] ?? error.message;\n }\n // `Error#message` can carry internals (SQL, hostnames) to the UI.\n console.error(\"[privacy-settings]\", error);\n return t(\"errors.somethingWentWrong\");\n}\n\nconst PRIVACY_SETTINGS_PATH = \"/settings/privacy\";\nconst AUDIT_TRAIL_PAGE_SIZE = 50;\n\n/** The caller's own facts, most important and most confident first. */\nexport async function listFacts(): Promise<UserFact[]> {\n const { user, workspace } = await requireWorkspace();\n return listFactsService({ userId: user.id, workspaceId: workspace.id });\n}\n\n/** Delete one of the caller's own facts. Records the deletion in the audit trail. */\nexport async function deleteFact(factId: string): Promise<PrivacyActionResult> {\n const t = await getTranslations(\"privacy-settings\");\n try {\n const { user, workspace } = await requireWorkspace();\n await deleteFactService(\n { userId: user.id, workspaceId: workspace.id },\n factId\n );\n revalidatePath(PRIVACY_SETTINGS_PATH);\n return { success: true, data: undefined };\n } catch (error) {\n return { success: false, error: friendlyError(error, t) };\n }\n}\n\n/**\n * A GDPR-style export of everything stored about the caller: facts,\n * memories, their latest profile snapshot, and the full audit trail.\n * Client-triggered, in-browser download — not a streamed endpoint.\n */\nexport async function exportIdentity(): Promise<\n PrivacyActionResult<IdentityExport>\n> {\n const t = await getTranslations(\"privacy-settings\");\n try {\n const { user, workspace } = await requireWorkspace();\n const data = await exportIdentityService({\n userId: user.id,\n workspaceId: workspace.id,\n });\n return { success: true, data };\n } catch (error) {\n return { success: false, error: friendlyError(error, t) };\n }\n}\n\n/** The caller's most recent memory-audit rows, most recent first. */\nexport async function getAuditTrail(): Promise<UserMemoryAuditRow[]> {\n const { user, workspace } = await requireWorkspace();\n return getAuditTrailService(\n { userId: user.id, workspaceId: workspace.id },\n { limit: AUDIT_TRAIL_PAGE_SIZE }\n );\n}\n",
36
+ "type": "registry:file",
37
+ "target": "actions/privacy.ts"
38
+ },
39
+ {
40
+ "path": "base/privacy-settings/components/export-data-button.tsx",
41
+ "content": "\"use client\";\n\n/**\n * Triggers a GDPR-style export of everything the framework's identity\n * service knows about the current user. Calls the `exportIdentity`\n * server action directly and turns the JSON result into a browser\n * download — no dedicated API route needed for a one-off, per-user\n * dump this size.\n */\n\nimport { useState, useTransition } from \"react\";\nimport { useTranslations } from \"next-intl\";\nimport { Download } from \"lucide-react\";\nimport { toast } from \"sonner\";\n\nimport { Button } from \"@/components/ui/button\";\nimport { exportIdentity } from \"@/actions/privacy\";\n\nexport function ExportDataButton() {\n const t = useTranslations(\"privacy-settings\");\n const [loading, setLoading] = useState(false);\n const [, startTransition] = useTransition();\n\n function handleExport() {\n setLoading(true);\n startTransition(async () => {\n try {\n const result = await exportIdentity();\n if (!result.success) {\n toast.error(result.error);\n return;\n }\n\n const blob = new Blob([JSON.stringify(result.data, null, 2)], {\n type: \"application/json\",\n });\n const url = URL.createObjectURL(blob);\n const link = document.createElement(\"a\");\n link.href = url;\n link.download = `data-export-${new Date().toISOString().slice(0, 10)}.json`;\n document.body.appendChild(link);\n link.click();\n link.remove();\n URL.revokeObjectURL(url);\n toast.success(t(\"exportButton.success\"));\n } catch (error) {\n toast.error(t(\"exportButton.error\"));\n console.error(error);\n } finally {\n setLoading(false);\n }\n });\n }\n\n return (\n <Button onClick={handleExport} disabled={loading}>\n <Download className=\"mr-2 size-4\" />\n {loading ? t(\"exportButton.preparing\") : t(\"exportButton.download\")}\n </Button>\n );\n}\n",
42
+ "type": "registry:component",
43
+ "target": "components/privacy/export-data-button.tsx"
44
+ },
45
+ {
46
+ "path": "base/privacy-settings/components/fact-list.tsx",
47
+ "content": "\"use client\";\n\n/**\n * Per-category listing of the caller's own facts, with per-fact\n * delete.\n *\n * Category labels default to Title Case of the schema's category slug rather than a\n * message lookup: categories are open-ended, product-defined data (not\n * UI copy this item authors), so there is no fixed key set to\n * translate. A product that knows its own categories passes\n * `categoryLabels` — an already-localized slug → label map — and gets\n * proper names for the ones it recognizes, Title Case for the rest.\n *\n * Deletion is optimistic — the row disappears immediately — while the\n * `deleteFact` server action records the deletion in the audit trail\n * and revalidates the page server-side.\n */\n\nimport { useState, useTransition } from \"react\";\nimport { useTranslations } from \"next-intl\";\nimport { ChevronDown, ChevronRight, Trash2 } from \"lucide-react\";\n\nimport { Button } from \"@/components/ui/button\";\nimport { deleteFact } from \"@/actions/privacy\";\nimport type { UserFact } from \"@intelligo-dev/core/db/schema\";\n\nfunction titleCase(value: string): string {\n return value.replace(/_/g, \" \").replace(/\\b\\w/g, (c) => c.toUpperCase());\n}\n\nfunction groupByCategory(facts: UserFact[]): Map<string, UserFact[]> {\n const map = new Map<string, UserFact[]>();\n for (const fact of facts) {\n const list = map.get(fact.category) ?? [];\n list.push(fact);\n map.set(fact.category, list);\n }\n return map;\n}\n\nfunction readableValue(value: unknown): string {\n if (typeof value === \"string\") return value;\n if (value && typeof value === \"object\" && \"label\" in value) {\n return String((value as { label: unknown }).label);\n }\n return JSON.stringify(value);\n}\n\nexport function FactList({\n initialFacts,\n categoryLabels,\n}: {\n initialFacts: UserFact[];\n /** Localized labels for categories this product defines. */\n categoryLabels?: Record<string, string>;\n}) {\n const t = useTranslations(\"privacy-settings\");\n const [facts, setFacts] = useState(initialFacts);\n const [expanded, setExpanded] = useState<Set<string>>(\n () => new Set(groupByCategory(initialFacts).keys())\n );\n const [pending, startTransition] = useTransition();\n\n const grouped = groupByCategory(facts);\n\n function handleDelete(factId: string) {\n setFacts((current) => current.filter((fact) => fact.id !== factId));\n startTransition(async () => {\n await deleteFact(factId);\n });\n }\n\n function toggle(category: string) {\n setExpanded((current) => {\n const next = new Set(current);\n if (next.has(category)) next.delete(category);\n else next.add(category);\n return next;\n });\n }\n\n if (facts.length === 0) {\n return (\n <div className=\"rounded-md border border-dashed px-6 py-10 text-center\">\n <p className=\"text-sm font-medium\">{t(\"factList.empty\")}</p>\n <p className=\"mt-1 text-sm text-muted-foreground\">\n {t(\"factList.emptyHint\")}\n </p>\n </div>\n );\n }\n\n return (\n <div className=\"space-y-3\">\n {Array.from(grouped.entries()).map(([category, items]) => {\n const isOpen = expanded.has(category);\n return (\n <div key={category} className=\"overflow-hidden rounded-lg border\">\n <button\n type=\"button\"\n onClick={() => toggle(category)}\n className=\"flex w-full items-center justify-between px-3 py-2 text-left transition-colors hover:bg-muted/50\"\n >\n <span className=\"text-sm font-medium\">\n {categoryLabels?.[category] ?? titleCase(category)}{\" \"}\n <span className=\"text-muted-foreground\">({items.length})</span>\n </span>\n {isOpen ? (\n <ChevronDown className=\"size-4\" />\n ) : (\n <ChevronRight className=\"size-4\" />\n )}\n </button>\n {isOpen && (\n <ul className=\"divide-y divide-border\">\n {items.map((fact) => (\n <li\n key={fact.id}\n className=\"flex items-start justify-between gap-3 px-3 py-2\"\n >\n <div className=\"min-w-0 flex-1\">\n <p className=\"text-sm\">{readableValue(fact.value)}</p>\n <p className=\"mt-0.5 text-xs text-muted-foreground\">\n {t(\"factList.metaLine\", {\n confidence: fact.confidence.toFixed(2),\n importance: fact.importance,\n })}\n </p>\n </div>\n <Button\n size=\"sm\"\n variant=\"ghost\"\n disabled={pending}\n onClick={() => handleDelete(fact.id)}\n className=\"size-7 shrink-0 p-0\"\n aria-label={t(\"factList.deleteAria\")}\n >\n <Trash2 className=\"size-3.5\" />\n </Button>\n </li>\n ))}\n </ul>\n )}\n </div>\n );\n })}\n </div>\n );\n}\n",
48
+ "type": "registry:component",
49
+ "target": "components/privacy/fact-list.tsx"
50
+ },
51
+ {
52
+ "path": "base/privacy-settings/messages/en.json",
53
+ "content": "{\n \"page\": {\n \"title\": \"Privacy\",\n \"description\": \"See, export, and delete what the AI has learned about you.\",\n \"export\": {\n \"title\": \"Export your data\",\n \"description\": \"Download everything stored about you — every fact, memory, profile snapshot, and audit record — as a single JSON file.\"\n },\n \"yourData\": {\n \"title\": \"Your data\",\n \"description\": \"What the AI has learned about you, grouped by category. Deleting a fact removes it immediately.\"\n },\n \"auditTrail\": {\n \"title\": \"Audit trail\",\n \"description\": \"{count, plural, one {The last # change to your data, most recent first.} other {The last # changes to your data, most recent first.}}\",\n \"empty\": \"No changes recorded yet.\"\n }\n },\n \"exportButton\": {\n \"download\": \"Download my data\",\n \"preparing\": \"Preparing your download…\",\n \"success\": \"Your data was downloaded\",\n \"error\": \"Couldn't export your data. Please try again.\"\n },\n \"factList\": {\n \"empty\": \"Nothing has been recorded about you yet.\",\n \"metaLine\": \"confidence {confidence} · importance {importance}\",\n \"deleteAria\": \"Delete this fact\",\n \"emptyHint\": \"As you chat, anything the assistant learns about you shows up here — and you can delete any of it.\"\n },\n \"errors\": {\n \"notFound\": \"That item no longer exists, or isn't yours.\",\n \"invalidInput\": \"That request wasn't valid.\",\n \"databaseError\": \"Something went wrong. Please try again.\",\n \"somethingWentWrong\": \"Something went wrong.\"\n }\n}\n",
54
+ "type": "registry:file",
55
+ "target": "messages/en/privacy-settings.json"
56
+ },
57
+ {
58
+ "path": "base/privacy-settings/error.tsx",
59
+ "content": "\"use client\";\n\n/**\n * Error boundary for privacy settings. Renders the shared `RouteError` from the\n * `route-error` item — install it alongside this one.\n */\n\nimport { RouteError } from \"@/components/shared/route-error\";\n\nexport default function SegmentError({\n error,\n reset,\n}: {\n error: Error & { digest?: string };\n reset: () => void;\n}) {\n return <RouteError error={error} reset={reset} scope=\"settings-privacy\" />;\n}\n",
60
+ "type": "registry:page",
61
+ "target": "app/[locale]/(app)/settings/privacy/error.tsx"
62
+ }
63
+ ],
64
+ "type": "registry:block"
65
+ }
@@ -0,0 +1,68 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "profile-settings",
4
+ "title": "Profile Settings",
5
+ "description": "User profile name editing, read-only email, and account deletion with typed confirmation, backed by the framework's profile service.",
6
+ "dependencies": [
7
+ "@intelligo-dev/auth",
8
+ "@intelligo-dev/core",
9
+ "@intelligo-dev/next",
10
+ "sonner",
11
+ "server-only",
12
+ "next-intl"
13
+ ],
14
+ "registryDependencies": [
15
+ "@intelligo/button",
16
+ "card",
17
+ "@intelligo/dialog",
18
+ "@intelligo/input",
19
+ "label",
20
+ "skeleton",
21
+ "@intelligo/page-header"
22
+ ],
23
+ "files": [
24
+ {
25
+ "path": "base/profile-settings/page.tsx",
26
+ "content": "/**\n * No workspace or role gating: a profile belongs to the user, not to\n * any one workspace.\n */\n\nimport { getTranslations } from \"next-intl/server\";\n\nimport { profile } from \"@/lib/profile\";\nimport { ProfileSettingsForm } from \"@/components/profile/profile-settings-form\";\nimport {\n PageHeader,\n PageHeaderContent,\n PageHeaderDescription,\n PageHeaderTitle,\n} from \"@/components/ui/page-header\";\n\nexport default async function ProfileSettingsPage() {\n const t = await getTranslations(\"profile-settings\");\n const user = await profile.getProfile();\n\n return (\n <div className=\"space-y-6\">\n <PageHeader>\n <PageHeaderContent>\n <PageHeaderTitle level={2}>{t(\"page.title\")}</PageHeaderTitle>\n <PageHeaderDescription>{t(\"page.description\")}</PageHeaderDescription>\n </PageHeaderContent>\n </PageHeader>\n\n <ProfileSettingsForm user={user} />\n </div>\n );\n}\n",
27
+ "type": "registry:page",
28
+ "target": "app/[locale]/(app)/settings/profile/page.tsx"
29
+ },
30
+ {
31
+ "path": "base/profile-settings/loading.tsx",
32
+ "content": "import { Card } from \"@/components/ui/card\";\nimport { Skeleton } from \"@/components/ui/skeleton\";\n\nexport default function ProfileSettingsLoading() {\n return (\n <div className=\"space-y-6 max-w-2xl\">\n <div>\n <Skeleton className=\"h-7 w-32\" />\n <Skeleton className=\"mt-2 h-4 w-64\" />\n </div>\n\n <Card className=\"p-6\">\n <div className=\"space-y-6\">\n <div className=\"flex items-center gap-4\">\n <Skeleton className=\"size-14 rounded-full\" />\n <Skeleton className=\"h-4 w-40\" />\n </div>\n\n <div className=\"space-y-2\">\n <Skeleton className=\"h-4 w-24\" />\n <Skeleton className=\"h-10 w-full\" />\n </div>\n\n <div className=\"space-y-2\">\n <Skeleton className=\"h-4 w-24\" />\n <Skeleton className=\"h-10 w-full\" />\n </div>\n\n <div className=\"flex justify-end\">\n <Skeleton className=\"h-10 w-32\" />\n </div>\n </div>\n </Card>\n\n <Card className=\"p-6 border-destructive/30\">\n <div className=\"space-y-4\">\n <Skeleton className=\"h-5 w-28\" />\n <Skeleton className=\"h-4 w-72\" />\n <Skeleton className=\"h-10 w-40\" />\n </div>\n </Card>\n </div>\n );\n}\n",
33
+ "type": "registry:page",
34
+ "target": "app/[locale]/(app)/settings/profile/loading.tsx"
35
+ },
36
+ {
37
+ "path": "base/profile-settings/components/profile-settings-form.tsx",
38
+ "content": "\"use client\";\n\n/**\n * Profile settings form — name editor, read-only email, and an\n * account-deletion danger zone.\n *\n * The delete dialog requires typing DELETE to confirm: deleting an\n * account is irreversible and takes every workspace membership with it.\n *\n * `DELETE_CONFIRMATION` is not translated: the user types the literal\n * \"DELETE\" in every locale, and only the sentence around it is a\n * message.\n */\n\nimport { useState, useTransition, type FormEvent } from \"react\";\nimport { useTranslations } from \"next-intl\";\nimport { toast } from \"sonner\";\n\nimport { useRouter } from \"@/i18n/navigation\";\nimport { updateProfile, deleteAccount } from \"@/actions/profile\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from \"@/components/ui/card\";\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n DialogTrigger,\n} from \"@/components/ui/dialog\";\n\nconst DELETE_CONFIRMATION = \"DELETE\";\n\ninterface ProfileSettingsFormProps {\n user: {\n name: string | null;\n email: string;\n image?: string | null;\n };\n}\n\nfunction initialFrom(user: ProfileSettingsFormProps[\"user\"]): string {\n const source = user.name || user.email;\n return source.charAt(0).toUpperCase();\n}\n\nexport function ProfileSettingsForm({ user }: ProfileSettingsFormProps) {\n const t = useTranslations(\"profile-settings\");\n const router = useRouter();\n const [isPending, startTransition] = useTransition();\n const [name, setName] = useState(user.name ?? \"\");\n const [error, setError] = useState<string | null>(null);\n\n const [deleteOpen, setDeleteOpen] = useState(false);\n const [confirmText, setConfirmText] = useState(\"\");\n const [isDeleting, setIsDeleting] = useState(false);\n\n function handleSubmit(event: FormEvent<HTMLFormElement>) {\n event.preventDefault();\n setError(null);\n\n startTransition(async () => {\n const result = await updateProfile({ name });\n if (!result.success) {\n setError(result.error);\n return;\n }\n toast.success(t(\"form.updated\"));\n router.refresh();\n });\n }\n\n function handleDelete() {\n if (confirmText !== DELETE_CONFIRMATION) return;\n\n setIsDeleting(true);\n startTransition(async () => {\n const result = await deleteAccount();\n if (!result.success) {\n toast.error(result.error);\n setIsDeleting(false);\n return;\n }\n toast.success(t(\"dangerZone.deleted\"));\n router.push(\"/login\");\n router.refresh();\n });\n }\n\n function resetDeleteDialog(open: boolean) {\n setDeleteOpen(open);\n if (!open) {\n setConfirmText(\"\");\n setIsDeleting(false);\n }\n }\n\n return (\n <>\n <Card>\n <CardHeader>\n <CardTitle>{t(\"form.generalTitle\")}</CardTitle>\n <CardDescription>{t(\"form.generalDescription\")}</CardDescription>\n </CardHeader>\n <CardContent>\n <form onSubmit={handleSubmit} className=\"space-y-6\">\n {error && <p className=\"text-sm text-destructive\">{error}</p>}\n\n <div className=\"flex items-center gap-4\">\n <div className=\"flex size-14 items-center justify-center rounded-full bg-muted text-lg font-semibold text-foreground\">\n {initialFrom(user)}\n </div>\n <p className=\"text-sm text-muted-foreground\">\n {t(\"form.avatarCaption\")}\n </p>\n </div>\n\n <div className=\"space-y-2\">\n <Label htmlFor=\"name\">{t(\"form.nameLabel\")}</Label>\n <Input\n id=\"name\"\n value={name}\n onChange={(event) => setName(event.target.value)}\n disabled={isPending}\n required\n minLength={2}\n maxLength={100}\n />\n </div>\n\n <div className=\"space-y-2\">\n <Label htmlFor=\"email\">{t(\"form.emailLabel\")}</Label>\n <Input id=\"email\" value={user.email} disabled />\n <p className=\"text-xs text-muted-foreground\">\n {t(\"form.emailNote\")}\n </p>\n </div>\n\n <div className=\"flex justify-end\">\n <Button type=\"submit\" disabled={isPending}>\n {isPending ? t(\"form.savePending\") : t(\"form.save\")}\n </Button>\n </div>\n </form>\n </CardContent>\n </Card>\n\n <Card className=\"border-destructive/30\">\n <CardHeader>\n <CardTitle className=\"text-destructive\">\n {t(\"dangerZone.title\")}\n </CardTitle>\n <CardDescription>{t(\"dangerZone.description\")}</CardDescription>\n </CardHeader>\n <CardContent>\n <Dialog open={deleteOpen} onOpenChange={resetDeleteDialog}>\n <DialogTrigger render={<Button variant=\"destructive\" />}>\n {t(\"dangerZone.trigger\")}\n </DialogTrigger>\n <DialogContent>\n <DialogHeader>\n <DialogTitle>{t(\"dangerZone.dialogTitle\")}</DialogTitle>\n <DialogDescription>\n {t.rich(\"dangerZone.dialogDescription\", {\n token: () => (\n <span className=\"font-mono font-semibold\">\n {DELETE_CONFIRMATION}\n </span>\n ),\n })}\n </DialogDescription>\n </DialogHeader>\n\n <div className=\"space-y-2\">\n <Label htmlFor=\"confirm-delete\" className=\"sr-only\">\n {t(\"dangerZone.confirmLabel\", {\n token: DELETE_CONFIRMATION,\n })}\n </Label>\n <Input\n id=\"confirm-delete\"\n value={confirmText}\n onChange={(event) => setConfirmText(event.target.value)}\n placeholder={DELETE_CONFIRMATION}\n disabled={isDeleting}\n autoComplete=\"off\"\n />\n </div>\n\n <DialogFooter>\n <Button\n variant=\"outline\"\n onClick={() => resetDeleteDialog(false)}\n disabled={isDeleting}\n >\n {t(\"dangerZone.cancel\")}\n </Button>\n <Button\n variant=\"destructive\"\n onClick={handleDelete}\n disabled={isDeleting || confirmText !== DELETE_CONFIRMATION}\n >\n {isDeleting\n ? t(\"dangerZone.confirmPending\")\n : t(\"dangerZone.confirm\")}\n </Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n </CardContent>\n </Card>\n </>\n );\n}\n",
39
+ "type": "registry:component",
40
+ "target": "components/profile/profile-settings-form.tsx"
41
+ },
42
+ {
43
+ "path": "base/profile-settings/actions.ts",
44
+ "content": "\"use server\";\n\n/**\n * Profile settings server actions — thin transport over the bound\n * profile service (`@/lib/profile`): parse input with the schema from\n * `@intelligo-dev/auth`, call the service, map any `ProfileServiceError` to\n * a friendly message, and revalidate. No business rules here — those\n * live in the service.\n */\n\nimport { revalidatePath } from \"next/cache\";\nimport { getTranslations } from \"next-intl/server\";\n\nimport {\n isProfileServiceError,\n updateProfileSchema,\n type UpdateProfileInput,\n} from \"@intelligo-dev/auth\";\nimport type { ActionResult } from \"@intelligo-dev/next\";\n\nimport { routing } from \"@/i18n/routing\";\nimport { profile } from \"@/lib/profile\";\n\nexport type ProfileActionResult<T = undefined> = ActionResult<T>;\n\ntype Translator = Awaited<\n ReturnType<typeof getTranslations<\"profile-settings\">>\n>;\n\nfunction friendlyMessage(t: Translator): Partial<Record<string, string>> {\n return {\n forbidden: t(\"errors.forbidden\"),\n provider_error: t(\"errors.providerError\"),\n };\n}\n\nfunction friendlyError(error: unknown, t: Translator): string {\n if (isProfileServiceError(error)) {\n return friendlyMessage(t)[error.code] ?? error.message;\n }\n // `Error#message` can carry internals (SQL, hostnames) to the UI.\n console.error(\"[profile-settings]\", error);\n return t(\"errors.somethingWentWrong\");\n}\n\nexport async function updateProfile(\n input: UpdateProfileInput\n): Promise<ProfileActionResult> {\n const t = await getTranslations(\"profile-settings\");\n const parsed = updateProfileSchema.safeParse(input);\n if (!parsed.success) {\n return {\n success: false,\n error: parsed.error.issues[0]?.message ?? t(\"errors.invalidInput\"),\n };\n }\n\n try {\n await profile.updateProfile(parsed.data);\n revalidatePath(\"/settings/profile\");\n return { success: true, data: undefined };\n } catch (error) {\n return { success: false, error: friendlyError(error, t) };\n }\n}\n\n/**\n * Record the caller's preferred language — one of the locales the app\n * routes (`i18n/routing.ts`). An onboarding step or a settings control\n * calls it; switching the page's locale is the language switcher's job.\n */\nexport async function updatePreferredLanguage(\n locale: string\n): Promise<ProfileActionResult> {\n const t = await getTranslations(\"profile-settings\");\n if (!(routing.locales as readonly string[]).includes(locale)) {\n return { success: false, error: t(\"errors.invalidInput\") };\n }\n\n try {\n await profile.setPreferredLanguage(locale);\n revalidatePath(\"/\", \"layout\");\n return { success: true, data: undefined };\n } catch (error) {\n return { success: false, error: friendlyError(error, t) };\n }\n}\n\nexport async function deleteAccount(): Promise<ProfileActionResult> {\n const t = await getTranslations(\"profile-settings\");\n try {\n await profile.deleteAccount();\n revalidatePath(\"/\", \"layout\");\n return { success: true, data: undefined };\n } catch (error) {\n return { success: false, error: friendlyError(error, t) };\n }\n}\n",
45
+ "type": "registry:file",
46
+ "target": "actions/profile.ts"
47
+ },
48
+ {
49
+ "path": "base/profile-settings/lib/profile.ts",
50
+ "content": "import \"server-only\";\n\n/**\n * Binds the profile service (`@intelligo-dev/auth`) with an\n * account-deletion confirmation email sent through `sendEmail`\n * (`@intelligo-dev/core/email`). `sendEmail` never throws and the\n * service treats `onAccountDeleted` as fire-and-forget, so no error\n * handling is needed. Drop the binding to send no email.\n */\n\nimport { getTranslations } from \"next-intl/server\";\n\nimport { createProfileService } from \"@intelligo-dev/auth\";\nimport { sendEmail } from \"@intelligo-dev/core/email\";\n\nexport const profile = createProfileService({\n onAccountDeleted: async ({ email }) => {\n const t = await getTranslations(\"profile-settings\");\n await sendEmail({\n to: email,\n subject: t(\"email.subject\"),\n html: `\n <h2>${t(\"email.heading\")}</h2>\n <p>${t(\"email.body\", { email })}</p>\n <p>${t(\"email.warning\")}</p>\n `,\n });\n },\n});\n",
51
+ "type": "registry:file",
52
+ "target": "lib/profile.ts"
53
+ },
54
+ {
55
+ "path": "base/profile-settings/messages/en.json",
56
+ "content": "{\n \"page\": {\n \"title\": \"Profile\",\n \"description\": \"Manage your name, email, and account.\"\n },\n \"form\": {\n \"generalTitle\": \"General\",\n \"generalDescription\": \"Update your name. Your email is managed by your login provider.\",\n \"avatarCaption\": \"Generated automatically from your name.\",\n \"nameLabel\": \"Name\",\n \"emailLabel\": \"Email\",\n \"emailNote\": \"Your email address can't be changed here.\",\n \"save\": \"Save changes\",\n \"savePending\": \"Saving…\",\n \"updated\": \"Profile updated\"\n },\n \"dangerZone\": {\n \"title\": \"Danger zone\",\n \"description\": \"Deleting your account schedules it for permanent removal and signs you out everywhere. This cannot be undone.\",\n \"trigger\": \"Delete account\",\n \"dialogTitle\": \"Delete account\",\n \"dialogDescription\": \"This will schedule your account for deletion and sign you out of every session. Type <token>{token}</token> to confirm.\",\n \"confirmLabel\": \"Type {token} to confirm\",\n \"cancel\": \"Cancel\",\n \"confirm\": \"Delete account\",\n \"confirmPending\": \"Deleting…\",\n \"deleted\": \"Your account has been scheduled for deletion\"\n },\n \"errors\": {\n \"forbidden\": \"You don't have permission to do that.\",\n \"providerError\": \"Something went wrong. Please try again.\",\n \"somethingWentWrong\": \"Something went wrong.\",\n \"invalidInput\": \"Invalid input\"\n },\n \"email\": {\n \"subject\": \"Your account has been scheduled for deletion\",\n \"heading\": \"Account deletion confirmed\",\n \"body\": \"Your account ({email}) has been scheduled for deletion.\",\n \"warning\": \"If this wasn't you, contact support right away.\"\n }\n}\n",
57
+ "type": "registry:file",
58
+ "target": "messages/en/profile-settings.json"
59
+ },
60
+ {
61
+ "path": "base/profile-settings/error.tsx",
62
+ "content": "\"use client\";\n\n/**\n * Error boundary for profile settings. Renders the shared `RouteError` from the\n * `route-error` item — install it alongside this one.\n */\n\nimport { RouteError } from \"@/components/shared/route-error\";\n\nexport default function SegmentError({\n error,\n reset,\n}: {\n error: Error & { digest?: string };\n reset: () => void;\n}) {\n return <RouteError error={error} reset={reset} scope=\"settings-profile\" />;\n}\n",
63
+ "type": "registry:page",
64
+ "target": "app/[locale]/(app)/settings/profile/error.tsx"
65
+ }
66
+ ],
67
+ "type": "registry:block"
68
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "progress",
4
+ "title": "Progress",
5
+ "description": "A rounded bar whose fill glides to each new value.",
6
+ "dependencies": [
7
+ "@base-ui/react"
8
+ ],
9
+ "registryDependencies": [],
10
+ "files": [
11
+ {
12
+ "path": "base/ui/progress/progress.tsx",
13
+ "content": "\"use client\";\n\n/*\n * Progress: a rounded track whose fill glides to each new value.\n */\n\nimport { Progress as ProgressPrimitive } from \"@base-ui/react/progress\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction Progress({\n className,\n children,\n value,\n ...props\n}: ProgressPrimitive.Root.Props) {\n return (\n <ProgressPrimitive.Root\n value={value}\n data-slot=\"progress\"\n className={cn(\"flex flex-wrap gap-3\", className)}\n {...props}\n >\n {children}\n <ProgressTrack>\n <ProgressIndicator />\n </ProgressTrack>\n </ProgressPrimitive.Root>\n );\n}\n\nfunction ProgressTrack({ className, ...props }: ProgressPrimitive.Track.Props) {\n return (\n <ProgressPrimitive.Track\n className={cn(\n \"relative flex h-1.5 w-full items-center overflow-x-hidden rounded-full bg-muted-foreground/15\",\n className\n )}\n data-slot=\"progress-track\"\n {...props}\n />\n );\n}\n\nfunction ProgressIndicator({\n className,\n ...props\n}: ProgressPrimitive.Indicator.Props) {\n return (\n <ProgressPrimitive.Indicator\n data-slot=\"progress-indicator\"\n className={cn(\n \"h-full rounded-full bg-primary transition-[width] duration-slow ease-standard\",\n className\n )}\n {...props}\n />\n );\n}\n\nfunction ProgressLabel({ className, ...props }: ProgressPrimitive.Label.Props) {\n return (\n <ProgressPrimitive.Label\n className={cn(\"text-sm font-medium\", className)}\n data-slot=\"progress-label\"\n {...props}\n />\n );\n}\n\nfunction ProgressValue({ className, ...props }: ProgressPrimitive.Value.Props) {\n return (\n <ProgressPrimitive.Value\n className={cn(\n \"ml-auto text-sm text-muted-foreground tabular-nums\",\n className\n )}\n data-slot=\"progress-value\"\n {...props}\n />\n );\n}\n\nexport {\n Progress,\n ProgressTrack,\n ProgressIndicator,\n ProgressLabel,\n ProgressValue,\n};\n",
14
+ "type": "registry:ui",
15
+ "target": "components/ui/progress.tsx"
16
+ }
17
+ ],
18
+ "type": "registry:ui"
19
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "radio-group",
4
+ "title": "Radio Group",
5
+ "description": "One choice from a set; the dot springs out from the centre.",
6
+ "dependencies": [
7
+ "@base-ui/react",
8
+ "motion"
9
+ ],
10
+ "registryDependencies": [
11
+ "@intelligo/ai-motion"
12
+ ],
13
+ "files": [
14
+ {
15
+ "path": "base/ui/radio-group/radio-group.tsx",
16
+ "content": "\"use client\";\n\n/*\n * The radio group: the dot springs out from the centre.\n */\n\nimport { Radio as RadioPrimitive } from \"@base-ui/react/radio\";\nimport { RadioGroup as RadioGroupPrimitive } from \"@base-ui/react/radio-group\";\nimport { motion, useReducedMotion } from \"motion/react\";\n\nimport { SPRING_PRESS } from \"@/components/ui/ai-motion\";\nimport { cn } from \"@/lib/utils\";\n\nfunction RadioGroup({ className, ...props }: RadioGroupPrimitive.Props) {\n return (\n <RadioGroupPrimitive\n data-slot=\"radio-group\"\n className={cn(\"grid w-full gap-2\", className)}\n {...props}\n />\n );\n}\n\nfunction RadioGroupItem({ className, ...props }: RadioPrimitive.Root.Props) {\n const reduced = useReducedMotion() ?? false;\n\n return (\n <RadioPrimitive.Root\n data-slot=\"radio-group-item\"\n className={cn(\n \"group/radio-group-item peer relative flex aspect-square size-4 shrink-0 rounded-full border border-input bg-card transition-[background-color,border-color,box-shadow,scale] duration-fast ease-standard outline-none group-has-[:focus-visible]/field-label:ring-0 group-has-[:focus-visible]/field-label:not-data-checked:border-input after:absolute after:-inset-x-3 after:-inset-y-2 hover:border-ring/60 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/40 active:scale-92 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 aria-invalid:aria-checked:border-primary data-checked:border-primary data-checked:bg-primary data-checked:text-primary-foreground group-has-[:focus-visible]/field-label:data-checked:border-primary\",\n className\n )}\n {...props}\n >\n <RadioPrimitive.Indicator\n data-slot=\"radio-group-indicator\"\n keepMounted\n className=\"flex size-4 items-center justify-center\"\n render={(indicatorProps, state) => (\n <span {...indicatorProps}>\n {/* The dot pops in on a spring and shrinks away when cleared. */}\n <motion.span\n className=\"absolute top-1/2 left-1/2 size-1.5 -translate-x-1/2 -translate-y-1/2 rounded-full bg-primary-foreground\"\n initial={false}\n animate={{ scale: state.checked ? 1 : 0 }}\n transition={reduced ? { duration: 0 } : SPRING_PRESS}\n />\n </span>\n )}\n />\n </RadioPrimitive.Root>\n );\n}\n\nexport { RadioGroup, RadioGroupItem };\n",
17
+ "type": "registry:ui",
18
+ "target": "components/ui/radio-group.tsx"
19
+ }
20
+ ],
21
+ "type": "registry:ui"
22
+ }