handoff-app 2.0.0-9 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (303) hide show
  1. package/{Changelog.md → CHANGELOG.md} +85 -1
  2. package/{License.md → LICENSE.md} +1 -1
  3. package/README.md +813 -406
  4. package/RECIPES.md +975 -0
  5. package/UPGRADE.md +57 -9
  6. package/components.json +20 -20
  7. package/config/docs/foundations/colors/guidelines.md +0 -1
  8. package/config/docs/foundations/colors.md +0 -1
  9. package/config/docs/foundations/effects/guidelines.md +0 -1
  10. package/config/docs/foundations/effects/resources.md +0 -1
  11. package/config/docs/foundations/effects.md +0 -1
  12. package/config/docs/foundations/fonts.md +0 -1
  13. package/config/docs/foundations/grid.md +0 -1
  14. package/config/docs/foundations/icon.md +0 -1
  15. package/config/docs/foundations/icons/guidelines.md +0 -1
  16. package/config/docs/foundations/icons.md +0 -1
  17. package/config/docs/foundations/images/guidelines.md +0 -1
  18. package/config/docs/foundations/images/resources.md +0 -1
  19. package/config/docs/foundations/images.md +0 -1
  20. package/config/docs/foundations/logo/resources.md +0 -1
  21. package/config/docs/foundations/logo.md +0 -1
  22. package/config/docs/foundations/typography/guidelines.md +0 -1
  23. package/config/docs/foundations/typography/resources.md +0 -1
  24. package/config/docs/foundations/typography.md +0 -1
  25. package/config/docs/foundations.md +0 -1
  26. package/config/docs/guidelines.md +0 -1
  27. package/config/docs/index.md +0 -1
  28. package/config/docs/system/tokens/foundations/colors.md +0 -4
  29. package/config/docs/system/tokens/foundations/effects.md +0 -4
  30. package/config/docs/system/tokens/foundations/typography.md +0 -2
  31. package/config/docs/system.md +0 -2
  32. package/config/templates/page.md +2 -1
  33. package/config/templates/page.mdx +2 -1
  34. package/dist/ai/connections.d.ts +66 -0
  35. package/dist/ai/connections.js +149 -0
  36. package/dist/ai/define.d.ts +10 -0
  37. package/dist/ai/define.js +13 -0
  38. package/dist/ai/types.d.ts +26 -0
  39. package/dist/ai/types.js +10 -0
  40. package/dist/app-builder/build.d.ts +14 -7
  41. package/dist/app-builder/build.js +195 -66
  42. package/dist/app-builder/client-config.js +3 -2
  43. package/dist/app-builder/paths.d.ts +0 -12
  44. package/dist/app-builder/paths.js +3 -20
  45. package/dist/app-builder/static-export.js +1 -1
  46. package/dist/app-builder/vercel-output.d.ts +1 -1
  47. package/dist/app-builder/vercel-output.js +5 -4
  48. package/dist/cache/build-cache.d.ts +27 -4
  49. package/dist/cache/build-cache.js +114 -47
  50. package/dist/cache/build-inputs.d.ts +13 -0
  51. package/dist/cache/build-inputs.js +19 -0
  52. package/dist/cache/index.d.ts +2 -1
  53. package/dist/cache/index.js +5 -1
  54. package/dist/cli/create.js +1 -2
  55. package/dist/commands/build/index.d.ts +1 -1
  56. package/dist/commands/build/index.js +1 -1
  57. package/dist/commands/utils.d.ts +3 -3
  58. package/dist/config/defaults.js +7 -4
  59. package/dist/config/from-env.d.ts +4 -0
  60. package/dist/config/from-env.js +38 -1
  61. package/dist/config/helpers.js +2 -2
  62. package/dist/config/index.d.ts +0 -1
  63. package/dist/config/index.js +1 -4
  64. package/dist/config/loader.js +26 -2
  65. package/dist/config/resolve-env.d.ts +2 -1
  66. package/dist/config/resolve-env.js +28 -12
  67. package/dist/config/runtime.js +1 -1
  68. package/dist/index.d.ts +12 -7
  69. package/dist/index.js +18 -22
  70. package/dist/nav/index.d.ts +2 -2
  71. package/dist/nav/registry.d.ts +3 -3
  72. package/dist/nav/registry.js +1 -1
  73. package/dist/nav/workspace.d.ts +3 -3
  74. package/dist/nav/workspace.js +1 -1
  75. package/dist/pipeline/figma.d.ts +13 -2
  76. package/dist/pipeline/figma.js +86 -46
  77. package/dist/pipeline/styles.js +1 -1
  78. package/dist/pipeline/validation.d.ts +1 -1
  79. package/dist/pipeline/validation.js +3 -3
  80. package/dist/providers.d.ts +10 -0
  81. package/dist/providers.js +13 -0
  82. package/dist/registry/asset-storage/define.d.ts +5 -5
  83. package/dist/registry/asset-storage/define.js +4 -4
  84. package/dist/registry/asset-storage/resolve.d.ts +9 -19
  85. package/dist/registry/asset-storage/resolve.js +33 -21
  86. package/dist/registry/asset-storage/types.d.ts +37 -17
  87. package/dist/registry/asset-storage/types.js +5 -5
  88. package/dist/registry/auth/ai-keys.d.ts +32 -0
  89. package/dist/registry/auth/ai-keys.js +69 -0
  90. package/dist/registry/auth/crypto.d.ts +17 -0
  91. package/dist/registry/auth/crypto.js +61 -1
  92. package/dist/registry/auth/index.d.ts +2 -1
  93. package/dist/registry/auth/index.js +11 -2
  94. package/dist/registry/checkout/index.d.ts +2 -1
  95. package/dist/registry/checkout/index.js +57 -127
  96. package/dist/registry/client.js +53 -17
  97. package/dist/registry/db/driver.js +3 -3
  98. package/dist/registry/db/schema.d.ts +261 -3
  99. package/dist/registry/db/schema.js +29 -4
  100. package/dist/registry/email/define.d.ts +10 -0
  101. package/dist/registry/email/define.js +13 -0
  102. package/dist/registry/email/providers/smtp.d.ts +10 -0
  103. package/dist/registry/email/providers/smtp.js +73 -0
  104. package/dist/registry/email/resolve.d.ts +28 -0
  105. package/dist/registry/email/resolve.js +74 -0
  106. package/dist/registry/email/types.d.ts +24 -0
  107. package/dist/registry/email/types.js +6 -0
  108. package/dist/registry/path.d.ts +2 -0
  109. package/dist/registry/path.js +11 -1
  110. package/dist/registry/publish/package.js +8 -0
  111. package/dist/registry/transfer.d.ts +4 -0
  112. package/dist/store/registry.d.ts +3 -3
  113. package/dist/store/registry.js +4 -4
  114. package/dist/transformers/preview/component/builder.d.ts +1 -9
  115. package/dist/transformers/preview/component/builder.js +17 -17
  116. package/dist/transformers/preview/component/css.d.ts +2 -1
  117. package/dist/transformers/preview/component/css.js +8 -5
  118. package/dist/transformers/preview/component/html.d.ts +3 -1
  119. package/dist/transformers/preview/component/html.js +6 -2
  120. package/dist/transformers/preview/component/javascript.d.ts +2 -1
  121. package/dist/transformers/preview/component/javascript.js +10 -4
  122. package/dist/transformers/utils/build.d.ts +5 -0
  123. package/dist/transformers/utils/build.js +21 -4
  124. package/dist/types/config.d.ts +129 -18
  125. package/dist/utils/path.d.ts +1 -5
  126. package/dist/utils/path.js +6 -23
  127. package/docs/api.md +6 -4
  128. package/docs/cli.md +275 -54
  129. package/drizzle/0007_add_ai_keys.sql +11 -0
  130. package/drizzle/0008_add_source_dir.sql +2 -0
  131. package/drizzle/meta/0007_snapshot.json +2013 -0
  132. package/drizzle/meta/0008_snapshot.json +2025 -0
  133. package/drizzle/meta/_journal.json +14 -0
  134. package/package.json +19 -8
  135. package/src/ai/README.md +42 -0
  136. package/src/ai/connections.ts +189 -0
  137. package/src/ai/define.ts +15 -0
  138. package/src/ai/types.ts +30 -0
  139. package/src/app/components/AiAssistant/AiAssistantPanel.tsx +144 -0
  140. package/src/app/components/AiAssistant/AiAssistantProvider.tsx +215 -0
  141. package/src/app/components/AiAssistant/AiConversation.tsx +737 -0
  142. package/src/app/components/AiAssistant/AiToolResult.tsx +99 -0
  143. package/src/app/components/AiAssistant/dockState.ts +86 -0
  144. package/src/app/components/AiAssistant/toolLabel.ts +24 -0
  145. package/src/app/components/AiAssistant/useAiChat.ts +147 -0
  146. package/src/app/components/AiAssistant/useAiConnections.ts +86 -0
  147. package/src/app/components/AiAssistant/usePageContext.ts +63 -0
  148. package/src/app/components/Auth/AccountLayout.tsx +58 -19
  149. package/src/app/components/Auth/AuthShell.tsx +27 -33
  150. package/src/app/components/Auth/RegistryInstallGate.tsx +4 -8
  151. package/src/app/components/Auth/installStatus.ts +29 -0
  152. package/src/app/components/Component/Preview.tsx +5 -2
  153. package/src/app/components/Foundations/ColorGrid.tsx +13 -14
  154. package/src/app/components/Layout/Header.tsx +21 -15
  155. package/src/app/components/Layout/Main.tsx +2 -2
  156. package/src/app/components/Markdown/MarkdownComponents.tsx +30 -32
  157. package/src/app/components/McpIntegration/McpConfigDialog.tsx +12 -51
  158. package/src/app/components/Navigation/MobileNav.tsx +2 -2
  159. package/src/app/components/Navigation/SideNav.tsx +1 -15
  160. package/src/app/components/context/NavProvider.tsx +7 -3
  161. package/src/app/components/ui/attachment.tsx +50 -0
  162. package/src/app/components/ui/bubble.tsx +57 -0
  163. package/src/app/components/ui/button.tsx +3 -1
  164. package/src/app/components/ui/confirm-dialog.tsx +87 -0
  165. package/src/app/components/ui/dialog.tsx +7 -2
  166. package/src/app/components/ui/marker.tsx +56 -0
  167. package/src/app/components/ui/sidebar.tsx +3 -1
  168. package/src/app/components/util/index.ts +3 -12
  169. package/src/app/css/index.css +64 -0
  170. package/src/app/lib/ai/auth.ts +33 -0
  171. package/src/app/lib/ai/context.ts +99 -0
  172. package/src/app/lib/ai/model.ts +79 -0
  173. package/src/app/lib/ai/prompt.ts +36 -0
  174. package/src/app/lib/ai/resolve.ts +97 -0
  175. package/src/app/lib/ai/tools.ts +120 -0
  176. package/src/app/lib/api/methods.ts +13 -0
  177. package/src/app/lib/asset-storage.ts +35 -36
  178. package/src/app/lib/auth/api.ts +4 -12
  179. package/src/app/lib/auth/email.ts +96 -23
  180. package/src/app/lib/docs-api/page-rendering.ts +1 -1
  181. package/src/app/lib/docs-api/registry.ts +2 -3
  182. package/src/app/lib/docs-api/runtime-config.ts +47 -21
  183. package/src/app/lib/mcp/components.ts +523 -0
  184. package/src/app/lib/mcp/page-url.ts +24 -0
  185. package/src/app/lib/mcp/pages.ts +84 -0
  186. package/src/app/lib/mcp/result.ts +33 -0
  187. package/src/app/lib/mcp/server.ts +39 -318
  188. package/src/app/lib/mcp/tokens.ts +334 -0
  189. package/src/app/lib/mcp/tool.ts +19 -0
  190. package/src/app/lib/registry-api/asset-transfer.ts +119 -11
  191. package/src/app/lib/registry-api/errors.ts +2 -0
  192. package/src/app/lib/registry-api/handler.ts +7 -1
  193. package/src/app/lib/registry-api/transfer.ts +28 -6
  194. package/src/app/lib/server-module.ts +34 -0
  195. package/src/app/lib/utils.ts +20 -0
  196. package/src/app/next.config.mjs +26 -24
  197. package/src/app/pages/[...slug]/index.tsx +2 -1
  198. package/src/app/pages/_app.tsx +6 -2
  199. package/src/app/pages/_document.tsx +6 -0
  200. package/src/app/pages/account/ai.tsx +183 -0
  201. package/src/app/pages/account/index.tsx +68 -68
  202. package/src/app/pages/account/tokens.tsx +37 -7
  203. package/src/app/pages/account/users.tsx +75 -15
  204. package/src/app/pages/api/account/ai/keys/[connectionId].ts +60 -0
  205. package/src/app/pages/api/account/ai/keys/index.ts +20 -0
  206. package/src/app/pages/api/admin/users/[id]/[action].ts +80 -11
  207. package/src/app/pages/api/admin/users/index.ts +2 -2
  208. package/src/app/pages/api/ai/chat.ts +115 -0
  209. package/src/app/pages/api/ai/connections.ts +23 -0
  210. package/src/app/pages/api/auth/request-reset.ts +14 -16
  211. package/src/app/pages/api/docs/nav.json.ts +1 -1
  212. package/src/app/pages/api/install.ts +3 -3
  213. package/src/app/pages/api/mcp.ts +6 -0
  214. package/src/app/pages/api/registry/transfer/assets/blobs/[hash]/complete.ts +11 -0
  215. package/src/app/pages/api/registry/transfer/assets/blobs/[hash]/upload.ts +12 -0
  216. package/src/app/pages/api/registry/transfer/assets/blobs/[hash].ts +1 -1
  217. package/src/app/pages/cli/device.tsx +10 -5
  218. package/src/app/pages/foundations/colors.tsx +9 -8
  219. package/src/app/pages/foundations/index.tsx +3 -3
  220. package/src/app/pages/index.tsx +2 -2
  221. package/src/app/pages/install.tsx +7 -2
  222. package/src/app/pages/login.tsx +32 -27
  223. package/src/app/pages/reset-password.tsx +21 -20
  224. package/src/app/pages/system/component/[component]/index.tsx +2 -5
  225. package/src/app/pages/system/index.tsx +11 -11
  226. package/src/app/pages/system/pattern/[pattern]/index.tsx +2 -2
  227. package/src/app/pages/system/tokens/components/[component]/index.tsx +0 -4
  228. package/src/app-builder/README.md +2 -1
  229. package/src/app-builder/build.ts +218 -73
  230. package/src/app-builder/client-config.ts +3 -2
  231. package/src/app-builder/paths.ts +3 -22
  232. package/src/app-builder/static-export.ts +1 -1
  233. package/src/app-builder/vercel-output.ts +5 -4
  234. package/src/cache/README.md +4 -1
  235. package/src/cache/build-cache.ts +128 -43
  236. package/src/cache/build-inputs.ts +28 -0
  237. package/src/cache/index.ts +4 -0
  238. package/src/cli/create.ts +1 -2
  239. package/src/commands/build/index.ts +1 -1
  240. package/src/config/README.md +10 -9
  241. package/src/config/defaults.ts +7 -4
  242. package/src/config/from-env.ts +33 -0
  243. package/src/config/helpers.ts +1 -3
  244. package/src/config/index.ts +0 -3
  245. package/src/config/loader.ts +25 -3
  246. package/src/config/resolve-env.ts +28 -15
  247. package/src/config/runtime.ts +1 -1
  248. package/src/index.ts +33 -29
  249. package/src/nav/index.ts +2 -2
  250. package/src/nav/registry.ts +3 -3
  251. package/src/nav/workspace.ts +3 -3
  252. package/src/pipeline/README.md +1 -1
  253. package/src/pipeline/figma.ts +98 -49
  254. package/src/pipeline/styles.ts +1 -1
  255. package/src/pipeline/validation.ts +3 -3
  256. package/src/providers.ts +18 -0
  257. package/src/registry/asset-storage/define.ts +6 -6
  258. package/src/registry/asset-storage/resolve.ts +38 -37
  259. package/src/registry/asset-storage/types.ts +39 -17
  260. package/src/registry/auth/ai-keys.ts +62 -0
  261. package/src/registry/auth/crypto.ts +63 -1
  262. package/src/registry/auth/index.ts +11 -1
  263. package/src/registry/checkout/index.ts +60 -138
  264. package/src/registry/client.ts +60 -20
  265. package/src/registry/db/driver.ts +2 -2
  266. package/src/registry/db/schema.ts +49 -22
  267. package/src/registry/email/define.ts +15 -0
  268. package/src/registry/email/providers/smtp.ts +39 -0
  269. package/src/registry/email/resolve.ts +94 -0
  270. package/src/registry/email/types.ts +28 -0
  271. package/src/registry/path.ts +11 -0
  272. package/src/registry/publish/package.ts +9 -0
  273. package/src/registry/transfer.ts +4 -0
  274. package/src/store/registry.ts +7 -7
  275. package/src/transformers/preview/component/builder.ts +19 -25
  276. package/src/transformers/preview/component/css.ts +9 -4
  277. package/src/transformers/preview/component/html.ts +6 -2
  278. package/src/transformers/preview/component/javascript.ts +14 -4
  279. package/src/transformers/utils/build.ts +16 -3
  280. package/src/types/config.ts +142 -20
  281. package/src/utils/README.md +1 -1
  282. package/src/utils/path.ts +6 -29
  283. package/templates/default/handoff.config.js.tpl +10 -2
  284. package/templates/default/handoff.config.ts.tpl +10 -2
  285. package/templates/default/handoff.config.with-examples.js.tpl +10 -2
  286. package/templates/default/handoff.config.with-examples.ts.tpl +10 -2
  287. package/templates/default/package.json.tpl +2 -2
  288. package/.editorconfig +0 -19
  289. package/.eslintrc.json +0 -4
  290. package/.prettierignore +0 -0
  291. package/.prettierrc.json +0 -8
  292. package/.vscode/settings.json +0 -17
  293. package/dist/config/validator.d.ts +0 -9
  294. package/dist/config/validator.js +0 -24
  295. package/dist/registry/asset-storage/adapters/vercel-blob.d.ts +0 -17
  296. package/dist/registry/asset-storage/adapters/vercel-blob.js +0 -87
  297. package/src/app/components/old/Header.tsx +0 -89
  298. package/src/app/lib/mcp/shape.ts +0 -596
  299. package/src/app/pages/assets/fonts.tsx +0 -129
  300. package/src/app/pages/assets/index.tsx +0 -91
  301. package/src/app/pages/assets/logos.tsx +0 -75
  302. package/src/config/validator.ts +0 -22
  303. package/src/registry/asset-storage/adapters/vercel-blob.ts +0 -51
