create-ailk 0.6.0 → 0.6.1

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 (36) hide show
  1. package/README.md +1 -1
  2. package/dist/cli.js +54 -5
  3. package/dist/copy.d.ts +14 -0
  4. package/dist/copy.js +38 -1
  5. package/dist/lib/extract-module-bundle.d.ts +20 -1
  6. package/dist/lib/extract-module-bundle.js +64 -5
  7. package/dist/lib/module-license-gate.d.ts +6 -1
  8. package/dist/lib/module-license-gate.js +6 -1
  9. package/dist/lib/pro-callout.js +1 -1
  10. package/dist/modules/commerce/0.6.0.tar.gz +0 -0
  11. package/dist/modules/docs-blog/0.6.0.tar.gz +0 -0
  12. package/dist/modules/multi-tenant/0.6.0.tar.gz +0 -0
  13. package/dist/modules/scheduling/0.6.0.tar.gz +0 -0
  14. package/dist/modules/signal-collectors/0.6.0.tar.gz +0 -0
  15. package/dist/modules/workspace/0.6.0.tar.gz +0 -0
  16. package/dist/programmatic.js +14 -10
  17. package/package.json +3 -3
  18. package/site-templates/marketing-site/DESIGN.md +148 -0
  19. package/site-templates/marketing-site-free/DESIGN.md +148 -0
  20. package/site-templates/minimalism-site/DESIGN.md +148 -0
  21. package/site-templates/saas-product/DESIGN.md +148 -0
  22. package/site-templates/simple-service/DESIGN.md +148 -0
  23. package/templates/CLAUDE.md +2 -3
  24. package/templates/DESIGN.md +155 -0
  25. package/templates/README.md +42 -8
  26. package/templates/apps/web/PRODUCT.md +86 -0
  27. package/templates/apps/web/lib/__tests__/site-theme.test.ts +135 -20
  28. package/templates/apps/web/lib/site-brand.tsx +4 -8
  29. package/templates/apps/web/middleware.ts +1 -1
  30. package/templates/apps/web/src/lib/commerce-sections.ts +35 -3
  31. package/templates/content/_site.mdx +18 -3
  32. package/templates/database/CHANGELOG.md +7 -0
  33. package/templates/database/package.json +3 -2
  34. package/versions.json +33 -33
  35. package/templates/apps/web/public/logo-dark.svg +0 -21
  36. package/templates/apps/web/public/logo-light.svg +0 -21
