handoff-app 2.0.0-8 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (322) hide show
  1. package/{Changelog.md → CHANGELOG.md} +85 -1
  2. package/{License.md → LICENSE.md} +1 -1
  3. package/README.md +816 -393
  4. package/RECIPES.md +975 -0
  5. package/UPGRADE.md +57 -9
  6. package/components.json +20 -20
  7. package/config/docs/foundations/colors/guidelines.md +0 -1
  8. package/config/docs/foundations/colors.md +0 -1
  9. package/config/docs/foundations/effects/guidelines.md +0 -1
  10. package/config/docs/foundations/effects/resources.md +0 -1
  11. package/config/docs/foundations/effects.md +0 -1
  12. package/config/docs/foundations/fonts.md +0 -1
  13. package/config/docs/foundations/grid.md +0 -1
  14. package/config/docs/foundations/icon.md +0 -1
  15. package/config/docs/foundations/icons/guidelines.md +0 -1
  16. package/config/docs/foundations/icons.md +0 -1
  17. package/config/docs/foundations/images/guidelines.md +0 -1
  18. package/config/docs/foundations/images/resources.md +0 -1
  19. package/config/docs/foundations/images.md +0 -1
  20. package/config/docs/foundations/logo/resources.md +0 -1
  21. package/config/docs/foundations/logo.md +0 -1
  22. package/config/docs/foundations/typography/guidelines.md +0 -1
  23. package/config/docs/foundations/typography/resources.md +0 -1
  24. package/config/docs/foundations/typography.md +0 -1
  25. package/config/docs/foundations.md +0 -1
  26. package/config/docs/guidelines.md +0 -1
  27. package/config/docs/index.md +0 -1
  28. package/config/docs/system/tokens/foundations/colors.md +0 -4
  29. package/config/docs/system/tokens/foundations/effects.md +0 -4
  30. package/config/docs/system/tokens/foundations/typography.md +0 -2
  31. package/config/docs/system.md +0 -2
  32. package/config/templates/page.md +2 -1
  33. package/config/templates/page.mdx +2 -1
  34. package/dist/ai/connections.d.ts +66 -0
  35. package/dist/ai/connections.js +149 -0
  36. package/dist/ai/define.d.ts +10 -0
  37. package/dist/ai/define.js +13 -0
  38. package/dist/ai/types.d.ts +26 -0
  39. package/dist/ai/types.js +10 -0
  40. package/dist/app-builder/build.d.ts +14 -7
  41. package/dist/app-builder/build.js +195 -66
  42. package/dist/app-builder/client-config.js +3 -2
  43. package/dist/app-builder/paths.d.ts +0 -12
  44. package/dist/app-builder/paths.js +3 -20
  45. package/dist/app-builder/static-export.js +1 -1
  46. package/dist/app-builder/vercel-output.d.ts +1 -1
  47. package/dist/app-builder/vercel-output.js +5 -4
  48. package/dist/cache/build-cache.d.ts +27 -4
  49. package/dist/cache/build-cache.js +114 -47
  50. package/dist/cache/build-inputs.d.ts +13 -0
  51. package/dist/cache/build-inputs.js +19 -0
  52. package/dist/cache/index.d.ts +2 -1
  53. package/dist/cache/index.js +5 -1
  54. package/dist/catalog/normalize.js +3 -0
  55. package/dist/catalog/previews.js +5 -4
  56. package/dist/catalog/types.d.ts +8 -0
  57. package/dist/cli/create.js +1 -2
  58. package/dist/commands/build/index.d.ts +1 -1
  59. package/dist/commands/build/index.js +1 -1
  60. package/dist/commands/utils.d.ts +3 -3
  61. package/dist/config/defaults.js +7 -4
  62. package/dist/config/from-env.d.ts +4 -0
  63. package/dist/config/from-env.js +38 -1
  64. package/dist/config/helpers.js +2 -2
  65. package/dist/config/index.d.ts +0 -1
  66. package/dist/config/index.js +1 -4
  67. package/dist/config/loader.js +28 -2
  68. package/dist/config/resolve-env.d.ts +2 -1
  69. package/dist/config/resolve-env.js +28 -12
  70. package/dist/config/runtime.js +9 -11
  71. package/dist/handlebars.d.ts +10 -2
  72. package/dist/index.d.ts +12 -7
  73. package/dist/index.js +18 -22
  74. package/dist/nav/index.d.ts +2 -2
  75. package/dist/nav/registry.d.ts +3 -3
  76. package/dist/nav/registry.js +1 -1
  77. package/dist/nav/workspace.d.ts +3 -3
  78. package/dist/nav/workspace.js +1 -1
  79. package/dist/pipeline/figma.d.ts +13 -2
  80. package/dist/pipeline/figma.js +86 -46
  81. package/dist/pipeline/styles.js +1 -1
  82. package/dist/pipeline/validation.d.ts +1 -1
  83. package/dist/pipeline/validation.js +3 -3
  84. package/dist/providers.d.ts +10 -0
  85. package/dist/providers.js +13 -0
  86. package/dist/registry/asset-storage/define.d.ts +5 -5
  87. package/dist/registry/asset-storage/define.js +4 -4
  88. package/dist/registry/asset-storage/resolve.d.ts +9 -19
  89. package/dist/registry/asset-storage/resolve.js +33 -21
  90. package/dist/registry/asset-storage/types.d.ts +37 -17
  91. package/dist/registry/asset-storage/types.js +5 -5
  92. package/dist/registry/auth/ai-keys.d.ts +32 -0
  93. package/dist/registry/auth/ai-keys.js +69 -0
  94. package/dist/registry/auth/crypto.d.ts +17 -0
  95. package/dist/registry/auth/crypto.js +61 -1
  96. package/dist/registry/auth/index.d.ts +2 -1
  97. package/dist/registry/auth/index.js +11 -2
  98. package/dist/registry/checkout/index.d.ts +2 -1
  99. package/dist/registry/checkout/index.js +57 -127
  100. package/dist/registry/client.js +53 -17
  101. package/dist/registry/db/driver.js +3 -3
  102. package/dist/registry/db/schema.d.ts +261 -3
  103. package/dist/registry/db/schema.js +29 -4
  104. package/dist/registry/email/define.d.ts +10 -0
  105. package/dist/registry/email/define.js +13 -0
  106. package/dist/registry/email/providers/smtp.d.ts +10 -0
  107. package/dist/registry/email/providers/smtp.js +73 -0
  108. package/dist/registry/email/resolve.d.ts +28 -0
  109. package/dist/registry/email/resolve.js +74 -0
  110. package/dist/registry/email/types.d.ts +24 -0
  111. package/dist/registry/email/types.js +6 -0
  112. package/dist/registry/path.d.ts +2 -0
  113. package/dist/registry/path.js +11 -1
  114. package/dist/registry/publish/package.js +8 -0
  115. package/dist/registry/transfer.d.ts +4 -0
  116. package/dist/store/registry.d.ts +3 -3
  117. package/dist/store/registry.js +4 -4
  118. package/dist/transformers/plugins/ssr-render.js +40 -15
  119. package/dist/transformers/preview/component/builder.d.ts +1 -9
  120. package/dist/transformers/preview/component/builder.js +17 -17
  121. package/dist/transformers/preview/component/css.d.ts +3 -1
  122. package/dist/transformers/preview/component/css.js +92 -51
  123. package/dist/transformers/preview/component/html.d.ts +3 -1
  124. package/dist/transformers/preview/component/html.js +6 -2
  125. package/dist/transformers/preview/component/javascript.d.ts +2 -1
  126. package/dist/transformers/preview/component/javascript.js +10 -4
  127. package/dist/transformers/preview/pattern/html.js +3 -1
  128. package/dist/transformers/preview/types.d.ts +13 -0
  129. package/dist/transformers/utils/build.d.ts +24 -0
  130. package/dist/transformers/utils/build.js +100 -1
  131. package/dist/transformers/utils/module.js +2 -1
  132. package/dist/types/config.d.ts +135 -18
  133. package/dist/utils/path.d.ts +1 -5
  134. package/dist/utils/path.js +6 -23
  135. package/docs/api.md +6 -4
  136. package/docs/cli.md +275 -54
  137. package/drizzle/0007_add_ai_keys.sql +11 -0
  138. package/drizzle/0008_add_source_dir.sql +2 -0
  139. package/drizzle/meta/0007_snapshot.json +2013 -0
  140. package/drizzle/meta/0008_snapshot.json +2025 -0
  141. package/drizzle/meta/_journal.json +14 -0
  142. package/package.json +19 -8
  143. package/src/ai/README.md +42 -0
  144. package/src/ai/connections.ts +189 -0
  145. package/src/ai/define.ts +15 -0
  146. package/src/ai/types.ts +30 -0
  147. package/src/app/components/AiAssistant/AiAssistantPanel.tsx +144 -0
  148. package/src/app/components/AiAssistant/AiAssistantProvider.tsx +215 -0
  149. package/src/app/components/AiAssistant/AiConversation.tsx +737 -0
  150. package/src/app/components/AiAssistant/AiToolResult.tsx +99 -0
  151. package/src/app/components/AiAssistant/dockState.ts +86 -0
  152. package/src/app/components/AiAssistant/toolLabel.ts +24 -0
  153. package/src/app/components/AiAssistant/useAiChat.ts +147 -0
  154. package/src/app/components/AiAssistant/useAiConnections.ts +86 -0
  155. package/src/app/components/AiAssistant/usePageContext.ts +63 -0
  156. package/src/app/components/Auth/AccountLayout.tsx +58 -19
  157. package/src/app/components/Auth/AuthShell.tsx +27 -33
  158. package/src/app/components/Auth/RegistryInstallGate.tsx +4 -8
  159. package/src/app/components/Auth/installStatus.ts +29 -0
  160. package/src/app/components/Component/PageSliceResolver.tsx +1 -0
  161. package/src/app/components/Component/Preview.tsx +383 -189
  162. package/src/app/components/Component/slices/ComponentDisplaySlice.tsx +4 -0
  163. package/src/app/components/Foundations/ColorGrid.tsx +13 -14
  164. package/src/app/components/Layout/Header.tsx +21 -15
  165. package/src/app/components/Layout/Main.tsx +2 -2
  166. package/src/app/components/Markdown/MarkdownComponents.tsx +30 -32
  167. package/src/app/components/McpIntegration/McpConfigDialog.tsx +12 -51
  168. package/src/app/components/Navigation/MobileNav.tsx +2 -2
  169. package/src/app/components/Navigation/SideNav.tsx +1 -15
  170. package/src/app/components/context/NavProvider.tsx +7 -3
  171. package/src/app/components/ui/attachment.tsx +50 -0
  172. package/src/app/components/ui/bubble.tsx +57 -0
  173. package/src/app/components/ui/button.tsx +3 -1
  174. package/src/app/components/ui/confirm-dialog.tsx +87 -0
  175. package/src/app/components/ui/dialog.tsx +7 -2
  176. package/src/app/components/ui/marker.tsx +56 -0
  177. package/src/app/components/ui/sidebar.tsx +3 -1
  178. package/src/app/components/util/index.ts +3 -12
  179. package/src/app/css/index.css +64 -0
  180. package/src/app/lib/ai/auth.ts +33 -0
  181. package/src/app/lib/ai/context.ts +99 -0
  182. package/src/app/lib/ai/model.ts +79 -0
  183. package/src/app/lib/ai/prompt.ts +36 -0
  184. package/src/app/lib/ai/resolve.ts +97 -0
  185. package/src/app/lib/ai/tools.ts +120 -0
  186. package/src/app/lib/api/methods.ts +13 -0
  187. package/src/app/lib/asset-storage.ts +35 -36
  188. package/src/app/lib/auth/api.ts +4 -12
  189. package/src/app/lib/auth/email.ts +96 -23
  190. package/src/app/lib/docs-api/page-rendering.ts +1 -1
  191. package/src/app/lib/docs-api/registry.ts +2 -3
  192. package/src/app/lib/docs-api/runtime-config.ts +47 -21
  193. package/src/app/lib/mcp/components.ts +523 -0
  194. package/src/app/lib/mcp/page-url.ts +24 -0
  195. package/src/app/lib/mcp/pages.ts +84 -0
  196. package/src/app/lib/mcp/result.ts +33 -0
  197. package/src/app/lib/mcp/server.ts +39 -318
  198. package/src/app/lib/mcp/tokens.ts +334 -0
  199. package/src/app/lib/mcp/tool.ts +19 -0
  200. package/src/app/lib/registry-api/asset-transfer.ts +119 -11
  201. package/src/app/lib/registry-api/errors.ts +2 -0
  202. package/src/app/lib/registry-api/handler.ts +7 -1
  203. package/src/app/lib/registry-api/transfer.ts +28 -6
  204. package/src/app/lib/server-module.ts +34 -0
  205. package/src/app/lib/utils.ts +20 -0
  206. package/src/app/next.config.mjs +26 -24
  207. package/src/app/pages/[...slug]/index.tsx +2 -1
  208. package/src/app/pages/_app.tsx +6 -2
  209. package/src/app/pages/_document.tsx +6 -0
  210. package/src/app/pages/account/ai.tsx +183 -0
  211. package/src/app/pages/account/index.tsx +68 -68
  212. package/src/app/pages/account/tokens.tsx +37 -7
  213. package/src/app/pages/account/users.tsx +75 -15
  214. package/src/app/pages/api/account/ai/keys/[connectionId].ts +60 -0
  215. package/src/app/pages/api/account/ai/keys/index.ts +20 -0
  216. package/src/app/pages/api/admin/users/[id]/[action].ts +80 -11
  217. package/src/app/pages/api/admin/users/index.ts +2 -2
  218. package/src/app/pages/api/ai/chat.ts +115 -0
  219. package/src/app/pages/api/ai/connections.ts +23 -0
  220. package/src/app/pages/api/auth/request-reset.ts +14 -16
  221. package/src/app/pages/api/docs/nav.json.ts +1 -1
  222. package/src/app/pages/api/install.ts +3 -3
  223. package/src/app/pages/api/mcp.ts +6 -0
  224. package/src/app/pages/api/registry/transfer/assets/blobs/[hash]/complete.ts +11 -0
  225. package/src/app/pages/api/registry/transfer/assets/blobs/[hash]/upload.ts +12 -0
  226. package/src/app/pages/api/registry/transfer/assets/blobs/[hash].ts +1 -1
  227. package/src/app/pages/cli/device.tsx +10 -5
  228. package/src/app/pages/foundations/colors.tsx +9 -8
  229. package/src/app/pages/foundations/index.tsx +3 -3
  230. package/src/app/pages/index.tsx +2 -2
  231. package/src/app/pages/install.tsx +7 -2
  232. package/src/app/pages/login.tsx +32 -27
  233. package/src/app/pages/reset-password.tsx +21 -20
  234. package/src/app/pages/system/component/[component]/index.tsx +2 -5
  235. package/src/app/pages/system/index.tsx +11 -11
  236. package/src/app/pages/system/pattern/[pattern]/index.tsx +2 -2
  237. package/src/app/pages/system/tokens/components/[component]/index.tsx +0 -4
  238. package/src/app-builder/README.md +2 -1
  239. package/src/app-builder/build.ts +218 -73
  240. package/src/app-builder/client-config.ts +3 -2
  241. package/src/app-builder/paths.ts +3 -22
  242. package/src/app-builder/static-export.ts +1 -1
  243. package/src/app-builder/vercel-output.ts +5 -4
  244. package/src/cache/README.md +4 -1
  245. package/src/cache/build-cache.ts +128 -43
  246. package/src/cache/build-inputs.ts +28 -0
  247. package/src/cache/index.ts +4 -0
  248. package/src/catalog/README.md +3 -0
  249. package/src/catalog/normalize.ts +2 -0
  250. package/src/catalog/previews.ts +6 -3
  251. package/src/catalog/types.ts +8 -0
  252. package/src/cli/create.ts +1 -2
  253. package/src/commands/build/index.ts +1 -1
  254. package/src/config/README.md +10 -9
  255. package/src/config/defaults.ts +7 -4
  256. package/src/config/from-env.ts +33 -0
  257. package/src/config/helpers.ts +1 -3
  258. package/src/config/index.ts +0 -3
  259. package/src/config/loader.ts +27 -3
  260. package/src/config/resolve-env.ts +28 -15
  261. package/src/config/runtime.ts +10 -5
  262. package/src/handlebars.ts +8 -2
  263. package/src/index.ts +33 -29
  264. package/src/nav/index.ts +2 -2
  265. package/src/nav/registry.ts +3 -3
  266. package/src/nav/workspace.ts +3 -3
  267. package/src/pipeline/README.md +1 -1
  268. package/src/pipeline/figma.ts +98 -49
  269. package/src/pipeline/styles.ts +1 -1
  270. package/src/pipeline/validation.ts +3 -3
  271. package/src/providers.ts +18 -0
  272. package/src/registry/asset-storage/define.ts +6 -6
  273. package/src/registry/asset-storage/resolve.ts +38 -37
  274. package/src/registry/asset-storage/types.ts +39 -17
  275. package/src/registry/auth/ai-keys.ts +62 -0
  276. package/src/registry/auth/crypto.ts +63 -1
  277. package/src/registry/auth/index.ts +11 -1
  278. package/src/registry/checkout/index.ts +60 -138
  279. package/src/registry/client.ts +60 -20
  280. package/src/registry/db/driver.ts +2 -2
  281. package/src/registry/db/schema.ts +49 -22
  282. package/src/registry/email/define.ts +15 -0
  283. package/src/registry/email/providers/smtp.ts +39 -0
  284. package/src/registry/email/resolve.ts +94 -0
  285. package/src/registry/email/types.ts +28 -0
  286. package/src/registry/path.ts +11 -0
  287. package/src/registry/publish/package.ts +9 -0
  288. package/src/registry/transfer.ts +4 -0
  289. package/src/store/registry.ts +7 -7
  290. package/src/transformers/plugins/ssr-render.ts +53 -16
  291. package/src/transformers/preview/component/builder.ts +19 -25
  292. package/src/transformers/preview/component/css.ts +101 -53
  293. package/src/transformers/preview/component/html.ts +6 -2
  294. package/src/transformers/preview/component/javascript.ts +14 -4
  295. package/src/transformers/preview/pattern/html.ts +3 -1
  296. package/src/transformers/preview/types.ts +14 -0
  297. package/src/transformers/utils/build.ts +93 -0
  298. package/src/transformers/utils/module.ts +4 -2
  299. package/src/types/config.ts +148 -20
  300. package/src/utils/README.md +1 -1
  301. package/src/utils/path.ts +6 -29
  302. package/templates/default/handoff.config.js.tpl +10 -2
  303. package/templates/default/handoff.config.ts.tpl +10 -2
  304. package/templates/default/handoff.config.with-examples.js.tpl +10 -2
  305. package/templates/default/handoff.config.with-examples.ts.tpl +10 -2
  306. package/templates/default/package.json.tpl +2 -2
  307. package/.editorconfig +0 -19
  308. package/.eslintrc.json +0 -4
  309. package/.prettierignore +0 -0
  310. package/.prettierrc.json +0 -8
  311. package/.vscode/settings.json +0 -17
  312. package/dist/config/validator.d.ts +0 -9
  313. package/dist/config/validator.js +0 -24
  314. package/dist/registry/asset-storage/adapters/vercel-blob.d.ts +0 -17
  315. package/dist/registry/asset-storage/adapters/vercel-blob.js +0 -87
  316. package/src/app/components/old/Header.tsx +0 -89
  317. package/src/app/lib/mcp/shape.ts +0 -596
  318. package/src/app/pages/assets/fonts.tsx +0 -129
  319. package/src/app/pages/assets/index.tsx +0 -91
  320. package/src/app/pages/assets/logos.tsx +0 -75
  321. package/src/config/validator.ts +0 -22
  322. package/src/registry/asset-storage/adapters/vercel-blob.ts +0 -51