@@ -0,0 +1,215 @@
1
+ 'use client';
2
+
3
+ import { Sparkles } from 'lucide-react';
4
+ import { useSession } from 'next-auth/react';
5
+ import { useRouter } from 'next/router';
6
+ import * as React from 'react';
7
+
8
+ import { useIsMobile } from '../../hooks/use-mobile';
9
+ import { SheetClose } from '../ui/sheet';
10
+ import { AiAssistantPanel } from './AiAssistantPanel';
11
+ import { DEFAULT_DOCK, DOCK_WIDTH_VAR, clampWidth, dockWidth, isAiAssistantRoute, readDock, writeDock, type DockState } from './dockState';
12
+
13
+ /**
14
+ * Availability and dock state for the docs assistant.
15
+ *
16
+ * Availability comes from values baked in at build time (the `env` block in `next.config.mjs`): a
17
+ * static export has no API routes, and a build without `runtime.ai` has no chat route. The triggers
18
+ * are gated on the same values, so such a build shows no affordance rather than one that fails.
19
+ *
20
+ * The header hides its controls below the `@2xl` container width, so the mobile nav needs a trigger
21
+ * of its own. Both reach the one panel this provider renders, which is also what `⌘K` (`Ctrl K`
22
+ * away from Apple keyboards) toggles.
23
+ *
24
+ * Registry mode adds a second condition: the chat route authorizes on the session, so a signed-out
25
+ * reader sees no control rather than one that answers 401.
26
+ *
27
+ * Mounted in `_app`, above the page tree. That position is what makes the pinned panel possible:
28
+ * every page builds its own `Layout`, so anything inside one is unmounted by the next soft
29
+ * navigation, along with the conversation in it.
30
+ */
31
+ const isAvailable = process.env.HANDOFF_AI_ENABLED === 'true' && process.env.HANDOFF_BUILD_TARGET !== 'static';
32
+ const isRegistryRuntime = process.env.HANDOFF_RUNTIME_MODE === 'registry';
33
+
34
+ interface AiAssistantApi {
35
+ isOpen: boolean;
36
+ open: () => void;
37
+ toggle: () => void;
38
+ }
39
+
40
+ const AiAssistantContext = React.createContext<AiAssistantApi | null>(null);
41
+
42
+ /** The opener, or `null` where this build has no assistant. */
43
+ export const useAiAssistant = (): AiAssistantApi | null => React.useContext(AiAssistantContext);
44
+
45
+ export const AiAssistantProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
46
+ const router = useRouter();
47
+ const visible = isAiAssistantRoute(router.pathname);
48
+ const page = <div className={visible ? 'ai-dock-inset' : undefined}>{children}</div>;
49
+
50
+ return isAvailable && isRegistryRuntime ? (
51
+ <RegistryAiAssistantProvider visible={visible}>{page}</RegistryAiAssistantProvider>
52
+ ) : (
53
+ <AiAssistant available={isAvailable} visible={visible}>
54
+ {page}
55
+ </AiAssistant>
56
+ );
57
+ };
58
+
59
+ /**
60
+ * `useSession` needs the registry session provider, which only registry mode mounts.
61
+ *
62
+ * A loading session reports `undefined` and not `false`: the inline script may already have reserved
63
+ * the dock's column, and a `false` here would take it back and give it again once the session lands.
64
+ */
65
+ const RegistryAiAssistantProvider: React.FC<{ children: React.ReactNode; visible: boolean }> = ({ children, visible }) => {
66
+ const { data: session, status } = useSession();
67
+ return (
68
+ <AiAssistant available={status === 'loading' ? undefined : Boolean(session?.user)} visible={visible}>
69
+ {children}
70
+ </AiAssistant>
71
+ );
72
+ };
73
+
74
+ const AiAssistant: React.FC<{ children: React.ReactNode; available: boolean | undefined; visible: boolean }> = ({
75
+ children,
76
+ available,
77
+ visible,
78
+ }) => {
79
+ const isMobile = useIsMobile();
80
+ const [dock, setDock] = React.useState<DockState>(DEFAULT_DOCK);
81
+ // Read after mount: the server cannot see it, and a first render that disagreed would fail
82
+ // hydration.
83
+ const [restored, setRestored] = React.useState(false);
84
+
85
+ const change = React.useCallback((next: Partial<DockState>) => {
86
+ setDock((current) => ({ ...current, ...next, width: clampWidth(next.width ?? current.width) }));
87
+ }, []);
88
+
89
+ const value = React.useMemo<AiAssistantApi>(
90
+ () => ({
91
+ isOpen: dock.open,
92
+ open: () => change({ open: true }),
93
+ toggle: () => setDock((current) => ({ ...current, open: !current.open })),
94
+ }),
95
+ [change, dock.open]
96
+ );
97
+
98
+ React.useEffect(() => {
99
+ if (available !== true) return;
100
+ setDock(readDock());
101
+ setRestored(true);
102
+ }, [available]);
103
+
104
+ React.useEffect(() => {
105
+ if (restored) writeDock(dock);
106
+ }, [restored, dock]);
107
+
108
+ /**
109
+ * The column the page leaves free.
110
+ *
111
+ * Nothing is written while the stored state is still out: the inline script in `_document` has
112
+ * already put the right value there, and an intermediate `0px` would reflow the page. A reader
113
+ * with no assistant must take the column back, because the script cannot see a signed-out session.
114
+ */
115
+ React.useEffect(() => {
116
+ if (!visible) {
117
+ const root = document.documentElement;
118
+ root.style.setProperty(DOCK_WIDTH_VAR, '0px');
119
+ delete root.dataset.aiDockOpen;
120
+ return;
121
+ }
122
+ if (available === undefined) return;
123
+ if (available && !restored) return;
124
+ const root = document.documentElement;
125
+ root.style.setProperty(DOCK_WIDTH_VAR, `${available && !isMobile ? dockWidth(dock) : 0}px`);
126
+ if (available && dock.open) root.dataset.aiDockOpen = 'true';
127
+ else delete root.dataset.aiDockOpen;
128
+ }, [available, restored, isMobile, dock, visible]);
129
+
130
+ React.useEffect(() => {
131
+ if (available !== true || !visible) return;
132
+ const onKeyDown = (event: KeyboardEvent) => {
133
+ if (event.key.toLowerCase() === 'k' && (event.metaKey || event.ctrlKey)) {
134
+ event.preventDefault();
135
+ value.toggle();
136
+ }
137
+ };
138
+ window.addEventListener('keydown', onKeyDown);
139
+ return () => window.removeEventListener('keydown', onKeyDown);
140
+ }, [available, value, visible]);
141
+
142
+ if (available !== true || !visible) return <>{children}</>;
143
+
144
+ return (
145
+ <AiAssistantContext.Provider value={value}>
146
+ {children}
147
+ {restored && <AiAssistantPanel state={dock} isMobile={isMobile} onChange={change} />}
148
+ </AiAssistantContext.Provider>
149
+ );
150
+ };
151
+
152
+ /**
153
+ * The keyboard shortcut label, written for the keyboard in front of the reader.
154
+ *
155
+ * The value must settle after mount, not during render. This app is prerendered, so a value read
156
+ * from `navigator` during render disagrees with the markup that React hydrates. The prerendered
157
+ * HTML carries the Apple form, and every other platform corrects it in the first effect.
158
+ */
159
+ const useShortcutLabel = (): string => {
160
+ const [label, setLabel] = React.useState('⌘K');
161
+
162
+ React.useEffect(() => {
163
+ if (!/mac|iphone|ipad|ipod/i.test(navigator.platform || navigator.userAgent)) setLabel('Ctrl K');
164
+ }, []);
165
+
166
+ return label;
167
+ };
168
+
169
+ /**
170
+ * The header control. It must not look like a search field: it opens a conversation, not a result
171
+ * list.
172
+ */
173
+ export function AiAssistantTrigger() {
174
+ const assistant = useAiAssistant();
175
+ const shortcut = useShortcutLabel();
176
+ if (!assistant) return null;
177
+
178
+ return (
179
+ <span className="ai-assistant-trigger" data-open={assistant.isOpen} inert={assistant.isOpen}>
180
+ <button
181
+ type="button"
182
+ onClick={assistant.toggle}
183
+ title="Ask the design system"
184
+ aria-expanded={assistant.isOpen}
185
+ className="outline-hidden inline-flex h-8 shrink-0 items-center gap-2 rounded-full border bg-background px-3 shadow-sm transition-colors hover:bg-muted focus-visible:ring-1 focus-visible:ring-ring"
186
+ >
187
+ <Sparkles className="h-4 w-4 text-primary" />
188
+ <span className="text-sm font-medium">Ask</span>
189
+ <kbd className="rounded-sm border bg-muted px-1 font-mono text-[10px] leading-[14px] text-muted-foreground">{shortcut}</kbd>
190
+ </button>
191
+ </span>
192
+ );
193
+ }
194
+
195
+ /**
196
+ * The same entry point for the mobile nav. It must sit inside that nav's sheet: the assistant is a
197
+ * sheet of its own on a narrow viewport, and two open sheets fight over focus.
198
+ */
199
+ export function AiAssistantMobileTrigger() {
200
+ const assistant = useAiAssistant();
201
+ if (!assistant) return null;
202
+
203
+ return (
204
+ <SheetClose asChild>
205
+ <button
206
+ type="button"
207
+ onClick={assistant.open}
208
+ className="outline-hidden flex w-full items-center gap-2.5 rounded-lg border px-3 py-2.5 text-left transition-colors hover:bg-muted focus-visible:ring-1 focus-visible:ring-ring"
209
+ >
210
+ <Sparkles className="h-4 w-4 text-primary" />
211
+ <span className="text-sm font-medium">Ask</span>
212
+ </button>
213
+ </SheetClose>
214
+ );
215
+ }