@@ -0,0 +1,155 @@
1
+ ---
2
+ name: AI Launch Kit base scaffold
3
+ description: The foundation every AI Launch Kit site starts from — pure white, black ink, black hairline rules, system type, generous pacing.
4
+ colors:
5
+ bg: "#ffffff"
6
+ fg: "#000000"
7
+ surface: "#ffffff"
8
+ surface-secondary: "#ffffff"
9
+ border: "#000000"
10
+ text-muted: "#333333"
11
+ brand: "#000000"
12
+ fg-on-brand: "#ffffff"
13
+ accent: "#000000"
14
+ bg-dark: "#000000"
15
+ fg-dark: "#ffffff"
16
+ border-dark: "#ffffff"
17
+ brand-dark: "#ffffff"
18
+ typography:
19
+ display:
20
+ fontFamily: "system-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, sans-serif"
21
+ fontSize: "6rem"
22
+ fontWeight: 400
23
+ lineHeight: 1
24
+ letterSpacing: "-0.05em"
25
+ heading-section:
26
+ fontFamily: "system-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, sans-serif"
27
+ fontSize: "2.25rem"
28
+ fontWeight: 400
29
+ lineHeight: 1.25
30
+ letterSpacing: "-0.05em"
31
+ heading-item:
32
+ fontSize: "1.25rem"
33
+ fontWeight: 600
34
+ lineHeight: 1.375
35
+ body:
36
+ fontFamily: "system-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, sans-serif"
37
+ fontSize: "1.125rem"
38
+ fontWeight: 400
39
+ lineHeight: 1.5
40
+ eyebrow:
41
+ fontSize: "0.875rem"
42
+ fontWeight: 600
43
+ lineHeight: 1.4
44
+ letterSpacing: "0.05em"
45
+ rounded:
46
+ button: "0.25rem"
47
+ card: "0.75rem"
48
+ spacing:
49
+ section: "4rem"
50
+ card: "1.5rem"
51
+ grid: "1.5rem"
52
+ container: "1280px"
53
+ ---
54
+
55
+ <!--
56
+ Source template: _base (the AI Launch Kit base scaffold)
57
+ Version: create-ailk {{CREATE_AILK_VERSION}}
58
+
59
+ This file describes the visual system this scaffold ships with. The impeccable
60
+ design skill reads it at the start of every design session, so an agent
61
+ building a new page stays on-system.
62
+
63
+ A site template (installed with `--template <name>`) replaces this file with
64
+ its own DESIGN.md — same path, with a header naming the template it came
65
+ from. Edit whichever one your site has; the header tells you which.
66
+
67
+ The tokens above describe what the scaffold RENDERS today. Colours come from
68
+ content/_site.mdx (`theme.colors`); type, radius and spacing are platform
69
+ defaults a site cannot override yet — see "Where this is heading" below.
70
+ -->
71
+
72
+ # AI Launch Kit base scaffold
73
+
74
+ ## Overview
75
+
76
+ The foundation reads like a well-made wireframe: nothing on the page but the words, black on white, with rules where other sites use shadows and colour. It is the look of a site whose owner has not chosen a brand yet and does not pretend otherwise — every placeholder reads as a placeholder, and the structure of the argument is what the visitor sees. A brand is applied on top of it; it is not one.
77
+
78
+ Every page is composed from the AI Launch Kit component library (`@working-theory/ui`). Nothing is hand-built: a page is a list of registered sections, each with a bounded set of variants. Each section is one full-width band with a lot of air around it.
79
+
80
+ ## Colors
81
+
82
+ ### Primary
83
+
84
+ **Black** (`brand` `#000000`) fills the primary button; its label is white. The secondary button is white with a black hairline and black label.
85
+
86
+ ### Neutral
87
+
88
+ Pure white ground (`bg` `#ffffff`), pure black ink for headings (`fg` `#000000`), and near-black `#333333` for body copy — well above AA on white. **Borders are black** (`#000000`): card outlines, the rule under the navigation, the rules between sections and the rule above the footer are all 1px black lines. Cards are white, not tinted (`surface-secondary` is white too). There is no accent colour — `accent` is black.
89
+
90
+ Dark mode inverts: black ground, white ink, white rules, white primary button with black label.
91
+
92
+ ### Named Rules
93
+
94
+ - **Rules, not fills.** A container is drawn with a black hairline on the white ground. Grey fills are reserved for image placeholders.
95
+
96
+ ## Typography
97
+
98
+ System sans throughout; no web font.
99
+
100
+ ### Hierarchy
101
+
102
+ | Role | Size | Weight | Notes |
103
+ | --------------- | -------- | ------ | --------------------------------------------------------------------------- |
104
+ | Hero headline | 6rem | 400 | Tracking -0.05em, line-height 1 |
105
+ | Section heading | 2.25rem | 400 | Tracking -0.05em — light and tight, not bold |
106
+ | Item heading | 1.25rem | 600 | Card, feature and step titles |
107
+ | Body | 1.125rem | 400 | `#333333`, line-height 1.5 |
108
+ | Section label | 0.875rem | 600 | Uppercase, +0.05em tracking today — the small label above a section heading |
109
+
110
+ Measured from the rendered scaffold (1440px wide), not transcribed from tokens.
111
+
112
+ The small label above a section heading is **kept** as part of this foundation's voice (operator-disposed 2026-09-22) and is headed toward the treatment below.
113
+
114
+ ## Layout
115
+
116
+ Content sits in a 1280px container. Sections stack at the platform's 4rem rhythm today; a few sections choose a compact (2.5rem) or spacious (6rem) step internally. Card grids gap at 1.5rem. Breakpoints: 320 / 768 / 1024 / 1440.
117
+
118
+ ## Shapes
119
+
120
+ Two radii today: buttons at 0.25rem, cards and outlined boxes at 0.75rem. Headed toward square corners — see below.
121
+
122
+ ## Components
123
+
124
+ ### Buttons
125
+
126
+ Primary: black fill, white label, 0.25rem corners. Secondary: transparent with a black 1px outline and black label. Both are the library `Button`.
127
+
128
+ ### Cards / Containers
129
+
130
+ White panels with a black 1px outline, 0.75rem corners, `card` (1.5rem) padding. The one exception today: the "how it works" steps render as dark filled cards (see below).
131
+
132
+ ### Navigation
133
+
134
+ Wordmark or logo left, page links, then an outlined and a filled button at the far right. A black hairline separates it from the page. An optional announcement banner sits above it.
135
+
136
+ ## Where this is heading
137
+
138
+ The foundation's pinned direction is the pacing of a classic wireframe marketing kit — `standardissue.webflow.io` is the reference for rhythm, type scale, spacing density and section pacing only; no image, copy or code is taken from it. Six parts of that direction cannot be expressed through a site's theme today, so they wait on a platform change rather than being hand-coded. Until they land, this file describes the rendered site above, not these targets.
139
+
140
+ | Target | Reference value | Blocked by |
141
+ | ------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
142
+ | Square corners on buttons, cards and inputs | radius 0 | A site's `theme:` block accepts colours only — no radius |
143
+ | Bold, tighter headings | hero ~3.6rem and section headings ~2.9rem, both at weight 700 with tracking about -0.02em (today 6rem and 2.25rem at weight 400) | No per-site type-role override |
144
+ | Generous section pacing | about 10rem between sections, not 4rem | No per-site section-spacing override; `spacious` (6rem) is not author-selectable |
145
+ | Section label in sentence case, bold, no tracking | "Core Features", not "CORE FEATURES" | The eyebrow role bakes in uppercase and tracking |
146
+ | Whitespace between sections, not rules | no rule between bands | Section dividers share the one `border` colour with card outlines, so black outlines make black dividers |
147
+ | Outlined steps in "how it works" | three plain outlined columns | The steps section renders dark filled cards; no outlined variant |
148
+
149
+ ## Do's and Don'ts
150
+
151
+ - **Do** keep every page built from registered sections. If a section you need does not exist, request it rather than writing one-off markup.
152
+ - **Do** change colours in `content/_site.mdx` `theme.colors`; they reach every section.
153
+ - **Don't** add a colour accent. The foundation's one idea is black on white; a brand pass adds colour deliberately, through the theme.
154
+ - **Don't** replace a black rule with a shadow or a grey fill.
155
+ - **Don't** hard-code the "heading toward" values above into a page. They arrive through the platform, or not at all.
@@ -29,7 +29,7 @@ pnpm preflight # architecture + types + lint + build
29
29
  pnpm build # production build
