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.
- package/README.md +12 -4
- package/component-catalog.json +247 -33
- package/component-catalog.md +10 -6
- package/dist/apply-design.d.ts +21 -2
- package/dist/apply-design.js +120 -19
- package/dist/cli.js +1 -0
- package/dist/design-compile.d.ts +70 -2
- package/dist/design-compile.js +97 -1
- package/dist/design-home.d.ts +74 -0
- package/dist/design-home.js +256 -0
- package/dist/design-package.d.ts +39 -2
- package/dist/design-package.js +223 -9
- package/dist/design-presets.d.ts +17 -1
- package/dist/design-presets.js +17 -5
- package/dist/design-scaffold.d.ts +6 -25
- package/dist/design-scaffold.js +26 -74
- package/dist/index.d.ts +1 -0
- package/dist/lib/design-checks.d.ts +12 -1
- package/dist/lib/design-checks.js +38 -2
- package/dist/lib/module-license-gate.d.ts +9 -0
- package/dist/lib/module-license-gate.js +18 -0
- package/dist/module-architecture.js +27 -0
- package/dist/programmatic.js +70 -26
- package/dist/site-chrome.d.ts +38 -0
- package/dist/site-chrome.js +109 -2
- package/dist/site-fonts.d.ts +21 -0
- package/dist/site-fonts.js +54 -0
- package/dist/site-roster.d.ts +16 -0
- package/dist/site-roster.js +81 -0
- package/dist/starter-grid.js +15 -0
- package/dist/surfaces.js +12 -0
- package/dist/team-profiles.d.ts +29 -0
- package/dist/team-profiles.js +43 -0
- package/package.json +3 -3
- package/release-scope.json +1 -1
- package/site-templates/_base/content/en/TeamList/team.mdx +68 -0
- package/site-templates/_base/content/en/TeamMember/first-member.mdx +22 -0
- package/site-templates/_base/content/en/TeamMember/second-member.mdx +22 -0
- package/site-templates/_base/content/en/TeamMember/third-member.mdx +22 -0
- package/site-templates/designs/minimalism/DESIGN.md +164 -34
- package/site-templates/designs/minimalism/home/local-business-service.mdx +119 -0
- package/site-templates/designs/minimalism/home/organization-product.mdx +132 -0
- package/site-templates/designs/minimalism/home/organization-service.mdx +131 -0
- package/site-templates/designs/minimalism/home/person-service.mdx +101 -0
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +34 -1
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +20 -0
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +34 -1
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +22 -1
- package/site-templates/marketing-site-free/ailk-template.json +1 -1
- package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.json +34 -1
- package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.md +26 -7
- package/site-templates/shapes/local-business-service/ailk-template.json +1 -1
- package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.json +34 -1
- package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.md +26 -7
- package/site-templates/shapes/organization-service/ailk-template.json +1 -1
- package/templates/.env.example +23 -9
- package/templates/README.md +2 -0
- package/templates/apps/api/CLAUDE.md +24 -17
- package/templates/apps/api/src/__tests__/factories/agent-caller.ts +98 -0
- package/templates/apps/api/src/__tests__/factories/in-memory-prisma.ts +321 -0
- package/templates/apps/api/src/__tests__/schedule-forwarded-rate-limit.test.ts +117 -0
- package/templates/apps/api/src/config/__tests__/hub-require-spoke-key.test.ts +39 -14
- package/templates/apps/api/src/config/index.ts +35 -20
- package/templates/apps/api/src/lib/__mocks__/prisma-client.js +3 -0
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +14 -0
- package/templates/apps/api/src/lib/__tests__/agent-store.test.ts +468 -0
- package/templates/apps/api/src/lib/__tests__/spoke-store.test.ts +32 -2
- package/templates/apps/api/src/lib/agent-store.ts +521 -0
- package/templates/apps/api/src/lib/spoke-store.ts +13 -2
- package/templates/apps/api/src/middleware/__tests__/auth.bearer-path.test.ts +384 -1
- package/templates/apps/api/src/middleware/__tests__/auth.cookie-path.test.ts +61 -0
- package/templates/apps/api/src/middleware/__tests__/default-tenant.test.ts +176 -0
- package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +76 -0
- package/templates/apps/api/src/middleware/auth.ts +219 -5
- package/templates/apps/api/src/middleware/default-tenant.ts +29 -8
- package/templates/apps/api/src/middleware/rate-limit.ts +35 -13
- package/templates/apps/api/src/routes/leads/__tests__/spoke.test.ts +76 -15
- package/templates/apps/api/src/routes/leads/index.ts +14 -6
- package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +5 -6
- package/templates/apps/api/src/routes/project-listings/index.ts +14 -9
- package/templates/apps/api/src/routes/project-listings/me.ts +4 -3
- package/templates/apps/api/src/routes/project-listings/patch-copy.ts +7 -5
- package/templates/apps/api/src/routes/workspaces/__tests__/inbox-wire-schemas.test.ts +198 -0
- package/templates/apps/api/src/server.ts +27 -0
- package/templates/apps/api/src/services/__tests__/audit-log.test.ts +374 -57
- package/templates/apps/api/src/services/__tests__/leads-inbox.test.ts +25 -2
- package/templates/apps/api/src/services/__tests__/spoke-resolution.test.ts +303 -17
- package/templates/apps/api/src/services/audit-log.ts +278 -47
- package/templates/apps/api/src/services/default-tenant.ts +55 -5
- package/templates/apps/api/src/services/leads-inbox.ts +12 -21
- package/templates/apps/api/src/services/project-listings.ts +1 -21
- package/templates/apps/api/src/services/public-site-resolver.ts +103 -13
- package/templates/apps/mcp/CLAUDE.md +44 -5
- package/templates/apps/mcp/__tests__/parity.test.ts +53 -1
- package/templates/apps/mcp/__tests__/workspace-surface.test.ts +364 -0
- package/templates/apps/mcp/package.json +1 -1
- package/templates/apps/mcp/src/http-transport.ts +107 -1
- package/templates/apps/mcp/src/workspace-server.ts +193 -0
- package/templates/apps/web/.env.example +64 -37
- package/templates/apps/web/CLAUDE.md +11 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/settings.test.tsx +10 -0
- package/templates/apps/web/app/[locale]/(authed)/account/settings/__tests__/theme-modes.test.ts +42 -0
- package/templates/apps/web/app/[locale]/(authed)/account/settings/theme-modes.ts +8 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/clients/__tests__/page-renderer-migration.test.tsx +14 -1
- package/templates/apps/web/app/[locale]/contact/__tests__/site-contact-lines.test.ts +69 -0
- package/templates/apps/web/app/[locale]/contact/site-contact-lines.ts +64 -0
- package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +4 -4
- package/templates/apps/web/app/[locale]/downloads/__tests__/page-renderer-migration.test.tsx +15 -3
- package/templates/apps/web/app/[locale]/events/__tests__/page-renderer-migration.test.tsx +17 -6
- package/templates/apps/web/app/[locale]/for/__tests__/page-renderer-migration.test.tsx +14 -1
- package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +4 -6
- package/templates/apps/web/app/[locale]/layout.tsx +156 -53
- package/templates/apps/web/app/[locale]/page.tsx +37 -0
- package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/products/__tests__/page-renderer-migration.test.tsx +4 -6
- package/templates/apps/web/app/[locale]/resources/__tests__/page-renderer-migration.test.tsx +15 -2
- package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +43 -6
- package/templates/apps/web/app/[locale]/services/page.tsx +14 -2
- package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/team/[slug]/page.tsx +45 -0
- package/templates/apps/web/app/[locale]/team/__tests__/team-routes.test.tsx +155 -0
- package/templates/apps/web/app/[locale]/team/page.tsx +49 -0
- package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/testimonials/__tests__/page-renderer-migration.test.tsx +14 -1
- package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/not-found.tsx +47 -0
- package/templates/apps/web/app/v1/[...path]/route.ts +16 -0
- package/templates/apps/web/lib/__tests__/profile-back-link.test.ts +95 -0
- package/templates/apps/web/lib/__tests__/relay.test.ts +515 -0
- package/templates/apps/web/lib/__tests__/site-theme-head.test.ts +70 -0
- package/templates/apps/web/lib/__tests__/site-theme.test.ts +64 -1
- package/templates/apps/web/lib/profile-back-link.ts +51 -0
- package/templates/apps/web/lib/relay.ts +279 -0
- package/templates/apps/web/lib/site-chrome.ts +12 -0
- package/templates/apps/web/lib/site-fonts.ts +15 -0
- package/templates/apps/web/lib/site-logo.tsx +39 -1
- package/templates/apps/web/lib/site-theme-head.ts +33 -0
- package/templates/apps/web/lib/site-theme.ts +13 -1
- package/templates/apps/web/messages/en.json +7 -1
- package/templates/apps/web/middleware.ts +5 -4
- package/templates/apps/web/next.config.mjs +6 -0
- package/templates/database/CHANGELOG.md +22 -0
- package/templates/database/__tests__/agents.db.test.ts +235 -0
- package/templates/database/auth/schema.prisma +61 -0
- package/templates/database/migrations/20261005120000_agents/migration.sql +84 -0
- package/templates/database/package.json +1 -1
- package/versions.json +35 -35
- package/site-templates/designs/minimalism/sample/README.md +0 -48
- package/site-templates/designs/minimalism/sample/content/en/HomePage/home.mdx +0 -244
- package/site-templates/designs/minimalism/sample/public/avatar-placeholder-1.svg +0 -7
- package/site-templates/designs/minimalism/sample/public/avatar-placeholder-2.svg +0 -7
- package/site-templates/designs/minimalism/sample/public/avatar-placeholder-3.svg +0 -7
- package/site-templates/designs/minimalism/sample/public/avatar-placeholder-4.svg +0 -7
- package/site-templates/designs/minimalism/sample/public/avatar-placeholder-5.svg +0 -7
- package/site-templates/designs/minimalism/sample/public/avatar-placeholder-6.svg +0 -7
- package/site-templates/designs/minimalism/sample/public/hero-backdrop.svg +0 -17
- package/site-templates/designs/minimalism/sample/public/logo-placeholder-1.svg +0 -6
- package/site-templates/designs/minimalism/sample/public/logo-placeholder-2.svg +0 -6
- package/site-templates/designs/minimalism/sample/public/logo-placeholder-3.svg +0 -6
- 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
|
+
}
|
package/dist/design-package.d.ts
CHANGED
|
@@ -11,13 +11,31 @@ export type DesignColors = {
|
|
|
11
11
|
} & {
|
|
12
12
|
[K in DesignColorKeyOptional]?: string;
|
|
13
13
|
};
|
|
14
|
-
/**
|
|
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
|
-
/**
|
|
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
|
package/dist/design-package.js
CHANGED
|
@@ -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
|
-
/**
|
|
188
|
-
|
|
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 = [
|
|
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" ||
|
|
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 (!
|
|
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
|
-
|
|
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
|
/**
|