create-ailk 0.12.1 → 0.14.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 (163) hide show
  1. package/README.md +12 -4
  2. package/component-catalog.json +247 -33
  3. package/component-catalog.md +10 -6
  4. package/dist/apply-design.d.ts +21 -2
  5. package/dist/apply-design.js +120 -19
  6. package/dist/cli.js +1 -0
  7. package/dist/design-compile.d.ts +70 -2
  8. package/dist/design-compile.js +97 -1
  9. package/dist/design-home.d.ts +74 -0
  10. package/dist/design-home.js +256 -0
  11. package/dist/design-package.d.ts +39 -2
  12. package/dist/design-package.js +223 -9
  13. package/dist/design-presets.d.ts +17 -1
  14. package/dist/design-presets.js +17 -5
  15. package/dist/design-scaffold.d.ts +6 -25
  16. package/dist/design-scaffold.js +26 -74
  17. package/dist/index.d.ts +1 -0
  18. package/dist/lib/design-checks.d.ts +12 -1
  19. package/dist/lib/design-checks.js +38 -2
  20. package/dist/lib/module-license-gate.d.ts +9 -0
  21. package/dist/lib/module-license-gate.js +18 -0
  22. package/dist/module-architecture.js +27 -0
  23. package/dist/programmatic.js +70 -26
  24. package/dist/site-chrome.d.ts +38 -0
  25. package/dist/site-chrome.js +109 -2
  26. package/dist/site-fonts.d.ts +21 -0
  27. package/dist/site-fonts.js +54 -0
  28. package/dist/site-roster.d.ts +16 -0
  29. package/dist/site-roster.js +81 -0
  30. package/dist/starter-grid.js +15 -0
  31. package/dist/surfaces.js +12 -0
  32. package/dist/team-profiles.d.ts +29 -0
  33. package/dist/team-profiles.js +43 -0
  34. package/package.json +3 -3
  35. package/release-scope.json +1 -1
  36. package/site-templates/_base/content/en/TeamList/team.mdx +68 -0
  37. package/site-templates/_base/content/en/TeamMember/first-member.mdx +22 -0
  38. package/site-templates/_base/content/en/TeamMember/second-member.mdx +22 -0
  39. package/site-templates/_base/content/en/TeamMember/third-member.mdx +22 -0
  40. package/site-templates/designs/minimalism/DESIGN.md +164 -34
  41. package/site-templates/designs/minimalism/home/local-business-service.mdx +119 -0
  42. package/site-templates/designs/minimalism/home/organization-product.mdx +132 -0
  43. package/site-templates/designs/minimalism/home/organization-service.mdx +131 -0
  44. package/site-templates/designs/minimalism/home/person-service.mdx +101 -0
  45. package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +34 -1
  46. package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +20 -0
  47. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +34 -1
  48. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +22 -1
  49. package/site-templates/marketing-site-free/ailk-template.json +1 -1
  50. package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.json +34 -1
  51. package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.md +26 -7
  52. package/site-templates/shapes/local-business-service/ailk-template.json +1 -1
  53. package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.json +34 -1
  54. package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.md +26 -7
  55. package/site-templates/shapes/organization-service/ailk-template.json +1 -1
  56. package/templates/.env.example +23 -9
  57. package/templates/README.md +2 -0
  58. package/templates/apps/api/CLAUDE.md +24 -17
  59. package/templates/apps/api/src/__tests__/factories/agent-caller.ts +98 -0
  60. package/templates/apps/api/src/__tests__/factories/in-memory-prisma.ts +321 -0
  61. package/templates/apps/api/src/__tests__/schedule-forwarded-rate-limit.test.ts +117 -0
  62. package/templates/apps/api/src/config/__tests__/hub-require-spoke-key.test.ts +39 -14
  63. package/templates/apps/api/src/config/index.ts +35 -20
  64. package/templates/apps/api/src/lib/__mocks__/prisma-client.js +3 -0
  65. package/templates/apps/api/src/lib/__mocks__/prisma.ts +14 -0
  66. package/templates/apps/api/src/lib/__tests__/agent-store.test.ts +468 -0
  67. package/templates/apps/api/src/lib/__tests__/spoke-store.test.ts +32 -2
  68. package/templates/apps/api/src/lib/agent-store.ts +521 -0
  69. package/templates/apps/api/src/lib/spoke-store.ts +13 -2
  70. package/templates/apps/api/src/middleware/__tests__/auth.bearer-path.test.ts +384 -1
  71. package/templates/apps/api/src/middleware/__tests__/auth.cookie-path.test.ts +61 -0
  72. package/templates/apps/api/src/middleware/__tests__/default-tenant.test.ts +176 -0
  73. package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +76 -0
  74. package/templates/apps/api/src/middleware/auth.ts +219 -5
  75. package/templates/apps/api/src/middleware/default-tenant.ts +29 -8
  76. package/templates/apps/api/src/middleware/rate-limit.ts +35 -13
  77. package/templates/apps/api/src/routes/leads/__tests__/spoke.test.ts +76 -15
  78. package/templates/apps/api/src/routes/leads/index.ts +14 -6
  79. package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +5 -6
  80. package/templates/apps/api/src/routes/project-listings/index.ts +14 -9
  81. package/templates/apps/api/src/routes/project-listings/me.ts +4 -3
  82. package/templates/apps/api/src/routes/project-listings/patch-copy.ts +7 -5
  83. package/templates/apps/api/src/routes/workspaces/__tests__/inbox-wire-schemas.test.ts +198 -0
  84. package/templates/apps/api/src/server.ts +27 -0
  85. package/templates/apps/api/src/services/__tests__/audit-log.test.ts +374 -57
  86. package/templates/apps/api/src/services/__tests__/leads-inbox.test.ts +25 -2
  87. package/templates/apps/api/src/services/__tests__/spoke-resolution.test.ts +303 -17
  88. package/templates/apps/api/src/services/audit-log.ts +278 -47
  89. package/templates/apps/api/src/services/default-tenant.ts +55 -5
  90. package/templates/apps/api/src/services/leads-inbox.ts +12 -21
  91. package/templates/apps/api/src/services/project-listings.ts +1 -21
  92. package/templates/apps/api/src/services/public-site-resolver.ts +103 -13
  93. package/templates/apps/mcp/CLAUDE.md +44 -5
  94. package/templates/apps/mcp/__tests__/parity.test.ts +53 -1
  95. package/templates/apps/mcp/__tests__/workspace-surface.test.ts +364 -0
  96. package/templates/apps/mcp/package.json +1 -1
  97. package/templates/apps/mcp/src/http-transport.ts +107 -1
  98. package/templates/apps/mcp/src/workspace-server.ts +193 -0
  99. package/templates/apps/web/.env.example +64 -37
  100. package/templates/apps/web/CLAUDE.md +11 -0
  101. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/settings.test.tsx +10 -0
  102. package/templates/apps/web/app/[locale]/(authed)/account/settings/__tests__/theme-modes.test.ts +42 -0
  103. package/templates/apps/web/app/[locale]/(authed)/account/settings/theme-modes.ts +8 -0
  104. package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +2 -2
  105. package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +2 -2
  106. package/templates/apps/web/app/[locale]/clients/__tests__/page-renderer-migration.test.tsx +14 -1
  107. package/templates/apps/web/app/[locale]/contact/__tests__/site-contact-lines.test.ts +69 -0
  108. package/templates/apps/web/app/[locale]/contact/site-contact-lines.ts +64 -0
  109. package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +4 -4
  110. package/templates/apps/web/app/[locale]/downloads/__tests__/page-renderer-migration.test.tsx +15 -3
  111. package/templates/apps/web/app/[locale]/events/__tests__/page-renderer-migration.test.tsx +17 -6
  112. package/templates/apps/web/app/[locale]/for/__tests__/page-renderer-migration.test.tsx +14 -1
  113. package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +2 -2
  114. package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +4 -6
  115. package/templates/apps/web/app/[locale]/layout.tsx +156 -53
  116. package/templates/apps/web/app/[locale]/page.tsx +37 -0
  117. package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +2 -2
  118. package/templates/apps/web/app/[locale]/products/__tests__/page-renderer-migration.test.tsx +4 -6
  119. package/templates/apps/web/app/[locale]/resources/__tests__/page-renderer-migration.test.tsx +15 -2
  120. package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +43 -6
  121. package/templates/apps/web/app/[locale]/services/page.tsx +14 -2
  122. package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +2 -2
  123. package/templates/apps/web/app/[locale]/team/[slug]/page.tsx +45 -0
  124. package/templates/apps/web/app/[locale]/team/__tests__/team-routes.test.tsx +155 -0
  125. package/templates/apps/web/app/[locale]/team/page.tsx +49 -0
  126. package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +2 -2
  127. package/templates/apps/web/app/[locale]/testimonials/__tests__/page-renderer-migration.test.tsx +14 -1
  128. package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +2 -2
  129. package/templates/apps/web/app/not-found.tsx +47 -0
  130. package/templates/apps/web/app/v1/[...path]/route.ts +16 -0
  131. package/templates/apps/web/lib/__tests__/profile-back-link.test.ts +95 -0
  132. package/templates/apps/web/lib/__tests__/relay.test.ts +515 -0
  133. package/templates/apps/web/lib/__tests__/site-theme-head.test.ts +70 -0
  134. package/templates/apps/web/lib/__tests__/site-theme.test.ts +64 -1
  135. package/templates/apps/web/lib/profile-back-link.ts +51 -0
  136. package/templates/apps/web/lib/relay.ts +279 -0
  137. package/templates/apps/web/lib/site-chrome.ts +12 -0
  138. package/templates/apps/web/lib/site-fonts.ts +15 -0
  139. package/templates/apps/web/lib/site-logo.tsx +39 -1
  140. package/templates/apps/web/lib/site-theme-head.ts +33 -0
  141. package/templates/apps/web/lib/site-theme.ts +13 -1
  142. package/templates/apps/web/messages/en.json +7 -1
  143. package/templates/apps/web/middleware.ts +5 -4
  144. package/templates/apps/web/next.config.mjs +6 -0
  145. package/templates/database/CHANGELOG.md +22 -0
  146. package/templates/database/__tests__/agents.db.test.ts +235 -0
  147. package/templates/database/auth/schema.prisma +61 -0
  148. package/templates/database/migrations/20261005120000_agents/migration.sql +84 -0
  149. package/templates/database/package.json +1 -1
  150. package/versions.json +35 -35
  151. package/site-templates/designs/minimalism/sample/README.md +0 -48
  152. package/site-templates/designs/minimalism/sample/content/en/HomePage/home.mdx +0 -244
  153. package/site-templates/designs/minimalism/sample/public/avatar-placeholder-1.svg +0 -7
  154. package/site-templates/designs/minimalism/sample/public/avatar-placeholder-2.svg +0 -7
  155. package/site-templates/designs/minimalism/sample/public/avatar-placeholder-3.svg +0 -7
  156. package/site-templates/designs/minimalism/sample/public/avatar-placeholder-4.svg +0 -7
  157. package/site-templates/designs/minimalism/sample/public/avatar-placeholder-5.svg +0 -7
  158. package/site-templates/designs/minimalism/sample/public/avatar-placeholder-6.svg +0 -7
  159. package/site-templates/designs/minimalism/sample/public/hero-backdrop.svg +0 -17
  160. package/site-templates/designs/minimalism/sample/public/logo-placeholder-1.svg +0 -6
  161. package/site-templates/designs/minimalism/sample/public/logo-placeholder-2.svg +0 -6
  162. package/site-templates/designs/minimalism/sample/public/logo-placeholder-3.svg +0 -6
  163. package/site-templates/designs/minimalism/sample/public/wordmark-placeholder.svg +0 -28