30
30
  ```
31
31
 
32
- > Requires Node.js 20+ and `pnpm`. No vendor API keys are required for first boot — Stripe, Anthropic, and email-provider integrations are opt-in.
32
+ > Requires Node.js 20+ and `pnpm` — that is the whole prerequisite list. No database is needed to run or build this site (see [When you need a database](#when-you-need-a-database)), and no vendor API keys are required for first boot — Stripe, Anthropic, and email-provider integrations are opt-in. `pnpm install` generates the Prisma client for you; there is no generator step to remember.
33
33
 
34
34
  ## Content
35
35
 
@@ -63,16 +63,50 @@ Apply `myTheme` via the provider you use to wrap children. The full extension su
63
63
 
64
64
  Copy `.env.example` to `.env` and fill in only what you need. Most variables are optional. A complete reference:
65
65
 
66
- | Variable | Purpose |
67
- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
68
- | `AILK_LICENSE_KEY` | Optional. JWT issued by ailaunchkit.ai; verified locally to unlock Pro features. Get one at <https://ailaunchkit.ai/pro>. |
69
- | `NEXT_PUBLIC_STREAMING_AI_ENABLED` | Optional. Set to `true` to enable the opt-in streaming AI assist UI. Off by default. |
70
- | `ANTHROPIC_API_KEY` | Required only when `NEXT_PUBLIC_STREAMING_AI_ENABLED=true`. Used by the AI assist surface only. |
71
- | `AILK_LOCAL_LEAD_CAPTURE` | Optional. When `true`, leads from the contact form are written to a local file instead of an API. |
72
- | `AILK_LEADS_API_URL` | Optional. Override the upstream that receives lead-capture submissions. |
66
+ | Variable | Purpose |
67
+ | ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
68
+ | `AILK_LICENSE_KEY` | Optional. JWT issued by ailaunchkit.ai; verified locally to unlock Pro features. Get one at <https://ailaunchkit.ai/pricing>. |
69
+ | `NEXT_PUBLIC_STREAMING_AI_ENABLED` | Optional. Set to `true` to enable the opt-in streaming AI assist UI. Off by default. |
70
+ | `ANTHROPIC_API_KEY` | Required only when `NEXT_PUBLIC_STREAMING_AI_ENABLED=true`. Used by the AI assist surface only. |
71
+ | `AILK_LOCAL_LEAD_CAPTURE` | Optional. When `true`, leads from the contact form are written to a local file instead of an API. |
72
+ | `AILK_LEADS_API_URL` | Optional. Override the upstream that receives lead-capture submissions. |
73
73
 
74
74
  See [`.env.example`](./.env.example) for the full template with comments. Per-app overrides (e.g. `apps/api/.env`) are honored alongside the root `.env`.
75
75
 
76
+ ## When you need a database
77
+
78
+ **A free marketing site needs no database at all.** `pnpm install`, `pnpm dev` and `pnpm build` all run green on an empty machine with no Postgres, no `DATABASE_URL` and no `.env` — the pages render from `content/*.mdx`, and the Prisma client is generated for you by `database`'s `postinstall` so the workspace type-checks and builds either way.
79
+
80
+ You need Postgres once you turn on one of these four:
81
+
82
+ | Surface | What it stores |
83
+ | -------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
84
+ | **Auth** (sign-in, sessions, accounts) | users, sessions, accounts |
85
+ | **Flows** — `/save` and `/checkout` | a visitor's in-progress flow state, so they can resume or pay later |
86
+ | **Lead capture in DB mode** | submitted leads. The default file mode (`AILK_LOCAL_LEAD_CAPTURE=true`) writes to disk instead and needs nothing. |
87
+ | **`CONTENT_ADAPTER=database`** | pages authored in the Artifact store rather than in `content/*.mdx` |
88
+
89
+ ### One command to get one
90
+
91
+ ```bash
92
+ docker run --name ailk-postgres -e POSTGRES_PASSWORD=postgres -p 5432:5432 -d postgres:17
93
+ ```
94
+
95
+ Point the app at it in `.env` (both variables, same URL for local work — the second is the direct connection `prisma migrate` uses):
96
+
97
+ ```bash
98
+ DATABASE_URL=postgresql://postgres:postgres@localhost:5432/postgres
99
+ DATABASE_URL_NON_POOLED=postgresql://postgres:postgres@localhost:5432/postgres
100
+ ```
101
+
102
+ Then create the tables:
103
+
104
+ ```bash
105
+ pnpm db:migrate:deploy
106
+ ```
107
+
108
+ That is the whole setup. `pnpm db:studio` opens a browser UI over the data, and `pnpm db:seed` loads the sample rows. Turning the database off again is just unsetting the two variables — the marketing pages never read it.
109
+
76
110
  ## AI assist (optional)
77
111
 
78
112
  The site ships with an opt-in streaming AI assist. It is **off by default**. To turn it on, set `NEXT_PUBLIC_STREAMING_AI_ENABLED=true` in `.env` and provide an `ANTHROPIC_API_KEY`. The assist runs server-side so the API key never reaches the browser. With both unset, the chat UI is hidden and no AI traffic flows.
@@ -0,0 +1,86 @@
1
+ # Product
2
+
3
+ <!-- impeccable:product-schema 1 -->
4
+
5
+ <!--
6
+ This file is the product context the impeccable design skill reads before
7
+ every design decision on this app (apps/web). It holds facts about your
8
+ product, not the look (that is DESIGN.md) and not the pitch (that is your
9
+ strategy docs).
10
+
11
+ Platform, Stack and the accessibility floor below are facts about this
12
+ AI Launch Kit scaffold. Every other section is empty until it has a real
13
+ source: each one names the strategy doc it will come from. Until then, treat
14
+ it as unknown — never invent a customer, a number or a capability to fill it.
15
+
16
+ Each section carries a `source:` marker. `hand` means nothing generates the
17
+ section, so edit it directly (or answer `/impeccable init`'s questions).
18
+ -->
19
+
20
+ ## Platform
21
+
22
+ <!-- source: hand -->
23
+
24
+ web
25
+
26
+ ## Stack
27
+
28
+ <!-- source: hand -->
29
+
30
+ Next.js 16 (App Router) and React 19, in TypeScript, styled with Tailwind CSS 4. Pages are MDX content files rendered through the AI Launch Kit component library (`@working-theory/ui`, `@working-theory/theme`, `@working-theory/page-renderer`); translations through next-intl. pnpm workspace. Exact versions: `apps/web/package.json`.
31
+
32
+ ## Users
33
+
34
+ <!-- source: hand -->
35
+
36
+ [Not yet set — comes from your strategy run: Messaging Canvas §3 Champion profile]
37
+
38
+ ## Product Purpose
39
+
40
+ <!-- source: hand -->
41
+
42
+ [Not yet set — comes from your strategy run: ENTITY §1.4 Purpose]
43
+
44
+ ## Positioning
45
+
46
+ <!-- source: hand -->
47
+
48
+ [Not yet set — comes from your strategy run: Messaging Canvas §1 Inherited from positioning]
49
+
50
+ ## Operating Context
51
+
52
+ <!-- source: hand -->
53
+
54
+ [Not yet set — comes from your strategy run: Messaging Canvas §3 Champion profile, Reading context]
55
+
56
+ ## Capabilities and Constraints
57
+
58
+ <!-- source: hand -->
59
+
60
+ [Not yet set — comes from your strategy run: OFFERINGS §4 Offering catalog]
61
+
62
+ ## Brand Commitments
63
+
64
+ <!-- source: hand -->
65
+
66
+ [Not yet set — comes from your strategy run: VOICE §2 Voice anchor, and your brand_assets/ folder]
67
+
68
+ ## Evidence on Hand
69
+
70
+ <!-- source: hand -->
71
+
72
+ [Not yet set — comes from your strategy run: Messaging Canvas §5 Pillars, the proof each pillar carries]
73
+
74
+ ## Product Principles
75
+
76
+ <!-- source: hand -->
77
+
78
+ [Not yet set — comes from your strategy run: ENTITY § Product Principles]
79
+
80
+ ## Accessibility & Inclusion
81
+
82
+ <!-- source: hand -->
83
+
84
+ The AI Launch Kit build audits every section component in its library against WCAG 2 Level A and AA (axe-core, plus its best-practice rules), on each section's stories. Smaller components are audited only where a story opts in, and your own pages are not audited automatically. Every text-on-background pair in the platform's default colors meets 4.5:1 contrast for normal text and 3:1 for large text, in light and dark mode. Colors you set yourself in `content/_site.mdx` are not checked automatically — keep them to the same ratios.
85
+
86
+ Commitments above this floor: [Not yet set — comes from your strategy run: ENTITY, accessibility commitments]
@@ -4,11 +4,28 @@
4
4
  * This is the end-to-end claim the wiring makes — a site declares a theme and
5
5
  * the built page renders in those colours — asserted on the actual repo file
6
6
  * rather than a fixture, so deleting the `theme:` block would fail here.
7
+ *
8
+ * Every expectation is DERIVED from whichever `_site.mdx` the test reads, never
9
+ * hard-coded (#6460). This file is snapshotted into `create-ailk`'s
10
+ * `templates/`, so it runs against TWO different `_site.mdx` files: this
11
+ * monorepo's, and the scaffold's own. Hard-coded values made it a snapshot of
12
+ * one tree's palette masquerading as a round-trip test — green here, red in a
13
+ * scaffold whose theme differs, which is exactly what happened when the base
14
+ * scaffold moved to the black-on-white foundation. It also meant a CUSTOMER who
15
+ * took the file's own invitation to "declare only what you change" got a red
16
+ * suite for editing their own colours.
17
+ *
18
+ * Deriving keeps every claim the hard-coded form made — the mapping is applied,
19
+ * the values reach the CSS, and nothing else moves — while making all three
20
+ * true of any declared palette. Deleting the `theme:` block still fails, on the
21
+ * `toBeDefined()` below.
7
22
  */
