handoff-app 1.2.2-7 → 2.0.0-1

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 (351) hide show
  1. package/Changelog.md +37 -15
  2. package/README.md +372 -118
  3. package/dist/app-builder/build.d.ts +41 -2
  4. package/dist/app-builder/build.js +455 -18
  5. package/dist/app-builder/client-config.d.ts +15 -2
  6. package/dist/app-builder/client-config.js +43 -4
  7. package/dist/app-builder/config-diff/snapshot.d.ts +1 -6
  8. package/dist/app-builder/config-diff/snapshot.js +2 -9
  9. package/dist/app-builder/index.d.ts +1 -1
  10. package/dist/app-builder/index.js +3 -1
  11. package/dist/app-builder/nav-shell.d.ts +23 -0
  12. package/dist/app-builder/nav-shell.js +80 -0
  13. package/dist/app-builder/paths.js +25 -1
  14. package/dist/app-builder/static-export.d.ts +17 -0
  15. package/dist/app-builder/static-export.js +206 -0
  16. package/dist/app-builder/vercel-output.d.ts +39 -0
  17. package/dist/app-builder/vercel-output.js +259 -0
  18. package/dist/app-builder/watchers/utils.d.ts +2 -0
  19. package/dist/app-builder/watchers.d.ts +14 -4
  20. package/dist/app-builder/watchers.js +123 -28
  21. package/dist/app-builder/websocket.js +1 -1
  22. package/dist/artifacts/index.d.ts +2 -0
  23. package/dist/artifacts/index.js +16 -0
  24. package/dist/artifacts/types.d.ts +136 -0
  25. package/dist/artifacts/types.js +24 -0
  26. package/dist/artifacts/url.d.ts +67 -0
  27. package/dist/artifacts/url.js +116 -0
  28. package/dist/cache/build-cache.js +48 -4
  29. package/dist/cli.js +0 -0
  30. package/dist/commands/build/app.js +1 -2
  31. package/dist/commands/build/index.d.ts +17 -0
  32. package/dist/commands/build/index.js +66 -0
  33. package/dist/commands/checkout/index.d.ts +24 -0
  34. package/dist/commands/checkout/index.js +71 -0
  35. package/dist/commands/db/migrate.d.ts +11 -0
  36. package/dist/commands/db/migrate.js +33 -0
  37. package/dist/commands/index.js +8 -0
  38. package/dist/commands/publish/index.d.ts +21 -0
  39. package/dist/commands/publish/index.js +68 -0
  40. package/dist/config/defaults.js +15 -1
  41. package/dist/config/loader.js +6 -1
  42. package/dist/config/normalizers/declaration.js +4 -1
  43. package/dist/config/normalizers/page.d.ts +18 -0
  44. package/dist/config/normalizers/page.js +34 -0
  45. package/dist/config/normalizers/pattern.js +7 -1
  46. package/dist/config/runtime.d.ts +4 -0
  47. package/dist/config/runtime.js +53 -5
  48. package/dist/index.d.ts +65 -1
  49. package/dist/index.js +123 -3
  50. package/dist/nav/index.d.ts +83 -0
  51. package/dist/nav/index.js +9 -0
  52. package/dist/nav/registry.d.ts +15 -0
  53. package/dist/nav/registry.js +42 -0
  54. package/dist/nav/resolver.d.ts +8 -0
  55. package/dist/nav/resolver.js +192 -0
  56. package/dist/nav/workspace.d.ts +18 -0
  57. package/dist/nav/workspace.js +103 -0
  58. package/dist/pipeline/styles.js +7 -35
  59. package/dist/pipeline/token-transformers.d.ts +45 -0
  60. package/dist/pipeline/token-transformers.js +71 -0
  61. package/dist/registry/asset-storage/adapters/vercel-blob.d.ts +17 -0
  62. package/dist/registry/asset-storage/adapters/vercel-blob.js +87 -0
  63. package/dist/registry/asset-storage/define.d.ts +10 -0
  64. package/dist/registry/asset-storage/define.js +13 -0
  65. package/dist/registry/asset-storage/resolve.d.ts +45 -0
  66. package/dist/registry/asset-storage/resolve.js +36 -0
  67. package/dist/registry/asset-storage/types.d.ts +51 -0
  68. package/dist/registry/asset-storage/types.js +9 -0
  69. package/dist/registry/assets/layout.d.ts +22 -0
  70. package/dist/registry/assets/layout.js +30 -0
  71. package/dist/registry/assets/sets.d.ts +18 -0
  72. package/dist/registry/assets/sets.js +44 -0
  73. package/dist/registry/assets/transfer.d.ts +45 -0
  74. package/dist/registry/assets/transfer.js +11 -0
  75. package/dist/registry/checkout/assets.d.ts +16 -0
  76. package/dist/registry/checkout/assets.js +150 -0
  77. package/dist/registry/checkout/index.d.ts +33 -0
  78. package/dist/registry/checkout/index.js +610 -0
  79. package/dist/registry/checkout/tokens.d.ts +15 -0
  80. package/dist/registry/checkout/tokens.js +171 -0
  81. package/dist/registry/client.d.ts +61 -0
  82. package/dist/registry/client.js +211 -0
  83. package/dist/registry/connection.d.ts +30 -0
  84. package/dist/registry/connection.js +36 -0
  85. package/dist/registry/db/adapter.d.ts +43 -0
  86. package/dist/registry/db/adapter.js +59 -0
  87. package/dist/registry/db/client.d.ts +35 -0
  88. package/dist/registry/db/client.js +94 -0
  89. package/dist/registry/db/index.d.ts +5 -0
  90. package/dist/registry/db/index.js +24 -0
  91. package/dist/registry/db/migrate.d.ts +23 -0
  92. package/dist/registry/db/migrate.js +64 -0
  93. package/dist/registry/db/schema.d.ts +4869 -0
  94. package/dist/registry/db/schema.js +347 -0
  95. package/dist/registry/path.d.ts +4 -0
  96. package/dist/registry/path.js +31 -0
  97. package/dist/registry/publish/artifacts.d.ts +16 -0
  98. package/dist/registry/publish/artifacts.js +132 -0
  99. package/dist/registry/publish/assets.d.ts +16 -0
  100. package/dist/registry/publish/assets.js +129 -0
  101. package/dist/registry/publish/errors.d.ts +5 -0
  102. package/dist/registry/publish/errors.js +26 -0
  103. package/dist/registry/publish/index.d.ts +27 -0
  104. package/dist/registry/publish/index.js +126 -0
  105. package/dist/registry/publish/package.d.ts +27 -0
  106. package/dist/registry/publish/package.js +257 -0
  107. package/dist/registry/publish/publish-build.d.ts +12 -0
  108. package/dist/registry/publish/publish-build.js +43 -0
  109. package/dist/registry/publish/tokens.d.ts +18 -0
  110. package/dist/registry/publish/tokens.js +112 -0
  111. package/dist/registry/tokens/sets.d.ts +59 -0
  112. package/dist/registry/tokens/sets.js +109 -0
  113. package/dist/registry/tokens/transfer.d.ts +48 -0
  114. package/dist/registry/tokens/transfer.js +10 -0
  115. package/dist/registry/transfer.d.ts +75 -0
  116. package/dist/registry/transfer.js +11 -0
  117. package/dist/store/filesystem-assets.d.ts +17 -0
  118. package/dist/store/filesystem-assets.js +147 -0
  119. package/dist/store/filesystem.d.ts +76 -0
  120. package/dist/store/filesystem.js +208 -0
  121. package/dist/store/index.d.ts +20 -0
  122. package/dist/store/index.js +38 -0
  123. package/dist/store/registry.d.ts +77 -0
  124. package/dist/store/registry.js +350 -0
  125. package/dist/store/source-files.d.ts +24 -0
  126. package/dist/store/source-files.js +147 -0
  127. package/dist/store/types.d.ts +185 -0
  128. package/dist/store/types.js +12 -0
  129. package/dist/transformers/plugins/csf-render.js +17 -8
  130. package/dist/transformers/plugins/handlebars-previews.js +10 -3
  131. package/dist/transformers/plugins/ssr-render.js +116 -49
  132. package/dist/transformers/preview/component/api.js +3 -1
  133. package/dist/transformers/preview/component/artifacts.js +8 -1
  134. package/dist/transformers/preview/component/builder.js +13 -5
  135. package/dist/transformers/preview/component/css.js +9 -1
  136. package/dist/transformers/preview/component/javascript.d.ts +9 -3
  137. package/dist/transformers/preview/component/javascript.js +45 -17
  138. package/dist/transformers/preview/component/shared-artifacts.d.ts +83 -0
  139. package/dist/transformers/preview/component/shared-artifacts.js +92 -0
  140. package/dist/transformers/preview/component.js +5 -2
  141. package/dist/transformers/preview/pattern/builder.js +28 -9
  142. package/dist/transformers/preview/pattern/html.d.ts +13 -10
  143. package/dist/transformers/preview/pattern/html.js +62 -27
  144. package/dist/transformers/preview/types.d.ts +38 -0
  145. package/dist/transformers/utils/handlebars.d.ts +3 -0
  146. package/dist/transformers/utils/handlebars.js +19 -5
  147. package/dist/types/config.d.ts +124 -31
  148. package/dist/utils/menu-shell.d.ts +60 -0
  149. package/dist/utils/menu-shell.js +221 -0
  150. package/dist/utils/stable-stringify.d.ts +2 -0
  151. package/dist/utils/stable-stringify.js +8 -0
  152. package/docs/cli.md +4 -2
  153. package/drizzle/0000_init_registry_storage.sql +95 -0
  154. package/drizzle/0001_add_review_metadata.sql +2 -0
  155. package/drizzle/0002_add_pages.sql +28 -0
  156. package/drizzle/0003_add_tokens.sql +29 -0
  157. package/drizzle/0004_add_assets.sql +37 -0
  158. package/drizzle/meta/0000_snapshot.json +642 -0
  159. package/drizzle/meta/0001_snapshot.json +654 -0
  160. package/drizzle/meta/0002_snapshot.json +835 -0
  161. package/drizzle/meta/0003_snapshot.json +1023 -0
  162. package/drizzle/meta/0004_snapshot.json +1252 -0
  163. package/drizzle/meta/_journal.json +41 -0
  164. package/drizzle.config.ts +16 -0
  165. package/package.json +10 -1
  166. package/src/app/components/Component/Preview.tsx +3 -2
  167. package/src/app/components/Component/RegistryActions.tsx +150 -0
  168. package/src/app/components/Foundations/DisplayLogo.tsx +11 -3
  169. package/src/app/components/Layout/Header.tsx +21 -3
  170. package/src/app/components/Layout/Main.tsx +11 -3
  171. package/src/app/components/Navigation/MainNav.tsx +4 -2
  172. package/src/app/components/Navigation/MobileNav.tsx +4 -2
  173. package/src/app/components/Navigation/SideNav.tsx +46 -11
  174. package/src/app/components/NotFound.tsx +24 -0
  175. package/src/app/components/context/NavProvider.tsx +71 -0
  176. package/src/app/components/old/Header.tsx +3 -3
  177. package/src/app/components/util/index.ts +203 -380
  178. package/src/app/components/util/useCollectionAssets.ts +125 -0
  179. package/src/app/components/util/useFoundationTokens.ts +67 -0
  180. package/src/app/generated/default-pages.json +1 -0
  181. package/src/app/generated/nav-shell.json +1 -0
  182. package/src/app/lib/api/query.ts +6 -0
  183. package/src/app/lib/api/redact.ts +46 -0
  184. package/src/app/lib/asset-storage.ts +81 -0
  185. package/src/app/lib/docs-api/artifacts.ts +116 -0
  186. package/src/app/lib/docs-api/backend.ts +151 -0
  187. package/src/app/lib/docs-api/errors.ts +52 -0
  188. package/src/app/lib/docs-api/handler.ts +34 -0
  189. package/src/app/lib/docs-api/index.ts +7 -0
  190. package/src/app/lib/docs-api/records.ts +189 -0
  191. package/src/app/lib/docs-api/registry.ts +130 -0
  192. package/src/app/lib/docs-api/runtime-config.ts +173 -0
  193. package/src/app/lib/docs-api/serve.ts +37 -0
  194. package/src/app/lib/docs-api/token-detail.ts +19 -0
  195. package/src/app/lib/registry-api/allowlist.ts +163 -0
  196. package/src/app/lib/registry-api/asset-transfer.ts +363 -0
  197. package/src/app/lib/registry-api/auth.ts +68 -0
  198. package/src/app/lib/registry-api/entity-routes.ts +197 -0
  199. package/src/app/lib/registry-api/errors.ts +51 -0
  200. package/src/app/lib/registry-api/files.ts +135 -0
  201. package/src/app/lib/registry-api/handler.ts +96 -0
  202. package/src/app/lib/registry-api/index.ts +4 -0
  203. package/src/app/lib/registry-api/meta.ts +87 -0
  204. package/src/app/lib/registry-api/revalidate.ts +36 -0
  205. package/src/app/lib/registry-api/store.ts +348 -0
  206. package/src/app/lib/registry-api/token-transfer.ts +231 -0
  207. package/src/app/lib/registry-api/transfer.ts +513 -0
  208. package/src/app/lib/registry-api/validation.ts +74 -0
  209. package/src/app/lib/registry-connection.ts +58 -0
  210. package/src/app/lib/utils.ts +14 -0
  211. package/src/app/next.config.mjs +72 -13
  212. package/src/app/pages/[...slug]/index.tsx +74 -20
  213. package/src/app/pages/_app.tsx +5 -2
  214. package/src/app/pages/api/docs/artifacts/[...artifactPath].ts +16 -0
  215. package/src/app/pages/api/docs/assets/[collection]/[...path].ts +50 -0
  216. package/src/app/pages/api/docs/assets/[collection]/index.ts +22 -0
  217. package/src/app/pages/api/docs/components/[id]/assets/[asset].ts +33 -0
  218. package/src/app/pages/api/docs/components/[id]/inspect/[previewId].ts +20 -0
  219. package/src/app/pages/api/docs/components/[id]/previews/[previewId].ts +21 -0
  220. package/src/app/pages/api/docs/components/[id].ts +24 -0
  221. package/src/app/pages/api/docs/components.json.ts +12 -0
  222. package/src/app/pages/api/docs/nav.json.ts +48 -0
  223. package/src/app/pages/api/docs/patterns/[id]/preview.ts +19 -0
  224. package/src/app/pages/api/docs/patterns/[id].ts +24 -0
  225. package/src/app/pages/api/docs/patterns.json.ts +12 -0
  226. package/src/app/pages/api/docs/tokens/components/[id].ts +30 -0
  227. package/src/app/pages/api/docs/tokens/foundations/[type].ts +32 -0
  228. package/src/app/pages/api/registry/components/[id]/files/[...filePath].ts +19 -0
  229. package/src/app/pages/api/registry/components/[id]/files/index.ts +19 -0
  230. package/src/app/pages/api/registry/components/[id].ts +20 -0
  231. package/src/app/pages/api/registry/components/index.ts +19 -0
  232. package/src/app/pages/api/registry/health.ts +20 -0
  233. package/src/app/pages/api/registry/patterns/[id]/files/[...filePath].ts +19 -0
  234. package/src/app/pages/api/registry/patterns/[id]/files/index.ts +19 -0
  235. package/src/app/pages/api/registry/patterns/[id].ts +20 -0
  236. package/src/app/pages/api/registry/patterns/index.ts +19 -0
  237. package/src/app/pages/api/registry/transfer/[kind]/[id].ts +39 -0
  238. package/src/app/pages/api/registry/transfer/assets/[collection].ts +24 -0
  239. package/src/app/pages/api/registry/transfer/assets/blobs/[hash].ts +23 -0
  240. package/src/app/pages/api/registry/transfer/assets/blobs/have.ts +11 -0
  241. package/src/app/pages/api/registry/transfer/assets/index.ts +10 -0
  242. package/src/app/pages/api/registry/transfer/tokens/[...setId].ts +26 -0
  243. package/src/app/pages/api/registry/transfer/tokens/index.ts +11 -0
  244. package/src/app/pages/assets/fonts.tsx +41 -17
  245. package/src/app/pages/assets/index.tsx +2 -2
  246. package/src/app/pages/assets/logos.tsx +6 -3
  247. package/src/app/pages/foundations/colors.tsx +18 -8
  248. package/src/app/pages/foundations/effects.tsx +18 -8
  249. package/src/app/pages/foundations/icons/[name]/index.tsx +28 -16
  250. package/src/app/pages/foundations/icons/index.tsx +18 -12
  251. package/src/app/pages/foundations/index.tsx +1 -1
  252. package/src/app/pages/foundations/logo.tsx +12 -8
  253. package/src/app/pages/foundations/typography.tsx +19 -9
  254. package/src/app/pages/index.tsx +1 -1
  255. package/src/app/pages/system/component/[component]/index.tsx +87 -17
  256. package/src/app/pages/system/index.tsx +13 -4
  257. package/src/app/pages/system/pattern/[pattern]/index.tsx +70 -14
  258. package/src/app/pages/system/tokens/components/[component]/index.tsx +42 -14
  259. package/src/app/pages/system/tokens/foundations/colors.tsx +7 -5
  260. package/src/app/pages/system/tokens/foundations/effects.tsx +7 -5
  261. package/src/app/pages/system/tokens/foundations/typography.tsx +8 -4
  262. package/src/app/tsconfig.json +2 -3
  263. package/src/app-builder/build.ts +555 -11
  264. package/src/app-builder/client-config.ts +61 -4
  265. package/src/app-builder/config-diff/snapshot.ts +1 -11
  266. package/src/app-builder/index.ts +1 -1
  267. package/src/app-builder/nav-shell.ts +74 -0
  268. package/src/app-builder/paths.ts +27 -1
  269. package/src/app-builder/static-export.ts +242 -0
  270. package/src/app-builder/vercel-output.ts +297 -0
  271. package/src/app-builder/watchers/utils.ts +2 -0
  272. package/src/app-builder/watchers.ts +149 -25
  273. package/src/app-builder/websocket.ts +2 -2
  274. package/src/artifacts/README.md +14 -0
  275. package/src/artifacts/index.ts +24 -0
  276. package/src/artifacts/types.ts +146 -0
  277. package/src/artifacts/url.ts +122 -0
  278. package/src/cache/build-cache.ts +47 -2
  279. package/src/commands/build/app.ts +1 -2
  280. package/src/commands/build/index.ts +61 -0
  281. package/src/commands/checkout/index.ts +66 -0
  282. package/src/commands/db/migrate.ts +25 -0
  283. package/src/commands/index.ts +8 -0
  284. package/src/commands/publish/index.ts +63 -0
  285. package/src/config/defaults.ts +15 -0
  286. package/src/config/loader.ts +10 -1
  287. package/src/config/normalizers/declaration.ts +3 -0
  288. package/src/config/normalizers/page.ts +45 -0
  289. package/src/config/normalizers/pattern.ts +4 -1
  290. package/src/config/runtime.ts +48 -2
  291. package/src/index.ts +126 -3
  292. package/src/nav/index.ts +94 -0
  293. package/src/nav/registry.ts +54 -0
  294. package/src/nav/resolver.ts +217 -0
  295. package/src/nav/workspace.ts +87 -0
  296. package/src/pipeline/styles.ts +4 -35
  297. package/src/pipeline/token-transformers.ts +97 -0
  298. package/src/registry/asset-storage/adapters/vercel-blob.ts +51 -0
  299. package/src/registry/asset-storage/define.ts +15 -0
  300. package/src/registry/asset-storage/resolve.ts +72 -0
  301. package/src/registry/asset-storage/types.ts +46 -0
  302. package/src/registry/assets/layout.ts +38 -0
  303. package/src/registry/assets/sets.ts +48 -0
  304. package/src/registry/assets/transfer.ts +50 -0
  305. package/src/registry/checkout/assets.ts +126 -0
  306. package/src/registry/checkout/index.ts +596 -0
  307. package/src/registry/checkout/tokens.ts +152 -0
  308. package/src/registry/client.ts +234 -0
  309. package/src/registry/connection.ts +50 -0
  310. package/src/registry/db/adapter.ts +74 -0
  311. package/src/registry/db/client.ts +88 -0
  312. package/src/registry/db/index.ts +35 -0
  313. package/src/registry/db/migrate.ts +56 -0
  314. package/src/registry/db/schema.ts +444 -0
  315. package/src/registry/path.ts +26 -0
  316. package/src/registry/publish/artifacts.ts +147 -0
  317. package/src/registry/publish/assets.ts +129 -0
  318. package/src/registry/publish/errors.ts +34 -0
  319. package/src/registry/publish/index.ts +128 -0
  320. package/src/registry/publish/package.ts +272 -0
  321. package/src/registry/publish/publish-build.ts +51 -0
  322. package/src/registry/publish/tokens.ts +119 -0
  323. package/src/registry/tokens/sets.ts +132 -0
  324. package/src/registry/tokens/transfer.ts +53 -0
  325. package/src/registry/transfer.ts +82 -0
  326. package/src/store/filesystem-assets.ts +152 -0
  327. package/src/store/filesystem.ts +237 -0
  328. package/src/store/index.ts +62 -0
  329. package/src/store/registry.ts +334 -0
  330. package/src/store/source-files.ts +155 -0
  331. package/src/store/types.ts +210 -0
  332. package/src/transformers/plugins/csf-render.ts +31 -7
  333. package/src/transformers/plugins/handlebars-previews.ts +22 -3
  334. package/src/transformers/plugins/ssr-render.ts +145 -57
  335. package/src/transformers/preview/component/api.ts +2 -1
  336. package/src/transformers/preview/component/artifacts.ts +10 -1
  337. package/src/transformers/preview/component/builder.ts +9 -2
  338. package/src/transformers/preview/component/css.ts +10 -1
  339. package/src/transformers/preview/component/javascript.ts +55 -20
  340. package/src/transformers/preview/component/shared-artifacts.ts +146 -0
  341. package/src/transformers/preview/component.ts +5 -2
  342. package/src/transformers/preview/pattern/builder.ts +33 -7
  343. package/src/transformers/preview/pattern/html.ts +71 -27
  344. package/src/transformers/preview/types.ts +42 -0
  345. package/src/transformers/utils/handlebars.ts +31 -5
  346. package/src/types/config.ts +129 -31
  347. package/src/utils/menu-shell.ts +241 -0
  348. package/src/utils/stable-stringify.ts +7 -0
  349. package/templates/default/package.json.tpl +1 -1
  350. package/handoff.config.js +0 -90
  351. package/src/app/components/ComponentSearch.tsx +0 -93