@@ -0,0 +1,256 @@
1
+ /**
2
+ * A design's home arc (#7743, spec
3
+ * docs/specs/7743-minimalism-b5-the-design-seeds-spec.md D1, D3, D4) — the
4
+ * one place a design decides a page's SECTIONS rather than only their looks,
5
+ * and only for the home page, and only while the home is still a placeholder.
6
+ *
7
+ * A design package may carry `home/<shape>.mdx`: a complete `HomePage` whose
8
+ * `sections:` value is the design's arc for that shape. `planDesignHome`
9
+ * replaces a site home's `sections:` value with it when the home's content
10
+ * fingerprint is a known placeholder (the shape's own home, or a shipped
11
+ * design's arc of it); any authored home is left alone.
12
+ *
13
+ * Pure apart from `readDesignHomeSeeds`/`designHomeSeeds`: every directory it
14
+ * reads is passed in (no `import.meta.url` — Jest's CJS transform).
15
+ */
16
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
17
+ import { join } from "node:path";
18
+ import { DesignPackageError } from "./design-package.js";
19
+ import { locatePageSections, pageTypeOf, } from "./design-presets.js";
20
+ import { RECOGNIZED_SHAPES } from "./parse-args.js";
21
+ import { STARTER_DESIGNS, TemplateError } from "./template-manifest.js";
22
+ /** Where a site's home page lives, site-relative. */
23
+ export const HOME_PAGE_PATH = "content/en/HomePage/home.mdx";
24
+ // ── D1: reading a design's seeds ───────────────────────────────────────────
25
+ /** Flow punctuation: whitespace beside it is layout, never content. */
26
+ const FLOW_PUNCTUATION = new Set(["{", "}", "[", "]", ",", ":"]);
27
+ /**
28
+ * A raw flow value in a layout-free form: outside quoted strings, whitespace
29
+ * beside flow punctuation is removed and any other run (inside a plain
30
+ * scalar) becomes one space, and a comma before `]` or `}` is dropped.
31
+ * Quoted strings are kept byte-for-byte.
32
+ */
33
+ function normalizeRaw(raw) {
34
+ let out = "";
35
+ let pendingSpace = false;
36
+ for (let i = 0; i < raw.length; i++) {
37
+ const ch = raw[i];
38
+ if (/\s/.test(ch)) {
39
+ pendingSpace = out !== "";
40
+ continue;
41
+ }
42
+ if (pendingSpace && !FLOW_PUNCTUATION.has(ch) && !FLOW_PUNCTUATION.has(out.at(-1)))
43
+ out += " ";
44
+ pendingSpace = false;
45
+ if (ch === '"' || ch === "'") {
46
+ let j = i + 1;
47
+ while (j < raw.length) {
48
+ if (ch === '"' && raw[j] === "\\")
49
+ j += 2;
50
+ else if (raw[j] === ch && ch === "'" && raw[j + 1] === "'")
51
+ j += 2;
52
+ else if (raw[j] === ch)
53
+ break;
54
+ else
55
+ j++;
56
+ }
57
+ out += raw.slice(i, j + 1);
58
+ i = j;
59
+ }
60
+ else if ((ch === "]" || ch === "}") && out.endsWith(",")) {
61
+ out = out.slice(0, -1) + ch;
62
+ }
63
+ else {
64
+ out += ch;
65
+ }
66
+ }
67
+ return out;
68
+ }
69
+ /** The raw value of a located section's `data` entry, or undefined. */
70
+ function rawEntry(text, sections, index, key) {
71
+ const entry = sections[index]?.entries.find((e) => e.key === key);
72
+ return entry === undefined ? undefined : text.slice(entry.valueStart, entry.valueEnd);
73
+ }
74
+ /**
75
+ * Read and validate `<dir>/home/*` (D1). Refuses with a `DesignPackageError`
76
+ * naming the file, before any write: a basename that is not a recognized
77
+ * shape, a page whose `type` is not `HomePage`, `sections` that cannot be
78
+ * located (or are not in the flow form a carry can splice), a section type
79
+ * the catalog does not carry, or a `logoCloud` whose `logos` is not `[]`.
80
+ */
81
+ export function readDesignHomeSeeds(dir, opts) {
82
+ const homeDir = join(dir, "home");
83
+ if (!existsSync(homeDir))
84
+ return [];
85
+ const seeds = [];
86
+ for (const name of readdirSync(homeDir).sort()) {
87
+ const file = join(homeDir, name);
88
+ const shape = name.replace(/\.mdx$/, "");
89
+ if (!name.endsWith(".mdx") || !RECOGNIZED_SHAPES.includes(shape)) {
90
+ throw new DesignPackageError(file, "home", `is not named after a recognized shape (expected <shape>.mdx, one of ${RECOGNIZED_SHAPES.join(", ")})`);
91
+ }
92
+ const text = readFileSync(file, "utf8");
93
+ let pageType;
94
+ let sections;
95
+ try {
96
+ pageType = pageTypeOf(text, file);
97
+ sections = locatePageSections(text, file);
98
+ }
99
+ catch (err) {
100
+ if (!(err instanceof TemplateError))
101
+ throw err;
102
+ throw new DesignPackageError(file, "sections", `cannot be located (${err.message})`);
103
+ }
104
+ if (pageType !== "HomePage") {
105
+ throw new DesignPackageError(file, "type", `must be "HomePage" (got ${JSON.stringify(pageType ?? null)})`);
106
+ }
107
+ if (sections === undefined || sections.some((s) => s.form !== "flow")) {
108
+ throw new DesignPackageError(file, "sections", "must be a flow sequence of sections");
109
+ }
110
+ const catalog = opts.catalog();
111
+ if (sections.filter((s) => s.type === "logoCloud").length > 1) {
112
+ throw new DesignPackageError(file, "sections", "carries more than one logoCloud — the arc has one proof slide");
113
+ }
114
+ sections.forEach((section, index) => {
115
+ if (!Object.prototype.hasOwnProperty.call(catalog, section.type)) {
116
+ throw new DesignPackageError(file, `sections[${index}].type`, `"${section.type}" is not a registered section type`);
117
+ }
118
+ if (section.type === "logoCloud") {
119
+ const logos = rawEntry(text, sections, index, "logos");
120
+ if (logos === undefined || normalizeRaw(logos) !== "[]") {
121
+ throw new DesignPackageError(file, `sections[${index}].data.logos`, "must be [] — a design ships no logo; the home's own logos are carried into it");
122
+ }
123
+ }
124
+ });
125
+ seeds.push({ shape, file, text });
126
+ }
127
+ return seeds;
128
+ }
129
+ /**
130
+ * The seeds a resolved design uses (D1): its own `home/`, else — a
131
+ * byte-identical copy of a shipped design IS that design — the shipped
132
+ * design's, else none.
133
+ */
134
+ export function designHomeSeeds(resolved, opts) {
135
+ if (existsSync(join(resolved.dir, "home")))
136
+ return readDesignHomeSeeds(resolved.dir, opts);
137
+ if (STARTER_DESIGNS.includes(resolved.recorded)) {
138
+ return readDesignHomeSeeds(join(opts.designsDir, resolved.recorded), opts);
139
+ }
140
+ return [];
141
+ }
142
+ /** Every shipped design's seeds (D3's placeholder set reads them all). */
143
+ export function shippedHomeSeeds(opts) {
144
+ return STARTER_DESIGNS.flatMap((name) => readDesignHomeSeeds(join(opts.designsDir, name), opts));
145
+ }
146
+ /** The shape's own home: its shape tree's, else the overlay's (spec § 1.4). */
147
+ export function shapeHomePath(shape, opts) {
148
+ const own = join(opts.siteTemplatesDir, "shapes", shape, ...HOME_PAGE_PATH.split("/"));
149
+ return existsSync(own) ? own : join(opts.templatesDir, ...HOME_PAGE_PATH.split("/"));
150
+ }
151
+ // ── D3: the content fingerprint ────────────────────────────────────────────
152
+ function locate(text, file) {
153
+ const sections = locatePageSections(text, file);
154
+ if (sections === undefined)
155
+ throw new TemplateError(`${file}: sections — the page has none`);
156
+ return sections;
157
+ }
158
+ /**
159
+ * A home's content fingerprint (D3): per section in order, its type and its
160
+ * non-look `data` entries as `[key, normalized raw value]`, sorted by key.
161
+ * A reflow, a look-field change or a preset leaves it unchanged; ids, the
162
+ * title, the description and the body are not in it.
163
+ */
164
+ export function fingerprint(text, catalog, file = "home.mdx") {
165
+ const sections = locate(text, file);
166
+ return JSON.stringify(sections.map((section) => {
167
+ const look = new Set(catalog[section.type]?.look ?? []);
168
+ const entries = section.entries
169
+ .filter((e) => !look.has(e.key))
170
+ .map((e) => [e.key, normalizeRaw(text.slice(e.valueStart, e.valueEnd))])
171
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
172
+ return [section.type, entries];
173
+ }));
174
+ }
175
+ /** `page` with its `sections:` value replaced by `value`; nothing else moves. */
176
+ function spliceSections(page, value, file) {
177
+ const sections = locate(page, file);
178
+ return page.slice(0, sections.valueStart) + value + page.slice(sections.valueEnd);
179
+ }
180
+ // ── D4: the carry ──────────────────────────────────────────────────────────
181
+ /**
182
+ * The seed's `sections:` value with D4's carry applied from `home`: a seed
183
+ * `logoCloud` (its `logos: []`) takes the raw `logos` of the home's first
184
+ * `logoCloud`, or is dropped when the home has none.
185
+ */
186
+ export function applySeed(home, seed, file = "home.mdx") {
187
+ const seedSections = locate(seed.text, seed.file);
188
+ const homeSections = locate(home, file);
189
+ const homeCloud = homeSections.findIndex((s) => s.type === "logoCloud");
190
+ const homeLogos = homeCloud < 0 ? undefined : homeSections[homeCloud].entries.find((e) => e.key === "logos");
191
+ // From just after the key's `:`, so the value keeps the line break prettier gave it.
192
+ // Only a flow sequence can sit inside the seed's flow mapping; anything else is not carried.
193
+ const raw = homeLogos === undefined ? undefined : home.slice(home.lastIndexOf(":", homeLogos.valueStart) + 1, homeLogos.valueEnd);
194
+ const carried = raw !== undefined && raw.trimStart().startsWith("[") ? raw : undefined;
195
+ let text = seed.text;
196
+ // Right to left, so earlier spans stay valid (a seed holds at most one logoCloud, D1).
197
+ for (let index = seedSections.length - 1; index >= 0; index--) {
198
+ const section = seedSections[index];
199
+ if (section.type !== "logoCloud")
200
+ continue;
201
+ if (carried !== undefined) {
202
+ const logos = section.entries.find((e) => e.key === "logos");
203
+ text = text.slice(0, text.lastIndexOf(":", logos.valueStart) + 1) + carried + text.slice(logos.valueEnd);
204
+ continue;
205
+ }
206
+ const next = seedSections[index + 1];
207
+ const previous = seedSections[index - 1];
208
+ const [from, to] = next !== undefined
209
+ ? [section.start, next.start]
210
+ : previous !== undefined
211
+ ? [previous.end, section.end]
212
+ : [section.start, section.end];
213
+ text = text.slice(0, from) + text.slice(to);
214
+ }
215
+ const located = locate(text, seed.file);
216
+ return text.slice(located.valueStart, located.valueEnd);
217
+ }
218
+ // ── D3: the decision ───────────────────────────────────────────────────────
219
+ /**
220
+ * Plan a site home under a design (D3). Writes nothing: `text` is the new
221
+ * home when the outcome is `seeded` or `restored`, and absent otherwise.
222
+ */
223
+ export function planDesignHome(opts) {
224
+ const { current, shape, target, shapeHome, catalog } = opts;
225
+ if (current === undefined)
226
+ return { outcome: "no-home" };
227
+ if (shape === undefined)
228
+ return { outcome: "no-shape" };
229
+ let currentPrint;
230
+ try {
231
+ currentPrint = fingerprint(current, catalog, HOME_PAGE_PATH);
232
+ }
233
+ catch (err) {
234
+ // A home whose sections cannot be read is not a placeholder: never write it.
235
+ if (err instanceof TemplateError)
236
+ return { outcome: "authored" };
237
+ throw err;
238
+ }
239
+ const seeds = [...opts.shippedSeeds.filter((s) => s.shape === shape), ...(target !== undefined ? [target] : [])];
240
+ const placeholders = new Set([
241
+ fingerprint(shapeHome, catalog),
242
+ ...seeds.map((seed) => fingerprint(spliceSections(shapeHome, applySeed(shapeHome, seed), "shape home"), catalog)),
243
+ ]);
244
+ if (!placeholders.has(currentPrint))
245
+ return { outcome: "authored" };
246
+ const wanted = target !== undefined
247
+ ? applySeed(current, target, HOME_PAGE_PATH)
248
+ : (() => {
249
+ const own = locate(shapeHome, "shape home");
250
+ return shapeHome.slice(own.valueStart, own.valueEnd);
251
+ })();
252
+ const text = spliceSections(current, wanted, HOME_PAGE_PATH);
253
+ if (fingerprint(text, catalog) === currentPrint)
254
+ return { outcome: "unchanged" };
255
+ return { outcome: target !== undefined ? "seeded" : "restored", text };
256
+ }
@@ -11,13 +11,31 @@ export type DesignColors = {
11
11
  } & {
12
12
  [K in DesignColorKeyOptional]?: string;
13
13
  };
