create-ailk 0.21.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. package/component-catalog.json +37 -3
  2. package/component-catalog.md +6 -5
  3. package/dist/add-module.js +9 -5
  4. package/dist/derive-architecture.d.ts +8 -1
  5. package/dist/derive-architecture.js +38 -3
  6. package/dist/design-package.js +7 -2
  7. package/dist/lib/env-catalog.js +5 -1
  8. package/dist/module-architecture.js +4 -2
  9. package/dist/programmatic.js +19 -5
  10. package/dist/site-chrome.d.ts +8 -0
  11. package/dist/site-chrome.js +17 -1
  12. package/dist/spoke-checkout.d.ts +39 -18
  13. package/dist/spoke-checkout.js +177 -236
  14. package/dist/surfaces.js +19 -14
  15. package/dist/sync-routes.js +3 -2
  16. package/dist/template-record.d.ts +4 -3
  17. package/dist/template-record.js +6 -3
  18. package/dist/topology.js +25 -1
  19. package/dist/update.js +5 -0
  20. package/package.json +3 -3
  21. package/site-templates/designs/minimalism/DESIGN.md +1 -1
  22. package/templates/apps/api/CLAUDE.md +15 -6
  23. package/templates/apps/api/src/lib/__tests__/consent-store.test.ts +19 -0
  24. package/templates/apps/api/src/lib/__tests__/flow-completion-store.test.ts +149 -0
  25. package/templates/apps/api/src/lib/consent-store.ts +28 -0
  26. package/templates/apps/api/src/lib/flow-completion-store.ts +136 -0
  27. package/templates/apps/api/src/lib/hub-sign-in-store.ts +38 -2
  28. package/templates/apps/api/src/lib/proposal-paid-store.ts +21 -4
  29. package/templates/apps/api/src/middleware/__tests__/auth.hub-session-path.test.ts +14 -3
  30. package/templates/apps/api/src/middleware/auth.ts +13 -6
  31. package/templates/apps/api/src/openapi/spec.ts +30 -3
  32. package/templates/apps/api/src/routes/auth/__tests__/hub-sign-in.test.ts +49 -0
  33. package/templates/apps/api/src/routes/auth/hub-sign-in.ts +41 -0
  34. package/templates/apps/api/src/routes/consent/__tests__/account.test.ts +386 -0
  35. package/templates/apps/api/src/routes/consent/account.ts +266 -0
  36. package/templates/apps/api/src/routes/consent/index.ts +15 -7
  37. package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +36 -0
  38. package/templates/apps/api/src/routes/content/get-page.ts +4 -0
  39. package/templates/apps/api/src/routes/leads/__tests__/magnet-completion.test.ts +515 -0
  40. package/templates/apps/api/src/routes/leads/index.ts +161 -7
  41. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-checkout-consent.test.ts +649 -0
  42. package/templates/apps/api/src/routes/webhooks/stripe.ts +154 -19
  43. package/templates/apps/api/src/server.ts +28 -0
  44. package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +1038 -118
  45. package/templates/apps/api/src/services/__tests__/magnet-completion.test.ts +389 -0
  46. package/templates/apps/api/src/services/checkout-buyer.ts +100 -4
  47. package/templates/apps/api/src/services/consent.ts +95 -0
  48. package/templates/apps/api/src/services/lead-routing.ts +337 -8
  49. package/templates/apps/api/src/services/magnet-completion.ts +242 -0
  50. package/templates/apps/api/src/services/person.ts +40 -8
  51. package/templates/apps/api/src/services/plan-offerings.ts +59 -0
  52. package/templates/apps/api/src/services/public-site-resolver.ts +8 -1
  53. package/templates/apps/web/.env.example +14 -0
  54. package/templates/apps/web/CLAUDE.md +120 -9
  55. package/templates/apps/web/__tests__/__mocks__/next-intl-server.ts +6 -2
  56. package/templates/apps/web/app/[locale]/(auth)/__tests__/auth-pages.test.tsx +5 -4
  57. package/templates/apps/web/app/[locale]/(auth)/forgot-password/forgot-password-card.tsx +5 -1
  58. package/templates/apps/web/app/[locale]/(auth)/forgot-password/page.tsx +2 -0
  59. package/templates/apps/web/app/[locale]/(auth)/layout.tsx +3 -14
  60. package/templates/apps/web/app/[locale]/(auth)/reset-password/page.tsx +2 -0
  61. package/templates/apps/web/app/[locale]/(auth)/reset-password/reset-password-card.tsx +5 -1
  62. package/templates/apps/web/app/[locale]/(auth)/sign-in/__tests__/page.test.tsx +160 -8
  63. package/templates/apps/web/app/[locale]/(auth)/sign-in/__tests__/sign-in-card.test.tsx +12 -9
  64. package/templates/apps/web/app/[locale]/(auth)/sign-in/page.tsx +105 -2
  65. package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-card.tsx +36 -3
  66. package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-text.ts +47 -1
  67. package/templates/apps/web/app/[locale]/(auth)/sign-up/__tests__/sign-up-card.test.tsx +60 -22
  68. package/templates/apps/web/app/[locale]/(auth)/sign-up/page.tsx +16 -8
  69. package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up-card.tsx +85 -31
  70. package/templates/apps/web/app/[locale]/(auth)/use-auth-card-flow.ts +18 -4
  71. package/templates/apps/web/app/[locale]/(auth)/verify-email/page.tsx +3 -1
  72. package/templates/apps/web/app/[locale]/(authed)/__tests__/gate.test.tsx +7 -4
  73. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/agreements-prompt.test.tsx +227 -207
  74. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout-sign-out.test.tsx +17 -1
  75. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout.test.tsx +143 -1
  76. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/new-menu.test.tsx +4 -0
  77. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +385 -89
  78. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/popups.test.tsx +5 -0
  79. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/privacy.test.tsx +44 -5
  80. package/templates/apps/web/app/[locale]/(authed)/account/account-frame.tsx +68 -34
  81. package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +0 -8
  82. package/templates/apps/web/app/[locale]/(authed)/account/agreements-actions.ts +39 -151
  83. package/templates/apps/web/app/[locale]/(authed)/account/agreements-gate.ts +61 -0
  84. package/templates/apps/web/app/[locale]/(authed)/account/agreements-prompt.tsx +143 -140
  85. package/templates/apps/web/app/[locale]/(authed)/account/layout.tsx +40 -6
  86. package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +68 -54
  87. package/templates/apps/web/app/[locale]/(authed)/account/privacy/page.tsx +5 -1
  88. package/templates/apps/web/app/[locale]/careers/[slug]/apply.actions.ts +293 -0
  89. package/templates/apps/web/app/[locale]/careers/[slug]/job-application-form.ts +115 -0
  90. package/templates/apps/web/app/[locale]/careers/[slug]/page.tsx +146 -1
  91. package/templates/apps/web/app/[locale]/careers/__tests__/apply.actions.test.ts +310 -0
  92. package/templates/apps/web/app/[locale]/careers/__tests__/job-application-form.test.ts +143 -0
  93. package/templates/apps/web/app/[locale]/careers/__tests__/job-apply-page.test.tsx +350 -0
  94. package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +29 -3
  95. package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +2 -2
  96. package/templates/apps/web/app/[locale]/checkout/__tests__/pages.test.tsx +139 -0
  97. package/templates/apps/web/app/[locale]/checkout/__tests__/thank-you-account.test.tsx +136 -0
  98. package/templates/apps/web/app/[locale]/checkout/thank-you/page.tsx +114 -0
  99. package/templates/apps/web/app/[locale]/checkout/unavailable/page.tsx +51 -0
  100. package/templates/apps/web/app/[locale]/clients/__tests__/page-renderer-migration.test.tsx +1 -1
  101. package/templates/apps/web/app/[locale]/contact/ConversationalIntakeWrapper.tsx +17 -1
  102. package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +79 -4
  103. package/templates/apps/web/app/[locale]/contact/__tests__/contact-magnet-page.test.tsx +41 -2
  104. package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +438 -31
  105. package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +7 -0
  106. package/templates/apps/web/app/[locale]/contact/contact.actions.ts +168 -57
  107. package/templates/apps/web/app/[locale]/contact/intake-script.ts +26 -0
  108. package/templates/apps/web/app/[locale]/contact/page.tsx +49 -17
  109. package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +2 -2
  110. package/templates/apps/web/app/[locale]/downloads/__tests__/page-renderer-migration.test.tsx +1 -1
  111. package/templates/apps/web/app/[locale]/events/__tests__/page-renderer-migration.test.tsx +2 -2
  112. package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +106 -1
  113. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.invite.test.tsx +257 -0
  114. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/account-invite.actions.test.ts +69 -0
  115. package/templates/apps/web/app/[locale]/flows/[slug]/account-invite.actions.ts +35 -0
  116. package/templates/apps/web/app/[locale]/for/__tests__/page-renderer-migration.test.tsx +1 -1
  117. package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +1 -1
  118. package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +2 -2
  119. package/templates/apps/web/app/[locale]/page.tsx +13 -1
  120. package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +2 -2
  121. package/templates/apps/web/app/[locale]/products/__tests__/page-renderer-migration.test.tsx +2 -2
  122. package/templates/apps/web/app/[locale]/resources/__tests__/page-renderer-migration.test.tsx +2 -2
  123. package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +2 -2
  124. package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +1 -1
  125. package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +1 -1
  126. package/templates/apps/web/app/[locale]/testimonials/__tests__/page-renderer-migration.test.tsx +1 -1
  127. package/templates/apps/web/app/[locale]/thanks/[slug]/AccountInviteLink.tsx +89 -0
  128. package/templates/apps/web/app/[locale]/thanks/[slug]/__tests__/AccountInviteLink.test.tsx +104 -0
  129. package/templates/apps/web/app/[locale]/thanks/[slug]/__tests__/page.test.tsx +38 -0
  130. package/templates/apps/web/app/[locale]/thanks/[slug]/page.tsx +9 -0
  131. package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +1 -1
  132. package/templates/apps/web/app/api/auth/hub/authorize/__tests__/route.test.ts +114 -0
  133. package/templates/apps/web/app/api/auth/hub/authorize/route.ts +20 -3
  134. package/templates/apps/web/app/api/checkout/[offering]/route.ts +48 -0
  135. package/templates/apps/web/app/api/checkout/__tests__/route.test.ts +120 -0
  136. package/templates/apps/web/app/api/checkout/__tests__/success-path.test.ts +86 -0
  137. package/templates/apps/web/app/api/hub-session/__tests__/routes.test.ts +44 -1
  138. package/templates/apps/web/app/api/hub-session/sign-in/route.ts +22 -3
  139. package/templates/apps/web/app/api/webhooks/stripe/__tests__/route.test.ts +92 -0
  140. package/templates/apps/web/app/api/webhooks/stripe/route.ts +31 -0
  141. package/templates/apps/web/auth.ts +28 -2
  142. package/templates/apps/web/lib/__tests__/account-invite.test.ts +63 -0
  143. package/templates/apps/web/lib/__tests__/checkout.test.ts +243 -0
  144. package/templates/apps/web/lib/__tests__/consent-policies.test.ts +23 -0
  145. package/templates/apps/web/lib/__tests__/deck-page.test.ts +5 -0
  146. package/templates/apps/web/lib/__tests__/structured-data.test.tsx +34 -17
  147. package/templates/apps/web/lib/account-invite.ts +37 -0
  148. package/templates/apps/web/lib/checkout-config.ts +27 -0
  149. package/templates/apps/web/lib/checkout.ts +93 -0
  150. package/templates/apps/web/lib/consent-policies.ts +22 -0
  151. package/templates/apps/web/lib/deck-page.ts +6 -0
  152. package/templates/apps/web/lib/hub-session/__tests__/sign-in-gate.test.tsx +111 -0
  153. package/templates/apps/web/lib/hub-session/sign-in-gate.tsx +88 -32
  154. package/templates/apps/web/lib/prisma-for-auth.ts +8 -0
  155. package/templates/apps/web/lib/sign-up-agreements.ts +447 -0
  156. package/templates/apps/web/lib/site-chrome.ts +2 -0
  157. package/templates/apps/web/lib/telemetry-scrub.ts +5 -1
  158. package/templates/apps/web/messages/en.json +24 -7
  159. package/templates/apps/web/package.json +1 -0
  160. package/templates/apps/web/src/lib/__tests__/lead-write.test.ts +187 -0
  161. package/templates/apps/web/src/lib/__tests__/rate-limit.test.ts +67 -0
  162. package/templates/apps/web/src/lib/lead-write.ts +90 -0
  163. package/templates/apps/web/src/lib/rate-limit.ts +79 -0
  164. package/templates/database/CHANGELOG.md +38 -0
  165. package/templates/database/__tests__/consent-source-checkout-migration.test.ts +79 -0
  166. package/templates/database/__tests__/flow-completion-migration.test.ts +115 -0
  167. package/templates/database/__tests__/flow-completion.db.test.ts +136 -0
  168. package/templates/database/__tests__/seed.test.ts +34 -1
  169. package/templates/database/inbox/schema.prisma +17 -2
  170. package/templates/database/migrations/20261018120000_flow_completion/migration.sql +52 -0
  171. package/templates/database/migrations/20261019120000_consent_source_checkout/migration.sql +27 -0
  172. package/templates/database/package.json +1 -1
  173. package/templates/database/seed.ts +34 -3
  174. package/versions.json +34 -34
  175. package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up.actions.ts +0 -125
