create-ailk 0.13.0 → 0.15.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 +250 -36
- package/component-catalog.md +10 -6
- package/dist/add-module.d.ts +7 -0
- package/dist/add-module.js +34 -2
- package/dist/add-route.js +18 -2
- package/dist/apply-design.d.ts +32 -2
- package/dist/apply-design.js +155 -22
- package/dist/cli.js +24 -3
- package/dist/derive-architecture.d.ts +15 -2
- package/dist/derive-architecture.js +34 -6
- 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 +224 -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 +2 -1
- package/dist/index.js +1 -1
- package/dist/lib/chrome-readers.d.ts +21 -0
- package/dist/lib/chrome-readers.js +100 -0
- package/dist/lib/design-checks.d.ts +12 -1
- package/dist/lib/design-checks.js +38 -2
- package/dist/lib/extract-module-bundle.d.ts +11 -0
- package/dist/lib/extract-module-bundle.js +36 -6
- package/dist/lib/license-key-files.d.ts +6 -1
- package/dist/lib/license-key-files.js +9 -4
- package/dist/lib/module-license-gate.d.ts +9 -0
- package/dist/lib/module-license-gate.js +18 -0
- package/dist/module-architecture.d.ts +9 -0
- package/dist/module-architecture.js +6 -0
- package/dist/parse-args.d.ts +22 -0
- package/dist/parse-args.js +73 -0
- package/dist/programmatic.d.ts +20 -0
- package/dist/programmatic.js +180 -35
- package/dist/site-chrome.d.ts +42 -0
- package/dist/site-chrome.js +140 -2
- package/dist/site-fonts.d.ts +21 -0
- package/dist/site-fonts.js +54 -0
- package/dist/site-manifest.d.ts +12 -5
- package/dist/site-manifest.js +25 -11
- 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/sync-routes.js +11 -2
- package/dist/team-profiles.d.ts +29 -0
- package/dist/team-profiles.js +43 -0
- package/dist/topology.d.ts +131 -0
- package/dist/topology.js +533 -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 +166 -36
- 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/README.md +2 -0
- package/templates/apps/api/CLAUDE.md +6 -1
- package/templates/apps/api/package.json +1 -0
- package/templates/apps/api/src/__tests__/factories/agent-caller.ts +5 -1
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +10 -0
- package/templates/apps/api/src/lib/__tests__/agent-store.test.ts +114 -1
- package/templates/apps/api/src/lib/__tests__/site.test.ts +54 -0
- package/templates/apps/api/src/lib/agent-store.ts +95 -3
- package/templates/apps/api/src/lib/site.ts +37 -0
- package/templates/apps/api/src/lib/spoke-store.ts +18 -2
- package/templates/apps/api/src/middleware/__tests__/auth.bearer-path.test.ts +4 -0
- package/templates/apps/api/src/middleware/__tests__/default-tenant.test.ts +22 -0
- package/templates/apps/api/src/middleware/auth.ts +3 -0
- package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +23 -0
- package/templates/apps/api/src/openapi/spec.ts +38 -2
- package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +50 -2
- package/templates/apps/api/src/routes/checkout/sessions.ts +7 -0
- package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +275 -0
- package/templates/apps/api/src/routes/leads/index.ts +69 -6
- package/templates/apps/api/src/routes/webhooks/README.md +39 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-site-filter.test.ts +366 -0
- package/templates/apps/api/src/routes/webhooks/stripe.ts +61 -1
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +199 -0
- package/templates/apps/api/src/services/default-tenant.ts +41 -7
- package/templates/apps/api/src/services/lead-routing.ts +148 -29
- package/templates/apps/api/src/services/public-site-resolver.ts +48 -0
- package/templates/apps/mcp/__tests__/index.test.ts +5 -5
- package/templates/apps/mcp/package.json +1 -1
- package/templates/apps/mcp/src/index.ts +6 -4
- package/templates/apps/mcp/src/lib/content-adapter.ts +2 -2
- package/templates/apps/mcp/src/lib/topology.generated.ts +24 -0
- package/templates/apps/web/CLAUDE.md +12 -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]/[slug]/[detail]/__tests__/page.test.tsx +3 -0
- package/templates/apps/web/app/[locale]/[slug]/__tests__/page.test.tsx +3 -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__/page-renderer-migration.test.tsx +66 -14
- package/templates/apps/web/app/[locale]/contact/__tests__/site-contact-lines.test.ts +163 -0
- package/templates/apps/web/app/[locale]/contact/site-contact-lines.ts +68 -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 +212 -57
- package/templates/apps/web/app/[locale]/newsletter/__tests__/page.test.tsx +3 -0
- 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]/projects/__tests__/page-renderer-migration.test.tsx +5 -2
- 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]/thanks/[slug]/__tests__/page.test.tsx +3 -0
- 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/openapi.json/__tests__/route.test.ts +89 -1
- package/templates/apps/web/app/openapi.json/route.ts +69 -1
- package/templates/apps/web/instrumentation.ts +7 -4
- package/templates/apps/web/lib/__tests__/closing-footer-path.test.ts +96 -0
- package/templates/apps/web/lib/__tests__/profile-back-link.test.ts +95 -0
- package/templates/apps/web/lib/__tests__/relay.test.ts +290 -0
- package/templates/apps/web/lib/__tests__/site-brand.test.ts +52 -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/closing-footer-path.ts +50 -0
- package/templates/apps/web/lib/profile-back-link.ts +51 -0
- package/templates/apps/web/lib/relay.ts +206 -2
- package/templates/apps/web/lib/site-brand.tsx +22 -1
- package/templates/apps/web/lib/site-chrome.ts +15 -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/lib/topology.generated.ts +23 -0
- package/templates/apps/web/messages/en.json +7 -1
- package/templates/apps/web/package.json +1 -1
- package/templates/database/CHANGELOG.md +32 -0
- package/templates/database/__tests__/agents.db.test.ts +65 -0
- package/templates/database/__tests__/lead-idempotency-key.db.test.ts +92 -0
- package/templates/database/auth/schema.prisma +23 -0
- package/templates/database/inbox/schema.prisma +13 -0
- package/templates/database/jest.config.cjs +7 -11
- package/templates/database/migrations/20261008120000_user_last_workspace/migration.sql +35 -0
- package/templates/database/migrations/20261008130000_lead_idempotency_key/migration.sql +20 -0
- package/templates/database/migrations/20261009120000_agent_grants/migration.sql +33 -0
- package/templates/database/package.json +1 -1
- package/versions.json +35 -34
- 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
package/dist/site-chrome.js
CHANGED
|
@@ -29,6 +29,21 @@
|
|
|
29
29
|
* close): an unrecognized design NAME now fails in `resolveDesign`, before
|
|
30
30
|
* this function ever sees one.
|
|
31
31
|
*/
|
|
32
|
+
/**
|
|
33
|
+
* The `deckGrounds` knob's vocabulary (#7719 D3) — the shared section surface
|
|
34
|
+
* enum, duplicated from `@working-theory/validation`'s `SectionSurfaceSchema`
|
|
35
|
+
* because `create-ailk` cannot import it (one runtime dependency, § 1.5);
|
|
36
|
+
* `site-chrome.test.ts` proves the two lists equal.
|
|
37
|
+
*/
|
|
38
|
+
export const DECK_GROUND_VALUES = [
|
|
39
|
+
"none",
|
|
40
|
+
"gray-100",
|
|
41
|
+
"wash",
|
|
42
|
+
"wash-secondary",
|
|
43
|
+
"gradient-brand",
|
|
44
|
+
"gradient-dark",
|
|
45
|
+
"solid-dark",
|
|
46
|
+
];
|
|
32
47
|
/** Defaults (#5612 D1; banner off since #6469). */
|
|
33
48
|
export const DEFAULT_SITE_CHROME = {
|
|
34
49
|
nav: "full",
|
|
@@ -48,6 +63,17 @@ export class TemplateLayoutError extends Error {
|
|
|
48
63
|
const NAV_VALUES = ["full", "mark-only"];
|
|
49
64
|
const FOOTER_VALUES = ["columns", "centered"];
|
|
50
65
|
const HOME_VALUES = ["default", "deck"];
|
|
66
|
+
const INNER_PAGES_VALUES = ["default", "bare"];
|
|
67
|
+
const COLOR_MODE_VALUES = ["user", "light"];
|
|
68
|
+
const CHROME_STYLE_VALUES = ["default", "minimal"];
|
|
69
|
+
/**
|
|
70
|
+
* A `closingFooterPaths` member (#7797): a route path as the site writes it —
|
|
71
|
+
* a leading "/" and no query, fragment, whitespace or control character. The
|
|
72
|
+
* layout normalizes the visitor's pathname (locale prefix, trailing slash)
|
|
73
|
+
* before comparing, so "/about" and "/about/" name the same page.
|
|
74
|
+
*/
|
|
75
|
+
// eslint-disable-next-line no-control-regex -- the class deliberately refuses control characters
|
|
76
|
+
const CLOSING_FOOTER_PATH = /^\/[^\s?#\u0000-\u001f]*$/;
|
|
51
77
|
/**
|
|
52
78
|
* Validate an UNTRUSTED `layout` value (a bundle's `ailk-template.json`
|
|
53
79
|
* `layout` field — arbitrary JSON, never compile-time checked the way the
|
|
@@ -92,6 +118,50 @@ export function validateTemplateLayout(value, subject) {
|
|
|
92
118
|
}
|
|
93
119
|
result.home = raw.home;
|
|
94
120
|
}
|
|
121
|
+
if ("deckGrounds" in raw && raw.deckGrounds !== undefined) {
|
|
122
|
+
const grounds = raw.deckGrounds;
|
|
123
|
+
const badIndex = Array.isArray(grounds)
|
|
124
|
+
? grounds.findIndex((g) => !DECK_GROUND_VALUES.includes(g))
|
|
125
|
+
: -1;
|
|
126
|
+
if (!Array.isArray(grounds) || grounds.length === 0 || badIndex !== -1) {
|
|
127
|
+
throw new TemplateLayoutError(`${subject}: layout.deckGrounds must be a non-empty list of ${DECK_GROUND_VALUES.join(", ")} — got ${JSON.stringify(badIndex !== -1 ? grounds[badIndex] : grounds)}`);
|
|
128
|
+
}
|
|
129
|
+
result.deckGrounds = grounds;
|
|
130
|
+
}
|
|
131
|
+
if ("innerPages" in raw && raw.innerPages !== undefined) {
|
|
132
|
+
if (!INNER_PAGES_VALUES.includes(raw.innerPages)) {
|
|
133
|
+
throw new TemplateLayoutError(`${subject}: layout.innerPages must be one of ${INNER_PAGES_VALUES.join(", ")} — got ${JSON.stringify(raw.innerPages)}`);
|
|
134
|
+
}
|
|
135
|
+
result.innerPages = raw.innerPages;
|
|
136
|
+
}
|
|
137
|
+
if ("deckPresentation" in raw && raw.deckPresentation !== undefined) {
|
|
138
|
+
if (typeof raw.deckPresentation !== "boolean") {
|
|
139
|
+
throw new TemplateLayoutError(`${subject}: layout.deckPresentation must be a boolean — got ${JSON.stringify(raw.deckPresentation)}`);
|
|
140
|
+
}
|
|
141
|
+
result.deckPresentation = raw.deckPresentation;
|
|
142
|
+
}
|
|
143
|
+
if ("colorMode" in raw && raw.colorMode !== undefined) {
|
|
144
|
+
if (!COLOR_MODE_VALUES.includes(raw.colorMode)) {
|
|
145
|
+
throw new TemplateLayoutError(`${subject}: layout.colorMode must be one of ${COLOR_MODE_VALUES.join(", ")} — got ${JSON.stringify(raw.colorMode)}`);
|
|
146
|
+
}
|
|
147
|
+
result.colorMode = raw.colorMode;
|
|
148
|
+
}
|
|
149
|
+
if ("chromeStyle" in raw && raw.chromeStyle !== undefined) {
|
|
150
|
+
if (!CHROME_STYLE_VALUES.includes(raw.chromeStyle)) {
|
|
151
|
+
throw new TemplateLayoutError(`${subject}: layout.chromeStyle must be one of ${CHROME_STYLE_VALUES.join(", ")} — got ${JSON.stringify(raw.chromeStyle)}`);
|
|
152
|
+
}
|
|
153
|
+
result.chromeStyle = raw.chromeStyle;
|
|
154
|
+
}
|
|
155
|
+
if ("closingFooterPaths" in raw && raw.closingFooterPaths !== undefined) {
|
|
156
|
+
const paths = raw.closingFooterPaths;
|
|
157
|
+
const badIndex = Array.isArray(paths)
|
|
158
|
+
? paths.findIndex((p) => typeof p !== "string" || !CLOSING_FOOTER_PATH.test(p))
|
|
159
|
+
: -1;
|
|
160
|
+
if (!Array.isArray(paths) || paths.length === 0 || badIndex !== -1) {
|
|
161
|
+
throw new TemplateLayoutError(`${subject}: layout.closingFooterPaths must be a non-empty list of route paths ("/about") with no query or fragment — got ${JSON.stringify(badIndex !== -1 ? paths[badIndex] : paths)}`);
|
|
162
|
+
}
|
|
163
|
+
result.closingFooterPaths = paths;
|
|
164
|
+
}
|
|
95
165
|
return result;
|
|
96
166
|
}
|
|
97
167
|
/**
|
|
@@ -113,7 +183,7 @@ export function resolveSiteChrome(layout, designLayout) {
|
|
|
113
183
|
throw new TypeError(`resolveSiteChrome: designLayout must be a layout object, got ${JSON.stringify(designLayout)}`);
|
|
114
184
|
}
|
|
115
185
|
const fromDesign = designLayout ?? {};
|
|
116
|
-
|
|
186
|
+
const resolved = {
|
|
117
187
|
nav: layout?.nav ?? fromDesign.nav ?? DEFAULT_SITE_CHROME.nav,
|
|
118
188
|
footer: layout?.footer ?? fromDesign.footer ?? DEFAULT_SITE_CHROME.footer,
|
|
119
189
|
announcementBanner: layout?.announcementBanner ??
|
|
@@ -121,6 +191,35 @@ export function resolveSiteChrome(layout, designLayout) {
|
|
|
121
191
|
DEFAULT_SITE_CHROME.announcementBanner,
|
|
122
192
|
home: layout?.home ?? fromDesign.home ?? DEFAULT_SITE_CHROME.home,
|
|
123
193
|
};
|
|
194
|
+
// #7719 D3 — the grounds resolve as a whole list, and only a deck keeps them.
|
|
195
|
+
const deckGrounds = layout?.deckGrounds ?? fromDesign.deckGrounds;
|
|
196
|
+
if (deckGrounds !== undefined && resolved.home === "deck") {
|
|
197
|
+
resolved.deckGrounds = deckGrounds;
|
|
198
|
+
}
|
|
199
|
+
// #7721 D1 — only "bare" is ever a key, so a chrome that does not opt in
|
|
200
|
+
// (unset, or an explicit "default") resolves exactly as before.
|
|
201
|
+
if ((layout?.innerPages ?? fromDesign.innerPages) === "bare") {
|
|
202
|
+
resolved.innerPages = "bare";
|
|
203
|
+
}
|
|
204
|
+
// #7740 D1 — only `true` on a deck is ever a key: an explicit layout
|
|
205
|
+
// `false` wins over the design's `true`, and a non-deck home drops it.
|
|
206
|
+
if ((layout?.deckPresentation ?? fromDesign.deckPresentation) === true && resolved.home === "deck") {
|
|
207
|
+
resolved.deckPresentation = true;
|
|
208
|
+
}
|
|
209
|
+
// #7742 D1 — as with `innerPages`, only the opt-in value is ever a key.
|
|
210
|
+
if ((layout?.colorMode ?? fromDesign.colorMode) === "light") {
|
|
211
|
+
resolved.colorMode = "light";
|
|
212
|
+
}
|
|
213
|
+
if ((layout?.chromeStyle ?? fromDesign.chromeStyle) === "minimal") {
|
|
214
|
+
resolved.chromeStyle = "minimal";
|
|
215
|
+
}
|
|
216
|
+
// #7797 D1 — the list resolves as a whole (an explicit layout wins over the
|
|
217
|
+
// design's), and only a non-empty list is ever a key.
|
|
218
|
+
const closingFooterPaths = layout?.closingFooterPaths ?? fromDesign.closingFooterPaths;
|
|
219
|
+
if (closingFooterPaths !== undefined && closingFooterPaths.length > 0) {
|
|
220
|
+
resolved.closingFooterPaths = closingFooterPaths;
|
|
221
|
+
}
|
|
222
|
+
return resolved;
|
|
124
223
|
}
|
|
125
224
|
/**
|
|
126
225
|
* Render `apps/web/lib/site-chrome.ts` for a resolved layout — the same
|
|
@@ -130,6 +229,30 @@ export function resolveSiteChrome(layout, designLayout) {
|
|
|
130
229
|
* `snap="mandatory"` + `deckHero` to MarketingShell.
|
|
131
230
|
*/
|
|
132
231
|
export function renderSiteChrome(resolved) {
|
|
232
|
+
// #7719 D3 — each `deckGrounds` line is emitted only when the chrome has one,
|
|
233
|
+
// so a chrome without it renders today's text exactly.
|
|
234
|
+
const grounds = resolved.deckGrounds;
|
|
235
|
+
const groundsComment = grounds === undefined
|
|
236
|
+
? ""
|
|
237
|
+
: "\n// - deckGrounds → the deck home cycles these surfaces, slide by slide, onto slides that author none";
|
|
238
|
+
const groundsMember = grounds === undefined
|
|
239
|
+
? ""
|
|
240
|
+
: `\n deckGrounds?: readonly (${DECK_GROUND_VALUES.map((g) => JSON.stringify(g)).join(" | ")})[];`;
|
|
241
|
+
const groundsValue = grounds === undefined ? "" : `\n deckGrounds: [${grounds.map((g) => JSON.stringify(g)).join(", ")}],`;
|
|
242
|
+
// #7721 D1 — the value line only for a bare chrome; the interface member
|
|
243
|
+
// and comment bullet always, so the layout reads `siteChrome.innerPages`.
|
|
244
|
+
const innerPagesValue = resolved.innerPages === "bare" ? '\n innerPages: "bare",' : "";
|
|
245
|
+
// #7740 D1 — `innerPages`' form: the member and bullet always, the value
|
|
246
|
+
// line only when the chrome presents.
|
|
247
|
+
const presentationValue = resolved.deckPresentation === true ? "\n deckPresentation: true," : "";
|
|
248
|
+
// #7742 D1 — the same form again: member and bullet always, value lines
|
|
249
|
+
// only for the opt-in values.
|
|
250
|
+
const colorModeValue = resolved.colorMode === "light" ? '\n colorMode: "light",' : "";
|
|
251
|
+
const chromeStyleValue = resolved.chromeStyle === "minimal" ? '\n chromeStyle: "minimal",' : "";
|
|
252
|
+
// #7797 D1 — the same form: member and bullet always, the value line only for a non-empty list.
|
|
253
|
+
const closingFooterPathsValue = resolved.closingFooterPaths === undefined
|
|
254
|
+
? ""
|
|
255
|
+
: `\n closingFooterPaths: [${resolved.closingFooterPaths.map((p) => JSON.stringify(p)).join(", ")}],`;
|
|
133
256
|
return `// site-chrome.ts — resolved template layout knobs.
|
|
134
257
|
//
|
|
135
258
|
// GENERATED by create-ailk from the declared template's \`layout\` block
|
|
@@ -145,19 +268,34 @@ export function renderSiteChrome(resolved) {
|
|
|
145
268
|
// - home: "deck" → the locale-root route mounts MarketingShell with
|
|
146
269
|
// snap="mandatory" + deckHero (spec 5426 D7); every other route is
|
|
147
270
|
// unaffected.
|
|
271
|
+
// - innerPages: "bare" → every non-home page renders the bare header and
|
|
272
|
+
// the one-bar footer.
|
|
273
|
+
// - deckPresentation → the deck home presents slide by slide: fitted slides, slide anchors, the mark follows the ground
|
|
274
|
+
// - colorMode: "light" → the site is pinned to light and shows no theme
|
|
275
|
+
// switch; dark appears only as a deck ground.
|
|
276
|
+
// - chromeStyle: "minimal" → the deck's closing slide is the wordmark,
|
|
277
|
+
// contact lines and one legal line; the consent bar is a hairline bar on
|
|
278
|
+
// the page ground; the 404 renders in the site's theme.
|
|
279
|
+
// - closingFooterPaths → each listed inner page ends on the closing footer
|
|
280
|
+
// (the wordmark, contact lines and legal line) instead of its usual footer.${groundsComment}
|
|
148
281
|
|
|
149
282
|
export interface SiteChrome {
|
|
150
283
|
nav: "full" | "mark-only";
|
|
151
284
|
footer: "columns" | "centered";
|
|
152
285
|
announcementBanner: boolean;
|
|
153
286
|
home: "default" | "deck";
|
|
287
|
+
innerPages?: "bare";
|
|
288
|
+
deckPresentation?: true;
|
|
289
|
+
colorMode?: "light";
|
|
290
|
+
chromeStyle?: "minimal";
|
|
291
|
+
closingFooterPaths?: readonly string[];${groundsMember}
|
|
154
292
|
}
|
|
155
293
|
|
|
156
294
|
export const siteChrome: SiteChrome = {
|
|
157
295
|
nav: ${JSON.stringify(resolved.nav)},
|
|
158
296
|
footer: ${JSON.stringify(resolved.footer)},
|
|
159
297
|
announcementBanner: ${JSON.stringify(resolved.announcementBanner)},
|
|
160
|
-
home: ${JSON.stringify(resolved.home)}
|
|
298
|
+
home: ${JSON.stringify(resolved.home)},${groundsValue}${innerPagesValue}${presentationValue}${colorModeValue}${chromeStyleValue}${closingFooterPathsValue}
|
|
161
299
|
};
|
|
162
300
|
`;
|
|
163
301
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The web faces a design's typography sources (#7720, spec
|
|
3
|
+
* docs/specs/7720-a-web-font-source-and-spec.md D3.2) — rendered into the
|
|
4
|
+
* generated `apps/web/lib/site-fonts.ts` every scaffold gets, beside
|
|
5
|
+
* `site-chrome.ts` and in the same GENERATED-header idiom.
|
|
6
|
+
*
|
|
7
|
+
* next/font/google picks a face by the imported function's name and needs
|
|
8
|
+
* literal options at a module-scope call site, so a face cannot be chosen
|
|
9
|
+
* from the request-time `theme:` block — it is chosen here, at scaffold and
|
|
10
|
+
* apply-design time, as source text. The faces download at the site's
|
|
11
|
+
* `next build` and are served from its own origin, which the CSP's
|
|
12
|
+
* `font-src 'self'` already allows.
|
|
13
|
+
*
|
|
14
|
+
* With no sourced face the render is the empty default, byte-identical to the
|
|
15
|
+
* monorepo's own committed `apps/web/lib/site-fonts.ts`.
|
|
16
|
+
*/
|
|
17
|
+
import type { CompiledDesignFonts } from "./design-compile.js";
|
|
18
|
+
/** Template-relative path of the generated site-fonts module — beside `SITE_CHROME_PATH`. */
|
|
19
|
+
export declare const SITE_FONTS_PATH = "apps/web/lib/site-fonts.ts";
|
|
20
|
+
/** Render `apps/web/lib/site-fonts.ts` for `fonts` (D3.2). Ends with a trailing newline. */
|
|
21
|
+
export declare function renderSiteFonts(fonts: CompiledDesignFonts | undefined): string;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/** Template-relative path of the generated site-fonts module — beside `SITE_CHROME_PATH`. */
|
|
2
|
+
export const SITE_FONTS_PATH = "apps/web/lib/site-fonts.ts";
|
|
3
|
+
const HEADER = `// site-fonts.ts — the web faces the site's design sources.
|
|
4
|
+
//
|
|
5
|
+
// GENERATED by create-ailk from the design's \`typography.<role>.fontSource\`
|
|
6
|
+
// (#7720). A design that sources no face gets the empty default.
|
|
7
|
+
// \`apps/web/app/[locale]/layout.tsx\` passes this to \`applyDesignFonts\`
|
|
8
|
+
// (@working-theory/theme): \`families\` fills --font-display / --font-body
|
|
9
|
+
// unless the site's own \`brand.fonts\` sets them, and \`className\` (the
|
|
10
|
+
// next/font variable classes) goes on <html>.
|
|
11
|
+
`;
|
|
12
|
+
const INTERFACE = `export interface SiteFonts {
|
|
13
|
+
className: string;
|
|
14
|
+
families: { display?: string | undefined; body?: string | undefined };
|
|
15
|
+
}
|
|
16
|
+
`;
|
|
17
|
+
/** Render `apps/web/lib/site-fonts.ts` for `fonts` (D3.2). Ends with a trailing newline. */
|
|
18
|
+
export function renderSiteFonts(fonts) {
|
|
19
|
+
const faces = ["display", "body"].flatMap((role) => {
|
|
20
|
+
const face = fonts?.[role];
|
|
21
|
+
return face !== undefined ? [{ role, face }] : [];
|
|
22
|
+
});
|
|
23
|
+
if (faces.length === 0) {
|
|
24
|
+
return `${HEADER}\n${INTERFACE}\nexport const siteFonts: SiteFonts = { className: "", families: {} };\n`;
|
|
25
|
+
}
|
|
26
|
+
// One loader per distinct family, named for the first role using it (display before body).
|
|
27
|
+
const slotOf = new Map();
|
|
28
|
+
for (const { role, face } of faces) {
|
|
29
|
+
if (!slotOf.has(face.family))
|
|
30
|
+
slotOf.set(face.family, role);
|
|
31
|
+
}
|
|
32
|
+
const exportName = (family) => family.replace(/ /g, "_");
|
|
33
|
+
const imports = [...slotOf.keys()].map(exportName).sort();
|
|
34
|
+
const loaders = [...slotOf].map(([family, slot]) => `const ${slot}Face = ${exportName(family)}({ subsets: ${JSON.stringify(["latin"])}, variable: ${JSON.stringify(`--font-design-${slot}`)} });`);
|
|
35
|
+
const className = [...slotOf.values()].map((slot) => `${slot}Face.variable`).join(", ");
|
|
36
|
+
const families = faces.map(({ role, face }) => {
|
|
37
|
+
const slot = slotOf.get(face.family) ?? role;
|
|
38
|
+
const value = `var(--font-design-${slot})${face.fallback === "" ? "" : `, ${face.fallback}`}`;
|
|
39
|
+
return ` ${role}: ${JSON.stringify(value)},`;
|
|
40
|
+
});
|
|
41
|
+
return `${HEADER}
|
|
42
|
+
import { ${imports.join(", ")} } from "next/font/google";
|
|
43
|
+
|
|
44
|
+
${INTERFACE}
|
|
45
|
+
${loaders.join("\n")}
|
|
46
|
+
|
|
47
|
+
export const siteFonts: SiteFonts = {
|
|
48
|
+
className: [${className}].join(" "),
|
|
49
|
+
families: {
|
|
50
|
+
${families.join("\n")}
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
`;
|
|
54
|
+
}
|
package/dist/site-manifest.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { Level } from "./lib/module-tier.js";
|
|
|
2
2
|
import type { ModuleName } from "./parse-args.js";
|
|
3
3
|
import { type SiteRoster } from "./site-roster.js";
|
|
4
4
|
import type { MarkerMap } from "./substitute.js";
|
|
5
|
+
import type { Topology } from "./topology.js";
|
|
5
6
|
export interface PlanSiteManifestOptions {
|
|
6
7
|
targetDir: string;
|
|
7
8
|
/** The installed manifest, read after every layer landed. */
|
|
@@ -10,16 +11,22 @@ export interface PlanSiteManifestOptions {
|
|
|
10
11
|
modules: readonly ModuleName[];
|
|
11
12
|
/** The level the key path derived, or undefined when it derived none. */
|
|
12
13
|
level: Level | undefined;
|
|
14
|
+
/** The scaffold's topology (#7426 D10). */
|
|
15
|
+
topology: Topology;
|
|
16
|
+
/** The create-ailk version that wrote the scaffold (`CREATE_AILK_VERSION`). */
|
|
17
|
+
patternVersion: string;
|
|
13
18
|
siteTemplatesDir: string;
|
|
14
19
|
templatesDir: string;
|
|
15
20
|
markers: MarkerMap;
|
|
16
21
|
}
|
|
17
22
|
/**
|
|
18
|
-
* The manifest's next text: `modules` (and `level`, when derived)
|
|
19
|
-
* `requires`, and a row for every
|
|
23
|
+
* The manifest's next text: `modules` (and `level`, when derived), then
|
|
24
|
+
* `topology` and `patternVersion`, after `requires`, and a row for every
|
|
25
|
+
* route present on disk.
|
|
20
26
|
*
|
|
21
|
-
* @throws AddModuleRefusedError when the file already records `modules
|
|
22
|
-
* `level`, or its layout is not the
|
|
23
|
-
* at the top level) — guessing would
|
|
27
|
+
* @throws AddModuleRefusedError when the file already records `modules`,
|
|
28
|
+
* `level`, `topology` or `patternVersion`, or its layout is not the
|
|
29
|
+
* generator's (`rows` last, `requires` at the top level) — guessing would
|
|
30
|
+
* silently mis-record the site.
|
|
24
31
|
*/
|
|
25
32
|
export declare function planSiteManifest(opts: PlanSiteManifestOptions): string;
|
package/dist/site-manifest.js
CHANGED
|
@@ -15,6 +15,10 @@
|
|
|
15
15
|
* (`resolveModuleSet` / `deriveLevel`). Never inferred from `modules`:
|
|
16
16
|
* the level comes only from the key. An alias or `--modules` keep run
|
|
17
17
|
* derives none and records none;
|
|
18
|
+
* - `topology` and `patternVersion` (#7426 D10) — `"hub"` or `"spoke"`,
|
|
19
|
+
* and the create-ailk version that wrote the files, on every scaffold.
|
|
20
|
+
* A site with no `topology` predates the option and reads as a hub
|
|
21
|
+
* (`topology.ts` `readSiteTopology`);
|
|
18
22
|
* - a row for every route present on disk — each module's contributed rows
|
|
19
23
|
* through `planRosterAppend` (add-module's own append), the sample pages
|
|
20
24
|
* a module's bundle installs (`sampleRows`), and a code-route row for any
|
|
@@ -51,16 +55,18 @@ const CODE_ROUTE_ROW = {
|
|
|
51
55
|
sections: [],
|
|
52
56
|
};
|
|
53
57
|
/**
|
|
54
|
-
* The manifest's next text: `modules` (and `level`, when derived)
|
|
55
|
-
* `requires`, and a row for every
|
|
58
|
+
* The manifest's next text: `modules` (and `level`, when derived), then
|
|
59
|
+
* `topology` and `patternVersion`, after `requires`, and a row for every
|
|
60
|
+
* route present on disk.
|
|
56
61
|
*
|
|
57
|
-
* @throws AddModuleRefusedError when the file already records `modules
|
|
58
|
-
* `level`, or its layout is not the
|
|
59
|
-
* at the top level) — guessing would
|
|
62
|
+
* @throws AddModuleRefusedError when the file already records `modules`,
|
|
63
|
+
* `level`, `topology` or `patternVersion`, or its layout is not the
|
|
64
|
+
* generator's (`rows` last, `requires` at the top level) — guessing would
|
|
65
|
+
* silently mis-record the site.
|
|
60
66
|
*/
|
|
61
67
|
export function planSiteManifest(opts) {
|
|
62
68
|
const { roster } = opts;
|
|
63
|
-
for (const key of ["modules", "level"]) {
|
|
69
|
+
for (const key of ["modules", "level", "topology", "patternVersion"]) {
|
|
64
70
|
if (key in roster.doc) {
|
|
65
71
|
throw new AddModuleRefusedError(`${roster.path} already records "${key}"`);
|
|
66
72
|
}
|
|
@@ -81,7 +87,7 @@ export function planSiteManifest(opts) {
|
|
|
81
87
|
...codeRouteRows(opts.targetDir, declared),
|
|
82
88
|
];
|
|
83
89
|
const withRows = appendRows(appended, extra, roster);
|
|
84
|
-
return insertRecord(withRows, opts
|
|
90
|
+
return insertRecord(withRows, opts, roster.path);
|
|
85
91
|
}
|
|
86
92
|
/**
|
|
87
93
|
* The rows for the sample pages a module's bundle installs through its
|
|
@@ -233,8 +239,12 @@ function appendRows(text, rows, roster) {
|
|
|
233
239
|
? text.slice(0, end) + rendered + roster.trail + text.slice(close)
|
|
234
240
|
: text.slice(0, end) + "," + rendered + text.slice(end);
|
|
235
241
|
}
|
|
236
|
-
/**
|
|
237
|
-
|
|
242
|
+
/**
|
|
243
|
+
* `text` with `modules` (and `level`, when derived), `topology` and
|
|
244
|
+
* `patternVersion` inserted after the top-level `requires`.
|
|
245
|
+
*/
|
|
246
|
+
function insertRecord(text, opts, label) {
|
|
247
|
+
const { modules, level, topology, patternVersion } = opts;
|
|
238
248
|
const anchor = '\n "requires": ';
|
|
239
249
|
const at = text.indexOf(anchor);
|
|
240
250
|
if (at === -1 || text.indexOf(anchor, at + 1) !== -1) {
|
|
@@ -243,11 +253,15 @@ function insertRecord(text, modules, level, label) {
|
|
|
243
253
|
const end = text.indexOf("]", at) + 1;
|
|
244
254
|
const name = '"modules": ';
|
|
245
255
|
const record = `,\n ${name}${renderJson([...modules], 2, name.length, 1)}` +
|
|
246
|
-
(level !== undefined ? `,\n "level": ${JSON.stringify(level)}` : "")
|
|
256
|
+
(level !== undefined ? `,\n "level": ${JSON.stringify(level)}` : "") +
|
|
257
|
+
`,\n "topology": ${JSON.stringify(topology)}` +
|
|
258
|
+
`,\n "patternVersion": ${JSON.stringify(patternVersion)}`;
|
|
247
259
|
const next = text.slice(0, end) + record + text.slice(end);
|
|
248
260
|
const doc = JSON.parse(next);
|
|
249
261
|
if (!isDeepStrictEqual(doc["modules"], [...modules]) ||
|
|
250
|
-
doc["level"] !== level
|
|
262
|
+
doc["level"] !== level ||
|
|
263
|
+
doc["topology"] !== topology ||
|
|
264
|
+
doc["patternVersion"] !== patternVersion) {
|
|
251
265
|
throw new AddModuleRefusedError(`${label}: the module set could not be recorded beside "requires"`);
|
|
252
266
|
}
|
|
253
267
|
return next;
|
package/dist/site-roster.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { CompiledDesignTheme } from "./design-compile.js";
|
|
2
|
+
import { type SectionLookCatalog } from "./design-presets.js";
|
|
2
3
|
import { type PageRosterRow } from "./module-architecture.js";
|
|
3
4
|
import type { ModuleName } from "./parse-args.js";
|
|
4
5
|
import type { ResolvedSiteChrome } from "./site-chrome.js";
|
|
@@ -76,6 +77,21 @@ export interface SiteManifestDesignNext {
|
|
|
76
77
|
* never reaches this function (`scaffold()` guards on the file's presence).
|
|
77
78
|
*/
|
|
78
79
|
export declare function planSiteManifestDesign(text: string, next: SiteManifestDesignNext, label: string): string | undefined;
|
|
80
|
+
/**
|
|
81
|
+
* `build-template-manifest.mjs`'s `OPTION_KEYS` — the section `data` keys
|
|
82
|
+
* whose scalar values a manifest row's `sections[].options` records. A
|
|
83
|
+
* duplicate (the script is not importable here); a test pins the two equal.
|
|
84
|
+
*/
|
|
85
|
+
export declare const MANIFEST_OPTION_KEYS: readonly ["itemStyle", "actionsPosition", "gapBelow", "measure", "cardLayout", "layout", "size", "surface", "imagerySlot", "headingLevel", "align", "tone", "columns", "density", "fill", "accentStyle"];
|
|
86
|
+
/**
|
|
87
|
+
* Plan the manifest's `/` row `sections` as the home's sections (#7743 D6):
|
|
88
|
+
* `[{ type, component, options? }]`, `options` being the section's scalar
|
|
89
|
+
* `MANIFEST_OPTION_KEYS` values, as the generator writes them. `undefined`
|
|
90
|
+
* when the record already says so, or the manifest has no `/` row with a
|
|
91
|
+
* `sections` key. The value is spliced in place at its own depth; no other
|
|
92
|
+
* byte moves.
|
|
93
|
+
*/
|
|
94
|
+
export declare function planHomeRecord(manifestText: string, homeText: string, catalog: SectionLookCatalog): string | undefined;
|
|
79
95
|
/**
|
|
80
96
|
* The generator's row for each of a module's `pageContributions`, in
|
|
81
97
|
* declaration order — read from the committed manifest of the first
|
package/dist/site-roster.js
CHANGED
|
@@ -27,6 +27,8 @@ import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
|
27
27
|
import { join, relative, sep } from "node:path";
|
|
28
28
|
import { isDeepStrictEqual } from "node:util";
|
|
29
29
|
import { AddModuleRefusedError, readArchitectureHeader, } from "./add-module-core.js";
|
|
30
|
+
import { HOME_PAGE_PATH } from "./design-home.js";
|
|
31
|
+
import { locatePageSections } from "./design-presets.js";
|
|
30
32
|
import { diffTwoStrings } from "./lib/extract-module-bundle.js";
|
|
31
33
|
import { MODULE_SLICES, normalizeModules, } from "./module-architecture.js";
|
|
32
34
|
import { substituteMarkers } from "./substitute.js";
|
|
@@ -234,6 +236,85 @@ export function planSiteManifestDesign(text, next, label) {
|
|
|
234
236
|
}
|
|
235
237
|
return result;
|
|
236
238
|
}
|
|
239
|
+
// ── #7743 D6: the `/` row records the home's sections ─────────────────────
|
|
240
|
+
/**
|
|
241
|
+
* `build-template-manifest.mjs`'s `OPTION_KEYS` — the section `data` keys
|
|
242
|
+
* whose scalar values a manifest row's `sections[].options` records. A
|
|
243
|
+
* duplicate (the script is not importable here); a test pins the two equal.
|
|
244
|
+
*/
|
|
245
|
+
export const MANIFEST_OPTION_KEYS = [
|
|
246
|
+
"itemStyle",
|
|
247
|
+
"actionsPosition",
|
|
248
|
+
"gapBelow",
|
|
249
|
+
"measure",
|
|
250
|
+
"cardLayout",
|
|
251
|
+
"layout",
|
|
252
|
+
"size",
|
|
253
|
+
"surface",
|
|
254
|
+
"imagerySlot",
|
|
255
|
+
"headingLevel",
|
|
256
|
+
"align",
|
|
257
|
+
"tone",
|
|
258
|
+
"columns",
|
|
259
|
+
"density",
|
|
260
|
+
"fill",
|
|
261
|
+
"accentStyle",
|
|
262
|
+
];
|
|
263
|
+
/** A raw YAML scalar's value — a JSON scalar, a single-quoted string or a plain one — or undefined for a collection or null. */
|
|
264
|
+
function scalarValue(raw) {
|
|
265
|
+
const text = raw.trim();
|
|
266
|
+
if (text.startsWith("{") || text.startsWith("["))
|
|
267
|
+
return undefined;
|
|
268
|
+
if (text.startsWith("'"))
|
|
269
|
+
return text.slice(1, -1).replace(/''/g, "'");
|
|
270
|
+
try {
|
|
271
|
+
const value = JSON.parse(text);
|
|
272
|
+
return value === null || typeof value === "object" ? undefined : value;
|
|
273
|
+
}
|
|
274
|
+
catch {
|
|
275
|
+
return text;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Plan the manifest's `/` row `sections` as the home's sections (#7743 D6):
|
|
280
|
+
* `[{ type, component, options? }]`, `options` being the section's scalar
|
|
281
|
+
* `MANIFEST_OPTION_KEYS` values, as the generator writes them. `undefined`
|
|
282
|
+
* when the record already says so, or the manifest has no `/` row with a
|
|
283
|
+
* `sections` key. The value is spliced in place at its own depth; no other
|
|
284
|
+
* byte moves.
|
|
285
|
+
*/
|
|
286
|
+
export function planHomeRecord(manifestText, homeText, catalog) {
|
|
287
|
+
const file = parseRosterFile(manifestText, SITE_ROSTER_PATH);
|
|
288
|
+
const row = file.rows.find((r) => r.value["route"] === "/");
|
|
289
|
+
if (row === undefined || !("sections" in row.value))
|
|
290
|
+
return undefined;
|
|
291
|
+
const sections = (locatePageSections(homeText, HOME_PAGE_PATH) ?? []).map((section) => {
|
|
292
|
+
const options = {};
|
|
293
|
+
for (const key of MANIFEST_OPTION_KEYS) {
|
|
294
|
+
const entry = section.entries.find((e) => e.key === key);
|
|
295
|
+
const value = entry === undefined ? undefined : scalarValue(homeText.slice(entry.valueStart, entry.valueEnd));
|
|
296
|
+
if (value !== undefined)
|
|
297
|
+
options[key] = value;
|
|
298
|
+
}
|
|
299
|
+
return {
|
|
300
|
+
type: section.type,
|
|
301
|
+
component: catalog[section.type]?.component ?? section.type,
|
|
302
|
+
...(Object.keys(options).length > 0 ? { options } : {}),
|
|
303
|
+
};
|
|
304
|
+
});
|
|
305
|
+
if (isDeepStrictEqual(row.value["sections"], sections))
|
|
306
|
+
return undefined;
|
|
307
|
+
const rowStart = manifestText.indexOf(row.text, file.open);
|
|
308
|
+
const at = findKey(row.text, 0, "sections");
|
|
309
|
+
const end = endOfValue(row.text, at.valueStart);
|
|
310
|
+
const column = at.keyStart - (row.text.lastIndexOf("\n", at.keyStart) + 1);
|
|
311
|
+
const rendered = JSON.stringify(sections, null, 2)
|
|
312
|
+
.split("\n")
|
|
313
|
+
.map((line, i) => (i === 0 ? line : `${" ".repeat(column)}${line}`))
|
|
314
|
+
.join("\n");
|
|
315
|
+
const rowText = row.text.slice(0, at.valueStart) + rendered + row.text.slice(end);
|
|
316
|
+
return manifestText.slice(0, rowStart) + rowText + manifestText.slice(rowStart + row.text.length);
|
|
317
|
+
}
|
|
237
318
|
// ── D2: a module's rows, from the shipped manifests ───────────────────────
|
|
238
319
|
/** A template's shipped manifest, or undefined when its tree ships none. */
|
|
239
320
|
function readShippedManifest(siteTemplatesDir, templateName) {
|
package/dist/starter-grid.js
CHANGED
|
@@ -170,6 +170,18 @@ const PORTFOLIO_ROWS = [
|
|
|
170
170
|
{ route: "/portfolio", content: ["content/en/PortfolioList/portfolio.mdx"] },
|
|
171
171
|
{ route: "/portfolio/[slug]", contentDir: "content/en/PortfolioItem" },
|
|
172
172
|
];
|
|
173
|
+
/**
|
|
174
|
+
* The team pages (#7744, checklist D1) — a TeamList hub at `/team` and one
|
|
175
|
+
* TeamMember profile per person at `/team/<slug>`, each emitting a
|
|
176
|
+
* `ProfilePage` whose main entity is that `Person`. They belong to the three
|
|
177
|
+
* ORGANIZATION shapes; person-service is one person and carries none. The
|
|
178
|
+
* seeds live in the `_base` layer under `content/en/TeamMember/`; the detail
|
|
179
|
+
* row's `contentDir` installs them, exactly as `PORTFOLIO_ROWS` does.
|
|
180
|
+
*/
|
|
181
|
+
const TEAM_ROWS = [
|
|
182
|
+
{ route: "/team", content: ["content/en/TeamList/team.mdx"] },
|
|
183
|
+
{ route: "/team/[slug]", contentDir: "content/en/TeamMember" },
|
|
184
|
+
];
|
|
173
185
|
/**
|
|
174
186
|
* The two report/referral sample rows. Neither carries a `GATEABLE_SURFACES`
|
|
175
187
|
* entry: `/referral` is a single-segment `LandingPage` slug served by the
|
|
@@ -308,18 +320,21 @@ export const SHAPE_ROSTERS = Object.freeze({
|
|
|
308
320
|
FREE_BLOG_TEASER_ROW,
|
|
309
321
|
...FREE_MEET_PLACEHOLDER_ROWS,
|
|
310
322
|
...PORTFOLIO_ROWS,
|
|
323
|
+
...TEAM_ROWS,
|
|
311
324
|
],
|
|
312
325
|
"organization-service": [
|
|
313
326
|
...ORGANIZATION_SERVICE_ROWS,
|
|
314
327
|
FREE_BLOG_TEASER_ROW,
|
|
315
328
|
...FREE_MEET_PLACEHOLDER_ROWS,
|
|
316
329
|
...PORTFOLIO_ROWS,
|
|
330
|
+
...TEAM_ROWS,
|
|
317
331
|
],
|
|
318
332
|
"local-business-service": [
|
|
319
333
|
...LOCAL_BUSINESS_SERVICE_ROWS,
|
|
320
334
|
FREE_BLOG_TEASER_ROW,
|
|
321
335
|
...FREE_MEET_PLACEHOLDER_ROWS,
|
|
322
336
|
...PORTFOLIO_ROWS,
|
|
337
|
+
...TEAM_ROWS,
|
|
323
338
|
],
|
|
324
339
|
"person-service": [
|
|
325
340
|
...PERSON_SERVICE_ROWS,
|
package/dist/surfaces.js
CHANGED
|
@@ -299,6 +299,13 @@ export const GATEABLE_SURFACES = [
|
|
|
299
299
|
routes: [`${ROUTE_ROOT}/software`],
|
|
300
300
|
content: ["content/<locale>/SoftwareProduct"],
|
|
301
301
|
},
|
|
302
|
+
{
|
|
303
|
+
// #7744 — the team profile pages, module-less: a roster row enables it,
|
|
304
|
+
// so only the organization shapes ship /team (person-service does not).
|
|
305
|
+
id: "team",
|
|
306
|
+
routes: [`${ROUTE_ROOT}/team`],
|
|
307
|
+
content: ["content/<locale>/TeamList", "content/<locale>/TeamMember"],
|
|
308
|
+
},
|
|
302
309
|
{
|
|
303
310
|
id: "tech-articles",
|
|
304
311
|
routes: [`${ROUTE_ROOT}/tech-articles`],
|
|
@@ -583,6 +590,9 @@ export const ROUTE_SINCE = {
|
|
|
583
590
|
"services/[slug]": "0.0.0",
|
|
584
591
|
software: "0.0.0",
|
|
585
592
|
"software/[slug]": "0.0.0",
|
|
593
|
+
// #7744 — the team profile pages, a new route pair at today's line.
|
|
594
|
+
team: "0.35.0",
|
|
595
|
+
"team/[slug]": "0.35.0",
|
|
586
596
|
"tech-articles": "0.0.0",
|
|
587
597
|
"tech-articles/[slug]": "0.0.0",
|
|
588
598
|
terms: "0.22.0",
|
|
@@ -824,6 +834,8 @@ export const ROUTE_MESSAGE_NAMESPACES = {
|
|
|
824
834
|
"(authed)/admin/site": ["admin"],
|
|
825
835
|
"(authed)/admin/team": ["admin"],
|
|
826
836
|
"(authed)/waitlist": ["waitlistDashboard.gate"],
|
|
837
|
+
// #7744 — the /team hub's empty state reads the `team` namespace.
|
|
838
|
+
team: ["team"],
|
|
827
839
|
};
|
|
828
840
|
/** Template-relative path of the generated site-nav registry. */
|
|
829
841
|
export const NAV_CONFIG_PATH = "apps/web/lib/nav-config.ts";
|
package/dist/sync-routes.js
CHANGED
|
@@ -33,6 +33,7 @@ import { parseArgs } from "node:util";
|
|
|
33
33
|
import { copyTemplate } from "./copy.js";
|
|
34
34
|
import { substituteMarkers } from "./substitute.js";
|
|
35
35
|
import { GATEABLE_SURFACES, NON_ROUTE_DIRS, ROUTE_DEPENDENCIES, ROUTE_MESSAGE_NAMESPACES, ROUTE_SINCE, surfaceForRouteDir, } from "./surfaces.js";
|
|
36
|
+
import { isSpokeExcluded, readSiteTopology } from "./topology.js";
|
|
36
37
|
/** Template-relative prefix every `apps/web` route directory sits under. */
|
|
37
38
|
const ROUTE_ROOT = "apps/web/app/[locale]";
|
|
38
39
|
/**
|
|
@@ -163,12 +164,20 @@ export function syncRoutes(opts) {
|
|
|
163
164
|
skippedSurfaces.push(surface.id);
|
|
164
165
|
}
|
|
165
166
|
skippedSurfaces.sort();
|
|
167
|
+
// #7426 D13 — on a spoke, a route dir the spoke does not carry (the
|
|
168
|
+
// `(auth)` and `(authed)` groups) is out of scope: neither reported nor
|
|
169
|
+
// copied. This is the one filter point for add-module, add-route and the
|
|
170
|
+
// sync-routes subcommand alike; a hub's scope is unchanged.
|
|
171
|
+
const spoke = readSiteTopology(targetAbs) === "spoke"
|
|
172
|
+
? { mcp: existsSync(join(targetAbs, "apps", "mcp")) }
|
|
173
|
+
: undefined;
|
|
166
174
|
const inScope = listTemplateRouteDirs(opts.templatesDir)
|
|
167
175
|
.map(reportFor)
|
|
168
176
|
// "authed" (#5014): the `(authed)` route group ships unconditionally in
|
|
169
|
-
// every scaffold
|
|
177
|
+
// every hub scaffold, the same as "core" — it is checked regardless of
|
|
170
178
|
// `carried`, which only tracks GATEABLE_SURFACES opt-in.
|
|
171
|
-
.filter((r) => r.surface === "core" || r.surface === "authed" || carried.has(r.surface))
|
|
179
|
+
.filter((r) => r.surface === "core" || r.surface === "authed" || carried.has(r.surface))
|
|
180
|
+
.filter((r) => spoke === undefined || !isSpokeExcluded(`${ROUTE_ROOT}/${r.route}`, spoke));
|
|
172
181
|
// Identical to scaffoldSync's defaults, so a synced route matches a fresh
|
|
173
182
|
// scaffold of the same target byte-for-byte.
|
|
174
183
|
const markers = {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One people-slide / team-index member: the `team` section's member shape,
|
|
3
|
+
* declared here because create-ailk depends on neither
|
|
4
|
+
* `@working-theory/validation` nor `yaml`.
|
|
5
|
+
*/
|
|
6
|
+
export interface TeamSlideMember {
|
|
7
|
+
readonly name: string;
|
|
8
|
+
readonly role: string;
|
|
9
|
+
readonly links: readonly [
|
|
10
|
+
{
|
|
11
|
+
readonly type: "page";
|
|
12
|
+
readonly url: string;
|
|
13
|
+
readonly label: string;
|
|
14
|
+
}
|
|
15
|
+
];
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The three seeded people, in seed order: slug, the seed's `title` as `name`,
|
|
19
|
+
* "{Their title}", and a page link to `/team/<slug>` labelled "{View profile}".
|
|
20
|
+
*/
|
|
21
|
+
export declare const TEAM_SEED_MEMBERS: readonly (TeamSlideMember & {
|
|
22
|
+
readonly slug: string;
|
|
23
|
+
})[];
|
|
24
|
+
/**
|
|
25
|
+
* `TEAM_SEED_MEMBERS` whose `<siteRoot>/content/en/TeamMember/<slug>.mdx`
|
|
26
|
+
* exists (without `slug`), or `undefined` when none does (person-service, or
|
|
27
|
+
* a site that deleted them). Reads, never writes.
|
|
28
|
+
*/
|
|
29
|
+
export declare function teamSlideMembers(siteRoot: string): TeamSlideMember[] | undefined;
|