14
- /** Typography role fields, D2's `⊆ {fontFamily, fontSize, fontWeight, lineHeight, letterSpacing}` (`fontSize` required). */
14
+ /** The closed font-source enum (#7720 D1) — a Google Fonts family, self-hosted at build by next/font. */
15
+ export declare const DESIGN_FONT_SOURCE_VALUES: readonly ["google"];
16
+ /** Duplicated from site-config's `FONT_FAMILY_REGEX` (#7720 D1) — proven equal by test. */
17
+ export declare const DESIGN_FONT_STACK_ENTRY_REGEX: RegExp;
18
+ /**
19
+ * #7739 D1.3 — a type size a `typeScale: design` package compiles: a plain
20
+ * length in rem, px or em. Duplicated from site-config's `TYPE_SIZE_REGEX` —
21
+ * proven equal by test.
22
+ */
23
+ export declare const DESIGN_TYPE_SIZE_REGEX: RegExp;
24
+ /**
25
+ * Typography role fields, D2's `⊆ {fontFamily, fontSize, fontWeight, lineHeight, letterSpacing}`
26
+ * (`fontSize` required), plus #7720's `fontSource` (display/body only) and
27
+ * `lineHeightMobile` (display only), and #7739's `fontSizeMobile` (the three
28
+ * heading roles only) — each set only when declared.
29
+ */
15
30
  export interface DesignTypographyRole {
16
31
  fontFamily?: string;
17
32
  fontSize: string;
18
33
  fontWeight?: number;
19
34
  lineHeight?: number;
20
35
  letterSpacing?: string;
36
+ fontSource?: (typeof DESIGN_FONT_SOURCE_VALUES)[number];
37
+ lineHeightMobile?: number;
38
+ fontSizeMobile?: string;
21
39
  }
