handoff-app 1.2.2-6 → 2.0.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 (349) 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/static-export.d.ts +17 -0
  14. package/dist/app-builder/static-export.js +181 -0
  15. package/dist/app-builder/vercel-output.d.ts +39 -0
  16. package/dist/app-builder/vercel-output.js +259 -0
  17. package/dist/app-builder/watchers/utils.d.ts +2 -0
  18. package/dist/app-builder/watchers.d.ts +14 -4
  19. package/dist/app-builder/watchers.js +123 -28
  20. package/dist/app-builder/websocket.js +1 -1
  21. package/dist/artifacts/index.d.ts +2 -0
  22. package/dist/artifacts/index.js +13 -0
  23. package/dist/artifacts/types.d.ts +136 -0
  24. package/dist/artifacts/types.js +24 -0
  25. package/dist/artifacts/url.d.ts +49 -0
  26. package/dist/artifacts/url.js +86 -0
  27. package/dist/cache/build-cache.js +48 -4
  28. package/dist/commands/build/app.js +1 -2
  29. package/dist/commands/build/index.d.ts +17 -0
  30. package/dist/commands/build/index.js +66 -0
  31. package/dist/commands/checkout/index.d.ts +24 -0
  32. package/dist/commands/checkout/index.js +71 -0
  33. package/dist/commands/db/migrate.d.ts +11 -0
  34. package/dist/commands/db/migrate.js +33 -0
  35. package/dist/commands/index.js +8 -0
  36. package/dist/commands/publish/index.d.ts +21 -0
  37. package/dist/commands/publish/index.js +68 -0
  38. package/dist/config/defaults.js +15 -1
  39. package/dist/config/loader.js +6 -1
  40. package/dist/config/normalizers/declaration.js +4 -1
  41. package/dist/config/normalizers/page.d.ts +18 -0
  42. package/dist/config/normalizers/page.js +34 -0
  43. package/dist/config/normalizers/pattern.js +7 -1
  44. package/dist/config/runtime.d.ts +4 -0
  45. package/dist/config/runtime.js +53 -5
  46. package/dist/index.d.ts +65 -1
  47. package/dist/index.js +123 -3
  48. package/dist/nav/index.d.ts +83 -0
  49. package/dist/nav/index.js +9 -0
  50. package/dist/nav/registry.d.ts +15 -0
  51. package/dist/nav/registry.js +42 -0
  52. package/dist/nav/resolver.d.ts +8 -0
  53. package/dist/nav/resolver.js +192 -0
  54. package/dist/nav/workspace.d.ts +18 -0
  55. package/dist/nav/workspace.js +103 -0
  56. package/dist/pipeline/styles.js +7 -35
  57. package/dist/pipeline/token-transformers.d.ts +45 -0
  58. package/dist/pipeline/token-transformers.js +71 -0
  59. package/dist/registry/asset-storage/adapters/vercel-blob.d.ts +17 -0
  60. package/dist/registry/asset-storage/adapters/vercel-blob.js +87 -0
  61. package/dist/registry/asset-storage/define.d.ts +10 -0
  62. package/dist/registry/asset-storage/define.js +13 -0
  63. package/dist/registry/asset-storage/resolve.d.ts +45 -0
  64. package/dist/registry/asset-storage/resolve.js +36 -0
  65. package/dist/registry/asset-storage/types.d.ts +51 -0
  66. package/dist/registry/asset-storage/types.js +9 -0
  67. package/dist/registry/assets/layout.d.ts +22 -0
  68. package/dist/registry/assets/layout.js +30 -0
  69. package/dist/registry/assets/sets.d.ts +18 -0
  70. package/dist/registry/assets/sets.js +44 -0
  71. package/dist/registry/assets/transfer.d.ts +45 -0
  72. package/dist/registry/assets/transfer.js +11 -0
  73. package/dist/registry/checkout/assets.d.ts +16 -0
  74. package/dist/registry/checkout/assets.js +150 -0
  75. package/dist/registry/checkout/index.d.ts +33 -0
  76. package/dist/registry/checkout/index.js +610 -0
  77. package/dist/registry/checkout/tokens.d.ts +15 -0
  78. package/dist/registry/checkout/tokens.js +171 -0
  79. package/dist/registry/client.d.ts +61 -0
  80. package/dist/registry/client.js +211 -0
  81. package/dist/registry/connection.d.ts +30 -0
  82. package/dist/registry/connection.js +36 -0
  83. package/dist/registry/db/adapter.d.ts +43 -0
  84. package/dist/registry/db/adapter.js +59 -0
  85. package/dist/registry/db/client.d.ts +35 -0
  86. package/dist/registry/db/client.js +94 -0
  87. package/dist/registry/db/index.d.ts +5 -0
  88. package/dist/registry/db/index.js +24 -0
  89. package/dist/registry/db/migrate.d.ts +23 -0
  90. package/dist/registry/db/migrate.js +64 -0
  91. package/dist/registry/db/schema.d.ts +4869 -0
  92. package/dist/registry/db/schema.js +347 -0
  93. package/dist/registry/path.d.ts +4 -0
  94. package/dist/registry/path.js +31 -0
  95. package/dist/registry/publish/artifacts.d.ts +16 -0
  96. package/dist/registry/publish/artifacts.js +132 -0
  97. package/dist/registry/publish/assets.d.ts +16 -0
  98. package/dist/registry/publish/assets.js +129 -0
  99. package/dist/registry/publish/errors.d.ts +5 -0
  100. package/dist/registry/publish/errors.js +26 -0
  101. package/dist/registry/publish/index.d.ts +27 -0
  102. package/dist/registry/publish/index.js +126 -0
  103. package/dist/registry/publish/package.d.ts +27 -0
  104. package/dist/registry/publish/package.js +257 -0
  105. package/dist/registry/publish/publish-build.d.ts +12 -0
  106. package/dist/registry/publish/publish-build.js +43 -0
  107. package/dist/registry/publish/tokens.d.ts +18 -0
  108. package/dist/registry/publish/tokens.js +112 -0
  109. package/dist/registry/tokens/sets.d.ts +59 -0
  110. package/dist/registry/tokens/sets.js +109 -0
  111. package/dist/registry/tokens/transfer.d.ts +48 -0
  112. package/dist/registry/tokens/transfer.js +10 -0
  113. package/dist/registry/transfer.d.ts +75 -0
  114. package/dist/registry/transfer.js +11 -0
  115. package/dist/store/filesystem-assets.d.ts +17 -0
  116. package/dist/store/filesystem-assets.js +147 -0
  117. package/dist/store/filesystem.d.ts +76 -0
  118. package/dist/store/filesystem.js +208 -0
  119. package/dist/store/index.d.ts +20 -0
  120. package/dist/store/index.js +38 -0
  121. package/dist/store/registry.d.ts +77 -0
  122. package/dist/store/registry.js +350 -0
  123. package/dist/store/source-files.d.ts +24 -0
  124. package/dist/store/source-files.js +147 -0
  125. package/dist/store/types.d.ts +185 -0
  126. package/dist/store/types.js +12 -0
  127. package/dist/transformers/plugins/csf-render.js +17 -8
  128. package/dist/transformers/plugins/handlebars-previews.js +10 -3
  129. package/dist/transformers/plugins/ssr-render.js +116 -49
  130. package/dist/transformers/preview/component/api.js +3 -1
  131. package/dist/transformers/preview/component/artifacts.js +8 -1
  132. package/dist/transformers/preview/component/builder.js +13 -5
  133. package/dist/transformers/preview/component/css.js +9 -1
  134. package/dist/transformers/preview/component/javascript.d.ts +9 -3
  135. package/dist/transformers/preview/component/javascript.js +45 -17
  136. package/dist/transformers/preview/component/shared-artifacts.d.ts +83 -0
  137. package/dist/transformers/preview/component/shared-artifacts.js +92 -0
  138. package/dist/transformers/preview/component.js +5 -2
  139. package/dist/transformers/preview/pattern/builder.js +28 -9
  140. package/dist/transformers/preview/pattern/html.d.ts +13 -10
  141. package/dist/transformers/preview/pattern/html.js +62 -27
  142. package/dist/transformers/preview/types.d.ts +38 -0
  143. package/dist/transformers/utils/handlebars.d.ts +3 -0
  144. package/dist/transformers/utils/handlebars.js +19 -5
  145. package/dist/types/config.d.ts +118 -25
  146. package/dist/utils/menu-shell.d.ts +60 -0
  147. package/dist/utils/menu-shell.js +221 -0
  148. package/dist/utils/stable-stringify.d.ts +2 -0
  149. package/dist/utils/stable-stringify.js +8 -0
  150. package/docs/cli.md +4 -2
  151. package/drizzle/0000_init_registry_storage.sql +95 -0
  152. package/drizzle/0001_add_review_metadata.sql +2 -0
  153. package/drizzle/0002_add_pages.sql +28 -0
  154. package/drizzle/0003_add_tokens.sql +29 -0
  155. package/drizzle/0004_add_assets.sql +37 -0
  156. package/drizzle/meta/0000_snapshot.json +642 -0
  157. package/drizzle/meta/0001_snapshot.json +654 -0
  158. package/drizzle/meta/0002_snapshot.json +835 -0
  159. package/drizzle/meta/0003_snapshot.json +1023 -0
  160. package/drizzle/meta/0004_snapshot.json +1252 -0
  161. package/drizzle/meta/_journal.json +41 -0
  162. package/drizzle.config.ts +16 -0
  163. package/package.json +10 -1
  164. package/src/app/components/Component/ComponentLists.tsx +102 -102
  165. package/src/app/components/Component/Preview.tsx +3 -2
  166. package/src/app/components/Component/RegistryActions.tsx +150 -0
  167. package/src/app/components/Foundations/DisplayLogo.tsx +5 -3
  168. package/src/app/components/Layout/Header.tsx +21 -3
  169. package/src/app/components/Layout/Main.tsx +11 -3
  170. package/src/app/components/Navigation/MainNav.tsx +4 -2
  171. package/src/app/components/Navigation/MobileNav.tsx +4 -2
  172. package/src/app/components/Navigation/SideNav.tsx +46 -11
  173. package/src/app/components/NotFound.tsx +24 -0
  174. package/src/app/components/context/NavProvider.tsx +71 -0
  175. package/src/app/components/old/Header.tsx +3 -3
  176. package/src/app/components/util/index.ts +203 -380
  177. package/src/app/components/util/useCollectionAssets.ts +125 -0
  178. package/src/app/components/util/useFoundationTokens.ts +67 -0
  179. package/src/app/generated/default-pages.json +1 -0
  180. package/src/app/generated/nav-shell.json +1 -0
  181. package/src/app/lib/api/query.ts +6 -0
  182. package/src/app/lib/api/redact.ts +46 -0
  183. package/src/app/lib/asset-storage.ts +81 -0
  184. package/src/app/lib/docs-api/artifacts.ts +116 -0
  185. package/src/app/lib/docs-api/backend.ts +151 -0
  186. package/src/app/lib/docs-api/errors.ts +52 -0
  187. package/src/app/lib/docs-api/handler.ts +34 -0
  188. package/src/app/lib/docs-api/index.ts +7 -0
  189. package/src/app/lib/docs-api/records.ts +189 -0
  190. package/src/app/lib/docs-api/registry.ts +130 -0
  191. package/src/app/lib/docs-api/runtime-config.ts +173 -0
  192. package/src/app/lib/docs-api/serve.ts +37 -0
  193. package/src/app/lib/docs-api/token-detail.ts +19 -0
  194. package/src/app/lib/registry-api/allowlist.ts +163 -0
  195. package/src/app/lib/registry-api/asset-transfer.ts +363 -0
  196. package/src/app/lib/registry-api/auth.ts +68 -0
  197. package/src/app/lib/registry-api/entity-routes.ts +197 -0
  198. package/src/app/lib/registry-api/errors.ts +51 -0
  199. package/src/app/lib/registry-api/files.ts +135 -0
  200. package/src/app/lib/registry-api/handler.ts +96 -0
  201. package/src/app/lib/registry-api/index.ts +4 -0
  202. package/src/app/lib/registry-api/meta.ts +87 -0
  203. package/src/app/lib/registry-api/revalidate.ts +36 -0
  204. package/src/app/lib/registry-api/store.ts +348 -0
  205. package/src/app/lib/registry-api/token-transfer.ts +231 -0
  206. package/src/app/lib/registry-api/transfer.ts +513 -0
  207. package/src/app/lib/registry-api/validation.ts +74 -0
  208. package/src/app/lib/registry-connection.ts +58 -0
  209. package/src/app/lib/utils.ts +14 -0
  210. package/src/app/next.config.mjs +72 -13
  211. package/src/app/pages/[...slug]/index.tsx +74 -20
  212. package/src/app/pages/_app.tsx +5 -2
  213. package/src/app/pages/api/docs/artifacts/[...artifactPath].ts +16 -0
  214. package/src/app/pages/api/docs/assets/[collection]/[...path].ts +50 -0
  215. package/src/app/pages/api/docs/assets/[collection]/index.ts +22 -0
  216. package/src/app/pages/api/docs/components/[id]/assets/[asset].ts +33 -0
  217. package/src/app/pages/api/docs/components/[id]/inspect/[previewId].ts +20 -0
  218. package/src/app/pages/api/docs/components/[id]/previews/[previewId].ts +21 -0
  219. package/src/app/pages/api/docs/components/[id].ts +24 -0
  220. package/src/app/pages/api/docs/components.json.ts +12 -0
  221. package/src/app/pages/api/docs/nav.json.ts +48 -0
  222. package/src/app/pages/api/docs/patterns/[id]/preview.ts +19 -0
  223. package/src/app/pages/api/docs/patterns/[id].ts +24 -0
  224. package/src/app/pages/api/docs/patterns.json.ts +12 -0
  225. package/src/app/pages/api/docs/tokens/components/[id].ts +30 -0
  226. package/src/app/pages/api/docs/tokens/foundations/[type].ts +32 -0
  227. package/src/app/pages/api/registry/components/[id]/files/[...filePath].ts +19 -0
  228. package/src/app/pages/api/registry/components/[id]/files/index.ts +19 -0
  229. package/src/app/pages/api/registry/components/[id].ts +20 -0
  230. package/src/app/pages/api/registry/components/index.ts +19 -0
  231. package/src/app/pages/api/registry/health.ts +20 -0
  232. package/src/app/pages/api/registry/patterns/[id]/files/[...filePath].ts +19 -0
  233. package/src/app/pages/api/registry/patterns/[id]/files/index.ts +19 -0
  234. package/src/app/pages/api/registry/patterns/[id].ts +20 -0
  235. package/src/app/pages/api/registry/patterns/index.ts +19 -0
  236. package/src/app/pages/api/registry/transfer/[kind]/[id].ts +39 -0
  237. package/src/app/pages/api/registry/transfer/assets/[collection].ts +24 -0
  238. package/src/app/pages/api/registry/transfer/assets/blobs/[hash].ts +23 -0
  239. package/src/app/pages/api/registry/transfer/assets/blobs/have.ts +11 -0
  240. package/src/app/pages/api/registry/transfer/assets/index.ts +10 -0
  241. package/src/app/pages/api/registry/transfer/tokens/[...setId].ts +26 -0
  242. package/src/app/pages/api/registry/transfer/tokens/index.ts +11 -0
  243. package/src/app/pages/assets/fonts.tsx +41 -17
  244. package/src/app/pages/assets/index.tsx +2 -2
  245. package/src/app/pages/assets/logos.tsx +2 -2
  246. package/src/app/pages/foundations/colors.tsx +18 -8
  247. package/src/app/pages/foundations/effects.tsx +18 -8
  248. package/src/app/pages/foundations/icons/[name]/index.tsx +26 -14
  249. package/src/app/pages/foundations/icons/index.tsx +13 -9
  250. package/src/app/pages/foundations/index.tsx +1 -1
  251. package/src/app/pages/foundations/logo.tsx +7 -4
  252. package/src/app/pages/foundations/typography.tsx +19 -9
  253. package/src/app/pages/index.tsx +1 -1
  254. package/src/app/pages/system/component/[component]/index.tsx +87 -17
  255. package/src/app/pages/system/index.tsx +13 -4
  256. package/src/app/pages/system/pattern/[pattern]/index.tsx +70 -14
  257. package/src/app/pages/system/tokens/components/[component]/index.tsx +42 -14
  258. package/src/app/pages/system/tokens/foundations/colors.tsx +7 -5
  259. package/src/app/pages/system/tokens/foundations/effects.tsx +7 -5
  260. package/src/app/pages/system/tokens/foundations/typography.tsx +8 -4
  261. package/src/app/tsconfig.json +2 -3
  262. package/src/app-builder/build.ts +555 -11
  263. package/src/app-builder/client-config.ts +61 -4
  264. package/src/app-builder/config-diff/snapshot.ts +1 -11
  265. package/src/app-builder/index.ts +1 -1
  266. package/src/app-builder/nav-shell.ts +74 -0
  267. package/src/app-builder/static-export.ts +215 -0
  268. package/src/app-builder/vercel-output.ts +297 -0
  269. package/src/app-builder/watchers/utils.ts +2 -0
  270. package/src/app-builder/watchers.ts +149 -25
  271. package/src/app-builder/websocket.ts +2 -2
  272. package/src/artifacts/README.md +14 -0
  273. package/src/artifacts/index.ts +17 -0
  274. package/src/artifacts/types.ts +146 -0
  275. package/src/artifacts/url.ts +90 -0
  276. package/src/cache/build-cache.ts +47 -2
  277. package/src/commands/build/app.ts +1 -2
  278. package/src/commands/build/index.ts +61 -0
  279. package/src/commands/checkout/index.ts +66 -0
  280. package/src/commands/db/migrate.ts +25 -0
  281. package/src/commands/index.ts +8 -0
  282. package/src/commands/publish/index.ts +63 -0
  283. package/src/config/defaults.ts +15 -0
  284. package/src/config/loader.ts +10 -1
  285. package/src/config/normalizers/declaration.ts +3 -0
  286. package/src/config/normalizers/page.ts +45 -0
  287. package/src/config/normalizers/pattern.ts +4 -1
  288. package/src/config/runtime.ts +48 -2
  289. package/src/index.ts +126 -3
  290. package/src/nav/index.ts +94 -0
  291. package/src/nav/registry.ts +54 -0
  292. package/src/nav/resolver.ts +217 -0
  293. package/src/nav/workspace.ts +87 -0
  294. package/src/pipeline/styles.ts +4 -35
  295. package/src/pipeline/token-transformers.ts +97 -0
  296. package/src/registry/asset-storage/adapters/vercel-blob.ts +51 -0
  297. package/src/registry/asset-storage/define.ts +15 -0
  298. package/src/registry/asset-storage/resolve.ts +72 -0
  299. package/src/registry/asset-storage/types.ts +46 -0
  300. package/src/registry/assets/layout.ts +38 -0
  301. package/src/registry/assets/sets.ts +48 -0
  302. package/src/registry/assets/transfer.ts +50 -0
  303. package/src/registry/checkout/assets.ts +126 -0
  304. package/src/registry/checkout/index.ts +596 -0
  305. package/src/registry/checkout/tokens.ts +152 -0
  306. package/src/registry/client.ts +234 -0
  307. package/src/registry/connection.ts +50 -0
  308. package/src/registry/db/adapter.ts +74 -0
  309. package/src/registry/db/client.ts +88 -0
  310. package/src/registry/db/index.ts +35 -0
  311. package/src/registry/db/migrate.ts +56 -0
  312. package/src/registry/db/schema.ts +444 -0
  313. package/src/registry/path.ts +26 -0
  314. package/src/registry/publish/artifacts.ts +147 -0
  315. package/src/registry/publish/assets.ts +129 -0
  316. package/src/registry/publish/errors.ts +34 -0
  317. package/src/registry/publish/index.ts +128 -0
  318. package/src/registry/publish/package.ts +272 -0
  319. package/src/registry/publish/publish-build.ts +51 -0
  320. package/src/registry/publish/tokens.ts +119 -0
  321. package/src/registry/tokens/sets.ts +132 -0
  322. package/src/registry/tokens/transfer.ts +53 -0
  323. package/src/registry/transfer.ts +82 -0
  324. package/src/store/filesystem-assets.ts +152 -0
  325. package/src/store/filesystem.ts +237 -0
  326. package/src/store/index.ts +62 -0
  327. package/src/store/registry.ts +334 -0
  328. package/src/store/source-files.ts +155 -0
  329. package/src/store/types.ts +210 -0
  330. package/src/transformers/plugins/csf-render.ts +31 -7
  331. package/src/transformers/plugins/handlebars-previews.ts +22 -3
  332. package/src/transformers/plugins/ssr-render.ts +145 -57
  333. package/src/transformers/preview/component/api.ts +2 -1
  334. package/src/transformers/preview/component/artifacts.ts +10 -1
  335. package/src/transformers/preview/component/builder.ts +9 -2
  336. package/src/transformers/preview/component/css.ts +10 -1
  337. package/src/transformers/preview/component/javascript.ts +55 -20
  338. package/src/transformers/preview/component/shared-artifacts.ts +146 -0
  339. package/src/transformers/preview/component.ts +5 -2
  340. package/src/transformers/preview/pattern/builder.ts +33 -7
  341. package/src/transformers/preview/pattern/html.ts +71 -27
  342. package/src/transformers/preview/types.ts +42 -0
  343. package/src/transformers/utils/handlebars.ts +31 -5
  344. package/src/types/config.ts +123 -25
  345. package/src/utils/menu-shell.ts +241 -0
  346. package/src/utils/stable-stringify.ts +7 -0
  347. package/templates/default/package.json.tpl +1 -1
  348. package/handoff.config.js +0 -90
  349. package/src/app/components/ComponentSearch.tsx +0 -93