8
- import { mkdtempSync, writeFileSync } from "node:fs";
23
+ import { mkdtempSync, readFileSync, writeFileSync } from "node:fs";
9
24
  import { tmpdir } from "node:os";
10
25
  import { join } from "node:path";
11
26
 
27
+ import { parse as parseYaml } from "yaml";
28
+
12
29
  import { PartialColorPaletteSchema } from "@working-theory/site-config/schema";
13
30
  import { siteColorTokenMap, themeToCssVarsBothModes } from "@working-theory/theme";
14
31
 
@@ -26,6 +43,46 @@ function darkBlock(css: string): string {
26
43
  return css.slice(css.indexOf(':root[data-theme="dark"]'));
27
44
  }
28
45
 
46
+ /** The two modes, as the palette and the emitted CSS both name them. */
47
+ const MODES = ["light", "dark"] as const;
48
+
49
+ /**
50
+ * The `theme.colors` block exactly as the `_site.mdx` under test declares it —
51
+ * raw SLOT names (`text`, `primary`), not token keys. Read with the same
52
+ * frontmatter parse `site-theme.ts` uses, so the test cannot pass against a
53
+ * file the implementation reads differently.
54
+ */
55
+ function declaredColors(): Partial<Record<(typeof MODES)[number], Record<string, string>>> {
56
+ const raw = readFileSync(join(REPO_CONTENT_ROOT, "_site.mdx"), "utf8");
57
+ const frontmatter = /^---\r?\n([\s\S]*?)\r?\n---/.exec(raw);
58
+ const parsed = parseYaml(frontmatter?.[1] ?? "") as
59
+ | { theme?: { colors?: Record<string, Record<string, string>> } }
60
+ | null;
61
+ return parsed?.theme?.colors ?? {};
62
+ }
63
+
64
+ /**
65
+ * The CSS custom property a token key is emitted as — `fgOnBrand` →
66
+ * `--color-fg-on-brand`. The colon in every call site anchors the match, so
67
+ * `--color-surface` never collides with `--color-surface-secondary`.
68
+ */
69
+ function cssVar(tokenKey: string): string {
70
+ return `--color-${tokenKey.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`)}`;
71
+ }
72
+
73
+ /** `{ [tokenKey]: value }` for one mode's declared slots, mapping applied. */
74
+ function declaredTokens(mode: (typeof MODES)[number]): Record<string, string> {
75
+ const out: Record<string, string> = {};
76
+ for (const [slot, value] of Object.entries(declaredColors()[mode] ?? {})) {
77
+ const tokenKey = siteColorTokenMap[slot as keyof typeof siteColorTokenMap];
78
+ // A slot with no token counterpart cannot reach the CSS at all —
79
+ // `siteThemeToBrandTokens` throws on it, which the schema's own tests
80
+ // cover. Nothing to assert about it here.
81
+ if (typeof tokenKey === "string") out[tokenKey] = value;
82
+ }
83
+ return out;
84
+ }
85
+
29
86
  describe("loadSiteBrandTokens — the real content/_site.mdx", () => {
30
87
  const originalRoot = process.env.AILK_CONTENT_ROOT;
31
88
 
@@ -38,35 +95,93 @@ describe("loadSiteBrandTokens — the real content/_site.mdx", () => {
38
95
  else process.env.AILK_CONTENT_ROOT = originalRoot;
39
96
  });
40
97
 
41
- it("translates the declared theme into the token overlay", () => {
42
- expect(loadSiteBrandTokens()).toEqual({
43
- colors: {
44
- light: { bg: "#fdfcfa", fg: "#1c1917" },
45
- dark: { bg: "#0c0a09" },
46
- },
47
- });
98
+ it("translates every colour the file declares into its mapped token", () => {
99
+ const overlay = loadSiteBrandTokens();
100
+ // The `theme:` block exists and produced an overlay — the claim the
101
+ // hard-coded form made by naming values, kept without naming them.
102
+ expect(overlay).toBeDefined();
103
+
104
+ // `BrandTokenOverrides` is deliberately `Readonly<Record<string, unknown>>`
105
+ // — `@working-theory/theme` cannot name the overlay's shape without
106
+ // pinning it to one token set — so the test states the shape it asserts
107
+ // about. The hard-coded form sidestepped this by comparing whole objects.
108
+ const colors = (overlay?.["colors"] ?? {}) as Partial<
109
+ Record<(typeof MODES)[number], Record<string, string>>
110
+ >;
111
+
112
+ let asserted = 0;
113
+ for (const mode of MODES) {
114
+ const expectedTokens = declaredTokens(mode);
115
+ // Exactly the declared slots, nothing invented and nothing dropped.
116
+ expect(colors[mode] ?? {}).toEqual(expectedTokens);
117
+ asserted += Object.keys(expectedTokens).length;
118
+ }
119
+ // Non-vacuity: a file that declared nothing would satisfy the loop above.
120
+ expect(asserted).toBeGreaterThan(0);
48
121
  });
49
122
 
50
123
  it("reaches the emitted CSS custom properties", () => {
51
124
  const brand = loadSiteBrandTokens();
52
125
  const css = themeToCssVarsBothModes(brand ? { brand } : {});
53
-
54
- expect(lightBlock(css)).toContain("--color-bg: #fdfcfa;");
55
- expect(lightBlock(css)).toContain("--color-fg: #1c1917;");
56
- expect(darkBlock(css)).toContain("--color-bg: #0c0a09;");
126
+ const blockFor = { light: lightBlock(css), dark: darkBlock(css) };
127
+
128
+ let asserted = 0;
129
+ for (const mode of MODES) {
130
+ for (const [tokenKey, value] of Object.entries(declaredTokens(mode))) {
131
+ expect(blockFor[mode]).toContain(`${cssVar(tokenKey)}: ${value};`);
132
+ asserted += 1;
133
+ }
134
+ }
135
+ expect(asserted).toBeGreaterThan(0);
57
136
  });
58
137
 
59
138
  it("changes exactly the declared variables and nothing else", () => {
60
139
  const brand = loadSiteBrandTokens();
61
140
  const themed = themeToCssVarsBothModes(brand ? { brand } : {});
62
- const expected = themeToCssVarsBothModes()
63
- // light block — the two declared light colours
64
- .replace("--color-bg: #ffffff;", "--color-bg: #fdfcfa;")
65
- .replace("--color-fg: #030712;", "--color-fg: #1c1917;")
66
- // dark block — the one declared dark colour
67
- .replace("--color-bg: #0a0a0a;", "--color-bg: #0c0a09;");
68
-
69
- expect(themed).toBe(expected);
141
+
142
+ // Rebuild the expectation from the DEFAULT CSS by overwriting, per mode
143
+ // block, only the variables this site declares. Anything the overlay
144
+ // touched beyond them makes the final equality fail — which is the whole
145
+ // point of this case.
146
+ const base = themeToCssVarsBothModes();
147
+ const rewritten = { light: lightBlock(base), dark: darkBlock(base) };
148
+ for (const mode of MODES) {
149
+ for (const [tokenKey, value] of Object.entries(declaredTokens(mode))) {
150
+ const pattern = new RegExp(`(${cssVar(tokenKey)}: )[^;]+;`);
151
+ // The variable must exist in the default block for the overwrite to
152
+ // mean anything — a replace with no target would weaken the equality
153
+ // below into a tautology. Presence is the right check rather than
154
+ // "the text changed": a site may legitimately declare a value that
155
+ // already MATCHES the default (the black-on-white base declares
156
+ // `bg: #ffffff`, which light mode already is), and that no-op is
157
+ // correct, not vacuous.
158
+ expect(rewritten[mode]).toMatch(pattern);
159
+ rewritten[mode] = rewritten[mode].replace(pattern, `$1${value};`);
160
+ }
161
+
162
+ // `--color-surface-raised` is DERIVED, not independent: the emitter
163
+ // resolves it from `surfaceSecondary` in light mode and from `surface`
164
+ // in dark (`packages/theme/src/theme-to-css.ts`, and the rule is
165
+ // documented at `site-theme.ts`'s header). So a site that declares
166
+ // either of those moves it too, and the equality below has to account
167
+ // for it — re-derived here from whatever the block now carries rather
168
+ // than restated as a value, so the two cannot drift.
169
+ //
170
+ // Kept inside the total equality instead of excluded from it: this
171
+ // variable is SUPPOSED to move, and excluding it would stop the test
172
+ // noticing if it ever stopped moving.
173
+ const derivedFrom = mode === "light" ? "surfaceSecondary" : "surface";
174
+ const sourceValue = new RegExp(`${cssVar(derivedFrom)}: ([^;]+);`).exec(
175
+ rewritten[mode],
176
+ )?.[1];
177
+ expect(sourceValue).toBeDefined();
178
+ rewritten[mode] = rewritten[mode].replace(
179
+ /(--color-surface-raised: )[^;]+;/,
180
+ `$1${sourceValue};`,
181
+ );
182
+ }
183
+
184
+ expect(themed).toBe(rewritten.light + rewritten.dark);
70
185
  });