@@ -13,6 +13,11 @@ export default async function requestResetHandler(req: NextApiRequest, res: Next
13
13
  if (!method) return;
14
14
  const context = await prepareRegistryApi(req, res, { mutation: true });
15
15
  if (!context) return;
16
+ // The same answer for every address, so this reveals nothing about which accounts exist.
17
+ if (!registryEmailIsConfigured()) {
18
+ res.status(503).json({ error: 'Password reset by email is not available on this registry. Ask an administrator for a reset link.' });
19
+ return;
20
+ }
16
21
  const body = req.body && typeof req.body === 'object' ? req.body : {};
17
22
  const email = normalizeEmail(typeof body.email === 'string' ? body.email : '');
18
23
  const throttle = await consumeAuthRateLimit(context.db, {
@@ -27,26 +32,19 @@ export default async function requestResetHandler(req: NextApiRequest, res: Next
27
32
  return;
28
33
  }
29
34
 
30
- if (!registryEmailIsConfigured()) {
31
- res.status(200).json({ ok: true });
32
- return;
33
- }
34
35
  const result = await createPasswordReset(context.db, email);
35
36
  if (result.token && result.user) {
36
37
  const resetUrl = registryPageUrl('/reset-password', undefined, { token: result.token });
38
+ // The result is ignored so the response never reveals whether the account exists.
37
39
  if (resetUrl) {
38
- try {
39
- await sendRegistryAuthEmail({
40
- to: result.user.email,
41
- subject: 'Reset your Handoff Registry password',
42
- heading: 'Reset your password',
43
- message: 'Use this single-use link to choose a new password.',
44
- actionLabel: 'Reset password',
45
- actionUrl: resetUrl,
46
- });
47
- } catch {
48
- // Keep the response enumeration-safe. Operational email failures never reveal account state.
49
- }
40
+ await sendRegistryAuthEmail({
41
+ to: result.user.email,
42
+ subject: 'Reset your Handoff Registry password',
43
+ heading: 'Reset your password',
44
+ message: 'Use this single-use link to choose a new password. It expires in one hour.',
45
+ actionLabel: 'Reset password',
46
+ actionUrl: resetUrl,
47
+ });
50
48
  }
51
49
  }
52
50
  res.status(200).json({ ok: true });
@@ -5,7 +5,7 @@ import { ensureGet, resolveDocsBackend, sendDocsData, sendDocsError } from '@/li
5
5
  // Kept static so the baked structure is traced into every registry serverless bundle.
6
6
  import navShell from '@/generated/nav-shell.json';
7
7
 
8
- /** Thin registry refresh adapter. The adapter/resolver owns all navigation structure and filling. */
8
+ /** Thin route: `getRegistryNavData` builds and fills the whole navigation. */
9
9
  export default async function handler(req: NextApiRequest, res: NextApiResponse): Promise<void> {
10
10
  if (!ensureGet(req, res)) return;
11
11
 
@@ -2,7 +2,7 @@ import type { NextApiRequest, NextApiResponse } from 'next';
2
2
  import { getRegistryInstallationState, installRegistry } from '@handoff/registry/auth';
3
3
  import { allowApiMethods, prepareRegistryApi } from '../../lib/auth/api';
4
4
  import { canonicalRegistryUrl, registryAuthSecret } from '../../lib/auth/config';
5
- import { registryEmailIsConfigured } from '../../lib/auth/email';
5
+ import { missingRegistryEmailSettings, registryEmailIsConfigured, registryEmailProviderName } from '../../lib/auth/email';
6
6
 
7
7
  const check = (id: string, label: string, ok: boolean, message: string, optional = false) => ({
8
8
  id,
@@ -60,8 +60,8 @@ export default async function installHandler(req: NextApiRequest, res: NextApiRe
60
60
  'Email delivery',
61
61
  registryEmailIsConfigured(),
62
62
  registryEmailIsConfigured()
63
- ? 'Resend delivery is configured.'
64
- : 'Optional. Set RESEND_API_KEY and AUTH_FROM_EMAIL, or deliver invitation links manually.',
63
+ ? `Delivery through ${registryEmailProviderName()} is configured.`
64
+ : `Optional. Set ${missingRegistryEmailSettings().join(', ')}, or deliver invitation links manually.`,
65
65
  true
66
66
  ),
67
67
  ];
@@ -22,6 +22,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
22
22
  return;
23
23
  }
24
24
 
25
+ // The status probe of the docs app. A 204 shows only that the route is served, so the probe needs no credential.
26
+ if (req.method === 'HEAD') {
27
+ res.status(204).end();
28
+ return;
29
+ }
30
+
25
31
  if (!(await authorizeMcpRequest(req, res))) {
26
32
  return;
27
33
  }
@@ -0,0 +1,11 @@
1
+ import type { NextApiRequest, NextApiResponse } from 'next';
2
+ import { handleAssetBlobCompleteRoute } from '@/lib/registry-api/asset-transfer';
3
+
4
+ /**
5
+ * `POST /api/registry/transfer/assets/blobs/:hash/complete`: given `{ storageRef, size, contentType }`
6
+ * from a direct upload, confirm the object exists in storage and record the blob. Requires a
7
+ * write-scoped credential; registry-runtime only.
8
+ */
9
+ export default function handler(req: NextApiRequest, res: NextApiResponse): Promise<void> {
10
+ return handleAssetBlobCompleteRoute(req, res);
11
+ }
@@ -0,0 +1,12 @@
1
+ import type { NextApiRequest, NextApiResponse } from 'next';
2
+ import { handleAssetBlobUploadRoute } from '@/lib/registry-api/asset-transfer';
3
+
4
+ /**
5
+ * `POST /api/registry/transfer/assets/blobs/:hash/upload`: given `{ size, contentType }`, return
6
+ * `{ upload }`, a signed URL to upload the blob directly to storage, or `null` when the client must
7
+ * upload through `PUT /api/registry/transfer/assets/blobs/:hash`. Requires a write-scoped
8
+ * credential; registry-runtime only.
9
+ */
10
+ export default function handler(req: NextApiRequest, res: NextApiResponse): Promise<void> {
11
+ return handleAssetBlobUploadRoute(req, res);
12
+ }
@@ -5,7 +5,7 @@ import { handleAssetBlobRoute } from '@/lib/registry-api/asset-transfer';
5
5
  * `/api/registry/transfer/assets/blobs/:hash`: one content-addressed blob.
6
6
  *
7
7
  * - `PUT` stores the raw binary body through the active storage provider (inline `bytea` by default,
8
- * or an object-storage adapter). Idempotent by hash; requires a `registry:write` token.
8
+ * or a custom storage provider). Idempotent by hash; requires a `registry:write` token.
9
9
  * - `GET` returns the blob's bytes, or redirects to a provider URL for object-backed content.
10
10
  * Requires a `registry:read` token.
11
11
  *
@@ -1,3 +1,6 @@
1
+ import type { ClientConfig } from '@handoff/types/config';
2
+ import type { GetStaticProps } from 'next';
3
+ import { getClientRuntimeConfig } from '../../components/util';
1
4
  import { AuthShell } from '../../components/Auth/AuthShell';
2
5
  import { authApiUrl, readApiError } from '../../components/Auth/api';
3
6
  import { Alert, AlertDescription, AlertTitle } from '../../components/ui/alert';
@@ -13,15 +16,17 @@ import { FormEvent, useEffect, useState } from 'react';
13
16
 
14
17
  const normalizeCode = (value: string) => value.replace(/\s/g, '').toUpperCase();
15
18
 
16
- export default function DeviceApprovalPage() {
19
+ export const getStaticProps: GetStaticProps = async () => ({ props: { config: getClientRuntimeConfig() } });
20
+
21
+ export default function DeviceApprovalPage({ config }: { config: ClientConfig }) {
17
22
  if (process.env.HANDOFF_RUNTIME_MODE !== 'registry') {
18
- return <AuthShell title="Authorize CLI">{null}</AuthShell>;
23
+ return <AuthShell config={config} title="Authorize CLI">{null}</AuthShell>;
19
24
  }
20
25
 
21
- return <RegistryDeviceApprovalPage />;
26
+ return <RegistryDeviceApprovalPage config={config} />;
22
27
  }
23
28
 
24
- function RegistryDeviceApprovalPage() {
29
+ function RegistryDeviceApprovalPage({ config }: { config: ClientConfig }) {
25
30
  const router = useRouter();
26
31
  const { data: session, status } = useSession();
27
32
  const [userCode, setUserCode] = useState('');
@@ -60,7 +65,7 @@ function RegistryDeviceApprovalPage() {
60
65
  const callbackUrl = `/cli/device${router.asPath.includes('?') ? router.asPath.slice(router.asPath.indexOf('?')) : ''}`;
61
66
 
62
67
  return (
63
- <AuthShell title="Authorize CLI">
68
+ <AuthShell config={config} title="Authorize CLI">
64
69
  <Card>
65
70
  <CardHeader>
66
71
  <div className="mb-3 flex h-12 w-12 items-center justify-center rounded-full bg-muted">
@@ -33,6 +33,10 @@ export const getStaticProps: GetStaticProps = async () => {
33
33
  };
34
34
  };
35
35
 
36
+ // Tokens without a Figma group land under an empty key.
37
+ const groupTitle = (group: string) => upperFirst(group) || 'Other';
38
+ const groupId = (group: string) => group || 'other';
39
+
36
40
  const ColorsPage = ({
37
41
  content,
38
42
  menu,
@@ -82,20 +86,17 @@ const ColorsPage = ({
82
86
  <div className="lg:gap-10 lg:py-8 xl:grid xl:grid-cols-[1fr_280px]">
83
87
  <div className="flex flex-col gap-0">
84
88
  {Object.keys(colorGroups).map((group) => (
85
- <ColorGrid
86
- title={upperFirst(group)}
87
- group={group}
88
- description="Colors that are used most frequently across all pages and components."
89
- colors={colorGroups[group]}
90
- key={group}
91
- />
89
+ <ColorGrid title={groupTitle(group)} group={groupId(group)} colors={colorGroups[group]} key={group} />
92
90
  ))}
93
91
  <PrevNextNav previous={null} next={{ title: 'Typography', href: '/foundations/typography' }} />
94
92
  </div>
95
93
 
96
94
  <AnchorNav
97
95
  groups={[
98
- Object.assign({}, ...[...Object.keys(colorGroups).map((group) => ({ [`${group}-colors`]: `${upperFirst(group)} Colors` }))]),
96
+ Object.assign(
97
+ {},
98
+ ...[...Object.keys(colorGroups).map((group) => ({ [`${groupId(group)}-colors`]: `${groupTitle(group)} Colors` }))]
99
+ ),
99
100
  ]}
100
101
  />
101
102
 
@@ -50,7 +50,7 @@ const DesignPage = ({ content, menu, metadata, current, config }: DocumentationP
50
50
  },
51
51
  {
52
52
  title: 'Colors',
53
- description: 'Official logo used for all digital and offline materials.',
53
+ description: 'Official color palette used for all digital products.',
54
54
  icon: Palette,
55
55
  link: '/foundations/colors',
56
56
  cta: 'Explore Colors',
@@ -74,14 +74,14 @@ const DesignPage = ({ content, menu, metadata, current, config }: DocumentationP
74
74
  description: 'Downloadable icon set for use in digital and offline materials.',
75
75
  icon: Shapes,
76
76
  link: '/foundations/icons',
77
- cta: 'View Library',
77
+ cta: 'Explore Icons',
78
78
  },
79
79
  {
80
80
  title: 'Effects',
81
81
  description: 'Shadows, blurs, and other effects used in the design system.',
82
82
  icon: Sun,
83
83
  link: '/foundations/effects',
84
- cta: 'View Effects',
84
+ cta: 'Explore Effects',
85
85
  },
86
86
  ]}
87
87
  />
@@ -57,11 +57,11 @@ export const getStaticProps: GetStaticProps = async (context) => {
57
57
 
58
58
  export const Home = ({ content, menu, metadata, config, current }: DocumentationProps) => {
59
59
  return (
60
- <Layout config={config} menu={menu} current={current} metadata={metadata} fullWidthHero={true}>
60
+ <Layout config={config} menu={menu} current={current} metadata={{ ...metadata, metaTitle: metadata.metaTitle || config?.app?.title }} fullWidthHero={true}>
61
61
  <div className="w-full bg-linear-to-r py-12 dark:from-gray-900 dark:to-gray-800 sm:py-20">
62
62
  <div className="container mx-auto px-8">
63
63
  <HeadersType.H1 className="max-w-4xl text-3xl font-semibold leading-[-0.05px] sm:text-4xl">
64
- {config?.app?.client} Design System
64
+ {config?.app?.title}
65
65
  </HeadersType.H1>
66
66
  {content && (
67
67
  <div className="text-lg max-w-4xl empty:hidden [&>*]:mt-6 [&>p]:font-light [&>h2:first-child]:mt-0 [&>h2:first-child]:font-normal sm:text-xl">
@@ -1,3 +1,6 @@
1
+ import type { ClientConfig } from '@handoff/types/config';
2
+ import type { GetStaticProps } from 'next';
3
+ import { getClientRuntimeConfig } from '../components/util';
1
4
  import { CheckCircle2, CircleAlert, Mail, RefreshCw, UserRound } from 'lucide-react';
2
5
  import Link from 'next/link';
3
6
  import { useRouter } from 'next/router';
@@ -66,7 +69,9 @@ const normalizeChecks = (status: InstallStatus): PreflightCheck[] => {
66
69
  ];
67
70
  };
68
71
 
69
- export default function InstallPage() {
72
+ export const getStaticProps: GetStaticProps = async () => ({ props: { config: getClientRuntimeConfig() } });
73
+
74
+ export default function InstallPage({ config }: { config: ClientConfig }) {
70
75
  const router = useRouter();
71
76
  const [step, setStep] = useState<InstallStep>('preflight');
72
77
  const [checks, setChecks] = useState<PreflightCheck[]>([]);
@@ -161,7 +166,7 @@ export default function InstallPage() {
161
166
  };
162
167
 
163
168
  return (
164
- <AuthShell title="Install registry" description="Configure the first Handoff Registry administrator." hideNav>
169
+ <AuthShell config={config} title="Install registry" description="Configure the first Handoff Registry administrator." hideNav>
165
170
  <div className="mb-8">
166
171
  <p className="text-sm font-medium text-muted-foreground">Registry installation</p>
167
172
  <div className="mt-3 grid grid-cols-3 gap-2" aria-label="Installation progress">
@@ -1,32 +1,40 @@
1
- import { Alert, AlertDescription } from '../components/ui/alert';
2
1
  import { Button } from '../components/ui/button';
3
2
  import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '../components/ui/card';
4
3
  import { Input } from '../components/ui/input';
5
4
  import { Label } from '../components/ui/label';
6
- import { CircleAlert } from 'lucide-react';
5
+ import type { ClientConfig } from '@handoff/types/config';
6
+ import type { GetStaticProps } from 'next';
7
7
  import Link from 'next/link';
8
8
  import { signIn, useSession } from 'next-auth/react';
9
9
  import { useRouter } from 'next/router';
10
10
  import { FormEvent, useEffect, useState } from 'react';
11
11
  import { AuthShell } from '../components/Auth/AuthShell';
12
- import { authApiUrl } from '../components/Auth/api';
12
+ import { loadInstallStatus } from '../components/Auth/installStatus';
13
+ import { getClientRuntimeConfig } from '../components/util';
13
14
 
14
15
  const safeCallbackUrl = (value: unknown): string =>
15
16
  typeof value === 'string' && value.startsWith('/') && !value.startsWith('//') && !value.startsWith('/\\') ? value : '/';
16
17
 
17
- export default function LoginPage() {
18
+ export const getStaticProps: GetStaticProps = async () => ({ props: { config: getClientRuntimeConfig() } });
19
+
20
+ export default function LoginPage({ config }: { config: ClientConfig }) {
18
21
  if (process.env.HANDOFF_RUNTIME_MODE !== 'registry') {
19
- return <AuthShell title="Sign in">{null}</AuthShell>;
22
+ return (
23
+ <AuthShell config={config} title="Sign in" centered>
24
+ {null}
25
+ </AuthShell>
26
+ );
20
27
  }
21
28
 
22
- return <RegistryLoginPage />;
29
+ return <RegistryLoginPage config={config} />;
23
30
  }
24
31
 
25
- function RegistryLoginPage() {
32
+ function RegistryLoginPage({ config }: { config: ClientConfig }) {
26
33
  const router = useRouter();
27
34
  const { data: session, status } = useSession();
28
35
  const [pending, setPending] = useState(false);
29
36
  const [checkingInstall, setCheckingInstall] = useState(true);
37
+ const [emailConfigured, setEmailConfigured] = useState(true);
30
38
  const [error, setError] = useState<string | null>(null);
31
39
 
32
40
  useEffect(() => {
@@ -34,11 +42,11 @@ function RegistryLoginPage() {
34
42
  setCheckingInstall(false);
35
43
  return;
36
44
  }
37
- void fetch(authApiUrl('/api/install'), { credentials: 'include', cache: 'no-store' })
38
- .then(async (response) => {
39
- if (!response.ok) return;
40
- const body = (await response.json()) as { installed?: boolean };
41
- if (body.installed === false) await router.replace('/install');
45
+ void loadInstallStatus()
46
+ .then(async (status) => {
47
+ if (!status) return;
48
+ if (status.installed === false) await router.replace('/install');
49
+ setEmailConfigured(status.emailConfigured !== false);
42
50
  })
43
51
  .finally(() => setCheckingInstall(false));
44
52
  }, [router]);
@@ -74,25 +82,18 @@ function RegistryLoginPage() {
74
82
  };
75
83
 
76
84
  return (
77
- <AuthShell title="Sign in">
85
+ <AuthShell config={config} title="Sign in" centered>
78
86
  <Card>
79
87
  <CardHeader>
80
88
  <CardTitle>Sign in</CardTitle>
81
- <CardDescription>Use your Handoff Registry email and password.</CardDescription>
89
+ <CardDescription>Use your Handoff account email and password.</CardDescription>
82
90
  </CardHeader>
83
91
  <form onSubmit={submit}>
84
92
  <CardContent className="space-y-4">
85
93
  {router.query.updated === '1' ? (
86
- <Alert variant="green">
87
- <AlertDescription>Your password was updated. Sign in below.</AlertDescription>
88
- </Alert>
89
- ) : null}
90
- {error ? (
91
- <Alert variant="destructive">
92
- <CircleAlert />
93
- <AlertDescription>{error}</AlertDescription>
94
- </Alert>
94
+ <p className="text-sm text-green-600 dark:text-green-400">Your password was updated. Sign in below.</p>
95
95
  ) : null}
96
+ {error ? <p className="text-sm text-destructive">{error}</p> : null}
96
97
  <div className="space-y-2">
97
98
  <Label htmlFor="email">Email</Label>
98
99
  <Input id="email" name="email" type="email" autoComplete="email" required placeholder="you@company.com" />
@@ -102,13 +103,17 @@ function RegistryLoginPage() {
102
103
  <Input id="password" name="password" type="password" autoComplete="current-password" required />
103
104
  </div>
104
105
  </CardContent>
105
- <CardFooter className="flex-col gap-3">
106
+ <CardFooter className="flex flex-col gap-3">
106
107
  <Button type="submit" className="w-full" disabled={pending || checkingInstall}>
107
108
  {pending ? 'Signing in…' : 'Sign in'}
108
109
  </Button>
109
- <Link href="/reset-password" className="text-sm text-muted-foreground underline-offset-4 hover:underline">
110
- Forgot password?
111
- </Link>
110
+ {emailConfigured ? (
111
+ <Link href="/reset-password" className="text-center text-sm text-muted-foreground underline-offset-4 hover:underline">
112
+ Forgot password?
113
+ </Link>
114
+ ) : (
115
+ <p className="text-center text-sm text-muted-foreground">Forgot your password? Ask an administrator for a reset link.</p>
116
+ )}
112
117
  </CardFooter>
113
118
  </form>
114
119
  </Card>
@@ -1,16 +1,20 @@
1
- import { Alert, AlertDescription } from '../components/ui/alert';
2
1
  import { Button } from '../components/ui/button';
3
2
  import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '../components/ui/card';
4
3
  import { Input } from '../components/ui/input';
5
4
  import { Label } from '../components/ui/label';
5
+ import type { ClientConfig } from '@handoff/types/config';
6
+ import type { GetStaticProps } from 'next';
6
7
  import Head from 'next/head';
7
8
  import Link from 'next/link';
8
9
  import { useRouter } from 'next/router';
9
10
  import { FormEvent, useEffect, useState } from 'react';
10
11
  import { AuthShell } from '../components/Auth/AuthShell';
11
12
  import { authApiUrl, readApiError } from '../components/Auth/api';
13
+ import { getClientRuntimeConfig } from '../components/util';
12
14
 
13
- export default function ResetPasswordPage() {
15
+ export const getStaticProps: GetStaticProps = async () => ({ props: { config: getClientRuntimeConfig() } });
16
+
17
+ export default function ResetPasswordPage({ config }: { config: ClientConfig }) {
14
18
  const router = useRouter();
15
19
  const [token, setToken] = useState('');
16
20
  const [purpose, setPurpose] = useState<'invite' | 'reset'>('reset');
@@ -88,7 +92,7 @@ export default function ResetPasswordPage() {
88
92
  };
89
93
 
90
94
  return (
91
- <AuthShell title={token ? 'Set a new password' : 'Reset password'}>
95
+ <AuthShell config={config} title={token ? 'Set a new password' : 'Reset password'} centered>
92
96
  <Head>
93
97
  <meta name="referrer" content="no-referrer" />
94
98
  </Head>
@@ -97,32 +101,27 @@ export default function ResetPasswordPage() {
97
101
  <CardTitle>{token ? 'Set a new password' : 'Reset password'}</CardTitle>
98
102
  <CardDescription>
99
103
  {sent
100
- ? 'If an account exists for that email, a password reset link has been sent.'
104
+ ? 'If an account exists for that email, we sent a link to reset your password.'
101
105
  : token
102
- ? 'Choose a new password for your registry account.'
103
- : 'Enter your email and we will send a reset link if an account exists.'}
106
+ ? 'Choose a password at least 12 characters long.'
107
+ : 'Enter your email and we will send you a reset link if an account exists.'}
104
108
  </CardDescription>
105
109
  </CardHeader>
106
110
  {sent ? (
107
- <CardFooter>
108
- <Button asChild variant="outline" className="w-full">
109
- <Link href="/login">Return to sign in</Link>
110
- </Button>
111
+ <CardFooter className="flex flex-col gap-3">
112
+ <Link href="/login" className="text-center text-sm text-primary underline-offset-4 hover:underline">
113
+ Return to sign in
114
+ </Link>
111
115
  </CardFooter>
112
116
  ) : (
113
117
  <form onSubmit={token ? resetPassword : requestReset}>
114
118
  <CardContent className="space-y-4">
115
- {error ? (
116
- <Alert variant="destructive">
117
- <AlertDescription>{error}</AlertDescription>
118
- </Alert>
119
- ) : null}
119
+ {error ? <p className="text-sm text-destructive">{error}</p> : null}
120
120
  {token ? (
121
121
  <>
122
122
  <div className="space-y-2">
123
123
  <Label htmlFor="password">New password</Label>
124
124
  <Input id="password" name="password" type="password" autoComplete="new-password" required minLength={12} />
125
- <p className="text-xs text-muted-foreground">Use at least 12 characters.</p>
126
125
  </div>
127
126
  <div className="space-y-2">
128
127
  <Label htmlFor="passwordConfirmation">Confirm password</Label>
@@ -143,13 +142,15 @@ export default function ResetPasswordPage() {
143
142
  </div>
144
143
  )}
145
144
  </CardContent>
146
- <CardFooter className="flex-col gap-3">
145
+ <CardFooter className="flex flex-col gap-3">
147
146
  <Button type="submit" className="w-full" disabled={pending}>
148
147
  {pending ? 'Please wait…' : token ? 'Update password' : 'Send reset link'}
149
148
  </Button>
150
- <Link href="/login" className="text-sm text-muted-foreground underline-offset-4 hover:underline">
151
- Back to sign in
152
- </Link>
149
+ {token ? (
150
+ <Link href="/login" className="text-center text-sm text-muted-foreground underline-offset-4 hover:underline">
151
+ Back to sign in
152
+ </Link>
153
+ ) : null}
153
154
  </CardFooter>
154
155
  </form>
155
156
  )}
@@ -96,9 +96,6 @@ export const getStaticProps = async (context) => {
96
96
  const previousComponent = sameGroupComponents[groupIndex - 1] ?? null;
97
97
  const nextComponent = sameGroupComponents[groupIndex + 1] ?? null;
98
98
 
99
- const fallbackTitle = componentData.title || startCase(component as string);
100
- const fallbackMetaTitle = `${fallbackTitle}${config?.app?.client ? ` | ${config.app.client} Design System` : ''}`;
101
-
102
99
  return {
103
100
  props: {
104
101
  id: component,
@@ -110,7 +107,7 @@ export const getStaticProps = async (context) => {
110
107
  ...componentData,
111
108
  title: componentData.title || docs.metadata.title || startCase(component as string),
112
109
  description: componentData.description,
113
- metaTitle: docs.metadata.metaTitle || fallbackMetaTitle,
110
+ metaTitle: docs.metadata.metaTitle ?? '',
114
111
  metaDescription: docs.metadata.metaDescription || componentData.description,
115
112
  image: docs.metadata.image || 'hero-brand-assets',
116
113
  },
@@ -256,7 +253,7 @@ const GenericComponentPage = ({ menu, metadata, current, id, config, componentHo
256
253
  </Button>
257
254
  )}
258
255
  <Drawer direction="right">
259
- <DrawerTrigger>
256
+ <DrawerTrigger asChild>
260
257
  <Button variant="outline" size={'sm'} className="font-normal [&_svg]:size-3!">
261
258
  API Reference
262
259
  </Button>
@@ -107,20 +107,20 @@ const ComponentsPage = ({ content, menu, metadata, current, config }: ComponentP
107
107
  <div className="mt-3 flex flex-row justify-between gap-3">
108
108
  <p className="text-lg leading-relaxed text-gray-600 dark:text-gray-300">{metadata.description}</p>
109
109
  <Drawer direction="right">
110
- <DrawerTrigger>
111
- <TooltipProvider>
112
- <Tooltip>
113
- <TooltipTrigger asChild>
110
+ <TooltipProvider>
111
+ <Tooltip>
112
+ <TooltipTrigger asChild>
113
+ <DrawerTrigger asChild>
114
114
  <Button variant="outline">
115
115
  API <Webhook strokeWidth={1.5} />
116
116
  </Button>
117
- </TooltipTrigger>
118
- <TooltipContent side="left">
119
- <Badge>{apiUrl}</Badge>
120
- </TooltipContent>
121
- </Tooltip>
122
- </TooltipProvider>
123
- </DrawerTrigger>
117
+ </DrawerTrigger>
118
+ </TooltipTrigger>
119
+ <TooltipContent side="left">
120
+ <Badge>{apiUrl}</Badge>
121
+ </TooltipContent>
122
+ </Tooltip>
123
+ </TooltipProvider>
124
124
  <DrawerContent>
125
125
  <div className="mx-5 w-full max-w-lg">
126
126
  <DrawerHeader>
@@ -130,7 +130,7 @@ export const getStaticProps = async (context: { params: IParams }) => {
130
130
  metadata: {
131
131
  title: fallbackTitle,
132
132
  description: patternData?.description || '',
133
- metaTitle: `${fallbackTitle}${config?.app?.client ? ` | ${config.app.client} Design System` : ''}`,
133
+ metaTitle: '',
134
134
  metaDescription: patternData?.description || '',
135
135
  },
136
136
  previousPattern,
@@ -189,7 +189,7 @@ const PatternPage = ({ menu, metadata, current, id, config, previousPattern, nex
189
189
 
190
190
  if (patternNotFound) {
191
191
  return (
192
- <Layout config={config} menu={menu} current={current} metadata={{ ...metadata, title: '404 - Page Not Found', metaTitle: '404 - Page Not Found' }}>
192
+ <Layout config={config} menu={menu} current={current} metadata={{ ...metadata, title: 'Page Not Found', metaTitle: '' }}>
193
193
  <div className="flex min-h-[60vh] flex-col items-center justify-center">
194
194
  <NotFound />
195
195
  </div>
@@ -70,14 +70,10 @@ export const getStaticProps = async (context) => {
70
70
  description = componentData?.description;
71
71
  }
72
72
 
73
- const fallbackTitle = displayName || startCase(componentSlug);
74
- const fallbackMetaTitle = `${fallbackTitle}${config?.app?.client ? ` | ${config.app.client} Design System` : ''}`;
75
-
76
73
  markdownProps.metadata = {
77
74
  ...markdownProps.metadata,
78
75
  title: displayName || markdownProps.metadata.title || startCase(componentSlug),
79
76
  description: markdownProps.metadata.description || description || '',
80
- metaTitle: markdownProps.metadata.metaTitle || fallbackMetaTitle,
81
77
  metaDescription: markdownProps.metadata.metaDescription || description || '',
82
78
  };
83
79
 
@@ -9,7 +9,8 @@ Builds and runs the Next.js documentation app from runtime config + generated pr
9
9
 
10
10
  2. **Build component artifacts**
11
11
  - Production: `buildComponents` + `buildPatterns`.
12
- - Watch mode: `processComponents(..., { useCache: true })` to skip unchanged components.
12
+ - Watch mode: `processComponents(handoff)`.
13
+ - Every full component build uses the build cache to skip unchanged components (see `src/cache`).
13
14
 
14
15
  3. **Build global bundles**
15
16
  - Build main JS/CSS entry bundles when configured.