create-ailk 0.13.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/README.md +12 -4
  2. package/component-catalog.json +247 -33
  3. package/component-catalog.md +10 -6
  4. package/dist/apply-design.d.ts +21 -2
  5. package/dist/apply-design.js +120 -19
  6. package/dist/cli.js +1 -0
  7. package/dist/design-compile.d.ts +70 -2
  8. package/dist/design-compile.js +97 -1
  9. package/dist/design-home.d.ts +74 -0
  10. package/dist/design-home.js +256 -0
  11. package/dist/design-package.d.ts +39 -2
  12. package/dist/design-package.js +223 -9
  13. package/dist/design-presets.d.ts +17 -1
  14. package/dist/design-presets.js +17 -5
  15. package/dist/design-scaffold.d.ts +6 -25
  16. package/dist/design-scaffold.js +26 -74
  17. package/dist/index.d.ts +1 -0
  18. package/dist/lib/design-checks.d.ts +12 -1
  19. package/dist/lib/design-checks.js +38 -2
  20. package/dist/lib/module-license-gate.d.ts +9 -0
  21. package/dist/lib/module-license-gate.js +18 -0
  22. package/dist/programmatic.js +70 -26
  23. package/dist/site-chrome.d.ts +38 -0
  24. package/dist/site-chrome.js +109 -2
  25. package/dist/site-fonts.d.ts +21 -0
  26. package/dist/site-fonts.js +54 -0
  27. package/dist/site-roster.d.ts +16 -0
  28. package/dist/site-roster.js +81 -0
  29. package/dist/starter-grid.js +15 -0
  30. package/dist/surfaces.js +12 -0
  31. package/dist/team-profiles.d.ts +29 -0
  32. package/dist/team-profiles.js +43 -0
  33. package/package.json +3 -3
  34. package/release-scope.json +1 -1
  35. package/site-templates/_base/content/en/TeamList/team.mdx +68 -0
  36. package/site-templates/_base/content/en/TeamMember/first-member.mdx +22 -0
  37. package/site-templates/_base/content/en/TeamMember/second-member.mdx +22 -0
  38. package/site-templates/_base/content/en/TeamMember/third-member.mdx +22 -0
  39. package/site-templates/designs/minimalism/DESIGN.md +164 -34
  40. package/site-templates/designs/minimalism/home/local-business-service.mdx +119 -0
  41. package/site-templates/designs/minimalism/home/organization-product.mdx +132 -0
  42. package/site-templates/designs/minimalism/home/organization-service.mdx +131 -0
  43. package/site-templates/designs/minimalism/home/person-service.mdx +101 -0
  44. package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +34 -1
  45. package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +20 -0
  46. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +34 -1
  47. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +22 -1
  48. package/site-templates/marketing-site-free/ailk-template.json +1 -1
  49. package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.json +34 -1
  50. package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.md +26 -7
  51. package/site-templates/shapes/local-business-service/ailk-template.json +1 -1
  52. package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.json +34 -1
  53. package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.md +26 -7
  54. package/site-templates/shapes/organization-service/ailk-template.json +1 -1
  55. package/templates/README.md +2 -0
  56. package/templates/apps/api/CLAUDE.md +4 -1
  57. package/templates/apps/mcp/package.json +1 -1
  58. package/templates/apps/web/CLAUDE.md +11 -0
  59. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/settings.test.tsx +10 -0
  60. package/templates/apps/web/app/[locale]/(authed)/account/settings/__tests__/theme-modes.test.ts +42 -0
  61. package/templates/apps/web/app/[locale]/(authed)/account/settings/theme-modes.ts +8 -0
  62. package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +2 -2
  63. package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +2 -2
  64. package/templates/apps/web/app/[locale]/clients/__tests__/page-renderer-migration.test.tsx +14 -1
  65. package/templates/apps/web/app/[locale]/contact/__tests__/site-contact-lines.test.ts +69 -0
  66. package/templates/apps/web/app/[locale]/contact/site-contact-lines.ts +64 -0
  67. package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +4 -4
  68. package/templates/apps/web/app/[locale]/downloads/__tests__/page-renderer-migration.test.tsx +15 -3
  69. package/templates/apps/web/app/[locale]/events/__tests__/page-renderer-migration.test.tsx +17 -6
  70. package/templates/apps/web/app/[locale]/for/__tests__/page-renderer-migration.test.tsx +14 -1
  71. package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +2 -2
  72. package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +4 -6
  73. package/templates/apps/web/app/[locale]/layout.tsx +156 -53
  74. package/templates/apps/web/app/[locale]/page.tsx +37 -0
  75. package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +2 -2
  76. package/templates/apps/web/app/[locale]/products/__tests__/page-renderer-migration.test.tsx +4 -6
  77. package/templates/apps/web/app/[locale]/resources/__tests__/page-renderer-migration.test.tsx +15 -2
  78. package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +43 -6
  79. package/templates/apps/web/app/[locale]/services/page.tsx +14 -2
  80. package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +2 -2
  81. package/templates/apps/web/app/[locale]/team/[slug]/page.tsx +45 -0
  82. package/templates/apps/web/app/[locale]/team/__tests__/team-routes.test.tsx +155 -0
  83. package/templates/apps/web/app/[locale]/team/page.tsx +49 -0
  84. package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +2 -2
  85. package/templates/apps/web/app/[locale]/testimonials/__tests__/page-renderer-migration.test.tsx +14 -1
  86. package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +2 -2
  87. package/templates/apps/web/app/not-found.tsx +47 -0
  88. package/templates/apps/web/lib/__tests__/profile-back-link.test.ts +95 -0
  89. package/templates/apps/web/lib/__tests__/site-theme-head.test.ts +70 -0
  90. package/templates/apps/web/lib/__tests__/site-theme.test.ts +64 -1
  91. package/templates/apps/web/lib/profile-back-link.ts +51 -0
  92. package/templates/apps/web/lib/site-chrome.ts +12 -0
  93. package/templates/apps/web/lib/site-fonts.ts +15 -0
  94. package/templates/apps/web/lib/site-logo.tsx +39 -1
  95. package/templates/apps/web/lib/site-theme-head.ts +33 -0
  96. package/templates/apps/web/lib/site-theme.ts +13 -1
  97. package/templates/apps/web/messages/en.json +7 -1
  98. package/templates/database/CHANGELOG.md +7 -0
  99. package/templates/database/package.json +1 -1
  100. package/versions.json +34 -34
  101. package/site-templates/designs/minimalism/sample/README.md +0 -48
  102. package/site-templates/designs/minimalism/sample/content/en/HomePage/home.mdx +0 -244
  103. package/site-templates/designs/minimalism/sample/public/avatar-placeholder-1.svg +0 -7
  104. package/site-templates/designs/minimalism/sample/public/avatar-placeholder-2.svg +0 -7
  105. package/site-templates/designs/minimalism/sample/public/avatar-placeholder-3.svg +0 -7
  106. package/site-templates/designs/minimalism/sample/public/avatar-placeholder-4.svg +0 -7
  107. package/site-templates/designs/minimalism/sample/public/avatar-placeholder-5.svg +0 -7
  108. package/site-templates/designs/minimalism/sample/public/avatar-placeholder-6.svg +0 -7
  109. package/site-templates/designs/minimalism/sample/public/hero-backdrop.svg +0 -17
  110. package/site-templates/designs/minimalism/sample/public/logo-placeholder-1.svg +0 -6
  111. package/site-templates/designs/minimalism/sample/public/logo-placeholder-2.svg +0 -6
  112. package/site-templates/designs/minimalism/sample/public/logo-placeholder-3.svg +0 -6
  113. package/site-templates/designs/minimalism/sample/public/wordmark-placeholder.svg +0 -28
