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/component-catalog.md
CHANGED
|
@@ -15,13 +15,13 @@ The component inventory of `@working-theory/ui` and the shells of `@working-theo
|
|
|
15
15
|
3. Check the Primitives, Panels and Shells tables too — a low-level control, a collapsible layout container, or a route-level frame lives there, not under Sections or Blocks.
|
|
16
16
|
4. Only then build. If you do build, you are declaring the capability is genuinely absent.
|
|
17
17
|
|
|
18
|
-
Inventory: **
|
|
18
|
+
Inventory: **311 component files** across 16 tiers — 55 sections, 101 blocks, 18 chrome, 79 primitives, 6 panels, 8 shells, 5 docs-blog-blocks, 1 docs-blog-chrome, 2 docs-blog-shells, 2 scheduling-sections, 5 scheduling-blocks, 5 signal-collectors-sections, 9 signal-collectors-blocks, 2 commerce-sections, 12 commerce-blocks, 1 commerce-panels. 308 distinct names: `SchedulerEmbed`, `Separator`, `Tabs` exist in more than one tier as genuinely different components, listed once per tier with its own source path.
|
|
19
19
|
|
|
20
|
-
Description coverage: **
|
|
20
|
+
Description coverage: **307/311** components carry a capability description derived from source — 44 from the section registry's hand-authored when-to-use text, 232 from the component's own doc-comment header, 31 from its tier barrel's roster line. **4** have none; they are listed under [Undescribed components](#undescribed-components) rather than given a sentence invented from their name.
|
|
21
21
|
|
|
22
|
-
Of those, **
|
|
22
|
+
Of those, **276** are *strong* — a capability sentence you could match a requirement against — and **31** are *weak*: either one short clause, or a long one that spends itself on wiring (client boundaries, class names, which primitives it composes) instead of on what the thing is for. Weak rows are marked `(weak)` and listed in full under [Weak descriptions](#weak-descriptions). Coverage is 99%, but **findable-without-luck coverage is 89%**.
|
|
23
23
|
|
|
24
|
-
Per tier, strong out of total: **Sections** 54/55 · **Blocks**
|
|
24
|
+
Per tier, strong out of total: **Sections** 54/55 · **Blocks** 95/101 · **Chrome** 10/18 · **Primitives** 64/79 (4 undescribed) · **Panels** 5/6 · **Shells** 7/8 · **Docs + Blog Blocks** 5/5 · **Docs + Blog Chrome** 1/1 · **Docs + Blog Shells** 0/2 · **Scheduling Sections** 2/2 · **Scheduling Blocks** 5/5 · **Signal-Collectors Sections** 5/5 · **Signal-Collectors Blocks** 8/9 · **Commerce Sections** 2/2 · **Commerce Blocks** 12/12 · **Commerce Panels** 1/1. A tier low on this line is a tier where a capability search is most likely to come up empty on something that exists.
|
|
25
25
|
|
|
26
26
|
That grade is **mechanical**. It counts what it can measure — how much a description adds beyond the component's own name, how much of it is spent on wiring, whether the extraction came out damaged — and it cannot tell a fluent paragraph about the wrong subject from a useful one. A person reading these tables will find more rows unusable than the weak count admits. Treat it as a floor on the problem, not a measurement of it, and read the source before concluding a capability is absent.
|
|
27
27
|
|
|
@@ -119,7 +119,7 @@ Import: `import { X } from "@working-theory/ui/sections";` (or from the `@workin
|
|
|
119
119
|
|
|
120
120
|
---
|
|
121
121
|
|
|
122
|
-
## Blocks (
|
|
122
|
+
## Blocks (101)
|
|
123
123
|
|
|
124
124
|
Intermediate composition tiles — the pieces sections are built from. Compose primitives and panels; never import a section.
|
|
125
125
|
|
|
@@ -158,6 +158,7 @@ Import: `import { X } from "@working-theory/ui/blocks";` (or from the `@working-
|
|
|
158
158
|
| `DescriptionList` | Semantic <dl> of aligned term/definition rows (rows + inline layouts). A semantic <dl> rendering label/value rows from an `items` prop. Each item is a <dt> (muted term) paired with a <dd> (value). The value is a ReactNode, so callers compose richer values (spans, lists, mono text) inside it. | `import { DescriptionList } from "@working-theory/ui/blocks"` |
|
|
159
159
|
| `DetailCard` | The collapsible console / data-panel card: a composable card with a header (title + optional actions + collapse toggle), an optional tab bar, a body slot, and an optional footer. The "deferred SidePanel DetailCard" from, built as a general content card any console / detail view can compose without inventing its own card structure. | `import { DetailCard } from "@working-theory/ui/blocks"` |
|
|
160
160
|
| `DetailHeader` | The entity-bearing sibling of PageHeader: a Breadcrumb wayfinding trail above an EntityHeader identity row, plus a forwarded actions slot. | `import { DetailHeader } from "@working-theory/ui/blocks"` |
|
|
161
|
+
| `DisplayList` | Centred display-size rows of short labels, each optionally a link, with no rules, cards or dividers between them — the bondcap.com "what we offer" list and its report archive grid. | `import { DisplayList } from "@working-theory/ui/blocks"` |
|
|
161
162
|
| `DistributionList` | Per-option answer-share list over the Progress primitive. One question's answer distribution: a title, an "answered" count line, and one row per declared option (a label, a pre-formatted share `display` string, and a `Progress` bar). `display` is pre-formatted by the caller (Rule 10 — this block never formats numbers itself); `share` drives the `Progress` fill, scaled to the primitive's 0–100 range. | `import { DistributionList } from "@working-theory/ui/blocks"` |
|
|
162
163
|
| `EntityHeader` | The single identity-row source: an optional leading avatar, a title/name (Heading), an optional subtitle/meta line (Text), an optional type-badge slot above the title, and an optional trailing actions slot. | `import { EntityHeader } from "@working-theory/ui/blocks"` |
|
|
163
164
|
| `FaqItem` | Q&A pair block for FAQ section families (expanded + accordion). Defaults reproduce the current render except the deliberate item-spacing-asymmetry fix. | `import { FaqItem } from "@working-theory/ui/blocks"` |
|
|
@@ -208,6 +209,7 @@ Import: `import { X } from "@working-theory/ui/blocks";` (or from the `@working-
|
|
|
208
209
|
| `SectionHeader` | Alignment-parameterized section header (eyebrow/subtitle/description/supportLink). The same component serves two ranks (headingLevel="section" \| "subsection") and three alignments (align="start" \| "center" \| "end"). It knows nothing about columns/splits/carousels — it is width-flexible and fills whatever container the section places it in. | `import { SectionHeader } from "@working-theory/ui/blocks"` |
|
|
209
210
|
| `ShareBar` | The reader-facing counterpart to DocsActionBar. Both surfaces share the same SplitButton *affordance + slot* (outline variant, right-justified on the post title row) but fill it with different actions: DocsActionBar carries the AI-ingestion set (Copy page + Open in …), ShareBar carries the social-share set (X · LinkedIn · Bluesky · Copy link). One primitive, two configs. | `import { ShareBar } from "@working-theory/ui/blocks"` |
|
|
210
211
|
| `SinglePriceCard` | 2-column split card for the Pricing 'single-price-with-details' variant. *(weak)* | `import { SinglePriceCard } from "@working-theory/ui/blocks"` |
|
|
212
|
+
| `SlideAdvance` | The deck's slide-advance control. One ghost Button holding a hairline chevron: a down chevron moves to the next slide, an up chevron to the first. A native button, so a click, Enter and Space all fire the same move, and nothing else listens: no wheel, touch, scroll or key handler is added, so native scroll-snap still settles every user scroll (the argument against a scroll-hijacking deck). | `import { SlideAdvance } from "@working-theory/ui/blocks"` |
|
|
211
213
|
| `SocialIcons` | Horizontal row of social icon links. Renders brand icons (via Icon primitive + simple-icons paths) inside accessible Link primitives. Each platform link gets an aria-label. Shared by company-mission, simple-centered, and newsletter-below footer variants, and composed by TeamMemberCard for member social links. *(weak)* | `import { SocialIcons } from "@working-theory/ui/blocks"` |
|
|
212
214
|
| `SortableList` | The one @dnd-kit/sortable reorder block: mouse, touch and keyboard drag behind a six-dot grip, announced, controlled. `@dnd-kit/sortable` wrapped ONCE behind our six-dot grip. The ONLY file in the monorepo that imports `@dnd-kit/*` — enforced by tests/architecture/no-dnd-kit-outside-sortablelist.test.ts (SD-2). | `import { SortableList } from "@working-theory/ui/blocks"` |
|
|
213
215
|
| `StatTicker` | Static, horizontally overflow-scrollable workspace stat strip with an optional trailing CTA; no auto-scroll marquee (ploy-capture recomposition). The Overview stat strip captured off ploy.ai (behavior spec: -2, — `overview-ticker` "-viewport"/"-track"/"-segment"/"-value"/"-cta"). | `import { StatTicker } from "@working-theory/ui/blocks"` |
|
|
@@ -365,7 +367,7 @@ Import: `import { X } from "@working-theory/ui/panels";` (or from the `@working-
|
|
|
365
367
|
|
|
366
368
|
---
|
|
367
369
|
|
|
368
|
-
## Shells (
|
|
370
|
+
## Shells (8)
|
|
369
371
|
|
|
370
372
|
Route-level slot-frames — the outermost layout a page is placed into. They supply geometry and slots (nav / sidebar / main / footer), not content: you inject the real Nav, Footer and sidebar. A different package from every tier above: `@working-theory/templates`.
|
|
371
373
|
|
|
@@ -377,6 +379,8 @@ Import: `import { X } from "@working-theory/templates/shells";` (or from the `@w
|
|
|
377
379
|
| `AppShell` | Shared authed-app base frame. Layout ownership: AppShell is the SINGLE owner of the authed sidebar frame layout. The `sidebar` prop is CONTENT (e.g. a <SidebarNav>), never a layout component. Do NOT pass SidebarShell or FixedPanel as the sidebar — that nests two incompatible layout systems (a fixed-width shell vs a variable-width panel). | `import { AppShell } from "@working-theory/templates/shells"` |
|
|
378
380
|
| `AuthShell` | The frame the five auth pages (/sign-in, /sign-up, /forgot-password, /reset-password, /verify-email) share: a brand mark top-start, one centered content column, and a slim footer. No nav — an auth page has nothing to navigate to, and the marketing Nav is exactly what this shell exists to replace on those routes. | `import { AuthShell } from "@working-theory/templates/shells"` |
|
|
379
381
|
| `DeckHeroObserver` | Internal client island for MarketingShell's `deckHero` option. | internal — compose from `packages/templates/src/shells/DeckHeroObserver.tsx` |
|
|
382
|
+
| `DeckPresentation` | Internal client island for MarketingShell's `presentation` option. Not exported from the shells barrel (`./index.ts`) — MarketingShell renders it internally under `snap="mandatory"` with `presentation`, beside DeckHeroObserver, whose idiom it follows: a zero-size anchor, no React state, and every write made through the DOM after hydration. | internal — compose from `packages/templates/src/shells/DeckPresentation.tsx` |
|
|
383
|
+
| `DeckSlideAdvance` | Internal client island that puts one working slide-advance button at the foot of every slide of MarketingShell's scroll-snap deck: down to the next slide, and up from the last slide back to the first. | internal — compose from `packages/templates/src/shells/DeckSlideAdvance.tsx` |
|
|
380
384
|
| `MarketingShell` | Public-facing marketing site **slot-frame**. Server Component (RSC default). A shell is a slot-defining frame: it carves out *where chrome goes* (top + bottom) and *where sections go* (the body), owning **geometry only**. Chrome (Nav/Footer) and sections **fill** the slots — nothing is baked into the component. See (first instance). *(weak)* | `import { MarketingShell } from "@working-theory/templates/shells"` |
|
|
381
385
|
| `SignedInShell` | Every AILK signed-in area runs on this frame: the admin app's workspace (AppFrame supplies its switcher, Library and gating as content) and a site's own /account area. | `import { SignedInShell } from "@working-theory/templates/shells"` |
|
|
382
386
|
|
package/dist/add-module.d.ts
CHANGED
|
@@ -4,6 +4,13 @@ import { type ModulePatch } from "./module-architecture.js";
|
|
|
4
4
|
import { type ModuleName } from "./parse-args.js";
|
|
5
5
|
import type { MarkerMap, WorkspaceVersionMap } from "./substitute.js";
|
|
6
6
|
import { type SyncRoutesResult } from "./sync-routes.js";
|
|
7
|
+
import { type Topology } from "./topology.js";
|
|
8
|
+
/**
|
|
9
|
+
* The site's topology for add-module (#7426 D13): an unreadable manifest is a
|
|
10
|
+
* refusal this command names (exit 2), before the first write — the same
|
|
11
|
+
* posture as its unreadable-roster refusal.
|
|
12
|
+
*/
|
|
13
|
+
export declare function addModuleSiteTopology(targetDir: string): Topology;
|
|
7
14
|
export { AddModuleRefusedError, readArchitectureHeader };
|
|
8
15
|
/**
|
|
9
16
|
* One report row: a bundle file written, patched or skipped, or a seed the
|
package/dist/add-module.js
CHANGED
|
@@ -39,6 +39,22 @@ import { ParseError, parseModules } from "./parse-args.js";
|
|
|
39
39
|
import { SITE_ROSTER_PATH, moduleRosterRows, planRosterAppend, readSiteRoster, siteRosterProblems, } from "./site-roster.js";
|
|
40
40
|
import { MODULE_SURFACES, NAV_CONFIG_PATH, readNavConfigStamp, renderNavConfig, resolveSurfaces, } from "./surfaces.js";
|
|
41
41
|
import { syncRoutes } from "./sync-routes.js";
|
|
42
|
+
import { TopologyError, isSpokeExcluded, readSiteTopology, spokeModuleRefusal, } from "./topology.js";
|
|
43
|
+
/**
|
|
44
|
+
* The site's topology for add-module (#7426 D13): an unreadable manifest is a
|
|
45
|
+
* refusal this command names (exit 2), before the first write — the same
|
|
46
|
+
* posture as its unreadable-roster refusal.
|
|
47
|
+
*/
|
|
48
|
+
export function addModuleSiteTopology(targetDir) {
|
|
49
|
+
try {
|
|
50
|
+
return readSiteTopology(targetDir);
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
if (err instanceof TopologyError)
|
|
54
|
+
throw new AddModuleRefusedError(err.message);
|
|
55
|
+
throw err;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
42
58
|
export { AddModuleRefusedError, readArchitectureHeader };
|
|
43
59
|
/** `package.json` paths a `json` patch may write under the plus-config policy. */
|
|
44
60
|
const PACKAGE_JSON_PATCH_KEYS = new Set([
|
|
@@ -86,6 +102,17 @@ export function checkPlusConfigPatches(patches) {
|
|
|
86
102
|
export async function applyModuleToScaffold(opts) {
|
|
87
103
|
const targetAbs = resolve(opts.targetDir);
|
|
88
104
|
const archPath = join(targetAbs, "architecture.yaml");
|
|
105
|
+
// #7426 D13 — a spoke stays a spoke: a module it does not admit is refused
|
|
106
|
+
// before the first write, and an admitted one lands through the spoke
|
|
107
|
+
// filter. `mcp` follows whether the spoke still carries apps/mcp.
|
|
108
|
+
const spoke = addModuleSiteTopology(targetAbs) === "spoke"
|
|
109
|
+
? { mcp: existsSync(join(targetAbs, "apps", "mcp")) }
|
|
110
|
+
: undefined;
|
|
111
|
+
if (spoke !== undefined) {
|
|
112
|
+
const refusal = spokeModuleRefusal(opts.moduleName);
|
|
113
|
+
if (refusal !== undefined)
|
|
114
|
+
throw new AddModuleRefusedError(refusal);
|
|
115
|
+
}
|
|
89
116
|
const current = readArchitectureHeader(readFileSync(archPath, "utf8"));
|
|
90
117
|
const modules = normalizeModules([...current.modules, opts.moduleName]);
|
|
91
118
|
// Resolved before the first write: a header surface that is unknown, or
|
|
@@ -112,6 +139,9 @@ export async function applyModuleToScaffold(opts) {
|
|
|
112
139
|
markers: opts.markers,
|
|
113
140
|
existingFilePolicy: "skip-existing",
|
|
114
141
|
checkPatches: checkPlusConfigPatches,
|
|
142
|
+
...(spoke !== undefined
|
|
143
|
+
? { skip: (destination) => isSpokeExcluded(destination, spoke) }
|
|
144
|
+
: {}),
|
|
115
145
|
});
|
|
116
146
|
const routes = syncRoutes({
|
|
117
147
|
targetDir: targetAbs,
|
|
@@ -123,10 +153,12 @@ export async function applyModuleToScaffold(opts) {
|
|
|
123
153
|
// #6107 D8 — a module that brings `apps/admin` brings its auth hard-fail;
|
|
124
154
|
// the Admin app gets the web app's effective secret, and a secret already
|
|
125
155
|
// held in any Next-loaded env file is never shadowed.
|
|
126
|
-
|
|
156
|
+
// A spoke has no sign-in (#7426 D13), so it gets no secret.
|
|
157
|
+
if (spoke === undefined)
|
|
158
|
+
writeAuthSecretFile(targetAbs);
|
|
127
159
|
// The same composition `syncArchitectureSync` writes — a pure function of
|
|
128
160
|
// the module set, so an unchanged set rewrites identical bytes.
|
|
129
|
-
writeFileSync(archPath, deriveArchitecture(modules, surfaces));
|
|
161
|
+
writeFileSync(archPath, deriveArchitecture(modules, surfaces, spoke !== undefined ? { topology: "spoke", mcp: spoke.mcp } : {}));
|
|
130
162
|
// #7536 D3 — the paid-sections registry follows `apps/web/package.json`,
|
|
131
163
|
// which the bundle's `patches` just edited. Generated and always
|
|
132
164
|
// overwritten, like `architecture.yaml` above.
|
package/dist/add-route.js
CHANGED
|
@@ -39,6 +39,7 @@ import { substituteMarkers } from "./substitute.js";
|
|
|
39
39
|
import { GATEABLE_SURFACES, MODULE_SURFACES, parseSurfaces, resolveSurfaces, } from "./surfaces.js";
|
|
40
40
|
import { syncRoutes } from "./sync-routes.js";
|
|
41
41
|
import { BASE_LAYER_DIR } from "./template-manifest.js";
|
|
42
|
+
import { TopologyError, readSiteTopology } from "./topology.js";
|
|
42
43
|
/** A refusal the CLI exits 2 on, thrown before anything is written. */
|
|
43
44
|
export class AddRouteRefusedError extends Error {
|
|
44
45
|
exitCode = 2;
|
|
@@ -64,6 +65,20 @@ export function addRouteToScaffold(opts) {
|
|
|
64
65
|
if (!existsSync(archPath)) {
|
|
65
66
|
throw new Error(`not a scaffolded repo: no architecture.yaml at ${targetAbs}. Run \`create-ailk <target-dir>\` to scaffold first.`);
|
|
66
67
|
}
|
|
68
|
+
// #7426 D13 — a spoke stays a spoke. Read before the first write: an
|
|
69
|
+
// unreadable manifest is a refusal this command names (exit 2).
|
|
70
|
+
let spoke;
|
|
71
|
+
try {
|
|
72
|
+
spoke =
|
|
73
|
+
readSiteTopology(targetAbs) === "spoke"
|
|
74
|
+
? { mcp: existsSync(join(targetAbs, "apps", "mcp")) }
|
|
75
|
+
: undefined;
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
if (err instanceof TopologyError)
|
|
79
|
+
throw new AddRouteRefusedError(err.message);
|
|
80
|
+
throw err;
|
|
81
|
+
}
|
|
67
82
|
const route = canonicalRoute(opts.route);
|
|
68
83
|
const owner = Object.entries(MODULE_SURFACES).find(([, surfaces]) => surfaces.includes(route))?.[0];
|
|
69
84
|
let current;
|
|
@@ -106,8 +121,9 @@ export function addRouteToScaffold(opts) {
|
|
|
106
121
|
if (navRow !== undefined)
|
|
107
122
|
report.push(navRow);
|
|
108
123
|
// Last: the declared header is what makes a rerun a no-op, so it is
|
|
109
|
-
// written only once every other step has landed.
|
|
110
|
-
|
|
124
|
+
// written only once every other step has landed. A spoke's `syncRoutes`
|
|
125
|
+
// call above already skipped what it lacks (#7426 D13).
|
|
126
|
+
writeFileSync(archPath, deriveArchitecture(current.modules, surfaces, spoke !== undefined ? { topology: "spoke", mcp: spoke.mcp } : {}));
|
|
111
127
|
return { targetDir: targetAbs, route, surfaces, report, routes };
|
|
112
128
|
}
|
|
113
129
|
/** The canonical id for one route argument, refusing an unknown one. */
|
package/dist/apply-design.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { type HomeSeedOutcome } from "./design-home.js";
|
|
2
|
+
import { type UnreadChromeKnob } from "./lib/chrome-readers.js";
|
|
1
3
|
export interface ApplyDesignOptions {
|
|
2
4
|
/** The design: a shipped name, or a directory holding a DESIGN.md. */
|
|
3
5
|
design: string;
|
|
@@ -6,7 +8,11 @@ export interface ApplyDesignOptions {
|
|
|
6
8
|
/** Package-rooted inputs, supplied by the caller (cli.ts from its PKG_ROOT). */
|
|
7
9
|
designsDir: string;
|
|
8
10
|
siteTemplatesDir: string;
|
|
11
|
+
/** The overlay's `templates/` (#7743 D7) — where a treeless shape's home lives. Default: `templates` beside `siteTemplatesDir`. */
|
|
12
|
+
templatesDir?: string;
|
|
9
13
|
catalogPath: string;
|
|
14
|
+
/** #7781 — apply the design even when a reader file of the site ignores a chrome knob it sets; the pairs are reported instead of refused. */
|
|
15
|
+
allowUnreadChrome?: boolean;
|
|
10
16
|
/** Where a relative design path resolves from. Default process.cwd(). */
|
|
11
17
|
cwd?: string;
|
|
12
18
|
/** Read for AILK_LICENSE_KEY only (D6). Default process.env. */
|
|
@@ -21,6 +27,9 @@ export type ApplyDesignChange = {
|
|
|
21
27
|
} | {
|
|
22
28
|
path: string;
|
|
23
29
|
kind: "layout";
|
|
30
|
+
} | {
|
|
31
|
+
path: string;
|
|
32
|
+
kind: "fonts";
|
|
24
33
|
} | {
|
|
25
34
|
path: string;
|
|
26
35
|
kind: "design";
|
|
@@ -28,6 +37,11 @@ export type ApplyDesignChange = {
|
|
|
28
37
|
path: string;
|
|
29
38
|
kind: "preset";
|
|
30
39
|
fields: string[];
|
|
40
|
+
} | {
|
|
41
|
+
path: string;
|
|
42
|
+
kind: "home";
|
|
43
|
+
action: "seeded" | "restored";
|
|
44
|
+
fields: string[];
|
|
31
45
|
} | {
|
|
32
46
|
path: string;
|
|
33
47
|
kind: "manifest";
|
|
@@ -39,15 +53,31 @@ export interface ApplyDesignResult {
|
|
|
39
53
|
/** In write order (D4); [] when nothing changed. */
|
|
40
54
|
changes: ApplyDesignChange[];
|
|
41
55
|
manifest: "recorded" | "unchanged" | "no-roster" | "legacy-roster";
|
|
56
|
+
/** #7743 D7 — what the design did to the home page's sections. */
|
|
57
|
+
home: HomeSeedOutcome;
|
|
58
|
+
/** #7743 D7 — the design ships a home arc for this site, so a home it leaves alone is reported. */
|
|
59
|
+
homeArc?: true;
|
|
42
60
|
/** The site-relative roster file, when there is one. */
|
|
43
61
|
rosterPath?: string;
|
|
62
|
+
/** #7720 D4.2 — the distinct families written to site-fonts.ts, display first; set only when faces are written. */
|
|
63
|
+
fonts?: string[];
|
|
64
|
+
/** #7720 D4.2 — faces were written, but the site's layout.tsx does not read lib/site-fonts. */
|
|
65
|
+
fontsUnwired?: true;
|
|
66
|
+
/** #7781 — chrome knobs the design set that a reader file of the site does not read; present only when non-empty, which only `allowUnreadChrome` lets through. */
|
|
67
|
+
unreadChrome?: UnreadChromeKnob[];
|
|
68
|
+
}
|
|
69
|
+
/** #7781 — thrown before any write when the design sets a chrome knob that a reader file of the site does not read. */
|
|
70
|
+
export declare class UnreadChromeError extends Error {
|
|
71
|
+
readonly unread: readonly UnreadChromeKnob[];
|
|
72
|
+
constructor(unread: readonly UnreadChromeKnob[]);
|
|
44
73
|
}
|
|
45
74
|
export interface ApplyDesignArgs {
|
|
46
75
|
design: string | undefined;
|
|
47
76
|
target: string | undefined;
|
|
77
|
+
allowUnreadChrome: boolean;
|
|
48
78
|
help: boolean;
|
|
49
79
|
}
|
|
50
|
-
export declare const APPLY_DESIGN_HELP = "create-ailk apply-design \u2014 make an existing site wear a design.\n\nUsage:\n create-ailk apply-design <name> [--target <site-dir>]\n create-ailk apply-design <path> [--target <site-dir>]\n\nArguments:\n <name|path> A shipped design (standard, minimalism), or a\n directory holding a DESIGN.md. A relative path\n resolves from the current directory.\n\nOptions:\n --target <site-dir> An already-scaffolded repo (contains\n architecture.yaml). Defaults to the current dir.\n --help, -h Print this message.\n\nWrites only the look: DESIGN.md (replaced whole by the design's, keeping the\nsite's Source template and Version lines), the theme: block of\ncontent/_site.mdx, the generated apps/web/lib/site-chrome.ts, the look fields
|
|
80
|
+
export declare const APPLY_DESIGN_HELP = "create-ailk apply-design \u2014 make an existing site wear a design.\n\nUsage:\n create-ailk apply-design <name> [--target <site-dir>]\n create-ailk apply-design <path> [--target <site-dir>]\n\nArguments:\n <name|path> A shipped design (standard, minimalism), or a\n directory holding a DESIGN.md. A relative path\n resolves from the current directory.\n\nOptions:\n --target <site-dir> An already-scaffolded repo (contains\n architecture.yaml). Defaults to the current dir.\n --allow-unread-chrome Apply the design even when a file of the site does\n not read a chrome setting the design sets (a site\n scaffolded before the setting existed). Without it\n that is refused; with it each such file is reported.\n --help, -h Print this message.\n\nWrites only the look: DESIGN.md (replaced whole by the design's, keeping the\nsite's Source template and Version lines), the theme: block of\ncontent/_site.mdx, the generated apps/web/lib/site-chrome.ts, the generated\napps/web/lib/site-fonts.ts, the look fields of sections the site's pages\nalready place, and the design/layout/theme record in .ailk-site-manifest.json.\nNever adds, removes or renames a route or page, never touches a content field\n(except a placeholder home's sections, below) or the brand: block, and leaves\nevery other page's structured data unchanged.\n\nA design may seed the placeholder home's sections \u2014 its home arc \u2014 while they\nare still a placeholder: the shape's own, or a design's arc of them. It never\nchanges a home whose sections were edited; it reports it and leaves it. A\nseeded home takes the design's looks; look values on the sections it replaced\nare not kept. A design that ships no arc restores the shape's own sections on\na home that still wears another design's untouched arc.\n\nA design other than standard needs a Pro or Pro Plus key: the site's own\nailk.key (put one there with create-ailk license set <key>), else\nAILK_LICENSE_KEY. A malformed package, a preset the validator refuses or a\nmissing key is refused before any file is written.\n\nA preset sets the look fields it names and never unsets one: a field it does\nnot name keeps whatever value the page has. Running it again changes nothing.\n";
|
|
51
81
|
/**
|
|
52
82
|
* Parse the argv AFTER the `apply-design` word (D1). Strict `node:util`
|
|
53
83
|
* parseArgs, like `parseSyncRoutesArgs`: exactly one positional, the design
|
|
@@ -57,7 +87,7 @@ export declare function parseApplyDesignArgs(argv: string[]): ApplyDesignArgs;
|
|
|
57
87
|
/**
|
|
58
88
|
* Apply a design to an existing site (D2–D7). Refuses before any write
|
|
59
89
|
* (C1–C6); otherwise writes only the files whose text changes — page files,
|
|
60
|
-
* `site-chrome.ts`, `DESIGN.md`, `_site.mdx`, then the manifest last.
|
|
90
|
+
* `site-chrome.ts`, `site-fonts.ts`, `DESIGN.md`, `_site.mdx`, then the manifest last.
|
|
61
91
|
*/
|
|
62
92
|
export declare function applyDesign(opts: ApplyDesignOptions): Promise<ApplyDesignResult>;
|
|
63
93
|
/** The changed-file report (D9). */
|
package/dist/apply-design.js
CHANGED
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
* resolver and theme splicer, #6816's preset validator and applier, #6760's
|
|
8
8
|
* design gate and #6813's manifest writer. Every refusal happens before the
|
|
9
9
|
* first write (D3); every new text is planned in memory, then written in one
|
|
10
|
-
* pass (D4). Routes, content fields, `brand:` and structured data never move
|
|
10
|
+
* pass (D4). Routes, content fields, `brand:` and structured data never move —
|
|
11
|
+
* except a placeholder home's `sections:`, which a design's home arc may
|
|
12
|
+
* replace (#7743 D7).
|
|
11
13
|
*
|
|
12
14
|
* Reads no module URL of its own — every package directory it reads is passed in
|
|
13
15
|
* — so Jest's CJS transform can import it, unlike `programmatic.ts`.
|
|
@@ -16,15 +18,31 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSy
|
|
|
16
18
|
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
17
19
|
import { parseArgs } from "node:util";
|
|
18
20
|
import { planDesignMd } from "./apply-design-md.js";
|
|
19
|
-
import { compileDesignTheme } from "./design-compile.js";
|
|
21
|
+
import { compileDesignFonts, compileDesignTheme } from "./design-compile.js";
|
|
22
|
+
import { HOME_PAGE_PATH, designHomeSeeds, planDesignHome, shapeHomePath, shippedHomeSeeds, } from "./design-home.js";
|
|
20
23
|
import { applyDesignPresets, readSectionLookCatalog, shippedPageTypes, validateDesignPresets, } from "./design-presets.js";
|
|
21
24
|
import { resolveDesign, spliceThemeBlock } from "./design-scaffold.js";
|
|
25
|
+
import { chromeKnobReaders, unreadChromeKnobs } from "./lib/chrome-readers.js";
|
|
22
26
|
import { LICENSE_KEY_FILE } from "./lib/license-key-files.js";
|
|
23
27
|
import { CONVERSION_HINT_URL, LicenseRequiredError, assertDesignAllowed, deriveLevel, } from "./lib/module-license-gate.js";
|
|
24
28
|
import { LEVEL_LABEL } from "./lib/module-tier.js";
|
|
25
29
|
import { ParseError } from "./parse-args.js";
|
|
26
30
|
import { SITE_CHROME_PATH, renderSiteChrome, resolveSiteChrome, } from "./site-chrome.js";
|
|
27
|
-
import {
|
|
31
|
+
import { SITE_FONTS_PATH, renderSiteFonts } from "./site-fonts.js";
|
|
32
|
+
import { SITE_ROSTER_PATH, planHomeRecord, planSiteManifestDesign, readSiteRoster, } from "./site-roster.js";
|
|
33
|
+
/** #7781 — thrown before any write when the design sets a chrome knob that a reader file of the site does not read. */
|
|
34
|
+
export class UnreadChromeError extends Error {
|
|
35
|
+
unread;
|
|
36
|
+
constructor(unread) {
|
|
37
|
+
super(`design chrome would not render: the site's own copy of these files does not read the knob the design sets (update them from the current create-ailk overlay, or pass --allow-unread-chrome to apply anyway):\n${unread
|
|
38
|
+
.map(({ knob, file }) => ` ${knob} — ${file}`)
|
|
39
|
+
.join("\n")}`);
|
|
40
|
+
this.unread = unread;
|
|
41
|
+
this.name = "UnreadChromeError";
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/** The site's locale layout — the one reader of the generated site-fonts.ts. */
|
|
45
|
+
const SITE_LAYOUT_PATH = "apps/web/app/[locale]/layout.tsx";
|
|
28
46
|
export const APPLY_DESIGN_HELP = `create-ailk apply-design — make an existing site wear a design.
|
|
29
47
|
|
|
30
48
|
Usage:
|
|
@@ -39,15 +57,27 @@ Arguments:
|
|
|
39
57
|
Options:
|
|
40
58
|
--target <site-dir> An already-scaffolded repo (contains
|
|
41
59
|
architecture.yaml). Defaults to the current dir.
|
|
60
|
+
--allow-unread-chrome Apply the design even when a file of the site does
|
|
61
|
+
not read a chrome setting the design sets (a site
|
|
62
|
+
scaffolded before the setting existed). Without it
|
|
63
|
+
that is refused; with it each such file is reported.
|
|
42
64
|
--help, -h Print this message.
|
|
43
65
|
|
|
44
66
|
Writes only the look: DESIGN.md (replaced whole by the design's, keeping the
|
|
45
67
|
site's Source template and Version lines), the theme: block of
|
|
46
|
-
content/_site.mdx, the generated apps/web/lib/site-chrome.ts, the
|
|
47
|
-
of sections the site's pages
|
|
48
|
-
in .ailk-site-manifest.json.
|
|
68
|
+
content/_site.mdx, the generated apps/web/lib/site-chrome.ts, the generated
|
|
69
|
+
apps/web/lib/site-fonts.ts, the look fields of sections the site's pages
|
|
70
|
+
already place, and the design/layout/theme record in .ailk-site-manifest.json.
|
|
49
71
|
Never adds, removes or renames a route or page, never touches a content field
|
|
50
|
-
or the brand: block, and leaves
|
|
72
|
+
(except a placeholder home's sections, below) or the brand: block, and leaves
|
|
73
|
+
every other page's structured data unchanged.
|
|
74
|
+
|
|
75
|
+
A design may seed the placeholder home's sections — its home arc — while they
|
|
76
|
+
are still a placeholder: the shape's own, or a design's arc of them. It never
|
|
77
|
+
changes a home whose sections were edited; it reports it and leaves it. A
|
|
78
|
+
seeded home takes the design's looks; look values on the sections it replaced
|
|
79
|
+
are not kept. A design that ships no arc restores the shape's own sections on
|
|
80
|
+
a home that still wears another design's untouched arc.
|
|
51
81
|
|
|
52
82
|
A design other than standard needs a Pro or Pro Plus key: the site's own
|
|
53
83
|
ailk.key (put one there with create-ailk license set <key>), else
|
|
@@ -69,6 +99,7 @@ export function parseApplyDesignArgs(argv) {
|
|
|
69
99
|
allowPositionals: true,
|
|
70
100
|
options: {
|
|
71
101
|
target: { type: "string" },
|
|
102
|
+
"allow-unread-chrome": { type: "boolean", default: false },
|
|
72
103
|
help: { type: "boolean", default: false, short: "h" },
|
|
73
104
|
},
|
|
74
105
|
});
|
|
@@ -80,7 +111,12 @@ export function parseApplyDesignArgs(argv) {
|
|
|
80
111
|
if (design === undefined && !help) {
|
|
81
112
|
throw new ParseError("apply-design needs a design. Usage: create-ailk apply-design <name|path> [--target <site-dir>]");
|
|
82
113
|
}
|
|
83
|
-
return {
|
|
114
|
+
return {
|
|
115
|
+
design,
|
|
116
|
+
target: parsed.values.target,
|
|
117
|
+
allowUnreadChrome: parsed.values["allow-unread-chrome"] === true,
|
|
118
|
+
help,
|
|
119
|
+
};
|
|
84
120
|
}
|
|
85
121
|
/** `abs` relative to `root`, with `/` separators. */
|
|
86
122
|
function siteRelative(root, abs) {
|
|
@@ -118,7 +154,7 @@ function readSiteKey(targetAbs, env) {
|
|
|
118
154
|
/**
|
|
119
155
|
* Apply a design to an existing site (D2–D7). Refuses before any write
|
|
120
156
|
* (C1–C6); otherwise writes only the files whose text changes — page files,
|
|
121
|
-
* `site-chrome.ts`, `DESIGN.md`, `_site.mdx`, then the manifest last.
|
|
157
|
+
* `site-chrome.ts`, `site-fonts.ts`, `DESIGN.md`, `_site.mdx`, then the manifest last.
|
|
122
158
|
*/
|
|
123
159
|
export async function applyDesign(opts) {
|
|
124
160
|
const targetAbs = resolve(opts.targetDir);
|
|
@@ -146,13 +182,18 @@ export async function applyDesign(opts) {
|
|
|
146
182
|
const { pkg } = resolved;
|
|
147
183
|
const hasPresets = Object.keys(pkg.sections).length > 0;
|
|
148
184
|
// C4 — the presets, validated exactly as scaffold() validates them.
|
|
185
|
+
let lookCatalog;
|
|
186
|
+
const catalog = () => (lookCatalog ??= readSectionLookCatalog(opts.catalogPath));
|
|
149
187
|
if (hasPresets) {
|
|
150
188
|
validateDesignPresets(pkg.sections, {
|
|
151
|
-
catalog:
|
|
189
|
+
catalog: catalog(),
|
|
152
190
|
pageTypes: shippedPageTypes(opts.siteTemplatesDir),
|
|
153
191
|
file: join(resolved.dir, "DESIGN.md"),
|
|
154
192
|
});
|
|
155
193
|
}
|
|
194
|
+
// #7743 D7 — the home seeds (the design's and every shipped design's), validated beside the presets.
|
|
195
|
+
const designSeeds = designHomeSeeds(resolved, { designsDir: opts.designsDir, catalog });
|
|
196
|
+
const shippedSeeds = shippedHomeSeeds({ designsDir: opts.designsDir, catalog });
|
|
156
197
|
// C5 — the design gate, fed the site's own key (D6). A Free design reads no key.
|
|
157
198
|
if (!resolved.free) {
|
|
158
199
|
const key = readSiteKey(targetAbs, env);
|
|
@@ -170,20 +211,60 @@ export async function applyDesign(opts) {
|
|
|
170
211
|
throw new LicenseRequiredError(`design "${resolved.display}" is a Pro feature — this site's licence level is ${LEVEL_LABEL[level]}; put a Pro or Pro Plus key in the site with create-ailk license set <key>, or set AILK_LICENSE_KEY → ${CONVERSION_HINT_URL}`);
|
|
171
212
|
}
|
|
172
213
|
}
|
|
214
|
+
// #7781 — after the licence gate, before any plan: every chrome knob the design sets must be read by
|
|
215
|
+
// the site's own copy of each overlay file that reads it. The reader map is derived from the overlay.
|
|
216
|
+
const roster = readSiteRoster(targetAbs);
|
|
217
|
+
const chrome = resolveSiteChrome(roster?.template.layout, pkg.layout);
|
|
218
|
+
const templatesDir = opts.templatesDir ?? join(dirname(opts.siteTemplatesDir), "templates");
|
|
219
|
+
const unreadChrome = unreadChromeKnobs(chrome, chromeKnobReaders(templatesDir), targetAbs);
|
|
220
|
+
if (unreadChrome.length > 0 && opts.allowUnreadChrome !== true)
|
|
221
|
+
throw new UnreadChromeError(unreadChrome);
|
|
173
222
|
// C6 / D4 — plan every write in memory.
|
|
174
223
|
const theme = compileDesignTheme(pkg);
|
|
175
224
|
const siteMdxCurrent = readFileSync(siteMdxPath, "utf8");
|
|
176
225
|
const siteMdxNext = spliceThemeBlock(siteMdxCurrent, theme, siteMdxPath);
|
|
177
|
-
const roster = readSiteRoster(targetAbs);
|
|
178
|
-
const chrome = resolveSiteChrome(roster?.template.layout, pkg.layout);
|
|
179
226
|
const chromePath = join(targetAbs, SITE_CHROME_PATH);
|
|
180
227
|
const chromeCurrent = existsSync(chromePath) ? readFileSync(chromePath, "utf8") : undefined;
|
|
181
228
|
const chromeNext = renderSiteChrome(chrome);
|
|
229
|
+
// #7720 D4.2 — a face-less design never adds the file to a site that lacks it.
|
|
230
|
+
const fonts = compileDesignFonts(pkg);
|
|
231
|
+
const fontsPath = join(targetAbs, SITE_FONTS_PATH);
|
|
232
|
+
const fontsCurrent = existsSync(fontsPath) ? readFileSync(fontsPath, "utf8") : undefined;
|
|
233
|
+
const fontsNext = renderSiteFonts(fonts);
|
|
234
|
+
const writeFonts = fontsNext !== fontsCurrent && (fontsCurrent !== undefined || fonts !== undefined);
|
|
235
|
+
const fontFamilies = writeFonts && fonts !== undefined
|
|
236
|
+
? [...new Set([fonts.display?.family, fonts.body?.family])].filter((family) => family !== undefined)
|
|
237
|
+
: undefined;
|
|
238
|
+
const layoutPath = join(targetAbs, SITE_LAYOUT_PATH);
|
|
239
|
+
const fontsUnwired = fontFamilies !== undefined &&
|
|
240
|
+
!(existsSync(layoutPath) && readFileSync(layoutPath, "utf8").includes("lib/site-fonts"));
|
|
182
241
|
const designMdPath = join(targetAbs, "DESIGN.md");
|
|
183
242
|
const designMdCurrent = existsSync(designMdPath) ? readFileSync(designMdPath, "utf8") : undefined;
|
|
184
243
|
const designMdNext = planDesignMd(resolved.dir, targetAbs);
|
|
244
|
+
// #7743 D7 — the home is planned before the presets, so a seeded home is
|
|
245
|
+
// written once, carrying both. The one `planDesignHome` call site here.
|
|
246
|
+
const shape = roster?.template.shape;
|
|
247
|
+
const target = designSeeds.find((seed) => seed.shape === shape);
|
|
248
|
+
// The arc this site would take: the shape's seed, or (with no shape to pick one) any seed at all.
|
|
249
|
+
const homeArc = target !== undefined || (shape === undefined && designSeeds.length > 0);
|
|
250
|
+
const homePath = join(targetAbs, ...HOME_PAGE_PATH.split("/"));
|
|
251
|
+
const current = existsSync(homePath) ? readFileSync(homePath, "utf8") : undefined;
|
|
252
|
+
const homePlan = planDesignHome({
|
|
253
|
+
current,
|
|
254
|
+
shape,
|
|
255
|
+
target,
|
|
256
|
+
shapeHome: shape === undefined || current === undefined
|
|
257
|
+
? ""
|
|
258
|
+
: readFileSync(shapeHomePath(shape, {
|
|
259
|
+
siteTemplatesDir: opts.siteTemplatesDir,
|
|
260
|
+
templatesDir,
|
|
261
|
+
}), "utf8"),
|
|
262
|
+
shippedSeeds,
|
|
263
|
+
catalog: catalog(),
|
|
264
|
+
});
|
|
265
|
+
const homeAction = homePlan.outcome === "seeded" || homePlan.outcome === "restored" ? homePlan.outcome : undefined;
|
|
185
266
|
const pageWrites = [];
|
|
186
|
-
if (
|
|
267
|
+
if (roster !== undefined && (hasPresets || homeAction !== undefined)) {
|
|
187
268
|
const pageTypes = new Set();
|
|
188
269
|
for (const row of roster.rows) {
|
|
189
270
|
const pageType = row.value["pageType"];
|
|
@@ -191,17 +272,28 @@ export async function applyDesign(opts) {
|
|
|
191
272
|
pageTypes.add(pageType);
|
|
192
273
|
}
|
|
193
274
|
const contentDir = join(targetAbs, "content");
|
|
275
|
+
// An authored home kept from a design's arc is reported as kept, so no preset dresses it either (D3, F4).
|
|
276
|
+
const homeKept = homePlan.outcome === "authored" && homeArc;
|
|
194
277
|
const pages = listMdxFiles(contentDir)
|
|
195
|
-
.filter((path) => path !== siteMdxPath)
|
|
196
|
-
.map((path) => ({
|
|
197
|
-
|
|
198
|
-
|
|
278
|
+
.filter((path) => path !== siteMdxPath && !(homeKept && path === homePath))
|
|
279
|
+
.map((path) => ({
|
|
280
|
+
path,
|
|
281
|
+
text: path === homePath && homePlan.text !== undefined ? homePlan.text : readFileSync(path, "utf8"),
|
|
282
|
+
}));
|
|
283
|
+
const presetResult = hasPresets
|
|
284
|
+
? applyDesignPresets(pages, pkg.sections, { pageTypes })
|
|
285
|
+
: { pages: [], applied: [] };
|
|
286
|
+
for (const page of pages) {
|
|
287
|
+
const preset = presetResult.pages.find((p) => p.path === page.path);
|
|
288
|
+
const home = page.path === homePath ? homeAction : undefined;
|
|
289
|
+
if (preset === undefined && home === undefined)
|
|
290
|
+
continue;
|
|
199
291
|
const fields = [
|
|
200
292
|
...new Set(presetResult.applied
|
|
201
293
|
.filter((a) => a.path === page.path)
|
|
202
294
|
.map((a) => `${a.sectionType}.${a.field}`)),
|
|
203
295
|
].sort();
|
|
204
|
-
pageWrites.push({ path: page.path, text: page.text, fields });
|
|
296
|
+
pageWrites.push({ path: page.path, text: preset?.text ?? page.text, fields, ...(home !== undefined ? { home } : {}) });
|
|
205
297
|
}
|
|
206
298
|
}
|
|
207
299
|
let manifest = "no-roster";
|
|
@@ -210,23 +302,37 @@ export async function applyDesign(opts) {
|
|
|
210
302
|
if (roster !== undefined && rosterAbs !== undefined) {
|
|
211
303
|
if (roster.path === SITE_ROSTER_PATH) {
|
|
212
304
|
manifestNext = planSiteManifestDesign(readFileSync(rosterAbs, "utf8"), { design: resolved.recorded, layout: chrome, theme }, rosterAbs);
|
|
305
|
+
// #7743 D6 — the `/` row records the home as written, in the same single write.
|
|
306
|
+
const home = pageWrites.find((page) => page.home !== undefined);
|
|
307
|
+
if (home !== undefined) {
|
|
308
|
+
manifestNext =
|
|
309
|
+
planHomeRecord(manifestNext ?? readFileSync(rosterAbs, "utf8"), home.text, catalog()) ?? manifestNext;
|
|
310
|
+
}
|
|
213
311
|
manifest = manifestNext === undefined ? "unchanged" : "recorded";
|
|
214
312
|
}
|
|
215
313
|
else {
|
|
216
314
|
manifest = "legacy-roster";
|
|
217
315
|
}
|
|
218
316
|
}
|
|
219
|
-
// Write — page files, site-chrome.ts, DESIGN.md, _site.mdx, the manifest last (D4).
|
|
317
|
+
// Write — page files, site-chrome.ts, site-fonts.ts, DESIGN.md, _site.mdx, the manifest last (D4).
|
|
220
318
|
const changes = [];
|
|
221
319
|
for (const page of pageWrites) {
|
|
222
320
|
writeFileSync(page.path, page.text);
|
|
223
|
-
|
|
321
|
+
const path = siteRelative(targetAbs, page.path);
|
|
322
|
+
changes.push(page.home !== undefined
|
|
323
|
+
? { path, kind: "home", action: page.home, fields: page.fields }
|
|
324
|
+
: { path, kind: "preset", fields: page.fields });
|
|
224
325
|
}
|
|
225
326
|
if (chromeNext !== chromeCurrent) {
|
|
226
327
|
mkdirSync(dirname(chromePath), { recursive: true });
|
|
227
328
|
writeFileSync(chromePath, chromeNext);
|
|
228
329
|
changes.push({ path: SITE_CHROME_PATH, kind: "layout" });
|
|
229
330
|
}
|
|
331
|
+
if (writeFonts) {
|
|
332
|
+
mkdirSync(dirname(fontsPath), { recursive: true });
|
|
333
|
+
writeFileSync(fontsPath, fontsNext);
|
|
334
|
+
changes.push({ path: SITE_FONTS_PATH, kind: "fonts" });
|
|
335
|
+
}
|
|
230
336
|
if (designMdNext !== designMdCurrent) {
|
|
231
337
|
writeFileSync(designMdPath, designMdNext);
|
|
232
338
|
changes.push({ path: "DESIGN.md", kind: "design" });
|
|
@@ -245,7 +351,12 @@ export async function applyDesign(opts) {
|
|
|
245
351
|
display: resolved.display,
|
|
246
352
|
changes,
|
|
247
353
|
manifest,
|
|
354
|
+
home: homePlan.outcome,
|
|
355
|
+
...(homeArc ? { homeArc: true } : {}),
|
|
248
356
|
...(roster !== undefined ? { rosterPath: roster.path } : {}),
|
|
357
|
+
...(fontFamilies !== undefined ? { fonts: fontFamilies } : {}),
|
|
358
|
+
...(fontsUnwired ? { fontsUnwired: true } : {}),
|
|
359
|
+
...(unreadChrome.length > 0 ? { unreadChrome } : {}),
|
|
249
360
|
};
|
|
250
361
|
}
|
|
251
362
|
/** The changed-file report (D9). */
|
|
@@ -257,11 +368,33 @@ export function formatApplyDesignReport(result) {
|
|
|
257
368
|
else {
|
|
258
369
|
lines.push(`[create-ailk] Applied design "${result.design}" to ${result.targetDir}`);
|
|
259
370
|
for (const change of result.changes) {
|
|
260
|
-
const suffix = change.kind === "preset"
|
|
371
|
+
const suffix = change.kind === "preset"
|
|
372
|
+
? ` (${change.fields.join(", ")})`
|
|
373
|
+
: change.kind === "home"
|
|
374
|
+
? ` (${[change.action, change.fields.join(", ")].filter((part) => part !== "").join("; ")})`
|
|
375
|
+
: "";
|
|
261
376
|
lines.push(` ${change.kind.padEnd(8)} ${change.path}${suffix}`);
|
|
262
377
|
}
|
|
263
378
|
const n = result.changes.length;
|
|
264
|
-
|
|
379
|
+
const homeWritten = result.changes.some((change) => change.kind === "home");
|
|
380
|
+
lines.push(`${n} file${n === 1 ? "" : "s"} changed. ${homeWritten
|
|
381
|
+
? "Routes are unchanged; the home page's sections were replaced."
|
|
382
|
+
: "Routes and structured data are unchanged."}`);
|
|
383
|
+
}
|
|
384
|
+
if (result.home === "authored" && result.homeArc === true) {
|
|
385
|
+
lines.push(`home kept: ${HOME_PAGE_PATH} has authored sections — the ${result.design} home arc was not applied`);
|
|
386
|
+
}
|
|
387
|
+
else if (result.home === "no-shape" && result.homeArc === true) {
|
|
388
|
+
lines.push("home not seeded: the site records no shape");
|
|
389
|
+
}
|
|
390
|
+
if (result.fonts !== undefined) {
|
|
391
|
+
lines.push(`fonts: ${result.fonts.join(", ")} load at next build through next/font/google — a family that is not a variable Google font fails there`);
|
|
392
|
+
}
|
|
393
|
+
if (result.fontsUnwired === true) {
|
|
394
|
+
lines.push(`fonts not loaded: ${SITE_LAYOUT_PATH} predates lib/site-fonts — update the layout from the current create-ailk overlay`);
|
|
395
|
+
}
|
|
396
|
+
for (const { knob, file } of result.unreadChrome ?? []) {
|
|
397
|
+
lines.push(`chrome not rendered: ${knob} — ${file} does not read it`);
|
|
265
398
|
}
|
|
266
399
|
if (result.manifest === "legacy-roster") {
|
|
267
400
|
lines.push(`manifest not recorded: ${result.rosterPath} predates the design record`);
|