@@ -17,7 +17,7 @@
17
17
  "importPath": "@working-theory/ui/blocks",
18
18
  "rootBarrel": "@working-theory/ui",
19
19
  "sourceRoot": "packages/ui/src/blocks",
20
- "count": 106
20
+ "count": 107
21
21
  },
22
22
  {
23
23
  "name": "chrome",
@@ -837,6 +837,17 @@
837
837
  "descriptionGrade": "strong",
838
838
  "sectionType": null
839
839
  },
840
+ {
841
+ "name": "AuthBackLink",
842
+ "tier": "blocks",
843
+ "sourcePath": "packages/ui/src/blocks/AuthBackLink.tsx",
844
+ "importPath": "@working-theory/ui/blocks",
845
+ "exported": true,
846
+ "description": "The auth shell's top-start \"← Back to <site>\". A hub's sign-in page renders it when the visitor arrived from a registered spoke, so the cross-domain handoff names where they came from and gives them the way back.",
847
+ "descriptionSource": "doc-comment",
848
+ "descriptionGrade": "strong",
849
+ "sectionType": null
850
+ },
840
851
  {
841
852
  "name": "AuthCard",
842
853
  "tier": "blocks",
@@ -5393,6 +5404,9 @@
5393
5404
  "headline": {
5394
5405
  "type": "string"
5395
5406
  },
5407
+ "subtitle": {
5408
+ "type": "string"
5409
+ },
5396
5410
  "body": {
5397
5411
  "type": "string"
5398
5412
  },
@@ -12177,7 +12191,7 @@
12177
12191
  "groups": [
12178
12192
  {
12179
12193
  "name": "colors",
12180
- "leafCount": 62,
12194
+ "leafCount": 64,
12181
12195
  "leaves": [
12182
12196
  {
12183
12197
  "path": "colors.light.bg",
@@ -12199,6 +12213,11 @@
12199
12213
  "type": "string",
12200
12214
  "value": "#475569"
12201
12215
  },
12216
+ {
12217
+ "path": "colors.light.link",
12218
+ "type": "string",
12219
+ "value": "#2563eb"
12220
+ },
12202
12221
  {
12203
12222
  "path": "colors.light.textMuted",
12204
12223
  "type": "string",
@@ -12354,6 +12373,11 @@
12354
12373
  "type": "string",
12355
12374
  "value": "#94a3b8"
12356
12375
  },
12376
+ {
12377
+ "path": "colors.dark.link",
12378
+ "type": "string",
12379
+ "value": "#60a5fa"
12380
+ },
12357
12381
  {
12358
12382
  "path": "colors.dark.textMuted",
12359
12383
  "type": "string",
@@ -13187,7 +13211,7 @@
13187
13211
  },
13188
13212
  {
13189
13213
  "name": "spacing",
13190
- "leafCount": 13,
13214
+ "leafCount": 15,
13191
13215
  "leaves": [
13192
13216
  {
13193
13217
  "path": "spacing.controlX",
@@ -13234,6 +13258,16 @@
13234
13258
  "type": "string",
13235
13259
  "value": "1rem"
13236
13260
  },
13261
+ {
13262
+ "path": "spacing.deckLabelLine",
13263
+ "type": "string",
13264
+ "value": "var(--spacing-section-lg)"
13265
+ },
13266
+ {
13267
+ "path": "spacing.deckActionLine",
13268
+ "type": "string",
13269
+ "value": "calc(2 * (var(--control-height-big) + var(--spacing-block-tight)))"
13270
+ },
13237
13271
  {
13238
13272
  "path": "spacing.paragraph",
13239
13273
  "type": "string",
@@ -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: **319 component files** across 16 tiers — 56 sections, 106 blocks, 19 chrome, 79 primitives, 6 panels, 8 shells, 5 docs-blog-blocks, 1 docs-blog-chrome, 2 docs-blog-shells, 2 scheduling-sections, 6 scheduling-blocks, 5 signal-collectors-sections, 9 signal-collectors-blocks, 2 commerce-sections, 12 commerce-blocks, 1 commerce-panels. 316 distinct names: `SchedulerEmbed`, `Separator`, `Tabs` exist in more than one tier as genuinely different components, listed once per tier with its own source path.
18
+ Inventory: **320 component files** across 16 tiers — 56 sections, 107 blocks, 19 chrome, 79 primitives, 6 panels, 8 shells, 5 docs-blog-blocks, 1 docs-blog-chrome, 2 docs-blog-shells, 2 scheduling-sections, 6 scheduling-blocks, 5 signal-collectors-sections, 9 signal-collectors-blocks, 2 commerce-sections, 12 commerce-blocks, 1 commerce-panels. 317 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: **315/319** components carry a capability description derived from source — 44 from the section registry's hand-authored when-to-use text, 236 from the component's own doc-comment header, 35 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.
20
+ Description coverage: **316/320** components carry a capability description derived from source — 44 from the section registry's hand-authored when-to-use text, 237 from the component's own doc-comment header, 35 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, **281** are *strong* — a capability sentence you could match a requirement against — and **34** 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 88%**.
22
+ Of those, **282** are *strong* — a capability sentence you could match a requirement against — and **34** 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 88%**.
23
23
 
24
- Per tier, strong out of total: **Sections** 55/56 · **Blocks** 98/106 · **Chrome** 10/19 · **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** 6/6 · **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.
24
+ Per tier, strong out of total: **Sections** 55/56 · **Blocks** 99/107 · **Chrome** 10/19 · **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** 6/6 · **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
 
@@ -120,7 +120,7 @@ Import: `import { X } from "@working-theory/ui/sections";` (or from the `@workin
120
120
 
121
121
  ---
122
122
 
123
- ## Blocks (106)
123
+ ## Blocks (107)
124
124
 
125
125
  Intermediate composition tiles — the pieces sections are built from. Compose primitives and panels; never import a section.
126
126
 
@@ -136,6 +136,7 @@ Import: `import { X } from "@working-theory/ui/blocks";` (or from the `@working-
136
136
  | `AskAiCommandGroup` | The ⌘K palette's composable "Ask AI" group; one CommandItem per provider seeding "About &lt;entity>: &lt;query>" (or the entity promptText when the query is empty); composed by the consumer's own CommandDialog. | `import { AskAiCommandGroup } from "@working-theory/ui/blocks"` |
137
137
  | `AskAiProviderRow` | The ONE shared provider-link row every ask-AI placement composes — the AskAi section (band + ticker variant) and the Hero `askAi` opt-in all render THIS block, never a per-placement fork. Renders a bounded set of five measured providers, each a deep link seeded with an AUTHORED `promptText` — the prompt is always consumer-supplied data, never derived from page context (headline, entity name, etc.). | `import { AskAiProviderRow } from "@working-theory/ui/blocks"` |
138
138
  | `AssetLightbox` | The asset detail lightbox: the image with zoom on the stage, a Details rail with in-place pencil-edit Title and Description, then Filename/Type/Size/Dimensions/Created, then Tags (from the seeded set the caller passes), then Copy link/Replace/Delete. Renders inside a ResponsiveSheet — the SAME field sequence in both layouts (: one field array, walked once, shared by both). | `import { AssetLightbox } from "@working-theory/ui/blocks"` |
139
+ | `AuthBackLink` | The auth shell's top-start "← Back to &lt;site>". A hub's sign-in page renders it when the visitor arrived from a registered spoke, so the cross-domain handoff names where they came from and gives them the way back. | `import { AuthBackLink } from "@working-theory/ui/blocks"` |
139
140
  | `AuthCard` | Multi-flow auth card: sign-in, sign-up, invitation, forgot, reset and sent, from one skeleton. One self-contained card renders every auth surface a site needs: signing in, signing up, a closed sign-up that requires an invitation, requesting a password reset, choosing a new password, and the check-your-email state that follows a magic link or a reset request. | `import { AuthCard } from "@working-theory/ui/blocks"` |
140
141
  | `AwardBadgePlaceholder` | The labeled placeholder an Awards badge slot renders when no image has been supplied — mirrors MediaPlaceholder's role but in an award-chevron/ laurel badge silhouette rather than a generic photo frame, since an unfilled award slot should read as "an award badge goes here," not "a photo goes here." The shape (a laurel-flanked medallion over a chevron ribbon) is a hand-drawn SVG outline — never a copied G2/Capterra/Apollo asset. | internal — compose from `packages/ui/src/blocks/AwardBadgePlaceholder.tsx` |
141
142
  | `BackgroundPattern` | Decorative SVG dot-grid / line pattern for ContactForm section. Decorative block (generalized beyond its original single consumer). Renders an absolutely-positioned decoration behind a section's content. Originally the pattern anchored behind ContactForm's form column; SpotlightPanel composes it as a full-bleed section backdrop (dimmed + blurred, behind a centered card) — same block, a second placement. | `import { BackgroundPattern } from "@working-theory/ui/blocks"` |
@@ -168,16 +168,20 @@ export async function applyModuleToScaffold(opts) {
168
168
  }
169
169
  : {}),
170
170
  });
171
- // #7838 D8 — commerce on a spoke brings the spoke's own checkout, stamped
172
- // from the site directory's name (the `projectName` scaffold() defaults to).
173
- // A file the site already has is left as it is. #8217 D8 — on a spoke that
174
- // signs in through its hub, the billing action and its seam render too, so
175
- // re-running add-module commerce gives an existing commerce spoke the action.
171
+ // #7838 D8 — commerce on a spoke brings the spoke's own checkout — the
172
+ // standard, copied from the template (#8748 D3) — stamped from the site
173
+ // directory's name (the `projectName` scaffold() defaults to). A file the
174
+ // site already has is left as it is. No record row: the record is written
175
+ // only at scaffold time. #8217 D8 — on a spoke that signs in through its
176
+ // hub, the billing action and its seam render too, so re-running
177
+ // add-module commerce gives an existing commerce spoke the action.
176
178
  const spokeCommerce = spoke !== undefined && opts.moduleName === "commerce";
177
179
  const checkoutRows = spokeCommerce
178
180
  ? writeSpokeCheckoutSlice(targetAbs, {
179
181
  site: spokeCheckoutSite(basename(targetAbs)),
182
+ projectName: basename(targetAbs),
180
183
  versions: opts.versions,
184
+ templatesDir: opts.templatesDir,
181
185
  signIn: spoke.signIn,
182
186
  })
183
187
  : [];
@@ -28,7 +28,14 @@ export interface DeriveArchitectureOptions {
28
28
  * hub — the billing action (#8217 D9, a reviewed change to the #7873 rule).
29
29
  */
30
30
  export declare const SPOKE_CHECKOUT_ROUTE_FILES: readonly string[];
31
- /** The forbidden patterns a spoke's apps/web carries (#7873, brief Part 2 §2). */
31
+ /**
32
+ * The spoke files that may import @working-theory/spoke-checkout to READ a
33
+ * paid session, and nothing else: the thank-you page's `readPaidCheckoutEmail`
34
+ * (#8757 D13). Kept apart from SPOKE_CHECKOUT_ROUTE_FILES so it does not
35
+ * also admit them to `lib/checkout`, the one file that fulfils an order.
36
+ */
37
+ export declare const SPOKE_CHECKOUT_READER_FILES: readonly string[];
38
+ /** The forbidden patterns a spoke's apps/web carries (#7873, brief Part 2 §2; #8748). */
32
39
  export declare const SPOKE_WEB_FORBIDDEN_PATTERNS: readonly ForbiddenPattern[];
33
40
  /** The same three for a spoke's kept apps/mcp — no MCP file may import the package (#7873). */
34
41
  export declare const SPOKE_MCP_FORBIDDEN_PATTERNS: readonly ForbiddenPattern[];
@@ -37,6 +37,15 @@ export const SPOKE_CHECKOUT_ROUTE_FILES = Object.freeze([
37
37
  "apps/web/app/api/webhooks/stripe/route.ts",
38
38
  "apps/web/app/[locale]/(authed)/account/billing/spoke-billing.action.ts",
39
39
  ]);
40
+ /**
41
+ * The spoke files that may import @working-theory/spoke-checkout to READ a
42
+ * paid session, and nothing else: the thank-you page's `readPaidCheckoutEmail`
43
+ * (#8757 D13). Kept apart from SPOKE_CHECKOUT_ROUTE_FILES so it does not
44
+ * also admit them to `lib/checkout`, the one file that fulfils an order.
45
+ */
46
+ export const SPOKE_CHECKOUT_READER_FILES = Object.freeze([
47
+ "apps/web/app/[locale]/checkout/thank-you/page.tsx",
48
+ ]);
40
49
  /**
41
50
  * A line-level regex for an import of `specifier` or one of its subpaths, in
42
51
  * every form the line can take: `from "…"` (a static or re-export, including
@@ -53,7 +62,7 @@ function specifierForm(specifier) {
53
62
  /** P1 with no exclusions — a kept apps/mcp's form; apps/web adds the route files. */
54
63
  const SPOKE_CHECKOUT_PATTERN = Object.freeze({
55
64
  pattern: specifierForm("@working-theory/spoke-checkout"),
56
- reason: "Spoke checkout: only the buy route, the webhook route and the billing action may import @working-theory/spoke-checkout (brief Part 2 §2, #7873, #8217)",
65
+ reason: "Spoke checkout: only the buy route, the webhook route, the billing action and the thank-you page may import @working-theory/spoke-checkout (brief Part 2 §2, #7873, #8217, #8757)",
57
66
  });
58
67
  const STRIPE_CLIENT_PATTERN = Object.freeze({
59
68
  pattern: specifierForm("@working-theory/stripe-client"),
@@ -63,11 +72,37 @@ const STRIPE_SDK_PATTERN = Object.freeze({
63
72
  pattern: specifierForm("stripe"),
64
73
  reason: "A spoke never imports the stripe SDK directly — it reaches Stripe only through @working-theory/spoke-checkout (#7873)",
65
74
  });
66
- /** The forbidden patterns a spoke's apps/web carries (#7873, brief Part 2 §2). */
75
+ /**
76
+ * The standard spoke checkout's fulfilment (`lib/checkout.ts`) sends the one
77
+ * `Idempotency-Key` the hub reads as a purchase, so only the files allowed to
78
+ * import the package may reach it (#8748 D3, SEC-8748-A): any relative or
79
+ * `@/`-aliased import (apps/web's tsconfig maps `@/*` to the app root) whose
80
+ * last segment is `checkout` (with or without a `.js`/`.ts`-family
81
+ * extension, which `bundler` resolution maps to the same file), in every form
82
+ * `specifierForm` covers. The
83
+ * closing quote straight after `checkout` keeps `checkout-config` and
84
+ * `checkout.actions` out of it. The checkout tests the writer copies are
85
+ * excluded too: the scaffold's lint reads test files.
86
+ */
87
+ const SPOKE_FULFILMENT_PATTERN = Object.freeze({
88
+ pattern: "\\b(?:from|import|require)\\s*\\(?\\s*['\"`](?:(?:\\.{1,2}/)+|@/)(?:[^'\"`]*/)?checkout(?:\\.[cm]?[jt]sx?)?['\"`]",
89
+ paths_excluded: [
90
+ ...SPOKE_CHECKOUT_ROUTE_FILES,
91
+ "apps/web/app/api/checkout/__tests__/**",
92
+ "apps/web/app/api/webhooks/stripe/__tests__/**",
93
+ "apps/web/lib/__tests__/checkout.test.ts",
94
+ ],
95
+ reason: "Spoke checkout: only the buy route, the webhook route and the billing action may import lib/checkout, the one file that sends the hub an Idempotency-Key (#8748)",
96
+ });
97
+ /** The forbidden patterns a spoke's apps/web carries (#7873, brief Part 2 §2; #8748). */
67
98
  export const SPOKE_WEB_FORBIDDEN_PATTERNS = Object.freeze([
68
- Object.freeze({ ...SPOKE_CHECKOUT_PATTERN, paths_excluded: [...SPOKE_CHECKOUT_ROUTE_FILES] }),
99
+ Object.freeze({
100
+ ...SPOKE_CHECKOUT_PATTERN,
101
+ paths_excluded: [...SPOKE_CHECKOUT_ROUTE_FILES, ...SPOKE_CHECKOUT_READER_FILES],
102
+ }),
69
103
  STRIPE_CLIENT_PATTERN,
70
104
  STRIPE_SDK_PATTERN,
105
+ SPOKE_FULFILMENT_PATTERN,
71
106
  ]);
72
107
  /** The same three for a spoke's kept apps/mcp — no MCP file may import the package (#7873). */
73
108
  export const SPOKE_MCP_FORBIDDEN_PATTERNS = Object.freeze([
@@ -486,13 +486,14 @@ function validateStringMap(raw, file, field) {
486
486
  }
487
487
  const LAYOUT_KEYS = ["nav", "footer", "announcementBanner", "home"];
488
488
  /** Every key a package's `layout` may carry — #7719 D3 adds the optional `deckGrounds` (a deck home only),
489
- * #7721 D1 the optional `innerPages`, #7740 D1 the optional `deckPresentation` (a deck home only),
489
+ * #7721 D1 the optional `innerPages`, #7740 D1 the optional `deckPresentation` (a deck home only), #8773 the optional `deckAnchors` (a presenting deck only),
490
490
  * #7742 D1 the optional `colorMode` and `chromeStyle`, #7797 D1 the optional `closingFooterPaths`. */
491
491
  const ALLOWED_LAYOUT_KEYS = new Set([
492
492
  ...LAYOUT_KEYS,
493
493
  "deckGrounds",
494
494
  "innerPages",
495
495
  "deckPresentation",
496
+ "deckAnchors",
496
497
  "colorMode",
497
498
  "chromeStyle",
498
499
  "closingFooterPaths",
@@ -524,11 +525,12 @@ function validateLayout(raw, file) {
524
525
  // `ResolvedSiteChrome` shape; an explicit "default" is the absent key.
525
526
  // #7740 D1 — and `deckPresentation` only as `true`.
526
527
  // #7742 D1 — and `colorMode` only as "light", `chromeStyle` only as "minimal".
527
- const { innerPages, deckPresentation, colorMode, chromeStyle, ...rest } = validated;
528
+ const { innerPages, deckPresentation, deckAnchors, colorMode, chromeStyle, ...rest } = validated;
528
529
  const layout = {
529
530
  ...rest,
530
531
  ...(innerPages === "bare" ? { innerPages } : {}),
531
532
  ...(deckPresentation === true ? { deckPresentation } : {}),
533
+ ...(deckAnchors === "bare" ? { deckAnchors } : {}),
532
534
  ...(colorMode === "light" ? { colorMode } : {}),
533
535
  ...(chromeStyle === "minimal" ? { chromeStyle } : {}),
534
536
  };
@@ -538,6 +540,9 @@ function validateLayout(raw, file) {
538
540
  if (deckPresentation === true && layout.home !== "deck") {
539
541
  refuse(file, "layout.deckPresentation", "applies only to home: deck");
540
542
  }
543
+ if (deckAnchors === "bare" && deckPresentation !== true) {
544
+ refuse(file, "layout.deckAnchors", "applies only with deckPresentation: true");
545
+ }
541
546
  return layout;
542
547
  }
543
548
  const TREATMENTS_ENUMS = {
@@ -52,7 +52,7 @@ export const ENV_CATALOG = {
52
52
  secret: false,
53
53
  },
54
54
  AILK_LEAD_SITE: {
55
- description: "Marks which site of a hub and its spokes captured an MCP lead; unset leaves the lead unmarked.",
55
+ description: "This site's slug, written as payload.site on every lead its /contact form and MCP server capture, so a hub can tell its sites apart; unset leaves the lead unmarked.",
56
56
  secret: false,
57
57
  },
58
58
  AILK_LICENSE_STORAGE_PATH: {
@@ -71,6 +71,10 @@ export const ENV_CATALOG = {
71
71
  description: "The scope captured leads are stamped with; on a single-tenant install, the default workspace id.",
72
72
  secret: false,
73
73
  },
74
+ AILK_SPOKE_KEY: {
75
+ description: "The hub-issued key this site sends as x-ailk-spoke-key; the checkout fulfilment refuses to run without it. Never committed.",
76
+ secret: true,
77
+ },
74
78
  AILK_VISITOR_FORWARD_KEY: {
75
79
  description: "This spoke's key for forwarding each visitor's IP to the hub, so the hub rate-limits per visitor. Server-only.",
76
80
  secret: true,
@@ -1813,8 +1813,10 @@ export const MODULE_SLICES = {
1813
1813
  // the flow results pages, dev/purchase — spoke-intake Rule 3). What lands
1814
1814
  // is the scaffold-commerce skill, the commerce-boundary rule and the
1815
1815
  // @working-theory/ui-commerce dependency (`patches` below); the spoke's own
1816
- // checkout — two routes, checkout-config.ts and two return pages — is
1817
- // rendered by create-ailk (`spoke-checkout.ts`), not carried here.
1816
+ // checkout — the standard spoke checkout, `apps/web` template source
1817
+ // (two routes, lib/checkout.ts, checkout-config.ts, two return pages and
1818
+ // their tests) — is copied by `writeSpokeCheckoutSlice`
1819
+ // (`spoke-checkout.ts`, #8748), not carried here.
1818
1820
  spoke: true,
1819
1821
  paidSectionsPackage: "@working-theory/ui-commerce",
1820
1822
  apps: {
@@ -44,7 +44,7 @@ import { SITE_CHROME_PATH, renderSiteChrome, resolveSiteChrome, } from "./site-c
44
44
  import { SITE_FONTS_PATH, renderSiteFonts } from "./site-fonts.js";
45
45
  import { planSiteManifest } from "./site-manifest.js";
46
46
  import { SITE_ROSTER_PATH, planHomeRecord, planSiteManifestDesign, readSiteRoster, withMeetPlaceholderSeeds, } from "./site-roster.js";
47
- import { spokeCheckoutSite, spokeEnvRowOptions, writeSpokeCheckoutSlice, } from "./spoke-checkout.js";
47
+ import { SPOKE_CHECKOUT_TEMPLATE_PATHS, dropSpokeCheckoutDependency, spokeCheckoutSite, spokeEnvRowOptions, writeSpokeCheckoutSlice, } from "./spoke-checkout.js";
48
48
  import { GATEABLE_SURFACES, MODULE_SURFACES, NAV_CONFIG_PATH, excludedRoutePaths, isUnservedSurfaceContent, renderNavConfig, resolveSurfaces, } from "./surfaces.js";
49
49
  import { syncRoutes } from "./sync-routes.js";
50
50
  import { BASE_LAYER_DIR, SHAPE_STARTERS, TEMPLATE_MANIFESTS, TemplateError, mergeRoster, resolveTemplate, resolveTemplateLineage, rosterDeclares, templateDelivery, templateTreeDir, } from "./template-manifest.js";
@@ -182,6 +182,9 @@ export function scaffoldSync(opts, copied) {
182
182
  ...(spoke !== undefined
183
183
  ? spokeExclusions(TEMPLATES_DIR, spoke)
184
184
  : SPOKE_ONLY_FILES),
185
+ // #8748 D3 — the standard spoke checkout lands only through
186
+ // `writeSpokeCheckoutSlice`, on a spoke carrying commerce.
187
+ ...SPOKE_CHECKOUT_TEMPLATE_PATHS,
185
188
  ],
186
189
  markers: buildScaffoldMarkers(projectName),
187
190
  ...(copied !== undefined
@@ -193,6 +196,9 @@ export function scaffoldSync(opts, copied) {
193
196
  }
194
197
  : {}),
195
198
  });
199
+ // #8748 D3 — the copied apps/web depends on the checkout package; only a
200
+ // commerce spoke keeps it, re-added by `writeSpokeCheckoutSlice` in scaffold().
201
+ dropSpokeCheckoutDependency(targetAbs);
196
202
  // #7426 D5/D6 — the root files a spoke rewrites, and the spoke seams.
197
203
  if (spoke !== undefined)
198
204
  applySpokeTransforms(targetAbs, spoke);
@@ -793,18 +799,26 @@ export async function scaffold(opts) {
793
799
  writeFileSync(rosterPath, manifestText);
794
800
  }
795
801
  }
796
- // #7838 D8 — a commerce spoke's own checkout: two routes, its config and
797
- // two return pages, once every layer has landed (so the spoke filter has
798
- // already dropped the hub-checkout files) and before the manifest, whose
799
- // code-route rows then record /checkout/thank-you and /checkout/unavailable.
802
+ // #7838 D8 — a commerce spoke's own checkout: the standard spoke checkout
803
+ // copied from the template (#8748 D3), its config stamped, once every layer
804
+ // has landed (so the spoke filter has already dropped the hub-checkout
805
+ // files) and before the manifest, whose code-route rows then record
806
+ // /checkout/thank-you and /checkout/unavailable. Each copied file is
807
+ // recorded like the base copy's, so `update` keeps it current.
800
808
  // #8217 D8 — a spoke that signs in through its hub also gets the billing
801
809
  // action and the web seam render that reaches it.
802
810
  const spokeCommerce = spoke !== undefined && result.modules.includes("commerce");
803
811
  if (spokeCommerce) {
804
812
  writeSpokeCheckoutSlice(result.targetDir, {
805
813
  site: spokeCheckoutSite(projectName),
814
+ projectName,
806
815
  versions,
816
+ templatesDir: TEMPLATES_DIR,
807
817
  signIn: spoke.signIn,
818
+ onWrite: (relPath, bytes) => {
819
+ if (isRecordable(relPath))
820
+ copied.set(relPath, sha256Hex(bytes));
821
+ },
808
822
  });
809
823
  }
810
824
  // #6944 — the manifest records what this scaffold IS, once every layer has
@@ -41,6 +41,10 @@ export type SiteChromeHome = "default" | "deck";
41
41
  /** The `innerPages` knob's vocabulary (#7721). `bare` → every non-home page
42
42
  * gets the bare header and the one-bar footer. */
43
43
  export type SiteChromeInnerPages = "default" | "bare";
44
+ /** The `deckAnchors` knob's vocabulary (#8773). `bare` → the deck home names
45
+ * its slides `#<id>` instead of `#slide-<id>`, keeping the `slide-` name only
46
+ * where the bare id would collide (the page renderer's `deckSlideAnchors`). */
47
+ export type SiteChromeDeckAnchors = "prefixed" | "bare";
44
48
  /** The `colorMode` knob's vocabulary (#7742). `light` pins the site to light
45
49
  * and removes every theme switch; dark then appears only as a deck ground. */
46
50
  export type SiteChromeColorMode = "user" | "light";
@@ -72,6 +76,8 @@ export interface TemplateLayout {
72
76
  innerPages?: SiteChromeInnerPages;
73
77
  /** #7740 D1 — `true`: the deck home presents slide by slide (a deck home only). */
74
78
  deckPresentation?: boolean;
79
+ /** #8773 — `bare`: a presenting deck names its slides `#<id>` (a presenting deck home only). */
80
+ deckAnchors?: SiteChromeDeckAnchors;
75
81
  /** #7742 D1 — `light`: the site is pinned to light, with no theme switch. */
76
82
  colorMode?: SiteChromeColorMode;
77
83
  /** #7742 D1 — `minimal`: the minimal closing slide, consent bar and themed 404. */
@@ -91,6 +97,8 @@ export interface ResolvedSiteChrome {
91
97
  innerPages?: "bare";
92
98
  /** Present only when it resolves to `true` AND `home` is `deck` (#7740 D1) — never `false`. */
93
99
  deckPresentation?: true;
100
+ /** Present only when it resolves to "bare" on a presenting deck home (#8773) — never "prefixed". */
101
+ deckAnchors?: "bare";
94
102
  /** Present only when it resolves to "light" — never "user" (#7742 D1). */
95
103
  colorMode?: "light";
96
104
  /** Present only when it resolves to "minimal" — never "default" (#7742 D1). */
@@ -64,6 +64,7 @@ const NAV_VALUES = ["full", "mark-only"];
64
64
  const FOOTER_VALUES = ["columns", "centered"];
65
65
  const HOME_VALUES = ["default", "deck"];
66
66
  const INNER_PAGES_VALUES = ["default", "bare"];
67
+ const DECK_ANCHORS_VALUES = ["prefixed", "bare"];
67
68
  const COLOR_MODE_VALUES = ["user", "light"];
68
69
  const CHROME_STYLE_VALUES = ["default", "minimal"];
69
70
  /**
@@ -140,6 +141,12 @@ export function validateTemplateLayout(value, subject) {
140
141
  }
141
142
  result.deckPresentation = raw.deckPresentation;
142
143
  }
144
+ if ("deckAnchors" in raw && raw.deckAnchors !== undefined) {
145
+ if (!DECK_ANCHORS_VALUES.includes(raw.deckAnchors)) {
146
+ throw new TemplateLayoutError(`${subject}: layout.deckAnchors must be one of ${DECK_ANCHORS_VALUES.join(", ")} — got ${JSON.stringify(raw.deckAnchors)}`);
147
+ }
148
+ result.deckAnchors = raw.deckAnchors;
149
+ }
143
150
  if ("colorMode" in raw && raw.colorMode !== undefined) {
144
151
  if (!COLOR_MODE_VALUES.includes(raw.colorMode)) {
145
152
  throw new TemplateLayoutError(`${subject}: layout.colorMode must be one of ${COLOR_MODE_VALUES.join(", ")} — got ${JSON.stringify(raw.colorMode)}`);
@@ -206,6 +213,11 @@ export function resolveSiteChrome(layout, designLayout) {
206
213
  if ((layout?.deckPresentation ?? fromDesign.deckPresentation) === true && resolved.home === "deck") {
207
214
  resolved.deckPresentation = true;
208
215
  }
216
+ // #8773 — only "bare" is ever a key, and only where the deck presents: the
217
+ // slide anchors it names exist only under the presentation dial.
218
+ if ((layout?.deckAnchors ?? fromDesign.deckAnchors) === "bare" && resolved.deckPresentation === true) {
219
+ resolved.deckAnchors = "bare";
220
+ }
209
221
  // #7742 D1 — as with `innerPages`, only the opt-in value is ever a key.
210
222
  if ((layout?.colorMode ?? fromDesign.colorMode) === "light") {
211
223
  resolved.colorMode = "light";
@@ -245,6 +257,8 @@ export function renderSiteChrome(resolved) {
245
257
  // #7740 D1 — `innerPages`' form: the member and bullet always, the value
246
258
  // line only when the chrome presents.
247
259
  const presentationValue = resolved.deckPresentation === true ? "\n deckPresentation: true," : "";
260
+ // #8773 — the same form: member and bullet always, the value line only for "bare".
261
+ const deckAnchorsValue = resolved.deckAnchors === "bare" ? '\n deckAnchors: "bare",' : "";
248
262
  // #7742 D1 — the same form again: member and bullet always, value lines
249
263
  // only for the opt-in values.
250
264
  const colorModeValue = resolved.colorMode === "light" ? '\n colorMode: "light",' : "";
@@ -271,6 +285,7 @@ export function renderSiteChrome(resolved) {
271
285
  // - innerPages: "bare" → every non-home page renders the bare header and
272
286
  // the one-bar footer.
273
287
  // - deckPresentation → the deck home presents slide by slide: fitted slides, slide anchors, the mark follows the ground
288
+ // - deckAnchors: "bare" → the presenting deck names its slides #<id> instead of #slide-<id>
274
289
  // - colorMode: "light" → the site is pinned to light and shows no theme
275
290
  // switch; dark appears only as a deck ground.
276
291
  // - chromeStyle: "minimal" → the deck's closing slide is the wordmark,
@@ -286,6 +301,7 @@ export interface SiteChrome {
286
301
  home: "default" | "deck";
287
302
  innerPages?: "bare";
288
303
  deckPresentation?: true;
304
+ deckAnchors?: "bare";
289
305
  colorMode?: "light";
290
306
  chromeStyle?: "minimal";
291
307
  closingFooterPaths?: readonly string[];${groundsMember}
@@ -295,7 +311,7 @@ export const siteChrome: SiteChrome = {
295
311
  nav: ${JSON.stringify(resolved.nav)},
296
312
  footer: ${JSON.stringify(resolved.footer)},
297
313
  announcementBanner: ${JSON.stringify(resolved.announcementBanner)},
298
- home: ${JSON.stringify(resolved.home)},${groundsValue}${innerPagesValue}${presentationValue}${colorModeValue}${chromeStyleValue}${closingFooterPathsValue}
314
+ home: ${JSON.stringify(resolved.home)},${groundsValue}${innerPagesValue}${presentationValue}${deckAnchorsValue}${colorModeValue}${chromeStyleValue}${closingFooterPathsValue}
299
315
  };
300
316
  `;
301
317
  }
@@ -4,17 +4,17 @@ import { type WorkspaceVersionMap } from "./substitute.js";
4
4
  import { type SignInMode } from "./topology.js";
5
5
  /** The checkout package a spoke's two route files import. */
6
6
  export declare const SPOKE_CHECKOUT_PACKAGE = "@working-theory/spoke-checkout";
7
- /** The site's checkout data — written once, the site's from then on. */
7
+ /** The site's checkout data — written once, the site's from then on (#8748 D2). */
8
8
  export declare const SPOKE_CHECKOUT_CONFIG_PATH = "apps/web/lib/checkout-config.ts";
9
9
  /** The two return pages: Stripe's success URL and every refused buy's target. */
10
10
  export declare const SPOKE_CHECKOUT_PAGE_FILES: readonly string[];
11
11
  /**
12
- * The marker `renderPage()` stamps into both generated return pages' header
13
- * comment (D3). `apps/web`'s structured-data test cannot import create-ailk
14
- * (the import boundary), so it carries this text as a literal, kept in sync
15
- * by `packages/create-ailk/__tests__/spoke-checkout.test.ts` (#8513 AC-5).
12
+ * The standard spoke checkout's template paths, scaffold-relative (#8748 D3).
13
+ * A directory entry covers every file under it, so the tests that sit beside
14
+ * the routes and pages travel with them. `copyTemplate` skips all of them for
15
+ * every scaffold; `writeSpokeCheckoutSlice` copies them into a commerce spoke.
16
16
  */
17
- export declare const SPOKE_CHECKOUT_PAGE_MARKER = "GENERATED by create-ailk (#7838)";
17
+ export declare const SPOKE_CHECKOUT_TEMPLATE_PATHS: readonly string[];
18
18
  /** The variables a commerce spoke depends on (D9). The package reads three of them. */
19
19
  export declare const SPOKE_CHECKOUT_ENV_CATALOG: Readonly<Record<string, EnvCatalogEntry>>;
20
20
  /** The names a commerce spoke declares although only the package reads them (D9). */
@@ -54,29 +54,50 @@ export declare const E2E_ACTION_PINS: Readonly<Record<string, string>>;
54
54
  */
55
55
  export declare const CHECKOUT_E2E_WORKFLOW: string;
56
56
  /**
57
- * The slice for `site`, in order: the two route files, the config, the two
58
- * pages (D3), then the checkout e2e workflow (#7839 D8), which does not
59
- * depend on `site`. With `signIn: "hub"` the billing action is the third
60
- * pair, so the first three paths are `SPOKE_CHECKOUT_ROUTE_FILES` (#8217 D8).
61
- * Throws `TopologyError` for an invalid stamp (SEC-7838-10).
57
+ * The rendered part of the slice for `site` (#8748 D3): with `signIn: "hub"`
58
+ * the billing action (#8217 D8), then the checkout e2e workflow (#7839 D8).
59
+ * Neither depends on `site`. The rest of the slice is template source,
60
+ * copied by `writeSpokeCheckoutSlice`. Throws `TopologyError` for an invalid
61
+ * stamp (SEC-7838-10).
62
62
  */
63
63
  export declare function renderSpokeCheckoutFiles(site: string, opts?: {
64
64
  signIn?: SignInMode | undefined;
65
65
  }): [string, string][];
66
66
  /**
67
- * Write the slice into `targetDir` (D3). A file that already exists is left
68
- * byte-for-byte and reported `skipped` — the config is the site's once
67
+ * Write the slice into `targetDir` (#7838 D3, #8748 D3). The standard's
68
+ * template files (`SPOKE_CHECKOUT_TEMPLATE_PATHS`) are copied from
69
+ * `templatesDir`, each rendered with `copyTemplate`'s own renderer and
70
+ * markers, so a recorded hash equals what `create-ailk update` renders; the
71
+ * config's one `const SITE` line is stamped with `site`. `onWrite` is called
72
+ * once per template file written, as `copyTemplate` calls it. Then come the
73
+ * rendered files (`renderSpokeCheckoutFiles`). A file that already exists is
74
+ * left byte-for-byte and reported `skipped` — the config is the site's once
69
75
  * written. With `signIn: "hub"` (#8217 D8) the slice carries the billing
70
76
  * action, and the web seam — generated, so always rewritten — becomes the
71
- * billing render: reported `patched` when it changed, not at all when equal. Adds the package to `apps/web/package.json` when absent, and
72
- * raises a plain pin older than the one it writes (#7839): the generated
73
- * checkout e2e workflow runs a command a `^0.2.0` pin never reaches. Any
74
- * other range — newer, `workspace:`, a tag — is the site's and is kept.
77
+ * billing render: reported `patched` when it changed, not at all when equal.
78
+ * Adds the package to `apps/web/package.json` when absent, and raises a plain
79
+ * pin older than the one it writes (#7839): the generated checkout e2e
80
+ * workflow runs a command a `^0.2.0` pin never reaches. Any other range —
81
+ * newer, `workspace:`, a tag — is the site's and is kept.
75
82
  * Every refusal (an invalid stamp, a version map without the package, a
76
- * missing or malformed `apps/web/package.json`) throws before the first write.
83
+ * missing or malformed `apps/web/package.json`, a template missing any
84
+ * `SPOKE_CHECKOUT_TEMPLATE_PATHS` entry, a template config without exactly
85
+ * one `const SITE` line, a destination to write that resolves outside
86
+ * `targetDir`) throws before the first write.
77
87
  */
78
88
  export declare function writeSpokeCheckoutSlice(targetDir: string, opts: {
79
89
  site: string;
90
+ projectName: string;
80
91
  versions: WorkspaceVersionMap;
92
+ templatesDir: string;
81
93
  signIn?: SignInMode | undefined;
94
+ onWrite?: (relPath: string, bytes: Uint8Array) => void;
82
95
  }): InstallReportRow[];
96
+ /**
97
+ * Drop the checkout package from a scaffold that does not get the slice — a
98
+ * hub, or a spoke without `commerce` (#8748 D3). The template's
99
+ * `apps/web/package.json` depends on it because the monorepo's `apps/web`
100
+ * carries the standard. Every other key is kept in place; a site without the
101
+ * dependency, or without `apps/web/package.json`, is left as it is.
102
+ */
103
+ export declare function dropSpokeCheckoutDependency(targetDir: string): void;