@@ -0,0 +1,41 @@
1
+ {
2
+ "version": "7",
3
+ "dialect": "postgresql",
4
+ "entries": [
5
+ {
6
+ "idx": 0,
7
+ "version": "7",
8
+ "when": 1781979217509,
9
+ "tag": "0000_init_registry_storage",
10
+ "breakpoints": true
11
+ },
12
+ {
13
+ "idx": 1,
14
+ "version": "7",
15
+ "when": 1782045350977,
16
+ "tag": "0001_add_review_metadata",
17
+ "breakpoints": true
18
+ },
19
+ {
20
+ "idx": 2,
21
+ "version": "7",
22
+ "when": 1782713759115,
23
+ "tag": "0002_add_pages",
24
+ "breakpoints": true
25
+ },
26
+ {
27
+ "idx": 3,
28
+ "version": "7",
29
+ "when": 1783354008332,
30
+ "tag": "0003_add_tokens",
31
+ "breakpoints": true
32
+ },
33
+ {
34
+ "idx": 4,
35
+ "version": "7",
36
+ "when": 1783442253186,
37
+ "tag": "0004_add_assets",
38
+ "breakpoints": true
39
+ }
40
+ ]
41
+ }
@@ -0,0 +1,16 @@
1
+ import { defineConfig } from 'drizzle-kit';
2
+
3
+ /**
4
+ * Drizzle Kit config for generating the registry's package-owned migrations.
5
+ *
6
+ * Migrations are generated into `./drizzle` and shipped with the `handoff-app` package; the
7
+ * bundled runner (`handoff-app db:migrate`) applies them at install/deploy time. Both built-in
8
+ * adapters (`pg`, `neon`) target the Postgres dialect, so a single migration set serves both.
9
+ *
10
+ * Generate after schema changes with: `npx drizzle-kit generate`
11
+ */
12
+ export default defineConfig({
13
+ dialect: 'postgresql',
14
+ schema: './src/registry/db/schema.ts',
15
+ out: './drizzle',
16
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "handoff-app",
3
- "version": "1.2.2-7",
3
+ "version": "2.0.0-1",
4
4
  "description": "Automated documentation toolchain for building client side documentation from figma",
5
5
  "author ": {
6
6
  "name": "Convertiv",
@@ -53,6 +53,7 @@
53
53
  },
54
54
  "dependencies": {
55
55
  "@clack/prompts": "^0.7.0",
56
+ "@neondatabase/serverless": "^1.1.0",
56
57
  "@radix-ui/react-accordion": "^1.2.8",
57
58
  "@radix-ui/react-collapsible": "^1.1.2",
58
59
  "@radix-ui/react-dialog": "^1.1.4",
@@ -78,6 +79,7 @@
78
79
  "@types/react-syntax-highlighter": "^15.5.7",
79
80
  "@types/refractor": "^3.0.2",
80
81
  "@types/ws": "^8.18.1",
82
+ "@vercel/blob": "^1.1.1",
81
83
  "@vitejs/plugin-react": "^4.5.2",
82
84
  "archiver": "^5.3.1",
83
85
  "chalk": "^4.1.2",
@@ -86,6 +88,7 @@
86
88
  "clsx": "^2.1.1",
87
89
  "cross-spawn": "^7.0.3",
88
90
  "dotenv": "^16.0.3",
91
+ "drizzle-orm": "^0.45.2",
89
92
  "esbuild": "^0.25.0",
90
93
  "eslint": "^8.23.1",
91
94
  "eslint-config-next": "^15.3.4",
@@ -101,6 +104,7 @@
101
104
  "next": "^15.3.4",
102
105
  "next-themes": "^0.4.4",
103
106
  "node-html-parser": "^6.1.4",
107
+ "pg": "^8.22.0",
104
108
  "postcss": "^8.4.16",
105
109
  "prettier": "^3.4.2",
106
110
  "react": "^19.1.0",
@@ -121,13 +125,18 @@
121
125
  "ws": "^8.18.3",
122
126
  "yargs": "^17.7.2"
123
127
  },
128
+ "peerDependencies": {
129
+ "typescript": ">=5.6 <7"
130
+ },
124
131
  "devDependencies": {
125
132
  "@types/archiver": "^5.3.1",
126
133
  "@types/cross-spawn": "^6.0.2",
127
134
  "@types/fs-extra": "^11.0.4",
128
135
  "@types/handlebars": "^4.1.0",
129
136
  "@types/node": "^20.11.18",
137
+ "@types/pg": "^8.20.0",
130
138
  "@types/yargs": "^17.0.33",
139
+ "drizzle-kit": "^0.31.10",
131
140
  "np": "^10.2.0",
132
141
  "prettier-plugin-tailwindcss": "^0.7.0",
133
142
  "sass-embedded": "^1.89.0",
@@ -1,3 +1,4 @@
1
+ import { buildArtifactUrl } from '@handoff/artifacts/url';
1
2
  import { SlotMetadata } from '@handoff/transformers/preview/component';
2
3
  import { PageSlice } from '@handoff/transformers/preview/types';
3
4
  import { PreviewObject } from '@handoff/types/preview';
@@ -314,7 +315,7 @@ export const ComponentDisplay: React.FC<{
314
315
  className="h-7 px-3 hover:bg-gray-300 [&_svg]:size-3"
315
316
  onClick={() => {
316
317
  // open in new tab
317
- window.open(`${process.env.HANDOFF_APP_BASE_PATH ?? ''}/api/component/${previewUrl}`, '_blank');
318
+ window.open(buildArtifactUrl(`component/${previewUrl}`, process.env.HANDOFF_APP_BASE_PATH ?? ''), '_blank');
318
319
  }}
319
320
  variant="ghost"
320
321
  >
@@ -344,7 +345,7 @@ export const ComponentDisplay: React.FC<{
344
345
  display: 'block',
345
346
  margin: '0 auto',
346
347
  }}
347
- src={`${process.env.HANDOFF_APP_BASE_PATH ?? ''}/api/component/${previewUrl}`}
348
+ src={buildArtifactUrl(`component/${previewUrl}`, process.env.HANDOFF_APP_BASE_PATH ?? '')}
348
349
  />
349
350
  </div>
350
351
  ) : (
@@ -0,0 +1,150 @@
1
+ 'use client';
2
+ import { CheckIcon, CopyIcon, Download } from 'lucide-react';
3
+ import React from 'react';
4
+
5
+ import type { RuntimeMode } from '@handoff/types/config';
6
+ import { cn } from '../../lib/utils';
7
+ import { Button } from '../ui/button';
8
+ import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover';
9
+
10
+ type RegistryEntityType = 'component' | 'pattern';
11
+
12
+ interface RegistryActionsProps {
13
+ /** Resolved runtime mode projected to the client. */
14
+ mode: RuntimeMode;
15
+ /** Whether this is a connected workspace (workspace mode + resolvable registry URL). */
16
+ connected: boolean;
17
+ /** The kind of entity the page documents. */
18
+ entityType: RegistryEntityType;
19
+ /** Stable id of the component/pattern, used in the copyable command. */
20
+ id: string;
21
+ }
22
+
23
+ type RegistryAction = {
24
+ label: string;
25
+ title: string;
26
+ description: React.ReactNode;
27
+ command: string;
28
+ };
29
+
30
+ /**
31
+ * Registry transfer affordance for the single component/pattern page. Surfaces a context-appropriate
32
+ * helper based on the runtime:
33
+ * - registry mode -> "Checkout" instructions (pull the entity into a connected workspace);
34
+ * - connected workspace (workspace mode + registry URL) -> "Publish" instructions.
35
+ *
36
+ * A static export is never a live runtime, so the publish affordance is suppressed there using the
37
+ * same `HANDOFF_BUILD_TARGET` gate the runtime-mode badge uses. Renders nothing when no action
38
+ * applies (e.g. a plain, unconnected workspace).
39
+ */
40
+ export const RegistryActions: React.FC<RegistryActionsProps> = ({ mode, connected, entityType, id }) => {
41
+ // The static export is a frozen snapshot, not a live workspace, so the publish hint carries no
42
+ // meaning there and is omitted (mirrors the runtime-mode badge in the header).
43
+ const isStaticSnapshot = process.env.HANDOFF_BUILD_TARGET === 'static';
44
+
45
+ const action = resolveAction({ mode, connected, entityType, id, isStaticSnapshot });
46
+ if (!action) {
47
+ return null;
48
+ }
49
+
50
+ return (
51
+ <Popover>
52
+ <PopoverTrigger asChild>
53
+ <Button variant="outline" size="sm" className="font-normal [&_svg]:size-3!">
54
+ <Download aria-hidden="true" />
55
+ {action.label}
56
+ </Button>
57
+ </PopoverTrigger>
58
+ <PopoverContent align="start" className="w-[360px] p-4">
59
+ <div className="flex flex-col gap-3">
60
+ <div className="flex items-center gap-2">
61
+ <Download className="size-4 text-gray-700 dark:text-gray-200" aria-hidden="true" />
62
+ <p className="text-sm font-semibold text-gray-900 dark:text-gray-100">{action.title}</p>
63
+ </div>
64
+ <p className="text-sm text-muted-foreground">{action.description}</p>
65
+ <div className="border-t" />
66
+ <div className="flex items-stretch overflow-hidden rounded-md border">
67
+ <code className="flex flex-1 items-center truncate px-3 py-2 font-mono text-xs text-gray-700 dark:text-gray-300">
68
+ {action.command}
69
+ </code>
70
+ <CopyCommandButton command={action.command} />
71
+ </div>
72
+ <p className="text-xs text-muted-foreground">Requires a workspace configured with registry access.</p>
73
+ </div>
74
+ </PopoverContent>
75
+ </Popover>
76
+ );
77
+ };
78
+
79
+ /** Copy-to-clipboard control for the command box. No tooltip — just a copy/checkmark toggle. */
80
+ const CopyCommandButton: React.FC<{ command: string }> = ({ command }) => {
81
+ const [copied, setCopied] = React.useState(false);
82
+
83
+ const handleCopy = async () => {
84
+ try {
85
+ await navigator.clipboard.writeText(command);
86
+ setCopied(true);
87
+ setTimeout(() => setCopied(false), 1500);
88
+ } catch (err) {
89
+ console.error('Failed to copy command: ', err);
90
+ }
91
+ };
92
+
93
+ return (
94
+ <Button
95
+ type="button"
96
+ variant="ghost"
97
+ size="icon"
98
+ onClick={handleCopy}
99
+ aria-label={copied ? 'Copied' : 'Copy command'}
100
+ className="relative h-auto w-9 self-stretch rounded-none border-0 border-l shadow-none"
101
+ >
102
+ <span className={cn('transition-all', copied ? 'scale-100 opacity-100' : 'scale-0 opacity-0')}>
103
+ <CheckIcon className="stroke-gray-600" size={16} aria-hidden="true" />
104
+ </span>
105
+ <span className={cn('absolute transition-all', copied ? 'scale-0 opacity-0' : 'scale-100 opacity-100')}>
106
+ <CopyIcon size={16} aria-hidden="true" />
107
+ </span>
108
+ </Button>
109
+ );
110
+ };
111
+
112
+ const resolveAction = ({
113
+ mode,
114
+ connected,
115
+ entityType,
116
+ id,
117
+ isStaticSnapshot,
118
+ }: RegistryActionsProps & { isStaticSnapshot: boolean }): RegistryAction | null => {
119
+ if (mode === 'registry') {
120
+ return {
121
+ label: 'Checkout',
122
+ title: `Checkout ${entityType}`,
123
+ description: (
124
+ <>
125
+ Run this in a connected workspace to pull <strong className="font-medium text-foreground">{id}</strong> from the
126
+ registry.
127
+ </>
128
+ ),
129
+ command: `npm run checkout -- ${entityType} ${id}`,
130
+ };
131
+ }
132
+
133
+ if (mode === 'workspace' && connected && !isStaticSnapshot) {
134
+ return {
135
+ label: 'Publish',
136
+ title: `Publish ${entityType}`,
137
+ description: (
138
+ <>
139
+ Run this in your connected workspace to publish <strong className="font-medium text-foreground">{id}</strong> to
140
+ the registry.
141
+ </>
142
+ ),
143
+ command: `npm run publish -- ${entityType} ${id}`,
144
+ };
145
+ }
146
+
147
+ return null;
148
+ };
149
+
150
+ export default RegistryActions;
@@ -1,10 +1,18 @@
1
- import { Types as CoreTypes } from 'handoff-core';
2
1
  import React from 'react';
2
+ import type { DisplayableAsset } from '../util/useCollectionAssets';
3
3
 
4
- export const DisplayLogo: React.FC<{ logo: CoreTypes.IAssetObject; content?: string; correct?: boolean }> = ({ logo, content }) => {
4
+ export const DisplayLogo: React.FC<{ logo: DisplayableAsset; content?: string; correct?: boolean }> = ({ logo, content }) => {
5
+ // Workspace/static ships the SVG body inline in tokens.json (`data`) — render it directly so the
6
+ // logo has no dependency on the fetch-only `/api/assets/logos/*` files (absent on a static host).
7
+ // Registry has no inline body; it falls back to the individual asset content URL (`src`).
8
+ const src = logo.src ?? `${process.env.HANDOFF_APP_BASE_PATH ?? ''}/api/assets/logos/${logo.path}`;
5
9
  return (
6
10
  <div className="flex flex-col gap-5">
7
- <img src={`${process.env.HANDOFF_APP_BASE_PATH ?? ''}/api/assets/logos/${logo.path}`} alt="Usage Cards" className="rounded-3xl" />
11
+ {logo.data ? (
12
+ <div className="rounded-3xl" dangerouslySetInnerHTML={{ __html: logo.data }} />
13
+ ) : (
14
+ <img src={src} alt={logo.name} className="rounded-3xl" />
15
+ )}
8
16
  <div className="flex flex-row gap-4">
9
17
  <img src={`${process.env.HANDOFF_APP_BASE_PATH ?? ''}/assets/images/check-circle.svg`} alt="Do" className="h-6 w-6" />
10
18
  <p className="leading-normal text-gray-500">
@@ -4,11 +4,22 @@ import { ModeToggle } from '../../components/ModeSwitcher';
4
4
  import { MainNav } from '../../components/Navigation/MainNav';
5
5
  import { MobileNav } from '../../components/Navigation/MobileNav';
6
6
  import { cn } from '../../lib/utils';
7
+ import { Badge } from '../ui/badge';
7
8
  import { useConfigContext } from '../context/ConfigContext';
8
9
 
10
+ const RUNTIME_MODE_LABEL: Record<'workspace' | 'registry', string> = {
11
+ workspace: 'Workspace',
12
+ registry: 'Registry',
13
+ };
14
+
9
15
  export function Header() {
10
16
  const context = useConfigContext();
11
17
  const [isScrolled, setIsScrolled] = useState(false);
18
+ const runtimeMode = context.config?.runtime?.mode ?? 'workspace';
19
+ const runtimeModeLabel = RUNTIME_MODE_LABEL[runtimeMode];
20
+ // The static export is a self-contained snapshot of the workspace, not a live runtime, so the
21
+ // runtime-mode badge carries no meaning there and is omitted.
22
+ const isStaticSnapshot = process.env.HANDOFF_BUILD_TARGET === 'static';
12
23
 
13
24
  useEffect(() => {
14
25
  const handleScroll = () => {
@@ -28,9 +39,16 @@ export function Header() {
28
39
  >
29
40
  <header className="border-grid container mx-auto w-full max-w-[1500px] bg-transparent px-8">
30
41
  <div className="mx-auto flex items-center justify-between @container">
31
- <Link href="/">
32
- <img className="max-h-5" src={`${process.env.HANDOFF_APP_BASE_PATH ?? ''}/logo.svg`} alt={context.config?.app?.title} />
33
- </Link>
42
+ <div className="flex items-center gap-3">
43
+ <Link href="/">
44
+ <img className="max-h-5" src={`${process.env.HANDOFF_APP_BASE_PATH ?? ''}/logo.svg`} alt={context.config?.app?.title} />
45
+ </Link>
46
+ {!isStaticSnapshot && (
47
+ <Badge variant={runtimeMode === 'registry' ? 'info' : 'default'} aria-label={`Runtime mode: ${runtimeModeLabel}`}>
48
+ {runtimeModeLabel}
49
+ </Badge>
50
+ )}
51
+ </div>
34
52
  <div className="hidden items-center gap-4 @2xl:flex">
35
53
  <MainNav />
36
54
  <ModeToggle />
@@ -5,6 +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
9
  import { SidebarInset, SidebarProvider } from '../ui/sidebar';
9
10
  import { SectionLink } from '../util';
10
11
 
@@ -21,6 +22,13 @@ interface LayoutComponentProps {
21
22
  fullWidthHero?: boolean;
22
23
  }
23
24
  export default function Layout<LayoutComponentProps>({ children, config, menu, metadata, current, fullWidthHero = false }) {
25
+ // In registry mode the per-page `current` is empty for lambda-rendered (fallback) pages, so mount
26
+ // the sidebar regardless and let SideNav resolve its section from the cached shell (and collapse
27
+ // itself to full width when that section has no renderable nav). Workspace/static know the section
28
+ // at render time, so drop the left column entirely for pages with no side-nav content (e.g. a
29
+ // standalone page with no children) and let the content go full width.
30
+ const isRegistry = config?.runtime?.mode === 'registry';
31
+ const showSidebar = isRegistry || hasRenderableNav(current as SectionLink);
24
32
  return (
25
33
  <div>
26
34
  <ConfigContextProvider defaultConfig={config} defaultMenu={menu}>
@@ -48,10 +56,10 @@ export default function Layout<LayoutComponentProps>({ children, config, menu, m
48
56
  <div className="mx-auto w-full">{children}</div>
49
57
  </div>
50
58
  </div>
51
- ) : current ? (
59
+ ) : showSidebar ? (
52
60
  <SidebarProvider>
53
61
  <div className="flex w-full">
54
- <SideNav menu={current} />
62
+ <SideNav />
55
63
  <SidebarInset className="relative bg-transparent py-8 pl-8 pr-8 md:pl-8 lg:gap-10 lg:py-16 lg:pl-16">
56
64
  <div className="mx-auto w-full">{children}</div>
57
65
  </SidebarInset>
@@ -59,7 +67,7 @@ export default function Layout<LayoutComponentProps>({ children, config, menu, m
59
67
  </SidebarProvider>
60
68
  ) : (
61
69
  <div className="flex w-full">
62
- <div className="relative bg-transparent px-16 py-8 lg:gap-10 lg:py-16 xl:grid">
70
+ <div className="relative w-full bg-transparent px-16 py-8 lg:gap-10 lg:py-16">
63
71
  <div className="mx-auto w-full">{children}</div>
64
72
  </div>
65
73
  </div>
@@ -10,6 +10,7 @@ import {
10
10
  } from '../../components/ui/navigation-menu';
11
11
  import { cn } from '../../lib/utils';
12
12
  import { useConfigContext } from '../context/ConfigContext';
13
+ import { useNav } from '../context/NavProvider';
13
14
 
14
15
  const trimSlashes = (input: string): string => {
15
16
  return input.replace(/^\/+|\/+$/g, '');
@@ -18,11 +19,12 @@ const trimSlashes = (input: string): string => {
18
19
  export function MainNav() {
19
20
  const context = useConfigContext();
20
21
  const router = useRouter();
22
+ const { menu } = useNav();
21
23
  return (
22
24
  <NavigationMenu>
23
25
  <NavigationMenuList>
24
- {context.menu &&
25
- context.menu.map((section) => {
26
+ {menu &&
27
+ menu.map((section) => {
26
28
  const isActive = trimSlashes(router.asPath).startsWith(trimSlashes(section.path));
27
29
  return (
28
30
  <NavigationMenuItem key={section.title}>
@@ -7,6 +7,7 @@ import { Button } from '../../components/ui/button';
7
7
  import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from '../../components/ui/sheet';
8
8
  import { cn } from '../../lib/utils';
9
9
  import { useConfigContext } from '../context/ConfigContext';
10
+ import { useNav } from '../context/NavProvider';
10
11
 
11
12
  const trimSlashes = (input: string): string => {
12
13
  return input.replace(/^\/+|\/+$/g, '');
@@ -16,6 +17,7 @@ export function MobileNav() {
16
17
  const context = useConfigContext();
17
18
  const router = useRouter();
18
19
  const { theme, setTheme } = useTheme();
20
+ const { menu } = useNav();
19
21
 
20
22
  const toggleTheme = () => {
21
23
  if (theme === 'light') {
@@ -48,8 +50,8 @@ export function MobileNav() {
48
50
  </SheetTrigger>
49
51
  </SheetHeader>
50
52
  <div className="mt-8 flex flex-col space-y-4">
51
- {context.menu &&
52
- context.menu.map((section) => {
53
+ {menu &&
54
+ menu.map((section) => {
53
55
  const isActive = trimSlashes(router.asPath).startsWith(trimSlashes(section.path));
54
56
  return (
55
57
  <Link
@@ -16,8 +16,9 @@ import {
16
16
  } from 'lucide-react';
17
17
  import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '../../components/ui/collapsible';
18
18
 
19
+ import Link from 'next/link';
19
20
  import { useRouter } from 'next/router';
20
- import React from 'react';
21
+ import React, { useEffect, useState } from 'react';
21
22
  import {
22
23
  Sidebar,
23
24
  SidebarContent,
@@ -30,8 +31,22 @@ import {
30
31
  SidebarMenuSub,
31
32
  SidebarSeparator,
32
33
  } from '../../components/ui/sidebar';
33
- import { normalizePathForMatch, toAbsolutePath } from '../../lib/utils';
34
- import { SectionLink } from '../util';
34
+ import { normalizePathForMatch } from '../../lib/utils';
35
+ import { useNav } from '../context/NavProvider';
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
+ };
35
50
 
36
51
  const NormalMenuItem = ({ title, icon, path }) => {
37
52
  const router = useRouter();
@@ -39,10 +54,13 @@ const NormalMenuItem = ({ title, icon, path }) => {
39
54
  return (
40
55
  <SidebarMenuItem>
41
56
  <SidebarMenuButton asChild isActive={isActive}>
42
- <a href={toAbsolutePath(path)} className="gap-3">
57
+ {/* Soft (client-side) navigation so moving between entity pages reuses the already-loaded
58
+ cached nav with no reload/flash. Prefetch is off: each prefetch would invoke the
59
+ fallback:'blocking' lambda data route, so fetch the target's data on click instead. */}
60
+ <Link href={stripBasePath(path)} prefetch={false} className="gap-3">
43
61
  <MenuIcon icon={icon} isActive={isActive} />
44
62
  <span>{title}</span>
45
- </a>
63
+ </Link>
46
64
  </SidebarMenuButton>
47
65
  </SidebarMenuItem>
48
66
  );
@@ -53,8 +71,15 @@ const CollapsibleMenuItem = ({ title, icon, path, menu }) => {
53
71
  const isActive = menu.some(
54
72
  (item) => normalizePathForMatch(router.asPath).startsWith(normalizePathForMatch(item.path))
55
73
  );
74
+ // Controlled so the active group auto-opens on soft navigation (an uncontrolled `defaultOpen` is
75
+ // only read on mount and would not re-open the active group when the route changes client-side),
76
+ // while still letting the user collapse/expand groups manually.
77
+ const [open, setOpen] = useState(isActive);
78
+ useEffect(() => {
79
+ if (isActive) setOpen(true);
80
+ }, [isActive]);
56
81
  return (
57
- <Collapsible defaultOpen={isActive} className="group/collapsible">
82
+ <Collapsible open={open} onOpenChange={setOpen} className="group/collapsible">
58
83
  <SidebarMenuItem>
59
84
  <CollapsibleTrigger asChild>
60
85
  <SidebarMenuButton className="h-9 gap-3">
@@ -118,13 +143,23 @@ const MenuIcon = ({ icon, isActive = false }) => {
118
143
  }
119
144
  };
120
145
 
121
- const SideNav = ({ menu }: { menu: SectionLink }) => {
146
+ const SideNav = () => {
147
+ const { current } = useNav();
148
+ const sections = current?.subSections ?? [];
149
+
150
+ // Collapse the sidebar entirely (render nothing, reserve no gutter) when the resolved section has
151
+ // no renderable content, so the page content goes full width. A `dynamic` slot counts as
152
+ // renderable even before its entities load, to avoid a full-width → gutter flash in registry mode.
153
+ const hasContent = sections.some((section) => (section.menu && section.menu.length > 0) || !!section.dynamic);
154
+ if (!hasContent) {
155
+ return null;
156
+ }
157
+
122
158
  return (
123
159
  <Sidebar className="sticky left-auto">
124
160
  <SidebarContent className="px-4 pt-5">
125
- {menu.subSections &&
126
- menu.subSections.length > 0 &&
127
- menu.subSections.map((section, index) => (
161
+ {sections.length > 0 &&
162
+ sections.map((section, index) => (
128
163
  <React.Fragment key={index}>
129
164
  <SidebarGroup>
130
165
  {!section.path && <SidebarGroupLabel>{section.title}</SidebarGroupLabel>}
@@ -138,7 +173,7 @@ const SideNav = ({ menu }: { menu: SectionLink }) => {
138
173
  </SidebarGroupContent>
139
174
  )}
140
175
  </SidebarGroup>
141
- {index < menu.subSections.length && <SidebarSeparator className="mx-4" />}
176
+ {index < sections.length && <SidebarSeparator className="mx-4" />}
142
177
  </React.Fragment>
143
178
  ))}
144
179
  </SidebarContent>
@@ -0,0 +1,24 @@
1
+ import React from 'react';
2
+
3
+ interface NotFoundProps {
4
+ title?: string;
5
+ description?: React.ReactNode;
6
+ linkHref?: string;
7
+ linkLabel?: string;
8
+ }
9
+
10
+ const NotFound: React.FC<NotFoundProps> = () => {
11
+ return (
12
+ <div className="flex flex-col items-center">
13
+ <div className="mb-2 text-7xl font-bold text-gray-800 dark:text-white">404</div>
14
+ <h1 className="mb-4 text-2xl font-semibold text-gray-700 dark:text-gray-300">Oops! Page not found.</h1>
15
+ <p className="mb-6 max-w-md text-center text-gray-500 dark:text-gray-400">
16
+ This page doesn&rsquo;t exist or has been moved.
17
+ <br />
18
+ Please check the URL or return to the homepage.
19
+ </p>
20
+ </div>
21
+ );
22
+ };
23
+
24
+ export default NotFound;
@@ -0,0 +1,71 @@
1
+ 'use client';
2
+
3
+ import type { NavData, SectionLink } from '@handoff/nav';
4
+ import React, { createContext, useContext, useEffect, useState } from 'react';
5
+
6
+ export type { NavData, NavEntity } from '@handoff/nav';
7
+
8
+ interface INavContext {
9
+ nav: NavData;
10
+ currentSectionId: string;
11
+ }
12
+
13
+ const EMPTY_NAV: NavData = { shell: [] };
14
+ const NavContext = createContext<INavContext>({ nav: EMPTY_NAV, currentSectionId: '' });
15
+ const isRegistryMode = process.env.HANDOFF_RUNTIME_MODE === 'registry';
16
+
17
+ // A module-level fulfilled cache and in-flight guard make the registry refresh a single hard-load
18
+ // operation, including StrictMode remounts. A browser hard reload resets both and observes publishes.
19
+ let cachedNav: NavData | null = null;
20
+ let inFlight: Promise<NavData | null> | null = null;
21
+
22
+ const loadNav = (): Promise<NavData | null> => {
23
+ if (cachedNav) return Promise.resolve(cachedNav);
24
+ if (inFlight) return inFlight;
25
+ inFlight = fetch(`${process.env.HANDOFF_APP_BASE_PATH ?? ''}/api/docs/nav.json`)
26
+ .then((res) => (res.ok ? res.json() : null))
27
+ .then((data: NavData | null) => {
28
+ if (data) cachedNav = data;
29
+ return cachedNav;
30
+ })
31
+ .catch(() => null)
32
+ .finally(() => {
33
+ inFlight = null;
34
+ });
35
+ return inFlight;
36
+ };
37
+
38
+ export const NavProvider: React.FC<{
39
+ children: React.ReactNode;
40
+ initialNav?: NavData;
41
+ currentSectionId?: string;
42
+ }> = ({ children, initialNav = EMPTY_NAV, currentSectionId = '' }) => {
43
+ // Registry alone owns refresh state. Workspace renders the latest page prop directly, so soft
44
+ // navigation cannot briefly expose the preceding page's tree and triggers no provider effect.
45
+ const [registryNav, setRegistryNav] = useState<NavData>(() => cachedNav ?? initialNav);
46
+
47
+ useEffect(() => {
48
+ if (!isRegistryMode || cachedNav) return;
49
+ let active = true;
50
+ loadNav().then((data) => {
51
+ if (active && data) setRegistryNav(data);
52
+ });
53
+ return () => {
54
+ active = false;
55
+ };
56
+ // eslint-disable-next-line react-hooks/exhaustive-deps
57
+ }, []);
58
+
59
+ const nav = isRegistryMode ? registryNav : initialNav;
60
+ return <NavContext.Provider value={{ nav, currentSectionId }}>{children}</NavContext.Provider>;
61
+ };
62
+
63
+ /** The sole view-facing navigation API: one active tree and an exact-match active section. */
64
+ export const useNav = (): { nav: NavData; menu: SectionLink[]; current: SectionLink | null } => {
65
+ const { nav, currentSectionId } = useContext(NavContext);
66
+ return {
67
+ nav,
68
+ menu: nav.shell,
69
+ current: nav.shell.find((section) => section.path === currentSectionId) ?? null,
70
+ };
71
+ };