@gradial/aci 0.1.52-rc.3 → 0.1.52-rc.34-e2e.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (276) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/README.md +86 -5
  3. package/dist/astro/error-page.js +1 -0
  4. package/dist/astro/index.js +4 -0
  5. package/dist/astro/page.d.ts +5 -2
  6. package/dist/astro/page.js +35 -23
  7. package/dist/astro/preview.js +2 -17
  8. package/dist/audience-data-models.d.ts +308 -194
  9. package/dist/audience-data-models.js +314 -51
  10. package/dist/cli/compile-registry.d.mts +5 -1
  11. package/dist/cli/compile-registry.mjs +31 -46
  12. package/dist/cli/mintlify-render.mjs +8 -0
  13. package/dist/cli/validate-content.d.mts +31 -1
  14. package/dist/cli/validate-content.mjs +50 -12
  15. package/dist/collect/aci.js +1 -1
  16. package/dist/collect/aci.js.sha256 +1 -1
  17. package/dist/collect/api.d.ts +6 -7
  18. package/dist/collect/api.js +21 -8
  19. package/dist/collect/assignments.d.ts +7 -9
  20. package/dist/collect/assignments.js +23 -32
  21. package/dist/collect/batcher.d.ts +9 -7
  22. package/dist/collect/batcher.js +6 -5
  23. package/dist/collect/consent/envelope.d.ts +7 -1
  24. package/dist/collect/consent/envelope.js +3 -1
  25. package/dist/collect/consent/mapping.d.ts +2 -0
  26. package/dist/collect/consent/mapping.js +1 -0
  27. package/dist/collect/consenthold.d.ts +38 -0
  28. package/dist/collect/consenthold.js +82 -0
  29. package/dist/collect/declarative.js +7 -5
  30. package/dist/collect/gapbuffer.d.ts +13 -4
  31. package/dist/collect/gapbuffer.js +21 -10
  32. package/dist/collect/index.js +67 -20
  33. package/dist/collect/pageviews.d.ts +11 -11
  34. package/dist/collect/pageviews.js +27 -22
  35. package/dist/consent-banner/index.d.ts +17 -0
  36. package/dist/consent-banner/index.js +23 -0
  37. package/dist/content/index.d.ts +2 -1
  38. package/dist/content/index.js +4 -1
  39. package/dist/content/provider.d.ts +16 -2
  40. package/dist/content/provider.js +5 -1
  41. package/dist/content/resolve-slots.d.ts +33 -4
  42. package/dist/content/resolve-slots.js +127 -19
  43. package/dist/content/types.d.ts +7 -1
  44. package/dist/edge/expression-capabilities.v1.d.ts +106 -0
  45. package/dist/edge/expression-capabilities.v1.js +165 -0
  46. package/dist/index.d.ts +1 -2
  47. package/dist/index.js +0 -1
  48. package/dist/internal/domain/edge/compose.d.ts +24 -0
  49. package/dist/internal/domain/edge/compose.js +89 -0
  50. package/dist/internal/domain/edge/identity.d.ts +5 -0
  51. package/dist/internal/domain/edge/identity.js +50 -14
  52. package/dist/internal/domain/edge/index.d.ts +2 -0
  53. package/dist/internal/domain/edge/index.js +2 -0
  54. package/dist/internal/domain/edge/mimic_token.d.ts +14 -0
  55. package/dist/internal/domain/edge/mimic_token.js +125 -0
  56. package/dist/internal/domain/edge/personalization-core.d.ts +61 -22
  57. package/dist/internal/domain/edge/personalization-core.js +789 -198
  58. package/dist/internal/domain/edge/personalization_index.d.ts +17 -0
  59. package/dist/internal/domain/edge/personalization_index.js +56 -0
  60. package/dist/internal/domain/edge/record/codec.d.ts +6 -32
  61. package/dist/internal/domain/edge/record/codec.js +86 -168
  62. package/dist/internal/domain/edge/resolve.js +76 -39
  63. package/dist/internal/domain/edge/testdata/flat_only_validator_v1.d.ts +10 -0
  64. package/dist/internal/domain/edge/testdata/flat_only_validator_v1.js +253 -0
  65. package/dist/internal/domain/edge/types.d.ts +93 -41
  66. package/dist/internal/domain/edge/types.js +3 -3
  67. package/dist/internal/domain/edge/variant_key.d.ts +3 -0
  68. package/dist/internal/domain/edge/variant_key.js +44 -0
  69. package/dist/internal/platform/collectionstore/errors.d.ts +24 -0
  70. package/dist/internal/platform/collectionstore/errors.js +48 -0
  71. package/dist/mimic/core.d.ts +2 -0
  72. package/dist/mimic/core.js +31 -0
  73. package/dist/mintlify/blocks.d.ts +6 -0
  74. package/dist/mintlify/blocks.js +201 -0
  75. package/dist/mintlify/config.d.ts +3 -0
  76. package/dist/mintlify/config.js +77 -0
  77. package/dist/mintlify/index.d.ts +3 -0
  78. package/dist/mintlify/index.js +2 -0
  79. package/dist/mintlify/json.d.ts +8 -0
  80. package/dist/mintlify/json.js +37 -0
  81. package/dist/mintlify/materialize.d.ts +14 -0
  82. package/dist/mintlify/materialize.js +254 -0
  83. package/dist/mintlify/types.d.ts +74 -0
  84. package/dist/mintlify/types.js +1 -0
  85. package/dist/mintlify/validate.d.ts +7 -0
  86. package/dist/mintlify/validate.js +256 -0
  87. package/dist/next/assignments-navigation.d.ts +4 -0
  88. package/dist/next/assignments-navigation.js +18 -0
  89. package/dist/next/assignments-script.d.ts +3 -2
  90. package/dist/next/assignments-script.js +5 -5
  91. package/dist/next/error-pages.d.ts +4 -0
  92. package/dist/next/error-pages.js +4 -0
  93. package/dist/next/error-seat-payload.d.ts +2 -1
  94. package/dist/next/error-seat-payload.js +14 -24
  95. package/dist/next/error-seat.js +7 -2
  96. package/dist/next/hosted-page-target.d.ts +7 -0
  97. package/dist/next/hosted-page-target.js +39 -0
  98. package/dist/next/index.d.ts +1 -1
  99. package/dist/next/index.js +3 -1
  100. package/dist/next/page.d.ts +29 -6
  101. package/dist/next/page.js +47 -25
  102. package/dist/next/release-context.d.ts +29 -0
  103. package/dist/next/release-context.js +55 -0
  104. package/dist/next/render.d.ts +1 -1
  105. package/dist/next/render.js +15 -5
  106. package/dist/next/server.d.ts +8 -0
  107. package/dist/next/server.js +44 -3
  108. package/dist/next/vercel-edge-projection-v1.d.ts +0 -9
  109. package/dist/next/vercel-edge-projection-v1.js +9 -539
  110. package/dist/preview/core.d.ts +1 -0
  111. package/dist/preview/core.js +27 -0
  112. package/dist/react/VideoPlayer.d.ts +3 -3
  113. package/dist/react/VideoPlayer.js +2 -2
  114. package/dist/react/index.d.ts +4 -4
  115. package/dist/react/index.js +2 -3
  116. package/dist/registry.d.ts +4 -2
  117. package/dist/testing/conformance/checks.d.ts +2 -2
  118. package/dist/testing/conformance/checks.js +2 -0
  119. package/dist/testing/conformance/index.d.ts +1 -0
  120. package/dist/testing/conformance/index.js +1 -0
  121. package/dist/testing/conformance/next-stubs.d.ts +6 -0
  122. package/dist/testing/conformance/next-stubs.js +9 -0
  123. package/dist/testing/conformance/preset.d.ts +3 -2
  124. package/dist/testing/conformance/preset.js +15 -8
  125. package/dist/testing/conformance/root-identity.d.ts +31 -0
  126. package/dist/testing/conformance/root-identity.js +361 -0
  127. package/dist/testing/conformance/stubs/next-image.d.ts +7 -0
  128. package/dist/testing/conformance/stubs/next-image.js +5 -0
  129. package/dist/testing/conformance/stubs/next-link.d.ts +7 -0
  130. package/dist/testing/conformance/stubs/next-link.js +4 -0
  131. package/dist/testing/conformance/types.d.ts +5 -2
  132. package/dist/testing/index.d.ts +1 -1
  133. package/dist/testing/index.js +4 -3
  134. package/dist/types/block-ref.d.ts +2 -0
  135. package/dist/types/block-ref.js +3 -2
  136. package/dist/types/component.d.ts +9 -0
  137. package/dist/types/config.d.ts +0 -1
  138. package/dist/types/image.d.ts +54 -17
  139. package/dist/types/image.js +102 -18
  140. package/dist/types/index.d.ts +0 -1
  141. package/dist/types/index.js +0 -1
  142. package/dist/types/media.d.ts +14 -6
  143. package/dist/types/page.d.ts +0 -1
  144. package/dist/types/video.d.ts +7 -3
  145. package/migrations/conformance-registry.md +22 -0
  146. package/package.json +21 -7
  147. package/src/cli/compile-registry.mjs +30 -42
  148. package/src/cli/mintlify-render.mjs +9 -0
  149. package/src/cli/validate-content.mjs +52 -13
  150. package/src/types/block-ref.test.ts +60 -0
  151. package/src/types/block-ref.ts +4 -2
  152. package/src/types/component.ts +10 -0
  153. package/src/types/config.ts +0 -1
  154. package/src/types/image.ts +133 -29
  155. package/src/types/index.ts +0 -1
  156. package/src/types/page.ts +0 -1
  157. package/src/types/video.ts +1 -1
  158. package/templates/astro/starter-content/src/components/chrome/Footer.astro +5 -3
  159. package/templates/astro/starter-content/src/components/chrome/Header.astro +5 -3
  160. package/templates/astro/starter-content/src/components/elements/Button.astro +5 -2
  161. package/templates/astro/starter-content/src/components/elements/ButtonBlock.astro +5 -3
  162. package/templates/astro/starter-content/src/components/sections/ContainerSection.astro +4 -3
  163. package/templates/astro/starter-content/src/components/sections/FeatureCard.astro +4 -3
  164. package/templates/astro/starter-content/src/components/sections/FeatureGrid.astro +4 -3
  165. package/templates/astro/starter-content/src/components/sections/HomeHero.astro +5 -3
  166. package/templates/astro/starter-content/src/components/sections/PersonalizationChoice.astro +4 -3
  167. package/templates/astro/starter-content/src/components/sections/StatsBar.astro +4 -3
  168. package/templates/astro/starter-content/src/fixtures/atomicBlocks.ts +7 -6
  169. package/templates/astro/starter-content/src/fixtures/composition.ts +3 -2
  170. package/templates/astro/starter-content/src/fixtures/sections.ts +3 -3
  171. package/templates/astro/starter-content/src/stories/CompositionStoryFrame.astro +7 -2
  172. package/templates/astro/template/.aci.yaml.tmpl +0 -1
  173. package/templates/astro/template/package.json.tmpl +5 -5
  174. package/templates/astro/template/src/layouts/SiteLayout.astro +2 -2
  175. package/templates/common/.agents/skills/aci/SKILL.md +58 -1
  176. package/templates/common/.agents/skills/authoring/SKILL.md +203 -43
  177. package/templates/common/.agents/skills/authoring/references/block-registry.md +19 -3
  178. package/templates/common/.agents/skills/authoring/references/block-slots.md +3 -0
  179. package/templates/common/.agents/skills/authoring/references/contracts.md +9 -30
  180. package/templates/common/.agents/skills/authoring/references/repo-structure.md +5 -4
  181. package/templates/common/.agents/skills/authoring/references/sdk-server-functions.md +3 -3
  182. package/templates/common/.agents/skills/dam-delivery/SKILL.md +217 -0
  183. package/templates/common/.agents/skills/dam-delivery/references/permalink-grammar.md +167 -0
  184. package/templates/common/.agents/skills/migration/references/canon-migration.md +18 -67
  185. package/templates/common/.agents/skills/migration/references/selector-map.md +1 -1
  186. package/templates/common/.content/fragments/navbar/_index.json +24 -0
  187. package/templates/common/.content/fragments/site-footer/_index.json +61 -0
  188. package/templates/common/.content/pages/404/_index.json +4 -2
  189. package/templates/common/.content/pages/{home/_index.json → _index.json} +10 -1
  190. package/templates/common/.content/pages/error/_index.json +4 -2
  191. package/templates/common/.content/schemas/site.schema.json +11 -1
  192. package/templates/common/.content/templates/error.json +24 -0
  193. package/templates/common/.content/templates/home.json +28 -0
  194. package/templates/common/.content/templates/site.json +8 -0
  195. package/templates/common/.content/templates/standard.json +24 -0
  196. package/templates/common/.nvmrc +1 -1
  197. package/templates/common/starter-content/.content/.rules-baseline.json +8 -0
  198. package/templates/common/starter-content/.content/config/site.json +1 -1
  199. package/templates/common/starter-content/.content/content-rules/no-public-resource-paths.json +6 -0
  200. package/templates/common/starter-content/.content/dam/README.md +74 -0
  201. package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_home_hero → 000000000000000000000000101}/asset.json +1 -1
  202. package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_about_hero → 000000000000000000000000101}/governance.json +2 -1
  203. package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_about_hero → 000000000000000000000000102}/asset.json +1 -1
  204. package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_home_hero → 000000000000000000000000102}/governance.json +2 -1
  205. package/templates/common/starter-content/.content/fragments/navbar/_index.json +23 -20
  206. package/templates/common/starter-content/.content/fragments/site-footer/_index.json +59 -56
  207. package/templates/common/starter-content/.content/pages/404/_index.json +2 -2
  208. package/templates/common/starter-content/.content/pages/{home/_index.json → _index.json} +23 -3
  209. package/templates/common/starter-content/.content/pages/about/_index.json +5 -2
  210. package/templates/common/starter-content/.content/pages/error/_index.json +2 -2
  211. package/templates/common/starter-content/.content/patterns/components/button.json +55 -0
  212. package/templates/common/starter-content/.content/patterns/components/container.json +67 -0
  213. package/templates/common/starter-content/.content/patterns/components/feature_card.json +30 -0
  214. package/templates/common/starter-content/.content/patterns/components/feature_grid.json +59 -0
  215. package/templates/common/starter-content/.content/patterns/components/home_hero.json +151 -0
  216. package/templates/common/starter-content/.content/patterns/components/personalization_choice.json +42 -0
  217. package/templates/common/starter-content/.content/patterns/components/site-footer.json +203 -0
  218. package/templates/common/starter-content/.content/patterns/components/site-navigation.json +115 -0
  219. package/templates/common/starter-content/.content/patterns/components/stats_bar.json +45 -0
  220. package/templates/common/starter-content/.content/schemas/site.schema.json +11 -1
  221. package/templates/nextjs/starter-content/src/components/chrome/Footer.tsx +4 -3
  222. package/templates/nextjs/starter-content/src/components/chrome/Header.tsx +4 -3
  223. package/templates/nextjs/starter-content/src/components/elements/Button.tsx +4 -3
  224. package/templates/nextjs/starter-content/src/components/elements/ButtonBlock.tsx +4 -3
  225. package/templates/nextjs/starter-content/src/components/sections/ContainerSection.tsx +4 -2
  226. package/templates/nextjs/starter-content/src/components/sections/FeatureCard.tsx +4 -3
  227. package/templates/nextjs/starter-content/src/components/sections/FeatureGrid.tsx +4 -2
  228. package/templates/nextjs/starter-content/src/components/sections/HomeHero.tsx +4 -3
  229. package/templates/nextjs/starter-content/src/components/sections/PersonalizationChoice.tsx +4 -2
  230. package/templates/nextjs/starter-content/src/components/sections/StatsBar.tsx +4 -3
  231. package/templates/nextjs/template/.aci.yaml +9 -0
  232. package/templates/nextjs/template/.aci.yaml.tmpl +0 -1
  233. package/templates/nextjs/template/package.json.tmpl +4 -3
  234. package/templates/nextjs/template/src/app/_components/c15t-bridge.ts +30 -0
  235. package/templates/nextjs/template/src/app/_components/c15t-consent-ui.tsx +37 -0
  236. package/templates/nextjs/template/src/app/_components/consent-manager.css +63 -0
  237. package/templates/nextjs/template/src/app/_components/consent-manager.tsx +76 -0
  238. package/templates/nextjs/template/src/app/fonts/inter/OFL.txt +93 -0
  239. package/templates/nextjs/template/src/app/fonts/inter/inter-latin.woff2 +0 -0
  240. package/templates/nextjs/template/src/app/fonts/space-grotesk/OFL.txt +93 -0
  241. package/templates/nextjs/template/src/app/fonts/space-grotesk/space-grotesk-latin.woff2 +0 -0
  242. package/templates/nextjs/template/src/app/layout.tsx +12 -7
  243. package/templates/nextjs/template/tests/conformance.test.ts.tmpl +1 -0
  244. package/dist/define-layout.d.ts +0 -6
  245. package/dist/define-layout.js +0 -14
  246. package/dist/edge/personalization-core.d.ts +0 -11
  247. package/dist/edge/personalization-core.js +0 -269
  248. package/dist/next/collect-route.d.ts +0 -19
  249. package/dist/next/collect-route.js +0 -459
  250. package/dist/next/envelope-digest.d.ts +0 -21
  251. package/dist/next/envelope-digest.js +0 -58
  252. package/dist/next/middleware.d.ts +0 -40
  253. package/dist/next/middleware.js +0 -475
  254. package/dist/next/personalization-host.d.ts +0 -134
  255. package/dist/next/personalization-host.js +0 -270
  256. package/dist/next/root-layout.d.ts +0 -4
  257. package/dist/next/root-layout.js +0 -5
  258. package/dist/next/vercel-collect-sdk-route.d.ts +0 -27
  259. package/dist/next/vercel-collect-sdk-route.js +0 -181
  260. package/dist/next/vercel-personalization.d.ts +0 -4
  261. package/dist/next/vercel-personalization.js +0 -4
  262. package/dist/next/vercel-s3-personalization.d.ts +0 -79
  263. package/dist/next/vercel-s3-personalization.js +0 -570
  264. package/dist/types/layout.d.ts +0 -24
  265. package/src/types/layout.ts +0 -29
  266. package/templates/astro/starter-content/src/render/RenderPage.astro +0 -67
  267. package/templates/astro/template/src/cms/contracts/layouts/index.ts +0 -18
  268. package/templates/nextjs/starter-content/src/components/elements/AtomicBlock.tsx +0 -13
  269. package/templates/nextjs/template/src/cms/contracts/layouts/index.ts +0 -16
  270. /package/dist/{types/layout.js → cli/mintlify-render.d.mts} +0 -0
  271. /package/templates/common/.content/overlays/audience/preference-blue/pages/{home/_index.json → _index.json} +0 -0
  272. /package/templates/common/.content/overlays/audience/preference-red/pages/{home/_index.json → _index.json} +0 -0
  273. /package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_home_hero → 000000000000000000000000101}/editorial.json +0 -0
  274. /package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_home_hero → 000000000000000000000000101}/versions/v0001.json +0 -0
  275. /package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_about_hero → 000000000000000000000000102}/editorial.json +0 -0
  276. /package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_about_hero → 000000000000000000000000102}/versions/v0001.json +0 -0
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
+ import { realpathSync } from 'node:fs';
2
3
  import { mkdir, writeFile } from 'node:fs/promises';