71
186
 
72
187
  it("treats a bare `theme:` key as no theme, rather than throwing", () => {
@@ -108,14 +108,10 @@ export const SITE_BASE_URL =
108
108
  process.env.NEXT_PUBLIC_BASE_URL ?? siteFrontmatter?.baseUrl ?? FALLBACK_BASE_URL;
109
109
 
110
110
  /**
111
- * Shared brand mark — the light/dark logo pair, mode-switched via the theme
112
- * `dark:` utility. RSC-safe (no hooks, no client state).
111
+ * Shared brand mark — the square logomark (the central brand library's
112
+ * `logomark/loop/icon.svg`). The Nav pairs it with `/wordmark.svg`.
113
+ * RSC-safe (no hooks, no client state).
113
114
  */
114
115
  export function siteLogo(): ReactElement {
115
- return (
116
- <>
117
- <img src="/logo-light.svg" alt={SITE_NAME} height={28} className="block dark:hidden" />
118
- <img src="/logo-dark.svg" alt={SITE_NAME} height={28} className="hidden dark:block" />
119
- </>
120
- );
116
+ return <img src="/logomark.svg" alt={SITE_NAME} width={28} height={28} className="block" />;
121
117
  }
@@ -79,7 +79,7 @@ export function middleware(request: NextRequest): NextResponse {
79
79
  // The trailing `.*\\..*` alternative excludes any path whose last segment
80
80
  // contains a dot, i.e. every file with an extension. Without it the negative
81
81
  // lookahead named only favicon.ico, so next-intl's locale rewrite swallowed
82
- // every OTHER public/ asset — logo-light.svg, logo-dark.svg, site.webmanifest,
82
+ // every OTHER public/ asset — logomark.svg, wordmark.svg, site.webmanifest,
83
83
  // og-default.png — and each 404'd (#4090 item 4). robots.txt and sitemap.xml are
84
84
  // route handlers rather than public/ files, but they carry extensions too and
85
85
  // are equally not locale-scoped, so the same rule covers them.
@@ -43,13 +43,45 @@
43
43
  */
44
44
  let registration: Promise<boolean> | undefined;
45
45
 
46
+ /**
47
+ * The paid package's specifier, held in a CONST rather than written inline at
48
+ * the `import()` call (#6440 AC2).
49
+ *
50
+ * `webpackIgnore` defers only WEBPACK's resolution; `tsc` resolves a dynamic
51
+ * `import()` whose argument is a string LITERAL exactly as eagerly as a static
52
+ * one, so the literal form failed a keyless scaffold's type-check with TS2307
53
+ * "Cannot find module '@working-theory/ui-commerce'" — the build error #6440
54
+ * was filed for. This monorepo never saw it: `packages-paid/ui-commerce` is on
55
+ * disk here and `apps/web/package.json` declares it `workspace:*`, so the
56
+ * specifier resolves and the defect is reachable only from a scaffold.
57
+ *
58
+ * A non-literal specifier is the whole fix. `tsc` does not attempt module
59
+ * resolution on an `import()` it cannot constant-fold, so the paid package's
60
+ * ABSENCE stops being a type error — while webpack still honours the magic
61
+ * comment beside it and Node still performs the real resolution at runtime,
62
+ * leaving the `catch` below as the one and only thing that observes whether
63
+ * the package is installed. The behaviour is unchanged in both directions:
64
+ * a licensed deployment registers the sections; a keyless one returns false.
65
+ *
66
+ * The file deliberately stays in the BASE TREE rather than moving into the
67
+ * commerce slice: `/purchase/success` and `/purchase/cancel` import it
68
+ * statically, so slicing it out would take the return pages with it and
69
+ * regress #6084 D12 (disposed again on #6440, 2026-09-22).
70
+ */
71
+ const COMMERCE_UI_PACKAGE = "@working-theory/ui-commerce";
72
+
73
+ /** The one export this module needs from the paid package. */
74
+ interface CommerceSectionsModule {
75
+ registerCommerceSections: () => void;
76
+ }
77
+
46
78
  export function ensureCommerceSectionsRegistered(): Promise<boolean> {
47
79
  if (!registration) {
48
80
  registration = (async () => {
49
81
  try {
50
- const mod = await import(
51
- /* webpackIgnore: true */ "@working-theory/ui-commerce"
52
- );
82
+ const mod = (await import(
83
+ /* webpackIgnore: true */ COMMERCE_UI_PACKAGE
84
+ )) as CommerceSectionsModule;
53
85
  mod.registerCommerceSections();
54
86
  return true;
55
87
  } catch {
@@ -17,10 +17,25 @@ analytics:
17
17
  theme:
18
18
  colors:
19
19
  light:
20
- bg: "#fdfcfa"
21
- text: "#1c1917"
20
+ bg: "#ffffff"
21
+ bgMuted: "#ffffff"
22
+ surface: "#ffffff"
23
+ border: "#000000"
24
+ text: "#000000"
25
+ textMuted: "#333333"
26
+ primary: "#000000"
27
+ primaryFg: "#ffffff"
28
+ accent: "#000000"
22
29
  dark:
23
- bg: "#0c0a09"
30
+ bg: "#000000"
31
+ bgMuted: "#000000"
32
+ surface: "#000000"
33
+ border: "#ffffff"
34
+ text: "#ffffff"
35
+ textMuted: "#cccccc"
36
+ primary: "#ffffff"
37
+ primaryFg: "#000000"
38
+ accent: "#ffffff"
24
39
  ---
25
40
 
26
41
  <!--
@@ -1,5 +1,12 @@
1
1
  # @working-theory/database
2
2
 
3
+ ## 0.26.0
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies []:
8
+ - @working-theory/validation@0.26.0
9
+
3
10
  ## 0.25.0
4
11
 
5
12
  ### Minor Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@working-theory/database",
3
- "version": "0.25.0",
3
+ "version": "0.26.0",
4
4
  "license": "Apache-2.0",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -60,7 +60,8 @@
60
60
  "db:seed:test": "tsx seed-test.ts",
61
61
  "db:studio": "prisma studio",
62
62
  "test": "jest --config jest.config.cjs",
63
- "test:watch": "jest --config jest.config.cjs --watch"
63
+ "test:watch": "jest --config jest.config.cjs --watch",
64
+ "postinstall": "prisma generate"
64
65
  },
65
66
  "dependencies": {
66
67
  "@prisma/adapter-pg": "^7.8.0",