@@ -38,6 +38,24 @@ export type SiteChromeFooter = "columns" | "centered";
38
38
  /** The `home` knob's vocabulary. `deck` → the locale-root route mounts
39
39
  * MarketingShell with `snap="mandatory"` + `deckHero` (spec 5426 D7). */
40
40
  export type SiteChromeHome = "default" | "deck";
41
+ /** The `innerPages` knob's vocabulary (#7721). `bare` → every non-home page
42
+ * gets the bare header and the one-bar footer. */
43
+ export type SiteChromeInnerPages = "default" | "bare";
44
+ /** The `colorMode` knob's vocabulary (#7742). `light` pins the site to light
45
+ * and removes every theme switch; dark then appears only as a deck ground. */
46
+ export type SiteChromeColorMode = "user" | "light";
47
+ /** The `chromeStyle` knob's vocabulary (#7742). `minimal` → the deck's closing
48
+ * slide is the wordmark + contact lines + one legal line, and the consent bar
49
+ * is a ground-coloured hairline bar that never covers the down cue. */
50
+ export type SiteChromeChromeStyle = "default" | "minimal";
51
+ /**
52
+ * The `deckGrounds` knob's vocabulary (#7719 D3) — the shared section surface
53
+ * enum, duplicated from `@working-theory/validation`'s `SectionSurfaceSchema`
54
+ * because `create-ailk` cannot import it (one runtime dependency, § 1.5);
55
+ * `site-chrome.test.ts` proves the two lists equal.
56
+ */
57
+ export declare const DECK_GROUND_VALUES: readonly ["none", "gray-100", "wash", "wash-secondary", "gradient-brand", "gradient-dark", "solid-dark"];
58
+ export type DeckGround = (typeof DECK_GROUND_VALUES)[number];
41
59
  /**
42
60
  * A template's declared layout block — every field optional. Lives on
43
61
  * `TemplateManifest.layout` (the code registry, `template-manifest.ts`) and
@@ -48,6 +66,16 @@ export interface TemplateLayout {
48
66
  footer?: SiteChromeFooter;
49
67
  announcementBanner?: boolean;
50
68
  home?: SiteChromeHome;
69
+ /** #7719 D3 — the surfaces a deck home cycles, slide by slide, onto slides that author none. */
70
+ deckGrounds?: readonly DeckGround[];
71
+ /** #7721 D1 — `bare`: every non-home page wears the bare header and the one-bar footer. */
72
+ innerPages?: SiteChromeInnerPages;
73
+ /** #7740 D1 — `true`: the deck home presents slide by slide (a deck home only). */
74
+ deckPresentation?: boolean;
75
+ /** #7742 D1 — `light`: the site is pinned to light, with no theme switch. */
76
+ colorMode?: SiteChromeColorMode;
77
+ /** #7742 D1 — `minimal`: the minimal closing slide, consent bar and themed 404. */
78
+ chromeStyle?: SiteChromeChromeStyle;
51
79
  }