3
4
  import path from 'node:path';
4
- import { pathToFileURL } from 'node:url';
5
+ import { fileURLToPath, pathToFileURL } from 'node:url';
5
6
  import { z } from 'zod';
6
7
  import { ImageSchema } from '../types/image.ts';
7
8
  import { VideoSchema } from '../types/video.ts';
@@ -19,24 +20,27 @@ try {
19
20
  if (!globalThis.React) globalThis.React = React;
20
21
  } catch { /* react not available — component files without JSX will still work */ }
21
22
 
22
- const args = parseArgs(process.argv.slice(2));
23
-
24
- try {
25
- const result = await compileRegistry(args);
26
- console.log(JSON.stringify({ success: true, ...result, errors: [] }, null, 2));
27
- } catch (error) {
28
- console.error(JSON.stringify({ success: false, errors: [String(error?.message ?? error)] }, null, 2));
29
- process.exitCode = 1;
23
+ if (isMainModule()) {
24
+ const args = parseArgs(process.argv.slice(2));
25
+ try {
26
+ const result = await compileRegistry(args);
27
+ console.log(JSON.stringify({ success: true, ...result, errors: [] }, null, 2));
28
+ } catch (error) {
29
+ console.error(JSON.stringify({ success: false, errors: [String(error?.message ?? error)] }, null, 2));
30
+ process.exitCode = 1;
31
+ }
30
32
  }
31
33
 
32
34
  async function compileRegistry(options) {
33
- const required = ['components', 'layouts', 'output'];
35
+ if (options.layouts) {
36
+ throw new Error('--layouts is retired; author page composition under /templates instead');
37
+ }
38
+ const required = ['components', 'output'];
34
39
  for (const key of required) {
35
40
  if (!options[key]) throw new Error(`missing --${key}`);
36
41
  }
37
42
 
38
43
  const components = await loadDefaultArray(options.components, 'component registry');
39
- const layouts = await loadDefaultArray(options.layouts, 'layout registry');
40
44
  const dataModels = options.data
41
45
  ? await loadDefaultArray(options.data, 'data model registry')
42
46
  : [];
@@ -66,9 +70,6 @@ async function compileRegistry(options) {
66
70
  });
67
71
  }
