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
@@ -1,8 +1,11 @@
1
1
  import { ArrowLeft } from 'lucide-react';
2
2
  import Head from 'next/head';
3
3
  import Link from 'next/link';
4
+ import type { ClientConfig } from '@handoff/types/config';
4
5
  import type { ReactNode } from 'react';
5
- import { Badge } from '../ui/badge';
6
+ import { pageTitle } from '../../lib/utils';
7
+ import { ConfigContextProvider } from '../context/ConfigContext';
8
+ import { Header } from '../Layout/Header';
6
9
  import { Button } from '../ui/button';
7
10
  import { ThemeProvider } from '../util/theme-provider';
8
11
 
@@ -10,16 +13,26 @@ const isRegistryRuntime = process.env.HANDOFF_RUNTIME_MODE === 'registry';
10
13
 
11
14
  interface AuthShellProps {
12
15
  children: ReactNode;
16
+ config?: ClientConfig;
13
17
  title: string;
14
18
  description?: string;
15
19
  wide?: boolean;
16
20
  hideNav?: boolean;
21
+ centered?: boolean;
17
22
  }
18
23
 
19
- export function AuthShell({ children, title, description, wide = false, hideNav = false }: AuthShellProps) {
24
+ export function AuthShell({ children, config, title, description, wide = false, hideNav = false, centered = false }: AuthShellProps) {
25
+ const head = (
26
+ <Head>
27
+ <title>{pageTitle({ title }, config?.app?.title)}</title>
28
+ {description ? <meta name="description" content={description} /> : null}
29
+ </Head>
30
+ );
31
+
20
32
  if (!isRegistryRuntime) {
21
33
  return (
22
34
  <main className="flex min-h-screen items-center justify-center px-6">
35
+ {head}
23
36
  <div className="max-w-md text-center">
24
37
  <h1 className="text-2xl font-semibold">Registry only</h1>
25
38
  <p className="mt-2 text-sm text-muted-foreground">Account management is available only in a deployed registry.</p>
@@ -36,38 +49,19 @@ export function AuthShell({ children, title, description, wide = false, hideNav
36
49
 
37
50
  return (
38
51
  <ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
39
- <Head>
40
- <title>{title} · Handoff Registry</title>
41
- {description ? <meta name="description" content={description} /> : null}
42
- </Head>
43
- <div className="min-h-screen bg-background">
44
- <header className="border-b">
45
- <div className="container mx-auto flex h-16 max-w-[1200px] items-center justify-between px-6">
46
- {hideNav ? (
47
- <span className="flex items-center gap-3">
48
- {/* eslint-disable-next-line @next/next/no-img-element */}
49
- <img className="max-h-5" src={`${process.env.HANDOFF_APP_BASE_PATH ?? ''}/logo.svg`} alt="Handoff" />
50
- <Badge variant="info">Registry</Badge>
51
- </span>
52
- ) : (
53
- <Link href="/" className="flex items-center gap-3">
54
- {/* eslint-disable-next-line @next/next/no-img-element */}
55
- <img className="max-h-5" src={`${process.env.HANDOFF_APP_BASE_PATH ?? ''}/logo.svg`} alt="Handoff" />
56
- <Badge variant="info">Registry</Badge>
57
- </Link>
58
- )}
59
- {hideNav ? null : (
60
- <Button asChild variant="ghost" size="sm" className="gap-2 text-muted-foreground hover:text-foreground">
61
- <Link href="/">
62
- <ArrowLeft className="h-4 w-4" />
63
- Back to documentation
64
- </Link>
65
- </Button>
66
- )}
52
+ {head}
53
+ {centered ? (
54
+ <main className="flex min-h-[calc(100vh-8rem)] items-center justify-center bg-background px-4 py-12">
55
+ <div className="w-full max-w-md">{children}</div>
56
+ </main>
57
+ ) : (
58
+ <ConfigContextProvider defaultConfig={config}>
59
+ <div className="min-h-screen bg-background">
60
+ <Header hideNav={hideNav} />
61
+ <main className={`container mx-auto px-8 ${wide ? 'max-w-[1500px]' : 'max-w-xl py-10'}`}>{children}</main>
67
62
  </div>
68
- </header>
69
- <main className={`container mx-auto px-6 py-10 ${wide ? 'max-w-[1200px]' : 'max-w-xl'}`}>{children}</main>
70
- </div>
63
+ </ConfigContextProvider>
64
+ )}
71
65
  </ThemeProvider>
72
66
  );
73
67
  }
@@ -1,6 +1,6 @@
1
1
  import { useRouter } from 'next/router';
2
2
  import { useEffect, type ReactNode } from 'react';
3
- import { authApiUrl } from './api';
3
+ import { loadInstallStatus } from './installStatus';
4
4
 
5
5
  const isRegistryRuntime = process.env.HANDOFF_RUNTIME_MODE === 'registry';
6
6
 
@@ -10,13 +10,9 @@ export function RegistryInstallGate({ children }: { children: ReactNode }) {
10
10
 
11
11
  useEffect(() => {
12
12
  if (!isRegistryRuntime || !router.isReady || router.pathname === '/install') return;
13
- void fetch(authApiUrl('/api/install'), { credentials: 'include', cache: 'no-store' })
14
- .then(async (response) => {
15
- if (!response.ok) return;
16
- const status = (await response.json()) as { installed?: boolean };
17
- if (status.installed === false) await router.replace('/install');
18
- })
19
- .catch(() => undefined);
13
+ void loadInstallStatus().then(async (status) => {
14
+ if (status?.installed === false) await router.replace('/install');
15
+ });
20
16
  }, [router]);
21
17
 
22
18
  return <>{children}</>;
@@ -0,0 +1,29 @@
1
+ import { authApiUrl } from './api';
2
+
3
+ export interface InstallStatus {
4
+ installed?: boolean;
5
+ emailConfigured?: boolean;
6
+ }
7
+
8
+ // Installation is one-way, so an installed result is kept until a hard reload. Concurrent callers
9
+ // share one request.
10
+ let cachedStatus: InstallStatus | null = null;
11
+ let inFlight: Promise<InstallStatus | null> | null = null;
12
+
13
+ /** The registry installation status, or `null` when it could not be read. */
14
+ export const loadInstallStatus = (): Promise<InstallStatus | null> => {
15
+ if (cachedStatus) return Promise.resolve(cachedStatus);
16
+ if (inFlight) return inFlight;
17
+ inFlight = fetch(authApiUrl('/api/install'), { credentials: 'include', cache: 'no-store' })
18
+ .then(async (response) => {
19
+ if (!response.ok) return null;
20
+ const status = (await response.json()) as InstallStatus;
21
+ if (status.installed === true) cachedStatus = status;
22
+ return status;
23
+ })
24
+ .catch(() => null)
25
+ .finally(() => {
26
+ inFlight = null;
27
+ });
28
+ return inFlight;
29
+ };
@@ -285,11 +285,14 @@ export const ComponentDisplay: React.FC<{
285
285
  const originStyle: React.CSSProperties = originRect
286
286
  ? { top: `${originRect.top}%`, left: `${originRect.left}%`, width: `${originRect.width}%`, transform: 'translateY(0)' }
287
287
  : {};
288
+ // Positioned against the viewport, so it subtracts the assistant's column itself. The margin is a
289
+ // share of what is left, not of the whole viewport, so the card stays centered in usable space.
290
+ const freeWidth = 'calc(100vw - var(--ai-dock-width, 0px))';
288
291
  const fullSizeStyle: React.CSSProperties = {
289
292
  ...collapsedStyle,
290
293
  top: '50%',
291
- left: '2%',
292
- width: '96%',
294
+ left: `calc(${freeWidth} * 0.02)`,
295
+ width: `calc(${freeWidth} * 0.96)`,
293
296
  maxHeight: '92vh',
294
297
  transform: 'translateY(-50%)',
295
298
  };
@@ -16,7 +16,7 @@ import {
16
16
  import { Separator } from '../ui/separator';
17
17
  import { Sheet, SheetClose, SheetContent, SheetHeader } from '../ui/sheet';
18
18
  import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip';
19
- import { hexToRgb } from '../util/colors';
19
+ import { hexToRgb, hexToRgbaCss } from '../util/colors';
20
20
  import ColorContrast from './ColorContrast';
21
21
  import ColorInfo from './ColorInfo';
22
22
  import ColorSpaces from './ColorSpaces';
@@ -25,7 +25,6 @@ import ColorTailwind from './ColorTailwind';
25
25
  type ColorGridProps = {
26
26
  title: string;
27
27
  group: string;
28
- description: string;
29
28
  colors: CoreTypes.IColorObject[];
30
29
  };
31
30
 
@@ -33,7 +32,7 @@ const LargeColorGrid: React.FC<{ colors: CoreTypes.IColorObject[]; setOpen: (col
33
32
  <div className="@container">
34
33
  <div className="mb-6 grid grid-cols-1 gap-6 @md:grid-cols-2">
35
34
  {colors.map((color) => (
36
- <a href="#" className="flex flex-col items-start" key={color.group + '-' + color.name}>
35
+ <div className="flex flex-col items-start" key={color.group + '-' + color.name}>
37
36
  <div
38
37
  className="group relative mb-2 block h-32 w-full rounded-lg"
39
38
  style={{ background: color.value ?? '', backgroundBlendMode: color.blend ?? '' }}
@@ -42,7 +41,7 @@ const LargeColorGrid: React.FC<{ colors: CoreTypes.IColorObject[]; setOpen: (col
42
41
  </div>
43
42
  <p className="mb-1 text-sm font-medium line-clamp-1" title={color.name}>{color.name}</p>
44
43
  <small className="font-mono text-xs font-light text-gray-400 line-clamp-1" title={color.value}>{color.value}</small>
45
- </a>
44
+ </div>
46
45
  ))}
47
46
  </div>
48
47
  </div>
@@ -52,10 +51,11 @@ const SmallColorGrid: React.FC<{ colors: CoreTypes.IColorObject[]; setOpen: (col
52
51
  // group colors by subgroup
53
52
  const groupedColors = colors.reduce(
54
53
  (acc, color) => {
55
- if (!acc[color.subgroup]) {
56
- acc[color.subgroup] = [];
54
+ const subgroup = color.subgroup ?? '';
55
+ if (!acc[subgroup]) {
56
+ acc[subgroup] = [];
57
57
  }
58
- acc[color.subgroup].push(color);
58
+ acc[subgroup].push(color);
59
59
  return acc;
60
60
  },
61
61
  {} as Record<string, CoreTypes.IColorObject[]>
@@ -64,11 +64,11 @@ const SmallColorGrid: React.FC<{ colors: CoreTypes.IColorObject[]; setOpen: (col
64
64
  <>
65
65
  {Object.entries(groupedColors).map(([subgroup]) => (
66
66
  <div className="mb-5" key={`group-${subgroup}`}>
67
- <HeadersType.H4 key={subgroup}>{startCase(subgroup.replaceAll('-', ' '))}</HeadersType.H4>
67
+ {subgroup && <HeadersType.H4>{startCase(subgroup.replaceAll('-', ' '))}</HeadersType.H4>}
68
68
  <div className="grid grid-cols-[repeat(auto-fit,minmax(60px,60px))] gap-6">
69
69
  {groupedColors[subgroup].map((color) => (
70
70
  <React.Fragment key={`group-${subgroup}-${color.name}`}>
71
- <a href="" className="flex flex-col">
71
+ <div className="flex flex-col">
72
72
  <div
73
73
  className="group relative mb-2 block h-14 rounded-lg"
74
74
  style={{ background: color.value ?? '', backgroundBlendMode: color.blend ?? '' }}
@@ -77,7 +77,7 @@ const SmallColorGrid: React.FC<{ colors: CoreTypes.IColorObject[]; setOpen: (col
77
77
  </div>
78
78
  <p className="mb-1 text-xs font-medium line-clamp-1" title={color.name}>{color.name}</p>
79
79
  <small className="font-mono text-xs font-light text-gray-400 line-clamp-1" title={color.value}>{color.value}</small>
80
- </a>
80
+ </div>
81
81
  </React.Fragment>
82
82
  ))}
83
83
  </div>
@@ -87,7 +87,7 @@ const SmallColorGrid: React.FC<{ colors: CoreTypes.IColorObject[]; setOpen: (col
87
87
  );
88
88
  };
89
89
 
90
- const ColorGrid: React.FC<ColorGridProps> = ({ title, description, colors, group }) => {
90
+ const ColorGrid: React.FC<ColorGridProps> = ({ title, colors, group }) => {
91
91
  const [open, setOpen] = React.useState(false);
92
92
  const [selectedColor, setSelectedColor] = React.useState(null);
93
93
  const openSheet = (color) => {
@@ -96,8 +96,7 @@ const ColorGrid: React.FC<ColorGridProps> = ({ title, description, colors, group
96
96
  };
97
97
  return (
98
98
  <div id={`${group}-colors`} className="scroll-mt-24 scroll-smooth pb-10">
99
- <h3 className="text-lg font-medium">{title}</h3>
100
- <p className="mb-8">{description}</p>
99
+ <h3 className="mb-8 text-lg font-medium">{title}</h3>
101
100
  {colors.length < 5 ? <LargeColorGrid colors={colors} setOpen={openSheet} /> : <SmallColorGrid colors={colors} setOpen={openSheet} />}
102
101
  <ColorSheet color={selectedColor} open={open} setOpen={setOpen} />
103
102
  </div>
@@ -122,7 +121,7 @@ const ColorDropdown: React.FC<{ color: CoreTypes.IColorObject; openSheet: (color
122
121
  <Copy className="text-gray-400" /> HEX
123
122
  <DropdownMenuShortcut className="max-w-[100px] truncate">{color.value}</DropdownMenuShortcut>
124
123
  </DropdownMenuItem>
125
- <DropdownMenuItem onClick={() => navigator.clipboard.writeText('rgba(0, 49, 82, 1)')}>
124
+ <DropdownMenuItem onClick={() => navigator.clipboard.writeText(hexToRgbaCss(color.value) ?? color.value)}>
126
125
  <Copy className="text-gray-400" /> RGBA
127
126
  <DropdownMenuShortcut className="max-w-[100px] truncate">{hexToRgb(color.value)}</DropdownMenuShortcut>
128
127
  </DropdownMenuItem>
@@ -1,5 +1,6 @@
1
1
  import Link from 'next/link';
2
2
  import { useEffect, useState } from 'react';
3
+ import { AiAssistantTrigger } from '../../components/AiAssistant/AiAssistantProvider';
3
4
  import { McpConfigDialog } from '../../components/McpIntegration/McpConfigDialog';
4
5
  import { ModeToggle } from '../../components/ModeSwitcher';
5
6
  import { MainNav } from '../../components/Navigation/MainNav';
@@ -9,7 +10,7 @@ import { useConfigContext } from '../context/ConfigContext';
9
10
  import { AuthControls } from '../Auth/AuthControls';
10
11
  import { RuntimeModeBadge } from './RuntimeModeBadge';
11
12
 
12
- export function Header() {
13
+ export function Header({ hideNav = false }: { hideNav?: boolean }) {
13
14
  const context = useConfigContext();
14
15
  const [isScrolled, setIsScrolled] = useState(false);
15
16
  const runtimeMode = context.config?.runtime?.mode ?? 'workspace';
@@ -26,30 +27,35 @@ export function Header() {
26
27
  return () => window.removeEventListener('scroll', handleScroll);
27
28
  }, []);
28
29
 
30
+ const logo = <img className="max-h-5" src={`${process.env.HANDOFF_APP_BASE_PATH ?? ''}/logo.svg`} alt={context.config?.app?.title} />;
31
+
29
32
  return (
30
33
  <div
31
34
  className={cn(
32
- 'sticky top-0 z-50 py-4 shadow-[0_0_3px_0_rgba(0,0,0,0.15)] backdrop-blur-sm transition-all duration-300',
33
- isScrolled && 'bg-background/70 py-3 shadow-[0_0_4px_0_rgba(0,0,0,0.15)]'
35
+ 'ai-dock-bleed h-17 sticky top-0 z-50 py-4 shadow-[0_0_3px_0_rgba(0,0,0,0.15)] backdrop-blur-sm transition-all duration-300',
36
+ isScrolled && 'bg-background/70 shadow-[0_0_4px_0_rgba(0,0,0,0.15)]'
34
37
  )}
35
38
  >
36
39
  <header className="border-grid container mx-auto w-full max-w-[1500px] bg-transparent px-8">
37
40
  <div className="mx-auto flex items-center justify-between @container">
38
41
  <div className="flex items-center gap-3">
39
- <Link href="/">
40
- <img className="max-h-5" src={`${process.env.HANDOFF_APP_BASE_PATH ?? ''}/logo.svg`} alt={context.config?.app?.title} />
41
- </Link>
42
+ {hideNav ? logo : <Link href="/">{logo}</Link>}
42
43
  {!isStaticSnapshot && <RuntimeModeBadge mode={runtimeMode} />}
43
44
  </div>
44
- <div className="hidden items-center gap-4 @2xl:flex">
45
- <MainNav />
46
- <McpConfigDialog />
47
- {runtimeMode === 'registry' ? <AuthControls /> : null}
48
- <ModeToggle />
49
- </div>
50
- <div className="flex items-center gap-4 @2xl:hidden">
51
- <MobileNav />
52
- </div>
45
+ {hideNav ? null : (
46
+ <>
47
+ <div className="hidden items-center gap-4 @2xl:flex">
48
+ <MainNav />
49
+ <AiAssistantTrigger />
50
+ <McpConfigDialog />
51
+ {runtimeMode === 'registry' ? <AuthControls /> : null}
52
+ <ModeToggle />
53
+ </div>
54
+ <div className="flex items-center gap-4 @2xl:hidden">
55
+ <MobileNav />
56
+ </div>
57
+ </>
58
+ )}
53
59
  </div>
54
60
  </header>
55
61
  </div>
@@ -5,7 +5,7 @@ import { Header } from '../../components/Layout/Header';
5
5
  import { ThemeProvider } from '../../components/util/theme-provider';
6
6
  import SideNav from '../Navigation/SideNav';
7
7
  import { ConfigContextProvider } from '../context/ConfigContext';
8
- import { hasRenderableNav } from '../../lib/utils';
8
+ import { hasRenderableNav, pageTitle } from '../../lib/utils';
9
9
  import { SidebarInset, SidebarProvider } from '../ui/sidebar';
10
10
  import { SectionLink } from '../util';
11
11
 
@@ -34,7 +34,7 @@ export default function Layout<LayoutComponentProps>({ children, config, menu, m
34
34
  <ConfigContextProvider defaultConfig={config} defaultMenu={menu}>
35
35
  <ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
36
36
  <Head>
37
- <title>{metadata.metaTitle}</title>
37
+ <title>{pageTitle(metadata, config?.app?.title)}</title>
38
38
  <meta name="description" content={metadata.metaDescription} />
39
39
  </Head>
40
40
  <div className="absolute left-[-200px] top-[-200px] z-[-1] h-[400px] w-[600px] bg-[#111111] opacity-[0.05] blur-[350px]">
@@ -70,38 +70,36 @@ function extractText(node: any): string {
70
70
 
71
71
  const Headings = (element: any) => {
72
72
  const { children, node } = element;
73
- if (children[0]) {
74
- const type = node.tagName || 'h6';
75
-
76
- let anchor = extractText(children).toLowerCase();
77
- anchor = anchor.replace(/[^a-zA-Z0-9 ]/g, '');
78
- anchor = anchor.replace(/ /g, '-');
79
-
80
- const container = (children: React.ReactNode): React.ReactNode => (
81
- <>
82
- {children}
83
- <a id={anchor} href={`#${anchor}`} className="doc-link"></a>
84
- </>
85
- );
86
-
87
- switch (type) {
88
- case 'h1':
89
- return <h1 className="text-2xl font-bold">{container(children)}</h1>;
90
- case 'h2':
91
- return <h2 className="text-xl font-bold">{container(children)}</h2>;
92
- case 'h3':
93
- return <h3 className="text-lg font-bold">{container(children)}</h3>;
94
- case 'h4':
95
- return <h4 className="text-base font-bold">{container(children)}</h4>;
96
- case 'h5':
97
- return <h5 className="text-sm font-bold">{container(children)}</h5>;
98
- case 'h6':
99
- return <h6 className="text-xs font-bold">{container(children)}</h6>;
100
- default:
101
- return <h6 className="text-xs font-bold">{container(children)}</h6>;
102
- }
103
- } else {
104
- return <h1>children</h1>;
73
+ if (React.Children.count(children) === 0) return <></>;
74
+
75
+ const type = node.tagName || 'h6';
76
+
77
+ let anchor = extractText(children).toLowerCase();
78
+ anchor = anchor.replace(/[^a-zA-Z0-9 ]/g, '');
79
+ anchor = anchor.replace(/ /g, '-');
80
+
81
+ const container = (children: React.ReactNode): React.ReactNode => (
82
+ <>
83
+ {children}
84
+ <a id={anchor} href={`#${anchor}`} className="doc-link"></a>
85
+ </>
86
+ );
87
+
88
+ switch (type) {
89
+ case 'h1':
90
+ return <h1 className="text-2xl font-bold">{container(children)}</h1>;
91
+ case 'h2':
92
+ return <h2 className="text-xl font-bold">{container(children)}</h2>;
93
+ case 'h3':
94
+ return <h3 className="text-lg font-bold">{container(children)}</h3>;
95
+ case 'h4':
96
+ return <h4 className="text-base font-bold">{container(children)}</h4>;
97
+ case 'h5':
98
+ return <h5 className="text-sm font-bold">{container(children)}</h5>;
99
+ case 'h6':
100
+ return <h6 className="text-xs font-bold">{container(children)}</h6>;
101
+ default:
102
+ return <h6 className="text-xs font-bold">{container(children)}</h6>;
105
103
  }
106
104
  };
107
105
 
@@ -134,18 +134,13 @@ type EndpointStatus = 'checking' | 'live' | 'unreachable';
134
134
  let cachedStatus: EndpointStatus | null = null;
135
135
  let inFlight: Promise<EndpointStatus> | null = null;
136
136
 
137
- /**
138
- * Statuses that prove the route is being served. A GET is the cheapest question to ask: the handler
139
- * answers 405 without ever building an MCP server, and a registry answers 401 ahead of that because
140
- * the credential check runs first. Anything else, 404 and 5xx included, means it is not there.
141
- */
142
- const LIVE_STATUSES = new Set([200, 401, 403, 405]);
143
-
144
137
  const probeEndpoint = (url: string): Promise<EndpointStatus> => {
145
138
  if (cachedStatus) return Promise.resolve(cachedStatus);
146
139
  if (inFlight) return inFlight;
147
- inFlight = fetch(url, { method: 'GET' })
148
- .then((res): EndpointStatus => (LIVE_STATUSES.has(res.status) ? 'live' : 'unreachable'))
140
+ // The endpoint answers HEAD with 204 before the credential check. Thus a served route never logs an
141
+ // error status to the console.
142
+ inFlight = fetch(url, { method: 'HEAD' })
143
+ .then((res): EndpointStatus => (res.ok ? 'live' : 'unreachable'))
149
144
  .catch((): EndpointStatus => 'unreachable')
150
145
  .then((status) => {
151
146
  cachedStatus = status;
@@ -184,29 +179,19 @@ const StatusDot: React.FC<{ status: EndpointStatus; className?: string }> = ({ s
184
179
  <span role="status" aria-label={DOT[status].label} title={DOT[status].label} className={cn('size-1.5 rounded-full', DOT[status].className, className)} />
185
180
  );
186
181
 
187
- const Snippet: React.FC<{ label: string; value: string; compact?: boolean }> = ({ label, value, compact }) => (
182
+ const Snippet: React.FC<{ label: string; value: string }> = ({ label, value }) => (
188
183
  <div className="min-w-0 space-y-1.5">
189
184
  <div className="flex items-center justify-between gap-2">
190
185
  <code className="truncate rounded bg-muted px-1.5 py-0.5 text-[11px] text-muted-foreground">{label}</code>
191
186
  <CopyToClipboard value={value} tooltip={`Copy ${label}`} showTooltip={false} className="relative size-7 shrink-0" />
192
187
  </div>
193
- <pre
194
- className={cn(
195
- 'overflow-auto rounded-md border bg-muted/50 p-3 leading-relaxed',
196
- compact ? 'max-h-48 text-[10px]' : 'max-h-64 text-xs'
197
- )}
198
- >
188
+ <pre className="max-h-64 overflow-auto rounded-md border bg-muted/50 p-3 text-xs leading-relaxed">
199
189
  <code>{value}</code>
200
190
  </pre>
201
191
  </div>
202
192
  );
203
193
 
204
- const ClientConfig: React.FC<{ client: McpClient; name: string; target: McpTarget; compact?: boolean }> = ({
205
- client,
206
- name,
207
- target,
208
- compact,
209
- }) => {
194
+ const ClientConfig: React.FC<{ client: McpClient; name: string; target: McpTarget }> = ({ client, name, target }) => {
210
195
  // A one-click install would write the `<your-token>` placeholder verbatim and register a server
211
196
  // that cannot connect, so a registry gets the snippet and the token pointer instead.
212
197
  const deeplink = isRegistryRuntime ? undefined : client.deeplink?.(name, target);
@@ -222,8 +207,8 @@ const ClientConfig: React.FC<{ client: McpClient; name: string; target: McpTarge
222
207
  </a>
223
208
  </Button>
224
209
  )}
225
- {command && <Snippet label="command line" value={command} compact={compact} />}
226
- <Snippet label={client.configPath} value={JSON.stringify(client.file(name, target), null, 2)} compact={compact} />
210
+ {command && <Snippet label="command line" value={command} />}
211
+ <Snippet label={client.configPath} value={JSON.stringify(client.file(name, target), null, 2)} />
227
212
  <p className="text-xs text-muted-foreground">{client.note}</p>
228
213
  </div>
229
214
  );
@@ -261,8 +246,8 @@ const Credential: React.FC = () => (
261
246
  </p>
262
247
  );
263
248
 
264
- /** Endpoint, per-client config and credential guidance. Shared by the dialog and the mobile nav. */
265
- const ConnectionDetails: React.FC<{ url: string; status: EndpointStatus; compact?: boolean }> = ({ url, status, compact }) => {
249
+ /** Endpoint, per-client config and credential guidance. */
250
+ const ConnectionDetails: React.FC<{ url: string; status: EndpointStatus }> = ({ url, status }) => {
266
251
  const name = useServerName();
267
252
  const target: McpTarget = { url, headers: isRegistryRuntime ? CREDENTIAL_HEADERS : undefined };
268
253
 
@@ -279,7 +264,7 @@ const ConnectionDetails: React.FC<{ url: string; status: EndpointStatus; compact
279
264
  </TabsList>
280
265
  {CLIENTS.map((client) => (
281
266
  <TabsContent key={client.id} value={client.id} className="min-w-0">
282
- <ClientConfig client={client} name={name} target={target} compact={compact} />
267
+ <ClientConfig client={client} name={name} target={target} />
283
268
  </TabsContent>
284
269
  ))}
285
270
  </Tabs>
@@ -323,27 +308,3 @@ const McpDialog: React.FC = () => {
323
308
  </Dialog>
324
309
  );
325
310
  };
326
-
327
- /**
328
- * Inline variant for the mobile nav, which is itself a sheet, so a nested dialog would sit on top of
329
- * it. It owns its divider, so a build without the endpoint renders nothing and leaves no stray rule.
330
- */
331
- export function McpConfigMobile() {
332
- return isAvailable ? <McpMobile /> : null;
333
- }
334
-
335
- const McpMobile: React.FC = () => {
336
- const url = useMcpEndpoint();
337
- const status = useEndpointStatus(url);
338
-
339
- return (
340
- <div className="mt-4 space-y-3 border-t pt-4">
341
- <div className="flex flex-wrap items-center gap-2">
342
- <McpIcon className="h-4 w-4" />
343
- <span className="text-sm font-medium">Connect a coding agent</span>
344
- <RuntimeModeBadge mode={mode} />
345
- </div>
346
- <ConnectionDetails url={url} status={status} compact />
347
- </div>
348
- );
349
- };
@@ -3,7 +3,7 @@ import { Menu, Monitor, Moon, Sun, X } from 'lucide-react';
3
3
  import { useTheme } from 'next-themes';
4
4
  import Link from 'next/link';
5
5
  import { useRouter } from 'next/router';
6
- import { McpConfigMobile } from '../../components/McpIntegration/McpConfigDialog';
6
+ import { AiAssistantMobileTrigger } from '../../components/AiAssistant/AiAssistantProvider';
7
7
  import { Button } from '../../components/ui/button';
8
8
  import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from '../../components/ui/sheet';
9
9
  import { cn } from '../../lib/utils';
@@ -52,6 +52,7 @@ export function MobileNav() {
52
52
  </SheetTrigger>
53
53
  </SheetHeader>
54
54
  <div className="mt-8 flex flex-col space-y-4">
55
+ <AiAssistantMobileTrigger />
55
56
  {menu &&
56
57
  menu.map((section) => {
57
58
  const isActive = trimSlashes(router.asPath).startsWith(trimSlashes(section.path));
@@ -70,7 +71,6 @@ export function MobileNav() {
70
71
  </Link>
71
72
  );
72
73
  })}
73
- <McpConfigMobile />
74
74
  <div className="mt-4 border-t pt-4">
75
75
  <Button variant="ghost" className="w-full justify-start font-normal" onClick={toggleTheme}>
76
76
  {theme === 'light' && (
@@ -31,23 +31,9 @@ import {
31
31
  SidebarMenuSub,
32
32
  SidebarSeparator,
33
33
  } from '../../components/ui/sidebar';
34
- import { normalizePathForMatch } from '../../lib/utils';
34
+ import { normalizePathForMatch, stripBasePath } from '../../lib/utils';
35
35
  import { useNav } from '../context/NavProvider';
36
36
 
37
- /**
38
- * `next/link` re-applies `basePath`, but the menu item paths already carry the base prefix (built by
39
- * the server navigation resolver). Strip the leading base segment once so the link is not
40
- * double-prefixed; with no base path this just returns a leading-slash route.
41
- */
42
- const stripBasePath = (p: string): string => {
43
- const base = (process.env.HANDOFF_APP_BASE_PATH ?? '').replace(/^\/+|\/+$/g, '');
44
- let rel = p.replace(/^\/+/, '');
45
- if (base && (rel === base || rel.startsWith(`${base}/`))) {
46
- rel = rel.slice(base.length).replace(/^\/+/, '');
47
- }
48
- return `/${rel}`;
49
- };
50
-
51
37
  const NormalMenuItem = ({ title, icon, path }) => {
52
38
  const router = useRouter();
53
39
  const isActive = normalizePathForMatch(path) === normalizePathForMatch(router.asPath);
@@ -1,6 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import type { NavData, SectionLink } from '@handoff/nav';
4
+ import { useRouter } from 'next/router';
4
5
  import React, { createContext, useContext, useEffect, useState } from 'react';
5
6
 
6
7
  export type { NavData, NavEntity } from '@handoff/nav';
@@ -13,6 +14,8 @@ interface INavContext {
13
14
  const EMPTY_NAV: NavData = { shell: [] };
14
15
  const NavContext = createContext<INavContext>({ nav: EMPTY_NAV, currentSectionId: '' });
15
16
  const isRegistryMode = process.env.HANDOFF_RUNTIME_MODE === 'registry';
17
+ /** Routes that render no navigation, so they do not fetch it. */
18
+ const NAVLESS_ROUTES = ['/install', '/login', '/reset-password'];
16
19
 
17
20
  // A module-level fulfilled cache and in-flight guard make the registry refresh a single hard-load
18
21
  // operation, including StrictMode remounts. A browser hard reload resets both and observes publishes.
@@ -43,9 +46,11 @@ export const NavProvider: React.FC<{
43
46
  // Registry alone owns refresh state. Workspace renders the latest page prop directly, so soft
44
47
  // navigation cannot briefly expose the preceding page's tree and triggers no provider effect.
45
48
  const [registryNav, setRegistryNav] = useState<NavData>(() => cachedNav ?? initialNav);
49
+ const { pathname } = useRouter();
50
+ const hasNav = !NAVLESS_ROUTES.includes(pathname);
46
51
 
47
52
  useEffect(() => {
48
- if (!isRegistryMode || cachedNav) return;
53
+ if (!isRegistryMode || !hasNav || cachedNav) return;
49
54
  let active = true;
50
55
  loadNav().then((data) => {
51
56
  if (active && data) setRegistryNav(data);
@@ -53,8 +58,7 @@ export const NavProvider: React.FC<{
53
58
  return () => {
54
59
  active = false;
55
60
  };
56
- // eslint-disable-next-line react-hooks/exhaustive-deps
57
- }, []);
61
+ }, [hasNav]);
58
62
 
59
63
  const nav = isRegistryMode ? registryNav : initialNav;
60
64
  return <NavContext.Provider value={{ nav, currentSectionId }}>{children}</NavContext.Provider>;