@@ -4,15 +4,21 @@ import { ComponentDocumentationOptions, PreviewObject } from '@handoff/types/pre
4
4
  import * as fs from 'fs-extra';
5
5
  import matter from 'gray-matter';
6
6
  import { Types as CoreTypes } from 'handoff-core';
7
- import { groupBy, startCase, uniq } from 'lodash';
8
7
  import path from 'path';
9
8
  import { ParsedUrlQuery } from 'querystring';
9
+ import { KNOWN_PATHS } from '@handoff/utils/menu-shell';
10
+ import { getRegistryNavData, getWorkspaceNavData, type NavData, type NavTokenSet, type SectionLink } from '@handoff/nav';
11
+ import { resolveDocsBackend } from '../../lib/docs-api/backend';
12
+ import { tokenFormatStrings } from '../../lib/docs-api/token-detail';
13
+ // Build-time-baked navigation shell. Imported statically (same as `pages/api/docs/nav.json.ts`) so
14
+ // it is bundled into the route chunk and readable at request time in the Vercel registry lambda,
15
+ // where the markdown it is derived from is not traceable. See src/utils/menu-shell.ts.
16
+ import navShell from '@/generated/nav-shell.json';
10
17
  // Get the parsed url string type
11
18
  export interface IParams extends ParsedUrlQuery {
12
19
  slug: string | string[];
13
20
  }