22
40
  /** Role names are open (D2). */
23
41
  export type DesignTypography = Record<string, DesignTypographyRole>;
@@ -29,12 +47,29 @@ export declare const DESIGN_CORNERS_VALUES: readonly ["rounded", "square"];
29
47
  export declare const DESIGN_EYEBROW_CASE_VALUES: readonly ["upper", "sentence"];
30
48
  export declare const DESIGN_PLACEHOLDERS_VALUES: readonly ["outline", "fill"];
31
49
  export declare const DESIGN_CHECKMARKS_VALUES: readonly ["circle", "plain"];
32
- /** D2's optional `treatments` group — the four `SiteThemeSchema` shape settings. */
50
+ /** #7741 D7 — the button shape. */
51
+ export declare const DESIGN_BUTTON_SHAPE_VALUES: readonly ["control", "pill"];
52
+ export declare const DESIGN_DARK_INK_VALUES: readonly ["fg", "accent"];
53
+ export declare const DESIGN_FIGURES_VALUES: readonly ["platform", "display"];
54
+ /**
55
+ * #7739 D1 — whether the compile writes the design's own type roles into the
56
+ * theme. A compile instruction, not a runtime setting: never written into `theme:`.
57
+ */
58
+ export declare const DESIGN_TYPE_SCALE_VALUES: readonly ["platform", "design"];
59
+ /**
60
+ * D2's optional `treatments` group — the four `SiteThemeSchema` shape
61
+ * settings plus #7741's `buttonShape`, #7739's `darkInk` and `figures` (also runtime settings) and its
62
+ * `typeScale` compile instruction.
63
+ */
33
64
  export interface DesignTreatments {
34
65
  corners?: (typeof DESIGN_CORNERS_VALUES)[number];
35
66
  eyebrowCase?: (typeof DESIGN_EYEBROW_CASE_VALUES)[number];
36
67
  placeholders?: (typeof DESIGN_PLACEHOLDERS_VALUES)[number];
37
68
  checkmarks?: (typeof DESIGN_CHECKMARKS_VALUES)[number];
69
+ buttonShape?: (typeof DESIGN_BUTTON_SHAPE_VALUES)[number];
70
+ typeScale?: (typeof DESIGN_TYPE_SCALE_VALUES)[number];
71
+ darkInk?: (typeof DESIGN_DARK_INK_VALUES)[number];
72
+ figures?: (typeof DESIGN_FIGURES_VALUES)[number];
38
73
  }