68
72
 
69
- const seenLayouts = new Set();
70
- const layoutEntries = layouts.map((layout, index) => validateLayout(layout, index, seenLayouts));
71
-
72
73
  const seenDataModels = new Set();
73
74
  const dataModelEntries = [];
74
75
  if (dataModels.length > 0) {
@@ -102,7 +103,6 @@ async function compileRegistry(options) {
102
103
  manifestVersion: '1',
103
104
  generatedAt: new Date().toISOString(),
104
105
  components: componentEntries,
105
- layouts: layoutEntries,
106
106
  dataModels: dataModelEntries,
107
107
  };
108
108
  const registryPath = path.join(outDir, 'registry.json');
@@ -116,7 +116,7 @@ async function compileRegistry(options) {
116
116
 
117
117
  async function loadDefaultArray(modulePath, label) {
118
118
  const mod = await import(pathToFileURL(path.resolve(modulePath)).href);
119
- const value = mod.default ?? mod.registry ?? mod.components ?? mod.layouts ?? mod.dataModels;
119
+ const value = mod.default ?? mod.registry ?? mod.components ?? mod.dataModels;
120
120
  if (!Array.isArray(value)) throw new Error(`${label} must export an array`);
121
121
  return value;
122
122
  }
@@ -346,12 +346,10 @@ function isImageSchema(schema) {
346
346
  return compiled?.type === 'object'
347
347
  && properties.$type?.const === 'gradial.image'
348
348
  && properties.assetId?.type === 'string'
349
- && properties.versionId?.type === 'string'
350
349
  && properties.alt?.type === 'string'
351
350
  && properties.sources?.type === 'array'
352
351
  && required.has('$type')
353
352
  && required.has('assetId')
354
- && required.has('versionId')
355
353
  && required.has('alt')
356
354
  && required.has('sources');
357
355
  } catch {
@@ -388,27 +386,6 @@ function unwrapSchema(schema) {
388
386
  return current;
389
387
  }
390
388
 
391
- function validateLayout(layout, index, seen) {
392
- if (!layout || typeof layout !== 'object') throw new Error(`layouts[${index}] must be an object`);
393
- if (!layout.name) throw new Error(`layouts[${index}].name is required`);
394
- if (seen.has(layout.name)) throw new Error(`duplicate layout name ${layout.name}`);
395
- seen.add(layout.name);
396
- if (!Array.isArray(layout.slots)) throw new Error(`layouts[${index}].slots must be an array`);
397
- const slots = [];
398
- const seenSlots = new Set();
399
- for (const [slotIndex, slot] of layout.slots.entries()) {
400
- if (!slot?.name) throw new Error(`layouts[${index}].slots[${slotIndex}].name is required`);
401
- if (seenSlots.has(slot.name)) throw new Error(`duplicate slot name ${slot.name} in layout ${layout.name}`);
402
- seenSlots.add(slot.name);
403
- slots.push({ name: slot.name, required: Boolean(slot.required) });
404
- }
405
- const entry = { name: layout.name, slots };
406
- if (layout.defaults && typeof layout.defaults === 'object') {
407
- entry.defaults = layout.defaults;
408
- }
409
- return entry;
410
- }
411
-
412
389
  function compileSchema(schema, name) {
413
390
  const toJSONSchema = z.toJSONSchema;
414
391
  if (typeof toJSONSchema !== 'function') {
@@ -430,9 +407,10 @@ async function generatePageSchema(schemaDir, componentEntries, outDir) {
430
407
  const defs = {
431
408
  baseBlock: {
432
409
  type: 'object',
433
- required: ['id', 'component', 'props'],
410
+ required: ['$type', 'id', 'component', 'props'],
434
411
  additionalProperties: true,
435
412
  properties: {
413
+ $type: { const: 'component' },
436
414
  id: { type: 'string', minLength: 1 },
437
415
  component: { type: 'string' },
438
416
  props: { type: 'object' },
@@ -502,13 +480,12 @@ async function generatePageSchema(schemaDir, componentEntries, outDir) {
502
480
  $id: 'https://baremetal.local/schemas/page.schema.json',
503
481
  title: 'bare-metal Page',
504
482
  type: 'object',
505
- required: ['$type', 'id', 'status', 'layout', 'metadata', 'regions'],
483
+ required: ['$type', 'id', 'status', 'metadata', 'regions'],
506
484
  additionalProperties: true,
507
485
  properties: {
508
486
  $type: { const: 'page' },
509
487
  id: { type: 'string', minLength: 1 },
510
488
  status: { type: 'string', minLength: 1 },
511
- layout: { type: 'string', minLength: 1 },
512
489
  metadata: {
513
490
  type: 'object',
514
491
  required: ['title'],
@@ -741,3 +718,14 @@ function parseArgs(argv) {
741
718
  }
742
719
  return out;
743
720
  }
721
+
722
+ function isMainModule() {
723
+ if (!process.argv[1]) return false;
724
+ try {
725
+ return realpathSync(fileURLToPath(import.meta.url)) === realpathSync(path.resolve(process.argv[1]));
726
+ } catch {
727
+ return false;
728
+ }
729
+ }
730
+
731
+ export { compileRegistry, generatePageSchema };
@@ -0,0 +1,9 @@
1
+ import path from 'node:path';
2
+ import { loadCompiledMintlifySource, materializeMintlifySite } from '../../dist/mintlify/index.js';
3
+
4
+ const [sourceArg, compiledArg, outputArg] = process.argv.slice(2);
5
+ if (!sourceArg || !compiledArg || !outputArg) {
6
+ throw new Error('Mintlify renderer requires content, compiled, and output directories');
7
+ }
8
+ const source = await loadCompiledMintlifySource(path.resolve(sourceArg), path.resolve(compiledArg));
9
+ await materializeMintlifySite({ ...source, outputDir: path.resolve(outputArg) });
@@ -19,21 +19,24 @@
19
19
  * Uses the JSON schemas generated by compile-registry — portable, no React/CSS
20
20
  * imports needed, same schemas usable by Go backend or other tools.
21
21
  */
22
+ import { realpathSync } from 'node:fs';
22
23
  import { readdir, readFile } from 'node:fs/promises';
23
24
  import path from 'node:path';
24
-
25
- const args = parseArgs(process.argv.slice(2));
26
-
27
- try {
28
- const result = await validateContent(args);
29
- console.log(JSON.stringify(result, null, 2));
30
- if (!result.success) process.exitCode = 1;
31
- } catch (error) {
32
- console.error(JSON.stringify({
33
- success: false,
34
- errors: [{ message: String(error?.message ?? error) }],
35
- }, null, 2));
36
- process.exitCode = 1;
25
+ import { fileURLToPath } from 'node:url';
26
+
27
+ if (isMainModule()) {
28
+ const args = parseArgs(process.argv.slice(2));
29
+ try {
30
+ const result = await validateContent(args);
31
+ console.log(JSON.stringify(result, null, 2));
32
+ if (!result.success) process.exitCode = 1;
33
+ } catch (error) {
34
+ console.error(JSON.stringify({
35
+ success: false,
36
+ errors: [{ message: String(error?.message ?? error) }],
37
+ }, null, 2));
38
+ process.exitCode = 1;
39
+ }
37
40
  }
38
41
 
39
42
  // ---------------------------------------------------------------------------
@@ -95,6 +98,24 @@ async function validateContent(options) {
95
98
  for (const block of blocks) {
96
99
  blocksValidated++;
97
100
 
101
+ if (!block || typeof block !== 'object') {
102
+ collector.push({
103
+ page: relativePath,
104
+ blockId: '<unknown>',
105
+ message: 'page region entry must be a component block',
106
+ });
107
+ continue;
108
+ }
109
+
110
+ if (block.$type !== 'component') {
111
+ collector.push({
112
+ page: relativePath,
113
+ blockId: block.id ?? '<unknown>',
114
+ path: '$type',
115
+ message: 'block "$type" must be "component"',
116
+ });
117
+ }
118
+
98
119
  if (!block.component) {
99
120
  collector.push({
100
121
  page: relativePath,
@@ -380,6 +401,13 @@ function validateNestedSlots(parentBlock, componentMap, schemaMap, path = 'props
380
401
  issues.push({ path: nestedPath, message: `${parentBlock.component}: block ref child must be a block` });
381
402
  continue;
382
403
  }
404
+ if (nestedBlock.$type !== 'component') {
405
+ issues.push({
406
+ path: `${nestedPath}.$type`,
407
+ message: `${parentBlock.component}: block ref child "$type" must be "component"`,
408
+ });
409
+ continue;
410
+ }
383
411
 
384
412
  if (!componentMap.has(nestedBlock.component)) {
385
413
  issues.push({ path: `${nestedPath}.component`, message: `${parentBlock.component}: unknown nested component "${nestedBlock.component}"` });
@@ -450,3 +478,14 @@ function parseArgs(argv) {
450
478
  }
451
479
  return out;
452
480
  }
481
+
482
+ function isMainModule() {
483
+ if (!process.argv[1]) return false;
484
+ try {
485
+ return realpathSync(fileURLToPath(import.meta.url)) === realpathSync(path.resolve(process.argv[1]));
486
+ } catch {
487
+ return false;
488
+ }
489
+ }
490
+
491
+ export { validateContent };
@@ -0,0 +1,60 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+ import { z } from 'zod';
4
+
5
+ import { compileSchema } from '../compiler/zod-to-jsonschema.js';
6
+ import { defineComponentContract } from '../define-component.js';
7
+ import { blockRef, blockRefArray } from './block-ref.js';
8
+
9
+ const childContract = defineComponentContract({
10
+ id: 'child',
11
+ props: z.object({
12
+ label: z.string(),
13
+ }),
14
+ });
15
+
16
+ test('block references require and accept the component discriminator', () => {
17
+ const schema = blockRefArray([childContract]);
18
+ const typed = {
19
+ $type: 'component',
20
+ id: 'child-1',
21
+ component: 'child',
22
+ props: { label: 'Hello' },
23
+ };
24
+
25
+ assert.equal(schema.safeParse([typed]).success, true);
26
+ assert.equal(schema.safeParse([{
27
+ id: typed.id,
28
+ component: typed.component,
29
+ props: typed.props,
30
+ }]).success, false);
31
+ });
32
+
33
+ test('compiled block-reference schema exposes the component discriminator', () => {
34
+ const compiled = compileSchema(blockRefArray([childContract])) as {
35
+ items?: {
36
+ properties?: Record<string, { const?: string }>;
37
+ required?: string[];
38
+ additionalProperties?: boolean;
39
+ };
40
+ };
41
+
42
+ assert.equal(compiled.items?.properties?.$type?.const, 'component');
43
+ assert.equal(compiled.items?.required?.includes('$type'), true);
44
+ assert.equal(compiled.items?.additionalProperties, false);
45
+ });
46
+
47
+
48
+ test('block-reference contracts enforce allowed child IDs at runtime and in compiled schemas', () => {
49
+ const other = defineComponentContract({ id: 'other', props: z.object({}) });
50
+ const allowed = { $type: 'component', id: 'child-1', component: 'child', props: { label: 'Hello' } };
51
+ const disallowed = { ...allowed, component: 'button' };
52
+ for (const multiple of [false, true]) {
53
+ const schema = multiple ? blockRefArray([childContract, other]) : blockRef([childContract, other]);
54
+ assert.equal(schema.safeParse(multiple ? [allowed] : allowed).success, true);
55
+ assert.equal(schema.safeParse(multiple ? [disallowed] : disallowed).success, false, 'a nested component outside the contract must be refused');
56
+ const compiled = compileSchema(schema) as any;
57
+ const block = multiple ? compiled.items : compiled;
58
+ assert.deepEqual(block.properties.component.enum, ['child', 'other'], 'published JSON Schema must enforce the same finite child set');
59
+ }
60
+ });
@@ -9,6 +9,7 @@ export interface BlockRefMeta {
9
9
  }
10
10
 
11
11
  const blockRefBase = z.object({
12
+ $type: z.literal('component'),
12
13
  id: z.string().min(1),
13
14
  component: z.string().min(1),
14
15
  props: z.record(z.string(), z.unknown()),
@@ -21,6 +22,7 @@ type InferContractProps<T> = T extends ComponentContract<string, infer Schema>
21
22
  type InferContractId<T> = T extends ComponentContract<infer Id, unknown> ? Id : never;
22
23
 
23
24
  export type BlockRefEntry<T extends ComponentContract> = {
25
+ $type: 'component';
24
26
  id: string;
25
27
  component: InferContractId<T>;
26
28
  props: InferContractProps<T>;
@@ -48,7 +50,7 @@ export function blockRefArray<const T extends readonly ComponentContract[]>(
48
50
  contracts: T,
49
51
  ): z.ZodArray<z.ZodType<BlockRefUnion<T>>> {
50
52
  return attachMeta(
51
- z.array(blockRefBase) as unknown as z.ZodArray<z.ZodType<BlockRefUnion<T>>>,
53
+ z.array(blockRefBase.extend({ component: z.enum(contracts.map((c) => c.id)) })) as unknown as z.ZodArray<z.ZodType<BlockRefUnion<T>>>,
52
54
  contracts.map((c) => c.id),
53
55
  true,
54
56
  );
@@ -58,7 +60,7 @@ export function blockRef<const T extends readonly ComponentContract[]>(
58
60
  contracts: T,
59
61
  ): z.ZodType<BlockRefUnion<T>> {
60
62
  return attachMeta(
61
- blockRefBase as unknown as z.ZodType<BlockRefUnion<T>>,
63
+ blockRefBase.extend({ component: z.enum(contracts.map((c) => c.id)) }) as unknown as z.ZodType<BlockRefUnion<T>>,
62
64
  contracts.map((c) => c.id),
63
65
  false,
64
66
  );
@@ -23,6 +23,16 @@ export interface ComponentValidationIssue {
23
23
  severity?: 'error' | 'warning';
24
24
  }
25
25
 
26
+ /**
27
+ * Root identity props the SDK renderers inject into every page and fragment
28
+ * block (sdk I-19, ADR 0051). A component spreads them onto its own root
29
+ * element; the starter conformance test fails a component that drops them.
30
+ */
31
+ export interface RootIdentityProps {
32
+ 'data-aci-block'?: string;
33
+ 'data-aci-component'?: string;
34
+ }
35
+
26
36
  export interface ComponentDefinition<TId extends string = string, TSchema = unknown> {
27
37
  id: TId;
28
38
  displayName?: string;
@@ -9,7 +9,6 @@ export interface Config {
9
9
  };
10
10
  componentRegistry: string;
11
11
  dataRegistry?: string;
12
- layoutRegistry: string;
13
12
  routes: RouteConfig;
14
13
  dam?: DAMConfig;
15
14
  externalDependencies?: ExternalDependency[];
@@ -3,19 +3,28 @@ import { z } from 'zod';
3
3
  // ---------------------------------------------------------------------------
4
4
  // Image source variants
5
5
  //
6
- // Each source provides a primary src and may include a ready-made srcset
7
- // scoped by the optional media/sizes of the slot output that produced it.
6
+ // A DAM source carries the asset identity's slot presentation — the declared
7
+ // candidate widths, the fit, and the MIME format — and NEVER a URL: the SDK
8
+ // constructs the /dam/{assetId} permalink and its srcset at render time
9
+ // (ADR 0062: the compiler emits asset ids only). Static assets keep their
10
+ // authored src passthrough.
8
11
  // ---------------------------------------------------------------------------
9
12
 
10
13
  export interface ImageSource {
11
- /** Primary derivative URL for this rendition candidate. */
12
- src: string;
13
- /** Intrinsic width in pixels (0 when the format carries none, e.g. SVG). */
14
+ /** Authored asset path — static sources only; DAM sources never set it. */
15
+ src?: string;
16
+ /** Declared candidate widths for the srcset (DAM sources). Empty or absent
17
+ * when the slot declares no outputs: the SDK renders the bare permalink
18
+ * (no params, original bytes). */
19
+ widths?: number[];
20
+ /** Layout width in pixels — the largest declared width, or the intrinsic
21
+ * width for static/original-aspect sources. */
14
22
  width?: number;
15
- /** Intrinsic height in pixels (0 when the format carries none). */
23
+ /** Layout height in pixels — the height at `width` (aspect carrier). */
16
24
  height?: number;
17
- /** Optional width-descriptor rendition candidates. */
18
- srcset?: string;
25
+ /** Transform fit for DAM sources: 'max' fits inside the box; 'crop' fills
26
+ * the declared aspect and emits an h param per srcset width. */
27
+ fit?: 'max' | 'crop';
19
28
  /** sizes attribute from the slot contract. */
20
29
  sizes?: string;
21
30
  /** Media query scoping this source (art direction). */
@@ -25,7 +34,7 @@ export interface ImageSource {
25
34
  }
26
35
 
27
36
  // ---------------------------------------------------------------------------
28
- // Image — DAM-backed image asset with compiler-resolved derivatives
37
+ // Image — DAM-backed image asset with compiler-resolved live asset URLs
29
38
  //
30
39
  // Content authors write an asset reference: { "$type": "dam.assetRef", ... }
31
40
  // or for static files: { "$type": "static.assetRef", "src": "/path" }
@@ -37,8 +46,9 @@ export interface Image {
37
46
  $type: 'gradial.image';
38
47
  /** DAM asset identifier (or "static:<path>" for non-DAM assets). */
39
48
  assetId: string;
40
- /** DAM version identifier (or "static" for non-DAM assets). */
41
- versionId: string;
49
+ /** DAM version identifier — absent for DAM images, which compile to the
50
+ * versionless live URL /dam/{assetId}; never set for static images. */
51
+ versionId?: string;
42
52
  /** Accessible description of the image content. */
43
53
  alt: string;
44
54
  /** <source> candidates produced for the asset slot. */
@@ -66,10 +76,11 @@ export interface SlotOutput {
66
76
  // ---------------------------------------------------------------------------
67
77
 
68
78
  export const ImageSourceSchema = z.object({
69
- src: z.string().min(1),
70
- srcset: z.string().min(1).optional(),
79
+ src: z.string().min(1).optional(),
80
+ widths: z.array(z.number().int().positive()).optional(),
71
81
  width: z.number().int().nonnegative().optional(),
72
82
  height: z.number().int().nonnegative().optional(),
83
+ fit: z.enum(['max', 'crop']).optional(),
73
84
  sizes: z.string().min(1).optional(),
74
85
  media: z.string().min(1).optional(),
75
86
  type: z.string().min(1),
@@ -78,17 +89,113 @@ export const ImageSourceSchema = z.object({
78
89
  export const ImageSchema = z.object({
79
90
  $type: z.literal('gradial.image'),
80
91
  assetId: z.string().min(1),
81
- versionId: z.string().min(1),
92
+ versionId: z.string().min(1).optional(),
82
93
  alt: z.string(),
83
94
  sources: z.array(ImageSourceSchema).min(1),
84
95
  });
85
96
 
97
+ // ---------------------------------------------------------------------------
98
+ // DAM permalink construction (ADR 0062)
99
+ // ---------------------------------------------------------------------------
100
+
101
+ /** Default DAM delivery quality, matching the edge's DEFAULT_QUALITY. */
102
+ export const DAM_IMAGE_QUALITY = 82;
103
+
104
+ let damAssetBase = '';
105
+
106
+ /**
107
+ * Configures the origin for DAM permalinks. The default is the empty string:
108
+ * relative same-origin URLs (`/dam/{assetId}`), served by the site's own
109
+ * hosting edge. Set a base (e.g. `https://assets.example.com`) when images
110
+ * are consumed off-origin — email, external CMS fields, social cards — or
111
+ * when delivery is hosted on a dedicated domain. Call once during app setup,
112
+ * before the first render.
113
+ */
114
+ export function configureDamAssets(options: { baseUrl?: string } = {}): void {
115
+ damAssetBase = (options.baseUrl ?? '').replace(/\/+$/, '');
116
+ }
117
+
118
+ /**
119
+ * The DAM permalink for an asset: `{base}/dam/{assetId}`. No params means
120
+ * original bytes.
121
+ */
122
+ export function damAssetUrl(assetId: string): string {
123
+ return `${damAssetBase}/dam/${assetId}`;
124
+ }
125
+
126
+ const FORMAT_PARAMS: Record<string, string> = {
127
+ 'image/avif': 'avif',
128
+ 'image/webp': 'webp',
129
+ 'image/jpeg': 'jpg',
130
+ 'image/png': 'png',
131
+ };
132
+
133
+ function sourceFormatParams(source: ImageSource): string {
134
+ const fm = FORMAT_PARAMS[source.type] ?? 'webp';
135
+ return `fm=${fm}&q=${DAM_IMAGE_QUALITY}`;
136
+ }
137
+
138
+ function srcsetWidth(assetId: string, source: ImageSource, width: number): string {
139
+ const params = [`w=${width}`];
140
+ if (source.fit === 'crop' && source.width && source.height) {
141
+ const height = Math.round((width * source.height) / source.width);
142
+ params.push(`h=${height}`, 'fit=crop');
143
+ } else {
144
+ params.push('fit=max');
145
+ }
146
+ params.push(sourceFormatParams(source));
147
+ return `${damAssetUrl(assetId)}?${params.join('&')} ${width}w`;
148
+ }
149
+
150
+ function damImageAttrs(image: Image, primary: ImageSource): ImageAttrs {
151
+ const base: ImageAttrs = {
152
+ src: damAssetUrl(image.assetId),
153
+ alt: image.alt,
154
+ width: primary.width && primary.width > 0 ? primary.width : undefined,
155
+ height: primary.height && primary.height > 0 ? primary.height : undefined,
156
+ loading: 'lazy',
157
+ decoding: 'async',
158
+ };
159
+ if (primary.widths && primary.widths.length > 0) {
160
+ // Primary src at the largest declared width; srcset covers them all.
161
+ const largest = primary.widths[primary.widths.length - 1];
162
+ const params = [`w=${largest}`];
163
+ if (primary.fit === 'crop' && primary.width && primary.height) {
164
+ params.push(`h=${Math.round((largest * primary.height) / primary.width)}`, 'fit=crop');
165
+ } else {
166
+ params.push('fit=max');
167
+ }
168
+ params.push(sourceFormatParams(primary));
169
+ base.src = `${damAssetUrl(image.assetId)}?${params.join('&')}`;
170
+ if (primary.widths.length > 1) {
171
+ base.srcSet = primary.widths.map((w) => srcsetWidth(image.assetId, primary, w)).join(', ');
172
+ }
173
+ }
174
+ return base;
175
+ }
176
+
177
+ function staticImageAttrs(image: Image, primary: ImageSource): ImageAttrs {
178
+ return {
179
+ src: primary.src ?? '',
180
+ alt: image.alt,
181
+ width: primary.width && primary.width > 0 ? primary.width : undefined,
182
+ height: primary.height && primary.height > 0 ? primary.height : undefined,
183
+ loading: 'lazy',
184
+ decoding: 'async',
185
+ };
186
+ }
187
+
86
188
  // ---------------------------------------------------------------------------
87
189
  // Shared attrs helper — consumed by React, Astro, and Svelte components
88
190
  // ---------------------------------------------------------------------------
89
191
 
90
192
  export type ImageAttrs = Record<string, unknown>;
91
193
 
194
+ /** True when the image resolves to authored static bytes, not a DAM asset. */
195
+ export function isStaticImage(image: Image): boolean {
196
+ return image.assetId.startsWith('static:');
197
+ }
198
+
92
199
  /**
93
200
  * The primary rendition of an image — its first source entry.
94
201
  */
@@ -100,24 +207,21 @@ export function primaryImageSource(image: Image): ImageSource {
100
207
  * Computes the full set of <img> attributes for an Image.
101
208
  *
102
209
  * Framework components (React <Picture>, Astro <img>, Svelte <img>) call this
103
- * and spread the result into their native element. No HTML string
104
- * serialization. Media-scoped sources need a <picture> wrapper and are not
105
- * representable as plain <img> attributes, so srcset/sizes are taken from the
106
- * first unscoped source only.
210
+ * and spread the result into the native element. No HTML string
211
+ * serialization. DAM images construct their /dam/{assetId} permalink and
212
+ * srcset here from the slot-declared widths; static images pass their
213
+ * authored src through. Media-scoped sources need a <picture> wrapper and
214
+ * are not representable as plain <img> attributes, so srcset/sizes are taken
215
+ * from the first unscoped source only.
107
216
  */
108
217
  export function imageAttrs(image: Image, overrides: ImageAttrs = {}): ImageAttrs {
109
218
  const primary = image.sources[0];
110
219
  const unscoped = image.sources.find((s) => !s.media) ?? primary;
111
220
 
112
- return {
113
- src: primary.src,
114
- alt: image.alt,
115
- width: primary.width && primary.width > 0 ? primary.width : undefined,
116
- height: primary.height && primary.height > 0 ? primary.height : undefined,
117
- srcSet: unscoped?.srcset || undefined,
118
- sizes: unscoped?.sizes || undefined,
119
- loading: 'lazy',
120
- decoding: 'async',
121
- ...overrides,
122
- };
221
+ const base = isStaticImage(image) ? staticImageAttrs(image, primary) : damImageAttrs(image, primary);
222
+
223
+ if (unscoped?.sizes) {
224
+ base.sizes = unscoped.sizes;
225
+ }
226
+ return { ...base, ...overrides };
123
227
  }
@@ -1,7 +1,6 @@
1
1
  export * from './config.js';
2
2
  export * from './component.js';
3
3
  export * from './data.js';
4
- export * from './layout.js';
5
4
  export * from './page.js';
6
5
  export * from './rich-text.js';
7
6
  export * from './image.js';
package/src/types/page.ts CHANGED
@@ -34,5 +34,4 @@ export interface BlockNode {
34
34
  export interface FragmentRefNode {
35
35
  kind: 'fragment-ref';
36
36
  fragmentId: string;
37
- inline: boolean;
38
37
  }
@@ -11,7 +11,7 @@ export interface VideoSource {
11
11
  }
12
12
 
13
13
  // ---------------------------------------------------------------------------
14
- // Video — DAM-backed video asset with compiler-resolved derivatives
14
+ // Video — DAM-backed video asset with compiler-resolved live asset URLs
15
15
  //
16
16
  // Content authors write: { "$type": "dam.assetRef", ... }
17
17
  // or for static files: { "$type": "static.assetRef", "src": "/path" }
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  import Container from '../elements/Container.astro';
3
+ import type { RootIdentityProps } from '@gradial/aci';
3
4
  import Logo from '../elements/Logo.astro';
4
5
 
5
6
  interface FooterLink {
@@ -21,7 +22,7 @@ interface FooterColumn {
21
22
  links?: FooterLink[];
22
23
  }
23
24
 
24
- interface Props {
25
+ interface Props extends RootIdentityProps {
25
26
  brandHref: string;
26
27
  brandLabel?: string;
27
28
  brandLogo?: { src: string; alt?: string } | null;
@@ -38,7 +39,8 @@ const {
38
39
  social = [],
39
40
  columns = [],
40
41
  legalLinks = [],
41
- copyright = ''
42
+ copyright = '',
43
+ ...root
42
44
  } = Astro.props;
43
45
 
44
46
  function columnGroups(column: FooterColumn): FooterGroup[] {
@@ -54,7 +56,7 @@ function columnGroups(column: FooterColumn): FooterGroup[] {
54
56
  }
55
57
  ---
56
58
 
57
- <footer class="bm-footer [font-family:var(--font-family-base)] text-fg" id="footer">
59
+ <footer {...root} class="bm-footer [font-family:var(--font-family-base)] text-fg" id="footer">
58
60
  <Container className="py-[var(--spacing-footer-y)]">
59
61
  <div class="grid gap-12 pb-24 md:grid-cols-[var(--footer-grid-columns)] lg:gap-24">
60
62
  <a href={brandHref} class="self-start">