14
21
 
15
-
16
22
  // Type for the metadata from frontmatter
17
23
  export interface Metadata {
18
24
  title: string;
@@ -21,23 +27,7 @@ export interface Metadata {
21
27
  metaDescription: string;
22
28
  }
23
29
 
24
- // Define what a section link looks like
25
- export interface SectionLink {
26
- title: string;
27
- weight: number;
28
- external?: string | boolean;
29
- path: string;
30
- subSections: {
31
- title: string;
32
- path: string;
33
- image: string;
34
- menu?: {
35
- title: string;
36
- path: string;
37
- image: string;
38
- }[];
39
- }[];
40
- }
30
+ export type { SectionLink } from '@handoff/nav';
41
31
  // Documentation Page Properties
42
32
  export interface DocumentationProps {
43
33
  metadata: Metadata;
@@ -45,6 +35,8 @@ export interface DocumentationProps {
45
35
  options?: ComponentDocumentationOptions;
46
36
  menu: SectionLink[];
47
37
  current: SectionLink;
38
+ navData: NavData;
39
+ currentSectionId: string;
48
40
  config: ClientConfig;
49
41
  }
50
42
 
@@ -76,30 +68,10 @@ export interface FoundationDocumentationProps extends DocumentationWithTokensPro
76
68
  design: CoreTypes.IDocumentationObject['localStyles'];
77
69
  }
78
70
  /**
79
- * List the default paths
71
+ * Default paths that have dedicated route files (excluded from auto-scanned submenus). Single source
72
+ * of truth lives in `@handoff/utils/menu-shell` (shared with the build-time shell builder).
80
73
  */
81
- export const knownPaths = [
82
- 'assets',
83
- 'assets/fonts',
84
- 'assets/icons',
85
- 'assets/logos',
86
- 'foundations',
87
- 'foundations/colors',
88
- 'foundations/icons',
89
- 'foundations/effects',
90
- 'foundations/logos',
91
- 'foundations/logo',
92
- 'foundations/typography',
93
- 'system',
94
- 'system/component',
95
- 'system/tokens',
96
- 'system/tokens/foundations',
97
- 'system/tokens/foundations/colors',
98
- 'system/tokens/foundations/effects',
99
- 'system/tokens/foundations/typography',
100
- 'system/tokens/components',
101
- 'system/pattern',
102
- ];
74
+ export const knownPaths = KNOWN_PATHS;
103
75
 
104
76
  /**
105
77
  * Get the plural name of a component
@@ -146,12 +118,12 @@ const collectMarkdownPaths = (rootDir: string, relativeParts: string[] = []): st
146
118
  * Build catch-all static paths for all markdown pages at any depth.
147
119
  * Excludes paths in knownPaths (those have dedicated route files).
148
120
  */
149
- export const buildCatchAllStaticPaths = () => {
121
+ export const buildCatchAllStaticPaths = (includeWorkspacePages = true) => {
150
122
  const docRoot = path.resolve(process.env.HANDOFF_MODULE_PATH ?? '', 'config/docs');
151
123
  const pageRoot = path.resolve(process.env.HANDOFF_WORKING_PATH ?? '', 'pages');
152
124
 
153
125
  const docPaths = collectMarkdownPaths(docRoot);
154
- const pagePaths = collectMarkdownPaths(pageRoot);
126
+ const pagePaths = includeWorkspacePages ? collectMarkdownPaths(pageRoot) : [];
155
127
 
156
128
  const seen = new Set<string>();
157
129
  const allPaths: string[][] = [];
@@ -165,323 +137,24 @@ export const buildCatchAllStaticPaths = () => {
165
137
  }
166
138
  }
167
139
 
168
- return allPaths
169
- .filter((segments) => knownPaths.indexOf(segments.join('/')) < 0)
170
- .map((segments) => ({ params: { slug: segments } }));
171
- };
172
-
173
- /**
174
- * Recursively build menu entries from .md files in a directory.
175
- * Returns sub-section items with nested menu items for subdirectories.
176
- */
177
- const buildMenuFromDirectory = (dirPath: string, urlPrefix: string): any[] => {
178
- if (!fs.existsSync(dirPath)) return [];
179
- const entries = fs.readdirSync(dirPath);
180
- const items: any[] = [];
181
-
182
- for (const entry of entries) {
183
- const fullPath = path.join(dirPath, entry);
184
- if (fs.lstatSync(fullPath).isDirectory()) {
185
- const nestedItems = buildMenuFromDirectory(fullPath, `${urlPrefix}/${entry}`);
186
- if (nestedItems.length > 0) {
187
- items.push({
188
- title: startCase(entry),
189
- path: `${urlPrefix}/${entry}`,
190
- menu: nestedItems,
191
- });
192
- }
193
- } else if (entry.endsWith('.md') && entry !== 'index.md') {
194
- const slug = entry.replace('.md', '');
195
- const fullSlugPath = `${urlPrefix}/${slug}`.replace(/^\/+/, '');
196
- if (knownPaths.indexOf(fullSlugPath) >= 0) continue;
197
-
198
- const contents = fs.readFileSync(fullPath, 'utf-8');
199
- const { data: metadata } = matter(contents);
200
- if (metadata.enabled === false) continue;
201
-
202
- items.push({
203
- title: metadata.menuTitle ?? metadata.title ?? startCase(slug),
204
- path: `/${fullSlugPath}`,
205
- weight: metadata.weight ?? 0,
206
- });
207
- }
208
- }
209
-
210
- return items.sort((a, b) => (a.weight ?? 0) - (b.weight ?? 0));
140
+ return allPaths.filter((segments) => knownPaths.indexOf(segments.join('/')) < 0).map((segments) => ({ params: { slug: segments } }));
211
141
  };
212
142
 
213
143
  /**
214
- * Build the static menu for rendering pages
215
- * @returns SectionLink[]
144
+ * Fetch patterns from the mode-aware docs read API.
145
+ *
146
+ * Resolves through {@link resolveDocsBackend}, so the active `runtime.mode` decides the source:
147
+ * generated workspace artifacts in workspace mode, the registry database in registry mode.
216
148
  */
217
- export const staticBuildMenu = () => {
218
- const docRoot = path.join(process.env.HANDOFF_MODULE_PATH ?? '', 'config/docs');
219
- if (!fs.existsSync(docRoot)) {
220
- return [];
221
- }
222
- const files = fs.readdirSync(docRoot);
223
- let list = files;
224
- const workingPages = path.resolve(process.env.HANDOFF_WORKING_PATH ?? '', 'pages');
225
- let pages: string[] = [];
226
- if (fs.existsSync(workingPages)) {
227
- pages = fs.readdirSync(workingPages);
228
- list = list.concat(pages);
229
- }
230
- const sections: SectionLink[] = [];
231
- const custom = uniq(list)
232
- .map((fileName: string) => {
233
- let search = '';
234
- if (pages.includes(fileName)) {
235
- search = path.resolve(workingPages, fileName);
236
- } else {
237
- search = path.resolve(docRoot, fileName);
238
- }
239
- if (
240
- !fs.lstatSync(search).isDirectory() &&
241
- search !== path.resolve(docRoot, 'index.md') &&
242
- search !== path.resolve(workingPages, 'index.md') &&
243
- fileName.endsWith('md')
244
- ) {
245
- const contents = fs.readFileSync(search, 'utf-8');
246
- const { data: metadata } = matter(contents);
247
- if (metadata.enabled === false) {
248
- return undefined;
249
- }
250
-
251
- const filepath = `/${fileName.replace('.md', '')}`;
252
- let subSections = [];
253
-
254
- if (metadata.menu) {
255
- subSections = Object.keys(metadata.menu)
256
- .map((key) => {
257
- const sub = metadata.menu[key];
258
- if (sub.components) {
259
- return {
260
- title: sub.title,
261
- menu: staticBuildComponentMenu(sub.components),
262
- };
263
- }
264
- if (sub.tokens) {
265
- return {
266
- title: 'Tokens',
267
- menu: staticBuildTokensMenu(),
268
- };
269
- }
270
- if (sub.patterns) {
271
- const patternMenu = staticBuildPatternMenu();
272
- if (patternMenu.length > 0) {
273
- return {
274
- title: sub.title || 'Patterns',
275
- menu: patternMenu,
276
- };
277
- }
278
- return undefined;
279
- }
280
- if (sub.enabled !== false) {
281
- return sub;
282
- }
283
- })
284
- .filter(filterOutUndefined);
285
- } else {
286
- // Only auto-scan directories when no frontmatter menu is defined
287
- const dirName = fileName.replace('.md', '');
288
- const docDir = path.resolve(docRoot, dirName);
289
- const pagesDir = path.resolve(workingPages, dirName);
290
- const nestedFromDocs = buildMenuFromDirectory(docDir, `/${dirName}`);
291
- const nestedFromPages = buildMenuFromDirectory(pagesDir, `/${dirName}`);
292
-
293
- const seenPaths = new Set<string>();
294
- for (const item of [...nestedFromPages, ...nestedFromDocs]) {
295
- if (item.path && !seenPaths.has(item.path)) {
296
- seenPaths.add(item.path);
297
- subSections.push(item);
298
- }
299
- }
300
- }
301
-
302
- let external: string | boolean = false;
303
- if (
304
- typeof metadata.external === 'string' &&
305
- (metadata.external.startsWith('http://') || metadata.external.startsWith('https://') || metadata.external.startsWith('/'))
306
- ) {
307
- external = metadata.external;
308
- }
309
-
310
- return {
311
- title: metadata.menuTitle ?? metadata.title,
312
- external,
313
- weight: metadata.weight ?? 0,
314
- path: `${filepath}`,
315
- subSections,
316
- };
317
- }
318
- })
319
- .filter(filterOutUndefined);
320
- return sections.concat(custom).sort((a: SectionLink, b: SectionLink) => a.weight - b.weight);
321
- };
322
-
323
- const buildBasePath = () => {
324
- if (!process.env.HANDOFF_APP_BASE_PATH) {
325
- return '';
326
- }
327
- return (process.env.HANDOFF_APP_BASE_PATH ?? '').replace(/^\/+|\/+$/g, '') + '/';
328
- };
329
-
330
- const staticBuildComponentMenu = (type?: boolean | string) => {
331
- const basePath = buildBasePath();
332
- let menu = [];
333
- let components = fetchComponents({ includeTokens: false });
334
- if (typeof type === 'string' && type !== '') {
335
- components = components.filter((component) => component.type == type);
336
- }
337
- // Build the submenu of exportables (components)
338
- const groupedComponents = groupBy(components, (e) => e.group ?? '');
339
- Object.keys(groupedComponents).forEach((group) => {
340
- const menuGroup = { title: group || 'Uncategorized', menu: [] };
341
- groupedComponents[group].forEach((component) => {
342
- const docs = fetchDocPageMetadataAndContent('docs/system/', component.id);
343
- let title = startCase(component.id);
344
- if (docs.metadata.title) {
345
- title = docs.metadata.title;
346
- }
347
- if (component.name) {
348
- title = component.name;
349
- }
350
- menuGroup.menu.push({ path: `${basePath}system/component/${component.id}`, title });
351
- });
352
- // sort the menu group by name alphabetical
353
- menuGroup.menu = menuGroup.menu.sort((a, b) => a.title.localeCompare(b.title));
354
- menu.push(menuGroup);
355
- });
356
- // sort the menu by name alphabetical
357
- menu = menu.sort((a, b) => a.title.localeCompare(b.title));
358
- return menu;
359
- };
360
-
361
- const staticBuildTokensMenu = () => {
362
- const basePath = buildBasePath();
363
-
364
- const menu = [
365
- {
366
- title: `Foundations`,
367
- path: `${basePath}system/tokens/foundations`,
368
- menu: [
369
- {
370
- title: `Colors`,
371
- path: `${basePath}system/tokens/foundations/colors`,
372
- },
373
- {
374
- title: `Effects`,
375
- path: `${basePath}system/tokens/foundations/effects`,
376
- },
377
- {
378
- title: `Typography`,
379
- path: `${basePath}system/tokens/foundations/typography`,
380
- },
381
- ],
382
- },
383
- ];
384
-
385
- const componentMenuItems = [];
386
- const components = fetchComponents({ includeApi: false });
387
- // Build the submenu of exportables (components)
388
- const groupedComponents = groupBy(components, (e) => e.group ?? '');
389
- Object.keys(groupedComponents).forEach((group) => {
390
- groupedComponents[group].forEach((component) => {
391
- const docs = fetchDocPageMetadataAndContent('docs/system/', component.id);
392
- let title = startCase(component.id);
393
- if (docs.metadata.title) {
394
- title = docs.metadata.title;
395
- }
396
- if (component.name) {
397
- title = component.name;
398
- }
399
- componentMenuItems.push({ path: `${basePath}system/tokens/components/${component.id}`, title });
400
- });
401
- });
402
-
403
- if (componentMenuItems.length > 0) {
404
- menu.push({
405
- title: `Components`,
406
- path: `${basePath}system/tokens/components`,
407
- menu: componentMenuItems,
408
- });
409
- }
410
-
411
- return menu;
412
- };
413
-
414
- const staticBuildTokenMenu = () => {
415
- const basePath = buildBasePath();
416
-
417
- let subSections = {
418
- title: 'Tokens',
419
- path: `${basePath}system/tokens`,
420
- menu: [],
421
- };
422
- const tokens = getTokens();
423
-
424
- return subSections;
425
- };
426
-
427
- /**
428
- * Fetch patterns from the patterns.json API file
429
- */
430
- export const fetchPatterns = (): { id: string; title: string; description: string; group: string }[] => {
431
- const patternsFilePath = path.resolve(
432
- process.env.HANDOFF_MODULE_PATH ?? '',
433
- '.handoff',
434
- `${process.env.HANDOFF_PROJECT_ID}`,
435
- 'public',
436
- 'api',
437
- 'patterns.json'
438
- );
439
-
440
- if (!fs.existsSync(patternsFilePath)) {
441
- return [];
442
- }
443
-
149
+ export const fetchPatterns = async (): Promise<PatternListObject[]> => {
444
150
  try {
445
- const patternList = JSON.parse(
446
- fs.readFileSync(patternsFilePath, 'utf-8')
447
- ) as PatternListObject[];
448
-
449
- return patternList.map((p) => ({
450
- id: p.id,
451
- title: p.title || '',
452
- description: p.description || '',
453
- group: p.group || '',
454
- }));
151
+ const backend = await resolveDocsBackend();
152
+ return (await backend.listPatterns()) ?? [];
455
153
  } catch {
456
154
  return [];
457
155
  }
458
156
  };
459
157
 
460
- const staticBuildPatternMenu = () => {
461
- const basePath = buildBasePath();
462
- const patterns = fetchPatterns();
463
-
464
- if (patterns.length === 0) return [];
465
-
466
- const grouped = groupBy(patterns, (p) => p.group ?? '');
467
- let menu: { title: string; menu: { path: string; title: string }[] }[] = [];
468
-
469
- Object.keys(grouped).forEach((group) => {
470
- const menuGroup = { title: group || 'Uncategorized', menu: [] as { path: string; title: string }[] };
471
- grouped[group].forEach((pattern) => {
472
- menuGroup.menu.push({
473
- path: `${basePath}system/pattern/${pattern.id}`,
474
- title: pattern.title || startCase(pattern.id),
475
- });
476
- });
477
- menuGroup.menu.sort((a, b) => a.title.localeCompare(b.title));
478
- menu.push(menuGroup);
479
- });
480
-
481
- menu.sort((a, b) => a.title.localeCompare(b.title));
482
- return menu;
483
- };
484
-
485
158
  /**
486
159
  * Filter the menus by the current path
487
160
  * @param menu
@@ -491,14 +164,54 @@ const staticBuildPatternMenu = () => {
491
164
  export const getCurrentSection = (menu: SectionLink[], path: string): SectionLink | null =>
492
165
  menu.filter((section) => section.path === path)[0];
493
166
 
167
+ /**
168
+ * Resolve the render-ready first-paint navigation for every page. Registry initial delivery is
169
+ * deliberately shell-only; workspace delivery fills the same canonical tree from local records.
170
+ * The legacy `menu`/`current` aliases remain page-layout props while all navigation consumers read
171
+ * `navData` through `NavProvider`.
172
+ */
173
+ export const getNavProps = async (
174
+ currentSectionId: string
175
+ ): Promise<{ navData: NavData; currentSectionId: string; menu: SectionLink[]; current: SectionLink | null }> => {
176
+ let navData: NavData;
177
+ if (isRegistryRuntime()) {
178
+ navData = await getRegistryNavData({
179
+ shell: navShell as unknown as SectionLink[],
180
+ load: 'initial',
181
+ basePath: process.env.HANDOFF_APP_BASE_PATH,
182
+ fetchRecords: async () => ({ components: [], patterns: [], pages: [], tokenSets: [] }),
183
+ });
184
+ } else {
185
+ const backend = await resolveDocsBackend();
186
+ navData = await getWorkspaceNavData({
187
+ docRoot: path.resolve(process.env.HANDOFF_MODULE_PATH ?? '', 'config/docs'),
188
+ workingPagesDir: path.resolve(process.env.HANDOFF_WORKING_PATH ?? '', 'pages'),
189
+ basePath: process.env.HANDOFF_APP_BASE_PATH,
190
+ load: 'initial',
191
+ loaders: {
192
+ components: () => backend.listComponents(),
193
+ patterns: () => backend.listPatterns(),
194
+ pages: () => backend.listPages(),
195
+ tokenSets: async () => (await backend.listTokenSets()) as NavTokenSet[],
196
+ },
197
+ });
198
+ }
199
+ return {
200
+ navData,
201
+ currentSectionId,
202
+ menu: navData.shell,
203
+ current: getCurrentSection(navData.shell, currentSectionId) ?? null,
204
+ };
205
+ };
206
+
494
207
  /**
495
208
  * Build a static object for rending markdown pages
496
209
  * @param path
497
210
  * @param slug
498
211
  * @returns
499
212
  */
500
- export const fetchDocPageMarkdown = (path: string, slug: string | undefined, id: string, runtimeConfig?: RuntimeConfig) => {
501
- const menu = staticBuildMenu();
213
+ export const fetchDocPageMarkdown = async (path: string, slug: string | undefined, id: string, runtimeConfig?: RuntimeConfig) => {
214
+ const nav = await getNavProps(id);
502
215
  const { metadata, content, options } = fetchDocPageMetadataAndContent(path, slug, runtimeConfig);
503
216
  // Return props
504
217
  return {
@@ -506,8 +219,10 @@ export const fetchDocPageMarkdown = (path: string, slug: string | undefined, id:
506
219
  metadata,
507
220
  content,
508
221
  options,
509
- menu,
510
- current: getCurrentSection(menu, `${id}`) ?? null,
222
+ menu: nav.menu,
223
+ current: nav.current,
224
+ navData: nav.navData,
225
+ currentSectionId: nav.currentSectionId,
511
226
  },
512
227
  };
513
228
  };
@@ -519,10 +234,10 @@ export const fetchDocPageMarkdown = (path: string, slug: string | undefined, id:
519
234
  * @param id
520
235
  * @returns
521
236
  */
522
- export const fetchCompDocPageMarkdown = (path: string, slug: string | undefined, id: string, runtimeConfig?: RuntimeConfig) => {
237
+ export const fetchCompDocPageMarkdown = async (path: string, slug: string | undefined, id: string, runtimeConfig?: RuntimeConfig) => {
523
238
  return {
524
239
  props: {
525
- ...fetchDocPageMarkdown(path, slug, id, runtimeConfig).props,
240
+ ...(await fetchDocPageMarkdown(path, slug, id, runtimeConfig)).props,
526
241
  scss: slug ? fetchTokensString(slug, 'scss') : '',
527
242
  css: slug ? fetchTokensString(slug, 'css') : '',
528
243
  styleDictionary: slug ? fetchTokensString(slug, 'styleDictionary') : '',
@@ -531,19 +246,68 @@ export const fetchCompDocPageMarkdown = (path: string, slug: string | undefined,
531
246
  };
532
247
  };
533
248
 
534
- type FetchComponentsOptions = {
249
+ /**
250
+ * Fetch components from the mode-aware docs read API.
251
+ *
252
+ * Resolves through {@link resolveDocsBackend}, so the active `runtime.mode` decides the source:
253
+ * generated workspace artifacts in workspace mode, the registry database in registry mode. This is
254
+ * the entity list the navigation, detail, and preview views all share, so they agree on the same
255
+ * set for the active mode.
256
+ */
257
+ export const fetchComponents = async (): Promise<ComponentListObject[]> => {
258
+ try {
259
+ const backend = await resolveDocsBackend();
260
+ return (await backend.listComponents()) ?? [];
261
+ } catch {
262
+ return [];
263
+ }
264
+ };
265
+
266
+ /** A component token set resolved for a docs page: the token object + the four download strings. */
267
+ export interface ComponentTokensResult {
268
+ component: CoreTypes.IFileComponentObject;
269
+ css: string;
270
+ scss: string;
271
+ styleDictionary: string;
272
+ types: string;
273
+ }
274
+
275
+ /**
276
+ * Fetch a component's token set through the mode-aware docs read API. Resolves through
277
+ * {@link resolveDocsBackend}, so registry mode reads the DB-backed `component/<id>` set (mutable,
278
+ * publishable after deploy) while workspace mode reads the generated files. Returns `null` when the
279
+ * set does not exist.
280
+ */
281
+ export const fetchComponentTokens = async (componentId: string): Promise<ComponentTokensResult | null> => {
282
+ try {
283
+ const backend = await resolveDocsBackend();
284
+ const detail = await backend.getTokenSetDetail(`component/${componentId}`);
285
+ if (!detail) {
286
+ return null;
287
+ }
288
+ return { component: detail.record as CoreTypes.IFileComponentObject, ...tokenFormatStrings(detail.artifacts) };
289
+ } catch {
290
+ return null;
291
+ }
292
+ };
293
+
294
+ type FetchLocalComponentsOptions = {
535
295
  includeTokens?: boolean;
536
296
  includeApi?: boolean;
537
297
  };
538
298
 
539
299
  /**
540
- * Fetch exportables id's from the JSON files in the exportables directory
300
+ * Fetch components from the local, build-time workspace artifacts (`tokens.json` and the generated
301
+ * `components.json`). This is the legacy, mode-independent local read used **only** by the tokens
302
+ * views, which are not yet covered by the registry read path (out of scope); it intentionally stays
303
+ * local so token behavior is preserved exactly until tokens are migrated.
304
+ *
541
305
  * @param options - Configuration object to specify which component sources to include
542
306
  * @param options.includeTokens - Include components from tokens.json (default: true)
543
307
  * @param options.includeApi - Include components from components.json API (default: true)
544
- * @returns {string[]} Array of component objects with id, type, group, name, and description
308
+ * @returns Array of component objects with id, type, group, name, and description
545
309
  */
546
- export const fetchComponents = (options?: FetchComponentsOptions) => {
310
+ export const fetchLocalComponents = (options?: FetchLocalComponentsOptions) => {
547
311
  const includeTokens = options?.includeTokens ?? true;
548
312
  const includeApi = options?.includeApi ?? true;
549
313
 
@@ -619,36 +383,61 @@ let cachedClientConfig: ClientConfigCache | null = null;
619
383
 
620
384
  const getDefaultClientConfig = (): ClientConfigCache => {
621
385
  return {
622
- config: {} as ClientConfig,
386
+ config: { runtime: { mode: 'workspace', connected: false } } as ClientConfig,
623
387
  };
624
388
  };
625
389
 
390
+ /**
391
+ * Resolve the persisted client config path. Workspace dev/static reads it from the build-machine
392
+ * `.handoff/<projectId>` staging dir; the packaged registry app reads it from the entry
393
+ * dir the build copies it into (the standalone server's cwd), since the build-machine path does not
394
+ * exist on the deploy host.
395
+ */
396
+ const resolveClientConfigPath = (): string | null => {
397
+ const candidates = [
398
+ path.resolve(process.env.HANDOFF_MODULE_PATH ?? '', '.handoff', process.env.HANDOFF_PROJECT_ID ?? '', 'client.config.json'),
399
+ path.resolve(process.cwd(), 'client.config.json'),
400
+ ];
401
+ return candidates.find((candidate) => fs.existsSync(candidate)) ?? null;
402
+ };
403
+
404
+ /**
405
+ * Apply the build-time-baked runtime mode over a loaded client config. Mode is
406
+ * config-only and baked into the bundle, so the packaged registry app reports `registry` even when
407
+ * its client.config.json cannot be resolved on the deploy host.
408
+ */
409
+ const applyRuntimeModeOverride = (cache: ClientConfigCache): ClientConfigCache => {
410
+ const bakedMode = process.env.HANDOFF_RUNTIME_MODE?.trim();
411
+ if (bakedMode === 'workspace' || bakedMode === 'registry') {
412
+ // A baked registry mode is never a connected workspace, so clear `connected` to keep the
413
+ // projected state internally consistent regardless of the source project's config.
414
+ const connected = bakedMode === 'registry' ? false : (cache.config.runtime?.connected ?? false);
415
+ return { config: { ...cache.config, runtime: { ...cache.config.runtime, mode: bakedMode, connected } } };
416
+ }
417
+ return cache;
418
+ };
419
+
626
420
  const loadClientConfig = (): ClientConfigCache => {
627
421
  if (cachedClientConfig) {
628
422
  return cachedClientConfig;
629
423
  }
630
424
 
631
- const modulePath = process.env.HANDOFF_MODULE_PATH ?? '';
632
- const projectId = process.env.HANDOFF_PROJECT_ID ?? '';
633
- const clientConfigPath = path.resolve(modulePath, '.handoff', projectId, 'client.config.json');
634
-
635
- if (!fs.existsSync(clientConfigPath)) {
636
- // Return empty default instead of throwing to support running without fetch
637
- return getDefaultClientConfig();
425
+ const clientConfigPath = resolveClientConfigPath();
426
+ if (!clientConfigPath) {
427
+ // No persisted config: still honor a baked runtime mode so the packaged app reports correctly.
428
+ return applyRuntimeModeOverride(getDefaultClientConfig());
638
429
  }
639
430
 
640
431
  try {
641
432
  const cacheContent = fs.readFileSync(clientConfigPath, 'utf-8');
642
- cachedClientConfig = JSON.parse(cacheContent) as ClientConfigCache;
433
+ cachedClientConfig = applyRuntimeModeOverride(JSON.parse(cacheContent) as ClientConfigCache);
643
434
  return cachedClientConfig;
644
435
  } catch (e) {
645
436
  // Return empty default on error instead of throwing
646
- return getDefaultClientConfig();
437
+ return applyRuntimeModeOverride(getDefaultClientConfig());
647
438
  }
648
439
  };
649
440
 
650
-
651
-
652
441
  /**
653
442
  * Fetch Component Doc Page Markdown
654
443
  * @param path
@@ -656,14 +445,17 @@ const loadClientConfig = (): ClientConfigCache => {
656
445
  * @param id
657
446
  * @returns
658
447
  */
659
- export const fetchFoundationDocPageMarkdown = (path: string, slug: string | undefined, id: string) => {
448
+ export const fetchFoundationDocPageMarkdown = async (path: string, slug: string | undefined, id: string) => {
449
+ // In a registry build the download strings must not be frozen from the build machine's generated
450
+ // files; they hydrate at request time from the DB-backed docs read API (`useFoundationTokens`).
451
+ const readTokens = !isRegistryRuntime() && slug;
660
452
  return {
661
453
  props: {
662
- ...fetchDocPageMarkdown(path, slug, id).props,
663
- scss: slug ? fetchTokensString(pluralizeComponent(slug), 'scss') : '',
664
- css: slug ? fetchTokensString(pluralizeComponent(slug), 'css') : '',
665
- styleDictionary: slug ? fetchTokensString(pluralizeComponent(slug), 'styleDictionary') : '',
666
- types: slug ? fetchTokensString(pluralizeComponent(slug), 'types') : '',
454
+ ...(await fetchDocPageMarkdown(path, slug, id)).props,
455
+ scss: readTokens ? fetchTokensString(pluralizeComponent(slug), 'scss') : '',
456
+ css: readTokens ? fetchTokensString(pluralizeComponent(slug), 'css') : '',
457
+ styleDictionary: readTokens ? fetchTokensString(pluralizeComponent(slug), 'styleDictionary') : '',
458
+ types: readTokens ? fetchTokensString(pluralizeComponent(slug), 'types') : '',
667
459
  },
668
460
  };
669
461
  };
@@ -673,6 +465,35 @@ export const getClientRuntimeConfig = (): ClientConfig => {
673
465
  return clientConfig.config;
674
466
  };
675
467
 
468
+ /**
469
+ * Whether the docs app is running in registry mode. Drives the request-time behaviors (on-demand
470
+ * detail rendering and the live nav fetch) that must not affect the workspace dev or static-export
471
+ * builds. Resolves from the same baked/persisted runtime mode the docs read API uses.
472
+ */
473
+ export const isRegistryRuntime = (): boolean => getClientRuntimeConfig().runtime?.mode === 'registry';
474
+
475
+ /**
476
+ * Build-time foundation `localStyles` for a foundation page's `getStaticProps`. In workspace/static
477
+ * this is the local `tokens.json` snapshot (correct: static exports are immutable). In a **registry**
478
+ * build it is empty — foundation token data must not be frozen into build-time page props from the
479
+ * build machine's `tokens.json`; the page hydrates it at request time from the DB-backed docs read
480
+ * API (`useFoundationTokens`). Without this gate a registry build bakes the builder's local tokens,
481
+ * so foundations would appear before anything is published.
482
+ */
483
+ export const buildTimeFoundationDesign = (): CoreTypes.IDocumentationObject['localStyles'] =>
484
+ isRegistryRuntime()
485
+ ? ({ color: [], typography: [], effect: [] } as CoreTypes.IDocumentationObject['localStyles'])
486
+ : getTokens().localStyles;
487
+
488
+ /**
489
+ * Build-time asset collections for an asset page's `getStaticProps`. Workspace/static bakes the local
490
+ * `tokens.json` assets (correct: static exports are immutable snapshots). A **registry** build returns
491
+ * empty: asset bodies must not be frozen into build-time page props from the build machine; the page
492
+ * hydrates them at request time from the DB-backed docs read API (`useCollectionAssets`). Without this
493
+ * gate a registry build bakes the builder's local assets, so they would appear before any publish.
494
+ */
495
+ export const buildTimeAssets = (): CoreTypes.IDocumentationObject['assets'] => (isRegistryRuntime() ? {} : getTokens().assets);
496
+
676
497
  export const getTokens = (): CoreTypes.IDocumentationObject => {
677
498
  const exportedFilePath = process.env.HANDOFF_EXPORT_PATH
678
499
  ? path.resolve(process.env.HANDOFF_EXPORT_PATH, 'tokens.json')
@@ -731,7 +552,9 @@ export const fetchDocPageMetadataAndContent = (localPath: string, slug: string |
731
552
  const contentModuleFilePath = path.resolve(handoffModulePath, 'config', `${localPath}${slug}.md`);
732
553
  const contentWorkingFilePath = path.resolve(handoffWorkingPath, `${pagePath}${slug}.md`);
733
554
 
734
- if (fs.existsSync(contentWorkingFilePath)) {
555
+ // Registry serves package `config/docs` only; workspace overrides are invisible to it (custom
556
+ // content comes from the DB), so the working-path read is skipped in registry mode.
557
+ if (!isRegistryRuntime() && fs.existsSync(contentWorkingFilePath)) {
735
558
  currentContents = fs.readFileSync(contentWorkingFilePath, 'utf-8');
736
559
  } else if (!fs.existsSync(contentModuleFilePath)) {
737
560
  return { metadata: {}, content: currentContents, options: {} };