39
74
  /** D2's optional `sections` group — carried verbatim; look-field validation is #6803's. */
40
75
  export type DesignSections = Record<string, Record<string, unknown>>;
@@ -72,6 +107,8 @@ export declare class DesignPackageError extends Error {
72
107
  readonly field: string;
73
108
  constructor(file: string, field: string, reason: string);
74
109
  }
110
+ /** One stack entry, trimmed, with one matching pair of `'` or `"` removed. */
111
+ export declare function unquoteFamily(entry: string): string;
75
112
  /**
76
113
  * Read and validate `<dir>/DESIGN.md`'s frontmatter into a typed
77
114
  * `DesignPackage` (D1/D2). Refuses — naming the file and the field — on any
@@ -45,11 +45,30 @@ export const DESIGN_COLOR_KEYS_OPTIONAL = [
45
45
  ];
46
46
  /** Duplicated from `SiteThemeSchema`'s colour regex (D3) — proven equal by test. */
47
47
  export const DESIGN_COLOR_REGEX = /^(#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\(.+\)|rgba\(.+\)|hsl\(.+\)|hsla\(.+\)|var\(--[^)]+\))$/;
48
+ /** The closed font-source enum (#7720 D1) — a Google Fonts family, self-hosted at build by next/font. */
49
+ export const DESIGN_FONT_SOURCE_VALUES = ["google"];
50
+ /** Duplicated from site-config's `FONT_FAMILY_REGEX` (#7720 D1) — proven equal by test. */
51
+ export const DESIGN_FONT_STACK_ENTRY_REGEX = /^(?:[A-Za-z0-9 ._-]+|'[A-Za-z0-9 ._-]+'|"[A-Za-z0-9 ._-]+")$/;
52
+ /**
53
+ * #7739 D1.3 — a type size a `typeScale: design` package compiles: a plain
54
+ * length in rem, px or em. Duplicated from site-config's `TYPE_SIZE_REGEX` —
55
+ * proven equal by test.
56
+ */
57
+ export const DESIGN_TYPE_SIZE_REGEX = /^(?:\d+|\d*\.\d+)(?:rem|px|em)$/;
48
58
  /** Duplicated from `SiteThemeSchema`'s enums (D3) — proven equal by test. */
49
59
  export const DESIGN_CORNERS_VALUES = ["rounded", "square"];
50
60
  export const DESIGN_EYEBROW_CASE_VALUES = ["upper", "sentence"];
51
61
  export const DESIGN_PLACEHOLDERS_VALUES = ["outline", "fill"];
52
62
  export const DESIGN_CHECKMARKS_VALUES = ["circle", "plain"];
63
+ /** #7741 D7 — the button shape. */
64
+ export const DESIGN_BUTTON_SHAPE_VALUES = ["control", "pill"];
65
+ export const DESIGN_DARK_INK_VALUES = ["fg", "accent"];
66
+ export const DESIGN_FIGURES_VALUES = ["platform", "display"];
67
+ /**
68
+ * #7739 D1 — whether the compile writes the design's own type roles into the
69
+ * theme. A compile instruction, not a runtime setting: never written into `theme:`.
70
+ */
71
+ export const DESIGN_TYPE_SCALE_VALUES = ["platform", "design"];
53
72
  /** The closed copy-density enum (spec 6892 D4). */
54
73
  export const DESIGN_COPY_DENSITY_VALUES = ["standard", "sparse"];
55
74
  /**
@@ -184,8 +203,52 @@ function parseScalar(raw, lineNo, file) {
184
203
  }
185
204
  return parsePlainScalar(raw, lineNo, file);
186
205
  }
187
- /** Parse a block mapping starting at `pos.i`, whose keys sit at `indent`. */
188
- function parseMapping(lines, pos, indent, file) {
206
+ /**
207
+ * The one dotted key path whose value may be a block sequence (#7719 D2) —
208
+ * the deck's ordered ground list. Every other `- ` line is refused as before.
209
+ */
210
+ const SEQUENCE_PATHS = new Set(["layout.deckGrounds"]);
211
+ /**
212
+ * Parse a block sequence of scalars whose `- ` items sit at `pos.i`'s indent
213
+ * (#7719 D2). Each item's text is read by `parseScalar`, with every refusal it
214
+ * makes; an empty item, a mapping-shaped item, a nested sequence and any line
215
+ * deeper than the items are refused — the forms YAML would read as something
216
+ * other than a list of scalars.
217
+ */
218
+ function parseScalarSequence(lines, pos, file) {
219
+ const itemIndent = lines[pos.i].indent;
220
+ const items = [];
221
+ while (pos.i < lines.length && lines[pos.i].indent >= itemIndent) {
222
+ const line = lines[pos.i];
223
+ if (line.indent > itemIndent) {
224
+ refuseFrontmatter(file, line.lineNo, "a sequence item must be a single scalar — nested blocks are not supported");
225
+ }
226
+ if (line.content === "-") {
227
+ refuseFrontmatter(file, line.lineNo, "a sequence item has no value");
228
+ }
229
+ if (!line.content.startsWith("- ")) {
230
+ refuseFrontmatter(file, line.lineNo, `expected a "- " sequence item, got "${line.content}"`);
231
+ }
232
+ const raw = line.content.slice(2);
233
+ if (raw === "" || raw.startsWith(" ")) {
234
+ refuseFrontmatter(file, line.lineNo, 'expected a single space after "-"');
235
+ }
236
+ if (raw === "-" || raw.startsWith("- ") || raw === "?" || raw.startsWith("? ")) {
237
+ refuseFrontmatter(file, line.lineNo, "a sequence item must be a scalar — nested collections are not supported");
238
+ }
239
+ if (!/^["']/.test(raw) && raw.endsWith(":")) {
240
+ refuseFrontmatter(file, line.lineNo, "a sequence item must be a scalar — mappings are not supported");
241
+ }
242
+ items.push(parseScalar(raw, line.lineNo, file));
243
+ pos.i += 1;
244
+ }
245
+ return items;
246
+ }
247
+ /**
248
+ * Parse a block mapping starting at `pos.i`, whose keys sit at `indent`.
249
+ * `path` is the dotted key path of this mapping (`""` at the top level).
250
+ */
251
+ function parseMapping(lines, pos, indent, file, path) {
189
252
  const result = {};
190
253
  while (pos.i < lines.length && lines[pos.i].indent === indent) {
191
254
  const line = lines[pos.i];
@@ -202,13 +265,21 @@ function parseMapping(lines, pos, indent, file) {
202
265
  refuseFrontmatter(file, line.lineNo, `duplicate key "${key}"`);
203
266
  }
204
267
  pos.i += 1;
268
+ const keyPath = path === "" ? key : `${path}.${key}`;
205
269
  if (rawRest === "") {
206
270
  const next = lines[pos.i];
271
+ if (SEQUENCE_PATHS.has(keyPath) &&
272
+ next !== undefined &&
273
+ next.indent === indent + 2 &&
274
+ (next.content === "-" || next.content.startsWith("- "))) {
275
+ result[key] = parseScalarSequence(lines, pos, file);
276
+ continue;
277
+ }
207
278
  if (next !== undefined && next.indent > indent) {
208
279
  if (next.indent !== indent + 2) {
209
280
  refuseFrontmatter(file, next.lineNo, `bad indentation — expected ${indent + 2} spaces, got ${next.indent}`);
210
281
  }
211
- result[key] = parseMapping(lines, pos, next.indent, file);
282
+ result[key] = parseMapping(lines, pos, next.indent, file, keyPath);
212
283
  }
213
284
  else {
214
285
  refuseFrontmatter(file, line.lineNo, `"${key}" has an empty value`);
@@ -238,7 +309,7 @@ function parseFrontmatter(text, file) {
238
309
  }
239
310
  const lines = toSignificantLines(m[1] ?? "", file);
240
311
  const pos = { i: 0 };
241
- const result = parseMapping(lines, pos, 0, file);
312
+ const result = parseMapping(lines, pos, 0, file, "");
242
313
  if (pos.i < lines.length) {
243
314
  const stray = lines[pos.i];
244
315
  refuseFrontmatter(file, stray.lineNo, "unexpected indentation at the top level");
@@ -281,7 +352,51 @@ function validateColors(raw, file) {
281
352
  }
282
353
  return result;
283
354
  }
284
- const TYPOGRAPHY_ROLE_KEYS = ["fontFamily", "fontSize", "fontWeight", "lineHeight", "letterSpacing"];
355
+ const TYPOGRAPHY_ROLE_KEYS = [
356
+ "fontFamily",
357
+ "fontSize",
358
+ "fontWeight",
359
+ "lineHeight",
360
+ "letterSpacing",
361
+ "fontSource",
362
+ "lineHeightMobile",
363
+ "fontSizeMobile",
364
+ ];
365
+ /** The roles with a phone size (#7739 D1.2). */
366
+ const FONT_SIZE_MOBILE_ROLES = ["display", "heading-section", "heading-item"];
367
+ /** The roles `typeScale: design` compiles (#7739 D1.3, D2.2). */
368
+ const COMPILED_TYPE_ROLES = ["display", "heading-section", "heading-item", "body", "eyebrow"];
369
+ /** The roles with a runtime font slot (`--font-display` / `--font-body`, #7720 D1). */
370
+ const FONT_SOURCE_ROLES = ["display", "body"];
371
+ /** A Google Fonts family name, as next/font/google exports it with spaces → `_`. */
372
+ const GOOGLE_FAMILY_RE = /^[A-Za-z][A-Za-z0-9 ]*$/;
373
+ /** One stack entry, trimmed, with one matching pair of `'` or `"` removed. */
374
+ export function unquoteFamily(entry) {
375
+ const trimmed = entry.trim();
376
+ const m = /^(['"])(.*)\1$/.exec(trimmed);
377
+ return m ? m[2] : trimmed;
378
+ }
379
+ /**
380
+ * #7720 D1.2 — a `fontSource` names the role's first family as the face to
381
+ * load; the rest of the stack is written into CSS and TypeScript source as
382
+ * the fallback, so both are constrained here.
383
+ */
384
+ function validateFontSource(role, file, roleName) {
385
+ const field = `typography.${roleName}.fontSource`;
386
+ if (!FONT_SOURCE_ROLES.includes(roleName)) {
387
+ refuse(file, field, "only the display and body roles reach a runtime font slot");
388
+ }
389
+ if (role.fontFamily === undefined) {
390
+ refuse(file, field, "needs fontFamily on the same role");
391
+ }
392
+ const [first = "", ...rest] = role.fontFamily.split(",");
393
+ if (!GOOGLE_FAMILY_RE.test(unquoteFamily(first))) {
394
+ refuse(file, field, "the first family must be a Google Fonts family name");
395
+ }
396
+ if (!rest.every((entry) => DESIGN_FONT_STACK_ENTRY_REGEX.test(entry.trim()))) {
397
+ refuse(file, field, "fallback entries must be bare or singly-quoted family names");
398
+ }
399
+ }
285
400
  function validateTypographyRole(raw, file, role) {
286
401
  const obj = requireObject(raw, file, `typography.${role}`);
287
402
  for (const key of Object.keys(obj)) {
@@ -295,15 +410,24 @@ function validateTypographyRole(raw, file, role) {
295
410
  const result = { fontSize: "" };
296
411
  for (const [key, value] of Object.entries(obj)) {
297
412
  const field = `typography.${role}.${key}`;
298
- if (key === "fontFamily" || key === "fontSize" || key === "letterSpacing") {
413
+ if (key === "fontFamily" ||
414
+ key === "fontSize" ||
415
+ key === "letterSpacing" ||
416
+ key === "fontSizeMobile") {
299
417
  if (typeof value !== "string")
300
418
  refuse(file, field, "must be a string");
301
419
  if (key === "fontFamily")
302
420
  result.fontFamily = value;
303
421
  else if (key === "fontSize")
304
422
  result.fontSize = value;
305
- else
423
+ else if (key === "letterSpacing")
306
424
  result.letterSpacing = value;
425
+ else {
426
+ if (!FONT_SIZE_MOBILE_ROLES.includes(role)) {
427
+ refuse(file, field, "only the display, heading-section and heading-item roles have a phone size");
428
+ }
429
+ result.fontSizeMobile = value;
430
+ }
307
431
  }
308
432
  else if (key === "fontWeight" || key === "lineHeight") {
309
433
  if (typeof value !== "number")
@@ -313,7 +437,32 @@ function validateTypographyRole(raw, file, role) {
313
437
  else
314
438
  result.lineHeight = value;
315
439
  }
440
+ else if (key === "fontSource") {
441
+ if (typeof value !== "string" ||
442
+ !DESIGN_FONT_SOURCE_VALUES.includes(value)) {
443
+ refuse(file, field, `must be one of ${DESIGN_FONT_SOURCE_VALUES.join(", ")}`);
444
+ }
445
+ result.fontSource = value;
446
+ }
447
+ else if (key === "lineHeightMobile") {
448
+ if (typeof value !== "number")
449
+ refuse(file, field, "must be a number");
450
+ if (!(value > 0))
451
+ refuse(file, field, "must be greater than 0");
452
+ if (role !== "display")
453
+ refuse(file, field, "only the display role has a mobile line-height");
454
+ result.lineHeightMobile = value;
455
+ }
456
+ }
457
+ // The leading pair compiles into `theme:` (#7720 D2), whose schema takes
458
+ // only positive line-heights — refuse here, before a site reads it.
459
+ if (result.lineHeightMobile !== undefined &&
460
+ result.lineHeight !== undefined &&
461
+ !(result.lineHeight > 0)) {
462
+ refuse(file, `typography.${role}.lineHeight`, "must be greater than 0 when lineHeightMobile is declared");
316
463
  }
464
+ if (result.fontSource !== undefined)
465
+ validateFontSource(result, file, role);
317
466
  return result;
318
467
  }
319
468
  function validateTypography(raw, file) {
@@ -336,10 +485,21 @@ function validateStringMap(raw, file, field) {
336
485
  return result;
337
486
  }
338
487
  const LAYOUT_KEYS = ["nav", "footer", "announcementBanner", "home"];
488
+ /** Every key a package's `layout` may carry — #7719 D3 adds the optional `deckGrounds` (a deck home only),
489
+ * #7721 D1 the optional `innerPages`, #7740 D1 the optional `deckPresentation` (a deck home only),
490
+ * #7742 D1 the optional `colorMode` and `chromeStyle`. */
491
+ const ALLOWED_LAYOUT_KEYS = new Set([
492
+ ...LAYOUT_KEYS,
493
+ "deckGrounds",
494
+ "innerPages",
495
+ "deckPresentation",
496
+ "colorMode",
497
+ "chromeStyle",
498
+ ]);
339
499
  function validateLayout(raw, file) {
340
500
  const obj = requireObject(raw, file, "layout");
341
501
  for (const key of Object.keys(obj)) {
342
- if (!LAYOUT_KEYS.includes(key)) {
502
+ if (!ALLOWED_LAYOUT_KEYS.has(key)) {
343
503
  refuse(file, `layout.${key}`, "is not a recognized layout key");
344
504
  }
345
505
  }
@@ -348,8 +508,9 @@ function validateLayout(raw, file) {
348
508
  refuse(file, `layout.${key}`, "is required");
349
509
  }
350
510
  }
511
+ let validated;
351
512
  try {
352
- return validateTemplateLayout(obj, "design package");
513
+ validated = validateTemplateLayout(obj, "design package");
353
514
  }
354
515
  catch (error) {
355
516
  if (error instanceof TemplateLayoutError) {
@@ -358,12 +519,35 @@ function validateLayout(raw, file) {
358
519
  }
359
520
  throw error;
360
521
  }
522
+ // #7721 D1 — a read package holds `innerPages` only as "bare", the
523
+ // `ResolvedSiteChrome` shape; an explicit "default" is the absent key.
524
+ // #7740 D1 — and `deckPresentation` only as `true`.
525
+ // #7742 D1 — and `colorMode` only as "light", `chromeStyle` only as "minimal".
526
+ const { innerPages, deckPresentation, colorMode, chromeStyle, ...rest } = validated;
527
+ const layout = {
528
+ ...rest,
529
+ ...(innerPages === "bare" ? { innerPages } : {}),
530
+ ...(deckPresentation === true ? { deckPresentation } : {}),
531
+ ...(colorMode === "light" ? { colorMode } : {}),
532
+ ...(chromeStyle === "minimal" ? { chromeStyle } : {}),
533
+ };
534
+ if (layout.deckGrounds !== undefined && layout.home !== "deck") {
535
+ refuse(file, "layout.deckGrounds", "applies only to home: deck");
536
+ }
537
+ if (deckPresentation === true && layout.home !== "deck") {
538
+ refuse(file, "layout.deckPresentation", "applies only to home: deck");
539
+ }
540
+ return layout;
361
541
  }
362
542
  const TREATMENTS_ENUMS = {
363
543
  corners: DESIGN_CORNERS_VALUES,
364
544
  eyebrowCase: DESIGN_EYEBROW_CASE_VALUES,
365
545
  placeholders: DESIGN_PLACEHOLDERS_VALUES,
366
546
  checkmarks: DESIGN_CHECKMARKS_VALUES,
547
+ buttonShape: DESIGN_BUTTON_SHAPE_VALUES,
548
+ typeScale: DESIGN_TYPE_SCALE_VALUES,
549
+ darkInk: DESIGN_DARK_INK_VALUES,
550
+ figures: DESIGN_FIGURES_VALUES,
367
551
  };
368
552
  function validateTreatments(raw, file) {
369
553
  const obj = requireObject(raw, file, "treatments");
@@ -380,6 +564,33 @@ function validateTreatments(raw, file) {
380
564
  }
381
565
  return result;
382
566
  }
567
+ /**
568
+ * #7739 D1.3 — under `typeScale: design` the five compiled roles' sizes and
569
+ * weights are written into an inline `<style>`, so each is constrained here,
570
+ * after both `typography` and `treatments` have been read. Without the dial
571
+ * these fields are carried as before and never compiled.
572
+ */
573
+ function validateCompiledTypeScale(typography, file) {
574
+ for (const role of COMPILED_TYPE_ROLES) {
575
+ const def = typography[role];
576
+ if (def === undefined)
577
+ continue;
578
+ for (const key of ["fontSize", "fontSizeMobile"]) {
579
+ const value = def[key];
580
+ if (value !== undefined && !DESIGN_TYPE_SIZE_REGEX.test(value)) {
581
+ refuse(file, `typography.${role}.${key}`, "must be a length in rem, px or em");
582
+ }
583
+ }
584
+ const weight = def.fontWeight;
585
+ if (weight !== undefined && !(Number.isInteger(weight) && weight >= 1 && weight <= 1000)) {
586
+ refuse(file, `typography.${role}.fontWeight`, "must be an integer from 1 to 1000");
587
+ }
588
+ // The display and body leadings compile too, into a schema that takes only positive ones.
589
+ if ((role === "display" || role === "body") && def.lineHeight !== undefined && !(def.lineHeight > 0)) {
590
+ refuse(file, `typography.${role}.lineHeight`, "must be greater than 0");
591
+ }
592
+ }
593
+ }
383
594
  function validateSections(raw, file) {
384
595
  const obj = requireObject(raw, file, "sections");
385
596
  const result = {};
@@ -448,6 +659,9 @@ function validatePackage(raw, file) {
448
659
  if ("treatments" in raw) {
449
660
  pkg.treatments = validateTreatments(raw.treatments, file);
450
661
  }
662
+ if (pkg.treatments?.typeScale === "design") {
663
+ validateCompiledTypeScale(pkg.typography, file);
664
+ }
451
665
  return pkg;
452
666
  }
453
667
  /**