52
80
  /** `TemplateLayout` with every field resolved — what `site-chrome.ts` exports. */
53
81
  export interface ResolvedSiteChrome {
@@ -55,6 +83,16 @@ export interface ResolvedSiteChrome {
55
83
  footer: SiteChromeFooter;
56
84
  announcementBanner: boolean;
57
85
  home: SiteChromeHome;
86
+ /** Present only when a list resolves AND `home` is `deck` (#7719 D3) — never an `undefined` key. */
87
+ deckGrounds?: readonly DeckGround[];
88
+ /** Present only when it resolves to "bare" — never "default" (#7721 D1). */
89
+ innerPages?: "bare";
90
+ /** Present only when it resolves to `true` AND `home` is `deck` (#7740 D1) — never `false`. */
91
+ deckPresentation?: true;
92
+ /** Present only when it resolves to "light" — never "user" (#7742 D1). */
93
+ colorMode?: "light";
94
+ /** Present only when it resolves to "minimal" — never "default" (#7742 D1). */
95
+ chromeStyle?: "minimal";
58
96
  }
59
97
  /** Defaults (#5612 D1; banner off since #6469). */
60
98
  export declare const DEFAULT_SITE_CHROME: ResolvedSiteChrome;
@@ -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,9 @@ 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"];
51
69
  /**
52
70
  * Validate an UNTRUSTED `layout` value (a bundle's `ailk-template.json`
53
71
  * `layout` field — arbitrary JSON, never compile-time checked the way the
@@ -92,6 +110,40 @@ export function validateTemplateLayout(value, subject) {
92
110
  }
93
111
  result.home = raw.home;
94
112
  }
113
+ if ("deckGrounds" in raw && raw.deckGrounds !== undefined) {
114
+ const grounds = raw.deckGrounds;
115
+ const badIndex = Array.isArray(grounds)
116
+ ? grounds.findIndex((g) => !DECK_GROUND_VALUES.includes(g))
117
+ : -1;
118
+ if (!Array.isArray(grounds) || grounds.length === 0 || badIndex !== -1) {
119
+ 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)}`);
120
+ }
121
+ result.deckGrounds = grounds;
122
+ }
123
+ if ("innerPages" in raw && raw.innerPages !== undefined) {
124
+ if (!INNER_PAGES_VALUES.includes(raw.innerPages)) {
125
+ throw new TemplateLayoutError(`${subject}: layout.innerPages must be one of ${INNER_PAGES_VALUES.join(", ")} — got ${JSON.stringify(raw.innerPages)}`);
126
+ }
127
+ result.innerPages = raw.innerPages;
128
+ }
129
+ if ("deckPresentation" in raw && raw.deckPresentation !== undefined) {
130
+ if (typeof raw.deckPresentation !== "boolean") {
131
+ throw new TemplateLayoutError(`${subject}: layout.deckPresentation must be a boolean — got ${JSON.stringify(raw.deckPresentation)}`);
132
+ }
133
+ result.deckPresentation = raw.deckPresentation;
134
+ }
135
+ if ("colorMode" in raw && raw.colorMode !== undefined) {
136
+ if (!COLOR_MODE_VALUES.includes(raw.colorMode)) {
137
+ throw new TemplateLayoutError(`${subject}: layout.colorMode must be one of ${COLOR_MODE_VALUES.join(", ")} — got ${JSON.stringify(raw.colorMode)}`);
138
+ }
139
+ result.colorMode = raw.colorMode;
140
+ }
141
+ if ("chromeStyle" in raw && raw.chromeStyle !== undefined) {
142
+ if (!CHROME_STYLE_VALUES.includes(raw.chromeStyle)) {
143
+ throw new TemplateLayoutError(`${subject}: layout.chromeStyle must be one of ${CHROME_STYLE_VALUES.join(", ")} — got ${JSON.stringify(raw.chromeStyle)}`);
144
+ }
145
+ result.chromeStyle = raw.chromeStyle;
146
+ }
95
147
  return result;
96
148
  }
97
149
  /**
@@ -113,7 +165,7 @@ export function resolveSiteChrome(layout, designLayout) {
113
165
  throw new TypeError(`resolveSiteChrome: designLayout must be a layout object, got ${JSON.stringify(designLayout)}`);
114
166
  }
115
167
  const fromDesign = designLayout ?? {};
116
- return {
168
+ const resolved = {
117
169
  nav: layout?.nav ?? fromDesign.nav ?? DEFAULT_SITE_CHROME.nav,
118
170
  footer: layout?.footer ?? fromDesign.footer ?? DEFAULT_SITE_CHROME.footer,
119
171
  announcementBanner: layout?.announcementBanner ??
@@ -121,6 +173,29 @@ export function resolveSiteChrome(layout, designLayout) {
121
173
  DEFAULT_SITE_CHROME.announcementBanner,
122
174
  home: layout?.home ?? fromDesign.home ?? DEFAULT_SITE_CHROME.home,
123
175
  };
176
+ // #7719 D3 — the grounds resolve as a whole list, and only a deck keeps them.
177
+ const deckGrounds = layout?.deckGrounds ?? fromDesign.deckGrounds;
178
+ if (deckGrounds !== undefined && resolved.home === "deck") {
179
+ resolved.deckGrounds = deckGrounds;
180
+ }
181
+ // #7721 D1 — only "bare" is ever a key, so a chrome that does not opt in
182
+ // (unset, or an explicit "default") resolves exactly as before.
183
+ if ((layout?.innerPages ?? fromDesign.innerPages) === "bare") {
184
+ resolved.innerPages = "bare";
185
+ }
186
+ // #7740 D1 — only `true` on a deck is ever a key: an explicit layout
187
+ // `false` wins over the design's `true`, and a non-deck home drops it.
188
+ if ((layout?.deckPresentation ?? fromDesign.deckPresentation) === true && resolved.home === "deck") {
189
+ resolved.deckPresentation = true;
190
+ }
191
+ // #7742 D1 — as with `innerPages`, only the opt-in value is ever a key.
192
+ if ((layout?.colorMode ?? fromDesign.colorMode) === "light") {
193
+ resolved.colorMode = "light";
194
+ }
195
+ if ((layout?.chromeStyle ?? fromDesign.chromeStyle) === "minimal") {
196
+ resolved.chromeStyle = "minimal";
197
+ }
198
+ return resolved;
124
199
  }
125
200
  /**
126
201
  * Render `apps/web/lib/site-chrome.ts` for a resolved layout — the same
@@ -130,6 +205,26 @@ export function resolveSiteChrome(layout, designLayout) {
130
205
  * `snap="mandatory"` + `deckHero` to MarketingShell.
131
206
  */
132
207
  export function renderSiteChrome(resolved) {
208
+ // #7719 D3 — each `deckGrounds` line is emitted only when the chrome has one,
209
+ // so a chrome without it renders today's text exactly.
210
+ const grounds = resolved.deckGrounds;
211
+ const groundsComment = grounds === undefined
212
+ ? ""
213
+ : "\n// - deckGrounds → the deck home cycles these surfaces, slide by slide, onto slides that author none";
214
+ const groundsMember = grounds === undefined
215
+ ? ""
216
+ : `\n deckGrounds?: readonly (${DECK_GROUND_VALUES.map((g) => JSON.stringify(g)).join(" | ")})[];`;
217
+ const groundsValue = grounds === undefined ? "" : `\n deckGrounds: [${grounds.map((g) => JSON.stringify(g)).join(", ")}],`;
218
+ // #7721 D1 — the value line only for a bare chrome; the interface member
219
+ // and comment bullet always, so the layout reads `siteChrome.innerPages`.
220
+ const innerPagesValue = resolved.innerPages === "bare" ? '\n innerPages: "bare",' : "";
221
+ // #7740 D1 — `innerPages`' form: the member and bullet always, the value
222
+ // line only when the chrome presents.
223
+ const presentationValue = resolved.deckPresentation === true ? "\n deckPresentation: true," : "";
224
+ // #7742 D1 — the same form again: member and bullet always, value lines
225
+ // only for the opt-in values.
226
+ const colorModeValue = resolved.colorMode === "light" ? '\n colorMode: "light",' : "";
227
+ const chromeStyleValue = resolved.chromeStyle === "minimal" ? '\n chromeStyle: "minimal",' : "";
133
228
  return `// site-chrome.ts — resolved template layout knobs.
134
229
  //
135
230
  // GENERATED by create-ailk from the declared template's \`layout\` block
@@ -145,19 +240,31 @@ export function renderSiteChrome(resolved) {
145
240
  // - home: "deck" → the locale-root route mounts MarketingShell with
146
241
  // snap="mandatory" + deckHero (spec 5426 D7); every other route is
147
242
  // unaffected.
243
+ // - innerPages: "bare" → every non-home page renders the bare header and
244
+ // the one-bar footer.
245
+ // - deckPresentation → the deck home presents slide by slide: fitted slides, slide anchors, the mark follows the ground
246
+ // - colorMode: "light" → the site is pinned to light and shows no theme
247
+ // switch; dark appears only as a deck ground.
248
+ // - chromeStyle: "minimal" → the deck's closing slide is the wordmark,
249
+ // contact lines and one legal line; the consent bar is a hairline bar on
250
+ // the page ground; the 404 renders in the site's theme.${groundsComment}
148
251
 
149
252
  export interface SiteChrome {
150
253
  nav: "full" | "mark-only";
151
254
  footer: "columns" | "centered";
152
255
  announcementBanner: boolean;
153
256
  home: "default" | "deck";
257
+ innerPages?: "bare";
258
+ deckPresentation?: true;
259
+ colorMode?: "light";
260
+ chromeStyle?: "minimal";${groundsMember}
154
261
  }
155
262
 
156
263
  export const siteChrome: SiteChrome = {
157
264
  nav: ${JSON.stringify(resolved.nav)},
158
265
  footer: ${JSON.stringify(resolved.footer)},
159
266
  announcementBanner: ${JSON.stringify(resolved.announcementBanner)},
160
- home: ${JSON.stringify(resolved.home)},
267
+ home: ${JSON.stringify(resolved.home)},${groundsValue}${innerPagesValue}${presentationValue}${colorModeValue}${chromeStyleValue}
161
268
  };
162
269
  `;
163
270
  }
@@ -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
+ }
@@ -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
@@ -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) {
@@ -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";
@@ -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;
@@ -0,0 +1,43 @@
1
+ /**
2
+ * The seeded team (#7744 D8) — the one source of the three placeholder people
3
+ * the organization shapes ship.
4
+ *
5
+ * The `_base` layer installs one `TeamMember` profile per person at
6
+ * `/team/<slug>` and a `/team` index listing them (#7744 D4); minimalism's
7
+ * home seeds for the three organization shapes carry a `people` slide listing
8
+ * the same members, each linking to their profile. `team-profiles.test.ts`
9
+ * pins that every one of those seeds agrees with `TEAM_SEED_MEMBERS`.
10
+ *
11
+ * `node:fs` only: create-ailk ships one runtime dependency (`tar`), and its
12
+ * scaffold-time readers may not import `yaml` (see `design-package.ts`), so
13
+ * this module parses nothing — it only checks which profile files a site has.
14
+ */
15
+ import { existsSync } from "node:fs";
16
+ import { join } from "node:path";
17
+ function seedMember(slug, ordinal) {
18
+ return {
19
+ slug,
20
+ name: `{${ordinal} team member's name}`,
21
+ role: "{Their title}",
22
+ links: [{ type: "page", url: `/team/${slug}`, label: "{View profile}" }],
23
+ };
24
+ }
25
+ /**
26
+ * The three seeded people, in seed order: slug, the seed's `title` as `name`,
27
+ * "{Their title}", and a page link to `/team/<slug>` labelled "{View profile}".
28
+ */
29
+ export const TEAM_SEED_MEMBERS = [
30
+ seedMember("first-member", "First"),
31
+ seedMember("second-member", "Second"),
32
+ seedMember("third-member", "Third"),
33
+ ];
34
+ /**
35
+ * `TEAM_SEED_MEMBERS` whose `<siteRoot>/content/en/TeamMember/<slug>.mdx`
36
+ * exists (without `slug`), or `undefined` when none does (person-service, or
37
+ * a site that deleted them). Reads, never writes.
38
+ */
39
+ export function teamSlideMembers(siteRoot) {
40
+ const dir = join(siteRoot, "content", "en", "TeamMember");
41
+ const members = TEAM_SEED_MEMBERS.filter(({ slug }) => existsSync(join(dir, `${slug}.mdx`))).map(({ slug: _slug, ...member }) => member);
42
+ return members.length > 0 ? members : undefined;
43
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-ailk",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "description": "Scaffolder for AI Launch Kit — `pnpm create ailk@latest <dir>` produces a starter repo consuming @working-theory/* from npm (per ADR 0007).",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -27,8 +27,8 @@
27
27
  "tar": "^7.5.21"
28
28
  },
29
29
  "devDependencies": {
30
- "@working-theory/schema": "0.35.0",
31
- "@working-theory/templates": "0.35.0"
30
+ "@working-theory/schema": "0.36.0",
31
+ "@working-theory/templates": "0.36.0"
32
32
  },
33
33
  "repository": {
34
34
  "type": "git",
@@ -11,7 +11,7 @@
11
11
  "person-service": "released",
12
12
  "local-business-product": "deferred"
13
13
  },
14
- "designs": { "standard": "released", "minimalism": "gated" },
14
+ "designs": { "standard": "released", "minimalism": "released" },
15
15
  "modules": {
16
16
  "marketing": "released",
17
17
  "docs-blog": "released",