@homepages/template-kit 0.2.0 → 0.4.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 (165) hide show
  1. package/CHANGELOG.md +222 -0
  2. package/README.md +96 -17
  3. package/dist/asset-modules.d.ts +81 -0
  4. package/dist/base.css +9 -0
  5. package/dist/cli/check/config.js +41 -0
  6. package/dist/cli/check/css.js +131 -0
  7. package/dist/cli/check/diagnostics.js +32 -0
  8. package/dist/cli/check/index.js +87 -0
  9. package/dist/cli/check/loader.js +146 -0
  10. package/dist/cli/check/paths.js +15 -0
  11. package/dist/cli/check/relativize.js +18 -0
  12. package/dist/cli/check/render-invariants.js +24 -0
  13. package/dist/cli/check/resolve-tool.js +38 -0
  14. package/dist/cli/check/section-assets.js +58 -0
  15. package/dist/cli/check/stages/deps.js +337 -0
  16. package/dist/cli/check/stages/lint.js +46 -0
  17. package/dist/cli/check/stages/render.js +101 -0
  18. package/dist/cli/check/stages/size.js +274 -0
  19. package/dist/cli/check/stages/tree.js +206 -0
  20. package/dist/cli/check/stages/typecheck.js +46 -0
  21. package/dist/cli/check/stages/validate/catalog-exhaustiveness.js +18 -0
  22. package/dist/cli/check/stages/validate/facts.js +18 -0
  23. package/dist/cli/check/stages/validate/formatter-contract.js +14 -0
  24. package/dist/cli/check/stages/validate/group-contract.js +21 -0
  25. package/dist/cli/check/stages/validate/index.js +66 -0
  26. package/dist/cli/check/stages/validate/list-scalar-contract.js +22 -0
  27. package/dist/cli/check/stages/validate/nav-contract.js +93 -0
  28. package/dist/cli/check/stages/validate/nested-slot-contract.js +12 -0
  29. package/dist/cli/check/stages/validate/object-list-contract.js +31 -0
  30. package/dist/cli/check/stages/validate/orchestrator.js +241 -0
  31. package/dist/cli/check/stages/validate/produced-by-contract.js +18 -0
  32. package/dist/cli/check/stages/validate/row-contract.js +31 -0
  33. package/dist/cli/check/stages/validate/select-contract.js +13 -0
  34. package/dist/cli/check/stages/validate/source-contract.js +21 -0
  35. package/dist/cli/check/stages/validate/variant-contract.js +15 -0
  36. package/dist/cli/check/workspace.js +86 -0
  37. package/dist/cli/dev/build-css.js +91 -0
  38. package/dist/cli/dev/catalog.js +35 -0
  39. package/dist/cli/dev/compose-template.js +38 -0
  40. package/dist/cli/dev/content-override.js +58 -0
  41. package/dist/cli/dev/discover.js +35 -0
  42. package/dist/cli/dev/fill-state.js +75 -0
  43. package/dist/cli/dev/index.js +32 -0
  44. package/dist/cli/dev/inspect.js +38 -0
  45. package/dist/cli/dev/island-bootstrap.js +38 -0
  46. package/dist/cli/dev/island-map.js +35 -0
  47. package/dist/cli/dev/island-transform.js +31 -0
  48. package/dist/cli/dev/manifest-instances.js +31 -0
  49. package/dist/cli/dev/render-section.js +24 -0
  50. package/dist/cli/dev/screenshot-target.js +24 -0
  51. package/dist/cli/dev/section-page.js +54 -0
  52. package/dist/cli/dev/server.js +480 -0
  53. package/dist/cli/dev/slot-schema.js +12 -0
  54. package/dist/cli/dev/structure-summary.js +118 -0
  55. package/dist/cli/dev/tailwind.js +32 -0
  56. package/dist/cli/dev/vite-server.js +33 -0
  57. package/dist/cli/dev/workspace.js +63 -0
  58. package/dist/cli/link/index.js +74 -0
  59. package/dist/cli/link/overlay.js +59 -0
  60. package/dist/cli/link/watch.js +25 -0
  61. package/dist/cli/new/emit.js +48 -0
  62. package/dist/cli/new/index.js +69 -0
  63. package/dist/cli/new/scaffold/section/_Renderer.tsx +20 -0
  64. package/dist/cli/new/scaffold/section/_fill-spec.ts +18 -0
  65. package/dist/cli/new/scaffold/section/_fixtures.ts +13 -0
  66. package/dist/cli/new/scaffold/section/_schema.ts +24 -0
  67. package/dist/cli/new/scaffold/template/_manifest.json +9 -0
  68. package/dist/cli/new/scaffold/template/sections/hero/ExpandableText.tsx +45 -0
  69. package/dist/cli/new/scaffold/template/sections/hero/Renderer.tsx +40 -0
  70. package/dist/cli/new/scaffold/template/sections/hero/components/Feature.tsx +14 -0
  71. package/dist/cli/new/scaffold/template/sections/hero/fill-spec.ts +29 -0
  72. package/dist/cli/new/scaffold/template/sections/hero/fixtures.ts +41 -0
  73. package/dist/cli/new/scaffold/template/sections/hero/schema.ts +60 -0
  74. package/dist/cli/new/scaffold/template/theme.css +24 -0
  75. package/dist/cli/new/scaffold/template/theme.ts +27 -0
  76. package/dist/cli/new/scaffold-assets.js +20 -0
  77. package/dist/cli/pack/collect.js +44 -0
  78. package/dist/cli/pack/guards.js +57 -0
  79. package/dist/cli/pack/index.js +66 -0
  80. package/dist/cli/pack/manifest.js +15 -0
  81. package/dist/cli/pack/zip.js +86 -0
  82. package/dist/cli/theme/generate.js +43 -0
  83. package/dist/cli/theme/index.js +33 -0
  84. package/dist/cli/theme/load-theme.js +69 -0
  85. package/dist/cli.js +100 -16
  86. package/dist/contracts/fill-treatments.js +47 -0
  87. package/dist/design-system/theme.d.ts +30 -300
  88. package/dist/design-system/theme.js +112 -90
  89. package/dist/dev-client/assets/index-DfEfrp6P.css +1 -0
  90. package/dist/dev-client/assets/index-bpMP0b0W.js +50 -0
  91. package/dist/dev-client/index.html +13 -0
  92. package/dist/eslint/is-client-file.d.ts +4 -0
  93. package/dist/eslint/rules/no-hex.js +78 -6
  94. package/dist/eslint.js +1 -1
  95. package/dist/index.d.ts +3 -2
  96. package/dist/index.js +2 -2
  97. package/dist/islands/detect.d.ts +16 -0
  98. package/dist/islands/detect.js +22 -0
  99. package/dist/islands/discover.d.ts +5 -0
  100. package/dist/islands/discover.js +19 -0
  101. package/dist/islands/esbuild-plugin.d.ts +13 -0
  102. package/dist/islands/esbuild-plugin.js +39 -0
  103. package/dist/islands/wrap.d.ts +11 -0
  104. package/dist/islands/wrap.js +48 -0
  105. package/dist/node_modules/@jridgewell/sourcemap-codec/dist/sourcemap-codec.js +77 -0
  106. package/dist/node_modules/magic-string/dist/magic-string.es.js +939 -0
  107. package/dist/package.js +1 -1
  108. package/dist/primitives/Image.js +1 -1
  109. package/dist/schema/fill-spec.d.ts +80 -590
  110. package/dist/schema/fixture-schema.d.ts +5 -81
  111. package/dist/schema/manifest.d.ts +39 -475
  112. package/dist/schema/resolve-section-ref.js +10 -0
  113. package/dist/schema/rows.js +10 -0
  114. package/dist/schema/section-nav.js +9 -0
  115. package/dist/schema/section-schema.d.ts +51 -437
  116. package/dist/schema/slot-types.d.ts +12 -16
  117. package/dist/ssr.d.ts +31 -0
  118. package/dist/ssr.js +46 -0
  119. package/dist/styles.css +31 -88
  120. package/docs/INDEX.md +12 -3
  121. package/docs/assets.md +128 -0
  122. package/docs/check.md +124 -0
  123. package/docs/dev.md +187 -0
  124. package/docs/eslint.md +18 -8
  125. package/docs/islands.md +5 -2
  126. package/docs/llms.txt +59 -9
  127. package/docs/new.md +27 -0
  128. package/docs/overview.md +57 -0
  129. package/docs/pack.md +46 -0
  130. package/docs/primitives.md +10 -6
  131. package/docs/quickstart.md +102 -0
  132. package/docs/recipes/INDEX.md +27 -0
  133. package/docs/recipes/bind-property-fact.md +58 -0
  134. package/docs/recipes/collection-slot.md +100 -0
  135. package/docs/recipes/fill-spec-decision.md +66 -0
  136. package/docs/recipes/fixture-states.md +68 -0
  137. package/docs/recipes/image-slot-crop.md +98 -0
  138. package/docs/recipes/interactive-island.md +100 -0
  139. package/docs/recipes/organize-section-folder.md +72 -0
  140. package/docs/recipes/prepare-submission.md +55 -0
  141. package/docs/recipes/second-template.md +49 -0
  142. package/docs/recipes/select-slot.md +59 -0
  143. package/docs/recipes/static-asset.md +103 -0
  144. package/docs/recipes/third-party-package.md +90 -0
  145. package/docs/rules/INDEX.md +14 -5
  146. package/docs/rules/bundle-binary-asset.md +83 -0
  147. package/docs/rules/bundle-incomplete.md +2 -2
  148. package/docs/rules/css-reason.md +3 -3
  149. package/docs/rules/fixtures-invalid.md +96 -0
  150. package/docs/rules/license-denied.md +11 -3
  151. package/docs/rules/manifest-invalid.md +99 -0
  152. package/docs/rules/no-bare-css-import.md +8 -8
  153. package/docs/rules/no-css-import-from-render-path.md +7 -8
  154. package/docs/rules/no-hex.md +76 -10
  155. package/docs/rules/no-templates.md +87 -0
  156. package/docs/rules/parse-error.md +76 -0
  157. package/docs/rules/schema-invalid.md +96 -0
  158. package/docs/rules/size-assets.md +88 -0
  159. package/docs/rules/size-island-bundle.md +123 -0
  160. package/docs/rules/size-section-css.md +19 -14
  161. package/docs/schema-system.md +28 -28
  162. package/docs/theme-and-css.md +157 -92
  163. package/docs/vocabulary.md +98 -0
  164. package/package.json +14 -11
  165. package/tsconfig.json +1 -1
@@ -0,0 +1,99 @@
1
+ ---
2
+ purpose: The template-kit/manifest-invalid authoring rule — what it enforces, why, and how to fix it.
3
+ status: living
4
+ related: [INDEX.md, ../schema-system.md, schema-invalid.md, no-templates.md]
5
+ updated: 2026-07-14
6
+ ---
7
+ # template-kit/manifest-invalid
8
+
9
+ > `manifest.json` is the one file that says which sections make a page, and in what
10
+ > order. Every check on it lands under this id.
11
+
12
+ ## Rule
13
+
14
+ A template folder (`templates/<key>/`) must have a `manifest.json` at its root, and that
15
+ file must:
16
+
17
+ - **exist** — a template folder with none at all reports this once, naming the missing
18
+ file;
19
+ - **parse as JSON, and satisfy the manifest shape** — `key`, `name`, `deliverable_type`,
20
+ and an ordered `sections` array of section instances (`section`, `instance_id`,
21
+ `options`, `locked`, `required`, `reorderable`);
22
+ - **agree with its own folder** — the manifest's `key` must match the template folder
23
+ name it lives in;
24
+ - **reference real sections** — every `sections[].section` must resolve to a section this
25
+ workspace actually has;
26
+ - **override only what's real and overridable** — an instance's `options` may only name an
27
+ option the target section declares, only when that option is not
28
+ `template_overridable: false`, and only with a value that actually differs from the
29
+ section's own default;
30
+ - **satisfy the whole-composition nav contract** — the cross-section rules that need the
31
+ full, resolved section set rather than any one section in isolation.
32
+
33
+ Every violation collects into the same run; a manifest with several wrong references
34
+ reports all of them together.
35
+
36
+ ## Reason
37
+
38
+ A section's own four files describe that section in isolation — they say nothing about
39
+ which page it appears on, in what order, or which of its options a specific template has
40
+ chosen to lock in. `manifest.json` is where that composition is declared, and it is the
41
+ only place a downstream consumer (the publisher, the fill pipeline, the editor) can go to
42
+ learn a template's actual shape. A manifest that references a section that doesn't exist,
43
+ or restates a default as if it were an override, is a template whose real composition
44
+ nobody can trust the manifest to describe.
45
+
46
+ ## Fix
47
+
48
+ Read the message: it names the template, the section instance, and what's wrong.
49
+ Missing entirely, add one. Otherwise, fix the field the message points at.
50
+
51
+ ### Before
52
+
53
+ ```text
54
+ templates/acme-modern/
55
+ manifest.json
56
+ sections/
57
+ hero/
58
+ ```
59
+
60
+ ```json
61
+ // templates/acme-modern/manifest.json
62
+ {
63
+ "key": "acme-classic",
64
+ "name": "Acme Classic",
65
+ "deliverable_type": "single_property_website",
66
+ "sections": [
67
+ { "section": "hero", "instance_id": "hero-1", "options": {}, "locked": [], "required": true, "reorderable": false }
68
+ ],
69
+ "reconcile": []
70
+ }
71
+ ```
72
+
73
+ ```text
74
+ Template "acme-modern": manifest key "acme-classic" does not match the template folder name "acme-modern".
75
+ ```
76
+
77
+ ### After
78
+
79
+ ```json
80
+ // templates/acme-modern/manifest.json
81
+ {
82
+ "key": "acme-modern",
83
+ "name": "Acme Modern",
84
+ "deliverable_type": "single_property_website",
85
+ "sections": [
86
+ { "section": "hero", "instance_id": "hero-1", "options": {}, "locked": [], "required": true, "reorderable": false }
87
+ ],
88
+ "reconcile": []
89
+ }
90
+ ```
91
+
92
+ ## See also
93
+
94
+ - [The schema system](../schema-system.md#manifestjson--composing-sections-into-a-template) —
95
+ what a manifest declares and the type it's validated against.
96
+ - [`schema-invalid`](schema-invalid.md) — the same kind of cross-reference check, one
97
+ level down, on a single section's own contract files.
98
+ - [`no-templates`](no-templates.md) — reported instead when the workspace has no
99
+ `templates/<key>/` folder to check at all.
@@ -2,7 +2,7 @@
2
2
  purpose: The template-kit/no-bare-css-import authoring rule — what it enforces, why, and how to fix it.
3
3
  status: living
4
4
  related: [INDEX.md, ../theme-and-css.md]
5
- updated: 2026-07-14
5
+ updated: 2026-07-15
6
6
  ---
7
7
  # template-kit/no-bare-css-import
8
8
 
@@ -59,11 +59,11 @@ in the whole kit: the only signal is that the styles are not there.
59
59
  Delete the `@import`.
60
60
 
61
61
  **A package's stylesheet is reached from the code that needs it**, not from CSS. Write
62
- `import "swiper/css";` in the component, island, or enhancer that renders the thing — the
63
- CSS build bundles your `Renderer.tsx` and every marked enhancer, transitively, precisely to
64
- harvest those imports, and it compiles what they pull in into **this section's cascade
65
- layer**. Package CSS goes in first and your own hand-CSS second, so at equal specificity
66
- your rules win.
62
+ `import "swiper/css";` in the component or island that renders the thing — the CSS build
63
+ bundles your `Renderer.tsx` and everything it imports, transitively, precisely to harvest
64
+ those imports, and it compiles what they pull in into **this section's cascade layer**.
65
+ Package CSS goes in first and your own hand-CSS second, so at equal specificity your rules
66
+ win.
67
67
 
68
68
  Two things authors get wrong immediately after:
69
69
 
@@ -79,7 +79,7 @@ Two things authors get wrong immediately after:
79
79
 
80
80
  ```css
81
81
  /* sections/gallery/styles.css */
82
- /* css-reason: styles the slide DOM the Swiper enhancer builds at runtime. */
82
+ /* css-reason: styles the slide DOM the Swiper carousel builds at runtime. */
83
83
  @import "swiper/css"; /* template-kit/no-bare-css-import — silently dropped */
84
84
 
85
85
  .tr-gallery .swiper-pagination-bullet-active {
@@ -95,7 +95,7 @@ The `@import` is gone from the stylesheet:
95
95
 
96
96
  ```css
97
97
  /* sections/gallery/styles.css */
98
- /* css-reason: overrides the Swiper pagination DOM the enhancer builds at runtime —
98
+ /* css-reason: overrides the Swiper pagination DOM the carousel builds at runtime —
99
99
  those nodes carry the library's own classes, which Tailwind's scanner never sees. */
100
100
  .tr-gallery .swiper-pagination-bullet-active {
101
101
  background: var(--tr-color-primary);
@@ -2,7 +2,7 @@
2
2
  purpose: The template-kit/no-css-import-from-render-path authoring rule — what it enforces, why, and how to fix it.
3
3
  status: living
4
4
  related: [INDEX.md, ../theme-and-css.md]
5
- updated: 2026-07-14
5
+ updated: 2026-07-15
6
6
  ---
7
7
  # template-kit/no-css-import-from-render-path
8
8
 
@@ -31,8 +31,8 @@ server-rendered inline before it is hydrated.
31
31
  ## Reason
32
32
 
33
33
  The section's CSS build works by **bundling the render path itself** — `Renderer.tsx` and
34
- any enhancer that carries `export const enhancer` — and harvesting the CSS its imports
35
- pull in. A bare specifier resolves into `node_modules`, which is exactly what that build
34
+ every module it imports, components and islands alike — and harvesting the CSS those
35
+ imports pull in. A bare specifier resolves into `node_modules`, which is exactly what that build
36
36
  looks for: `import "swiper/css";` in `Renderer.tsx` is not a mistake, it is the mechanism.
37
37
 
38
38
  A **local** stylesheet is a different problem. Every `.css` file that already lives in the
@@ -47,11 +47,10 @@ Delete the import. A local stylesheet already ships — the build picks up every
47
47
  in the section folder on its own; importing it again only makes it double-emit.
48
48
 
49
49
  A package's stylesheet is different: import it **by its bare specifier**, from the module
50
- that needs it. The build bundles `Renderer.tsx` and every marked enhancer's import graph
51
- **transitively**, so a bare specifier is equally legal in the Renderer itself, in a marked
52
- enhancer, or in any component either one imports — not only at the top of the tree. That
53
- import is how the build finds the stylesheet and routes it into the section's cascade
54
- layer.
50
+ that needs it. The build bundles `Renderer.tsx`'s import graph **transitively**, so a bare
51
+ specifier is equally legal in the Renderer itself or in any component or island it
52
+ imports — not only at the top of the tree. That import is how the build finds the
53
+ stylesheet and routes it into the section's cascade layer.
55
54
 
56
55
  ### Before
57
56
 
@@ -2,7 +2,7 @@
2
2
  purpose: The template-kit/no-hex authoring rule — what it enforces, why, and how to fix it.
3
3
  status: living
4
4
  related: [INDEX.md, server-vs-client.md, ../theme-and-css.md]
5
- updated: 2026-07-14
5
+ updated: 2026-07-15
6
6
  ---
7
7
  # template-kit/no-hex
8
8
 
@@ -11,11 +11,20 @@ updated: 2026-07-14
11
11
 
12
12
  ## Rule
13
13
 
14
- Two shapes are banned:
14
+ This rule ships as a **warning**, not an error: a section spending theme tokens is
15
+ recommended, not gated. A hard-coded colour squiggles in your editor, but it does not
16
+ fail `template-kit check` (the check gates on errors only, so a warning never reaches it)
17
+ and so does not block ingest either. The guidance below is still the right thing to do —
18
+ it only changes what the linter does with a violation, not whether the violation is
19
+ worth fixing.
15
20
 
16
- - **A CSS colour function**, anywhere in a string or template literal:
17
- `oklch(…)`, `oklab(…)`, `rgb(…)`, `rgba(…)`, `hsl(…)`, `hsla(…)`, `color-mix(…)`.
18
- No context test nobody spells an anchor id `rgb(...)`.
21
+ The rule flags two shapes:
22
+
23
+ - **A CSS colour function that bakes a colour**, anywhere in a string or template
24
+ literal: `oklch(…)`, `oklab(…)`, `rgb(…)`, `rgba(…)`, `hsl(…)`, `hsla(…)`,
25
+ `color-mix(…)`. No context test — nobody spells an anchor id `rgb(...)`. **One
26
+ carve-out:** a `color-mix()` whose colour arguments are all theme-derived is legal —
27
+ see below.
19
28
  - **A hex literal** (3, 4, 6, or 8 digits) **in a colour context**. A colour context is
20
29
  a Tailwind arbitrary-value bracket that resolves to a colour:
21
30
  - a colour property — `[color:#fff]`, `[background:#fff]`, `[background-color:#fff]`,
@@ -35,6 +44,40 @@ anchor ids that happen to be spelled from hex digits.
35
44
  Every hex-shaped token in a string is checked, not just the first — an out-of-context
36
45
  token early in a `className` cannot shadow a genuine colour later in it.
37
46
 
47
+ ### The `color-mix()` carve-out
48
+
49
+ A `color-mix()` carries **no palette of its own** when every one of its colour arguments
50
+ is theme-derived — a `var(--…)` reference, `transparent`, `currentColor`, or a nested
51
+ `color-mix()` judged on these same terms. It composes a colour out of the theme the
52
+ section was handed, so it is as theme-agnostic as `bg-surface` and cannot break when
53
+ another template reuses the section. It is legal:
54
+
55
+ ```
56
+ hover:bg-[color-mix(in_srgb,var(--tr-color-surface)_92%,var(--tr-color-ink))]
57
+ bg-[color-mix(in_srgb,var(--tr-color-ink)_50%,transparent)] ← a translucent tint:
58
+ overlay, scrim, hover wash
59
+ ```
60
+
61
+ A mix weight (`92%`) is not a colour; only the colour arguments are tested.
62
+
63
+ `transparent` and `currentColor` are the only palette-free keywords: `transparent` names
64
+ the *absence* of a colour, and `currentColor` defers to the inherited (theme-driven)
65
+ `color`. Every other CSS named colour (`white`, `black`, `rebeccapurple`, …) is a baked
66
+ palette exactly as a hex is. So these report:
67
+
68
+ ```
69
+ color-mix(in srgb, var(--tr-color-primary) 80%, #fff) ← a hex literal
70
+ color-mix(in srgb, var(--tr-color-primary) 80%, white) ← a CSS named colour
71
+ color-mix(in srgb, var(--tr-color-accent, #fff) 80%, …) ← a var FALLBACK is a baked
72
+ colour the moment the
73
+ property is undefined
74
+ ```
75
+
76
+ The carve-out is `color-mix()` **only** — it is the one colour function that *composes* a
77
+ colour rather than stating one. The other six take raw channel values, so they bake a
78
+ colour by construction: `rgb(0 0 0)`, `oklch(0.7 0.1 20)`, and even
79
+ `rgb(var(--r) var(--g) var(--b))` report.
80
+
38
81
  ## Reason
39
82
 
40
83
  A section carries **zero palette of its own**. It is theme-agnostic, and must look
@@ -49,10 +92,14 @@ not a palette boundary.
49
92
 
50
93
  ## Fix
51
94
 
52
- Use a semantic token utility (`bg-surface-alt`, `text-ink`). If the value is genuinely
53
- brand decoration with no token, add it to the **template's** `theme.ts` palette and use
54
- the `--tr-color-<name>` utility it compiles to — which keeps the value in one place
55
- instead of in one section.
95
+ Use one of your theme's colour utilities. If the value is genuinely brand decoration with
96
+ no token yet, add it to `colors` in the **template's** `theme.ts` the key `lagoon`
97
+ compiles to `--tr-color-lagoon` and the `bg-lagoon` / `text-lagoon` utilities — which
98
+ keeps the value in one place instead of in one section.
99
+
100
+ If you need a colour the theme does not name — a hover, active, or pressed step — derive
101
+ it from the tokens the theme *does* name with `color-mix()`, rather than reaching for a
102
+ literal to lighten or darken with.
56
103
 
57
104
  ### Before
58
105
 
@@ -90,8 +137,27 @@ export function Renderer({ slots }: Props) {
90
137
  }
91
138
  ```
92
139
 
140
+ ### A state colour, derived
141
+
142
+ Lightening a hover step by mixing in a hard-coded white is both the violation and the
143
+ bug: under a dark theme it lightens a button the theme wanted dark. Mix toward a colour
144
+ the **theme** names, and the step follows whatever theme the section is handed.
145
+
146
+ ```tsx
147
+ // Before — `#fff` is a palette this section invented.
148
+ <button className="bg-primary hover:bg-[color-mix(in_srgb,var(--tr-color-primary)_80%,#fff)]">
149
+ {slots.cta}
150
+ </button>
151
+
152
+ // After — every colour argument is a theme var.
153
+ <button className="bg-primary hover:bg-[color-mix(in_srgb,var(--tr-color-primary)_80%,var(--tr-color-surface))]">
154
+ {slots.cta}
155
+ </button>
156
+ ```
157
+
93
158
  ## See also
94
159
 
95
- - [Theme and CSS](../theme-and-css.md#color-roles) — the token system and the colour roles.
160
+ - [Theme and CSS](../theme-and-css.md#tokentheme--your-templates-design-system) — the
161
+ token system, and how a `colors` entry becomes a utility.
96
162
  - [Server vs client](server-vs-client.md) — why the other bans stop at the hydration
97
163
  boundary and this one does not.
@@ -0,0 +1,87 @@
1
+ ---
2
+ purpose: The template-kit/no-templates authoring rule — what it enforces, why, and how to fix it.
3
+ status: living
4
+ related: [INDEX.md, manifest-invalid.md]
5
+ updated: 2026-07-14
6
+ ---
7
+ # template-kit/no-templates
8
+
9
+ > A run that gates nothing must never report success. This is what a workspace with no
10
+ > template to check reports instead of a silent pass.
11
+
12
+ ## Rule
13
+
14
+ `templates/` must hold at least one template folder — for the run you asked for:
15
+
16
+ - `template-kit check` (no argument, from the workspace root): `templates/` has no
17
+ folder in it at all.
18
+ - `template-kit check --all`: same — no template folder exists to check.
19
+ - `template-kit check <name>` (or a bare `check` run from inside a template's own
20
+ folder): that one named template does not exist. (A folder that exists but is
21
+ missing its `manifest.json` is a different, more specific failure —
22
+ [`manifest-invalid`](manifest-invalid.md) — because a folder under `templates/` is a
23
+ template whatever else is wrong with it.)
24
+
25
+ This is checked once per run, scoped to the workspace, not to any one template.
26
+
27
+ ## Reason
28
+
29
+ `every()` over an empty set is vacuously true. A `check` run that selected zero templates
30
+ and reported "check passed" would exit 0 having run not one gate — indistinguishable from
31
+ a workspace that passed every rule, when in truth no rule ran at all. Anything that
32
+ re-runs this command on your behalf (an ingest pipeline, a CI step) would accept that
33
+ silence as a clean submission.
34
+
35
+ Reporting a diagnostic instead means "nothing to check" is a failure your run surfaces,
36
+ not a false green light.
37
+
38
+ ## Fix
39
+
40
+ Add a template: `templates/<key>/` with a `manifest.json` declaring its section
41
+ composition, and a `sections/` folder holding the sections it composes. If you meant to
42
+ check a specific template and named it wrong, or ran `check` from a folder that isn't a
43
+ template folder, correct the name or your working directory instead.
44
+
45
+ ### Before
46
+
47
+ ```text
48
+ my-workspace/
49
+ package.json
50
+ package-lock.json
51
+ templates/ ← empty
52
+ ```
53
+
54
+ ```text
55
+ $ template-kit check
56
+ ✗ workspace — 1 problem(s)
57
+ [template-kit/no-templates] This workspace has no template to check: templates/ holds no template folder.
58
+ fix: Add a template: templates/<key>/ with a manifest.json declaring its section composition and a sections/ folder holding the sections it composes.
59
+ ```
60
+
61
+ ### After
62
+
63
+ ```text
64
+ my-workspace/
65
+ package.json
66
+ package-lock.json
67
+ templates/
68
+ acme-modern/
69
+ manifest.json
70
+ sections/
71
+ hero/
72
+ Renderer.tsx
73
+ schema.ts
74
+ fill-spec.ts
75
+ fixtures.ts
76
+ ```
77
+
78
+ ```text
79
+ $ template-kit check
80
+ ✓ acme-modern
81
+ check passed
82
+ ```
83
+
84
+ ## See also
85
+
86
+ - [`manifest-invalid`](manifest-invalid.md) — the check that takes over once a template
87
+ folder exists, on whether its `manifest.json` is present and correct.
@@ -0,0 +1,76 @@
1
+ ---
2
+ purpose: The template-kit/parse-error authoring rule — what it enforces, why, and how to fix it.
3
+ status: living
4
+ related: [INDEX.md, typecheck.md]
5
+ updated: 2026-07-14
6
+ ---
7
+ # template-kit/parse-error
8
+
9
+ > A file so broken it never became an AST. Every other rule needs one; this is what's
10
+ > reported when there isn't one to have an opinion about.
11
+
12
+ ## Rule
13
+
14
+ Every other lint rule runs against a parsed file — an AST it can walk and ask questions
15
+ of. When a `.ts`/`.tsx` file cannot even be parsed (invalid syntax: an unclosed brace, a
16
+ stray token, a JSX tag that never closes), there is no AST for any rule to run against,
17
+ and no rule id of its own to report. This id is what's reported in that one case instead:
18
+ the parser's own message, relocated to the file and line it names.
19
+
20
+ This is the only diagnostic under this id — it is not a rule you can violate by writing
21
+ working code a particular way, the way `no-hex` or `no-inline-style` are. It exists so
22
+ that "the file didn't parse" is a rule-id and a fix hint, not silence or a raw crash.
23
+
24
+ ## Reason
25
+
26
+ A stage never aborts the run — an author fixing one file's problem must see every other
27
+ file's problems in the same run, not discover them one release at a time. A parser
28
+ exception is the one case where that promise is hardest to keep: there is genuinely
29
+ nothing else to check about a file that doesn't parse. Reporting it under its own id,
30
+ rather than dropping it or letting it throw, keeps that promise intact.
31
+
32
+ You may see the **same file** flagged here and under
33
+ [`typecheck`](typecheck.md): the lint stage and the typecheck stage each parse the
34
+ workspace independently, and a syntax error fails both parsers, so both stages report it,
35
+ each in their own words. Fixing the syntax error clears both.
36
+
37
+ ## Fix
38
+
39
+ Read the message — it is the parser's own error, naming the file and the line. Fix the
40
+ syntax at that location.
41
+
42
+ ### Before
43
+
44
+ ```tsx
45
+ // sections/hero/Renderer.tsx
46
+ export function Renderer() {
47
+ return (
48
+ <Section>
49
+ <h1>{headline}</h1>
50
+ </Section
51
+ );
52
+ }
53
+ ```
54
+
55
+ ```text
56
+ sections/hero/Renderer.tsx:6
57
+ Parsing error: '>' expected.
58
+ ```
59
+
60
+ ### After
61
+
62
+ ```tsx
63
+ // sections/hero/Renderer.tsx
64
+ export function Renderer() {
65
+ return (
66
+ <Section>
67
+ <h1>{headline}</h1>
68
+ </Section>
69
+ );
70
+ }
71
+ ```
72
+
73
+ ## See also
74
+
75
+ - [`typecheck`](typecheck.md) — the sibling failure one layer up: a file that parses but
76
+ does not type-check.
@@ -0,0 +1,96 @@
1
+ ---
2
+ purpose: The template-kit/schema-invalid authoring rule — what it enforces, why, and how to fix it.
3
+ status: living
4
+ related: [INDEX.md, ../schema-system.md, fixtures-invalid.md, manifest-invalid.md]
5
+ updated: 2026-07-14
6
+ ---
7
+ # template-kit/schema-invalid
8
+
9
+ > `schema.ts` and `fill-spec.ts` are a matched pair. This is every check that makes sure
10
+ > they actually agree.
11
+
12
+ ## Rule
13
+
14
+ A section's `schema.ts` and `fill-spec.ts` must, together:
15
+
16
+ - **parse** — each is `as const satisfies` its shape (`SectionSchemaShape`,
17
+ `FillSpecShape`); a value that doesn't satisfy it fails here with the parse error;
18
+ - **cross-reference correctly** — every `fill-spec.ts` decision produces a slot that
19
+ actually exists in `schema.ts`, every `reads` entry names a real, author-declarable
20
+ facts key, every option's `affects_decisions`/`affects_slots` names a real decision or
21
+ slot;
22
+ - **satisfy the closed per-type contracts** — an `object_list` slot's `item_order`
23
+ covers every field, a `select` slot's cases match its transform, a group's members are
24
+ slots that exist, and the rest of the per-slot-type rules the schema system documents.
25
+
26
+ One violation is one diagnostic; a section with several wrong things reports all of them
27
+ in the same run, not one per fix.
28
+
29
+ This is the id you also see when a section **cannot be built or parsed at all** — a
30
+ syntax error in `schema.ts`, a missing export, an import that doesn't resolve. The
31
+ message in that case is the underlying build error, relocated to the file it points at.
32
+
33
+ ## Reason
34
+
35
+ `fill-spec.ts` is not free-standing — every decision it declares exists to fill a slot
36
+ `schema.ts` declared, and every fact it reads must be one the platform actually exposes.
37
+ A decision that produces a slot no schema declares, or reads a fact that doesn't exist,
38
+ is not a style problem: the fill pipeline would either drop the output on the floor or
39
+ crash trying to read a fact key that isn't there. Catching the mismatch here, at author
40
+ time, is the difference between a red check and a listing that silently fails to fill.
41
+
42
+ ## Fix
43
+
44
+ Read the message: it names the section, the decision or slot in question, and what's
45
+ wrong with it. Fix whichever side is actually stale — the schema, or the fill-spec.
46
+
47
+ ### Before
48
+
49
+ ```ts
50
+ // sections/facts/schema.ts
51
+ export const schema = {
52
+ meta: { displayName: "Facts" },
53
+ slots: {
54
+ year_built: { type: "text", required: false, produced_by: "year_built_text" },
55
+ },
56
+ options: {},
57
+ } as const satisfies SectionSchemaShape;
58
+ ```
59
+
60
+ ```ts
61
+ // sections/facts/fill-spec.ts
62
+ export const fillSpec = {
63
+ reads: [],
64
+ decisions: [
65
+ { id: "year_built_text", type: "text-block", produces: ["year_built_summary"], length_cap: 20 },
66
+ // ^ typo — schema.ts declares "year_built"
67
+ ],
68
+ } as const satisfies FillSpecShape;
69
+ ```
70
+
71
+ ```text
72
+ Section "facts": fill-spec decision "year_built_text" produces unknown slot "year_built_summary".
73
+ ```
74
+
75
+ ### After
76
+
77
+ ```ts
78
+ // sections/facts/fill-spec.ts
79
+ export const fillSpec = {
80
+ reads: [],
81
+ decisions: [
82
+ { id: "year_built_text", type: "text-block", produces: ["year_built"], length_cap: 20 },
83
+ ],
84
+ } as const satisfies FillSpecShape;
85
+ ```
86
+
87
+ ## See also
88
+
89
+ - [The schema system](../schema-system.md) — the full authoring contract for `schema.ts`
90
+ and `fill-spec.ts`.
91
+ - [`fixtures-invalid`](fixtures-invalid.md) — the sibling gate on the third contract
92
+ file, `fixtures.ts`.
93
+ - [`manifest-invalid`](manifest-invalid.md) — the same kind of cross-reference check, one
94
+ level up, on a template composing several sections.
95
+ - [`bundle-incomplete`](bundle-incomplete.md) — reported instead of this id when a
96
+ contract file is missing outright, rather than present and wrong.
@@ -0,0 +1,88 @@
1
+ ---
2
+ purpose: The template-kit/size-assets authoring rule — what it enforces, why, and how to fix it.
3
+ status: living
4
+ related: [INDEX.md, size-section-css.md, ../theme-and-css.md]
5
+ updated: 2026-07-15
6
+ ---
7
+ # template-kit/size-assets
8
+
9
+ > A section's static assets — the svg, images, and fonts it imports or `url()`-references —
10
+ > must total **≤ 250 KB raw.** Raw, not gzip: images and fonts are already compressed
11
+ > formats, so gzipping them again buys nothing.
12
+
13
+ ## Rule
14
+
15
+ A section's static assets are budgeted by **total raw bytes, 250 KB or less**. What is
16
+ measured is every asset the published section actually ships:
17
+
18
+ - an svg, image, or font `import`ed directly in `Renderer.tsx` (or an island), and
19
+ - an svg, image, or font `url()`-referenced from the section's own CSS or from a
20
+ package stylesheet its module graph pulls in — including a webfont `@font-face`s.
21
+
22
+ The same asset referenced twice (from both a stylesheet and a component, say) counts
23
+ once, not twice.
24
+
25
+ The budget is **visible in `template-kit check`'s configuration and is not
26
+ author-overridable**. A workspace cannot raise it, and there is no per-section exemption.
27
+
28
+ ## Reason
29
+
30
+ An svg icon or a product photo a section imports, and a webfont a package's stylesheet
31
+ pulls in, both ship to the same page load as the section's own code and CSS — they are
32
+ not free just because they never appear in a byte-budget an author is used to watching.
33
+
34
+ Fonts are the sharpest case. A `url()`-referenced webfont used to inline as part of the
35
+ compiled stylesheet, which made a large font look like a CSS problem: trimming your own
36
+ rules never fixed it, because the bytes were never your CSS to begin with. This rule
37
+ separates that weight into its own budget — a font, an icon, or an image is measured as
38
+ what it is, and the stylesheet budget ([`size-section-css`](size-section-css.md)) is left
39
+ to measure only what it actually contains.
40
+
41
+ ## Fix
42
+
43
+ Two moves, in order of how often they work:
44
+
45
+ 1. **Shrink or subset the asset.** A full variable font family, shipped for three
46
+ weights actually used, is the usual cause — subset it to the characters and weights
47
+ the section renders. An unoptimized PNG is the other common case — recompress it, or
48
+ switch to a format built for the content (`webp`/`avif` for photos, `svg` for icons).
49
+ 2. **Drop assets the section does not use.** A copied-in icon set or a leftover preview
50
+ image that never renders still ships its bytes — delete what the section does not
51
+ reference.
52
+
53
+ ### Before
54
+
55
+ ```tsx
56
+ // sections/hero/Renderer.tsx
57
+ import "brandfont/css"; // ← ships the package's full variable font family
58
+
59
+ export default function Renderer() {
60
+ return <h1 style={{ fontFamily: "BrandFont" }}>Welcome home</h1>;
61
+ }
62
+ ```
63
+
64
+ ### After
65
+
66
+ ```tsx
67
+ // sections/hero/Renderer.tsx
68
+ import heroIcon from "./assets/hero-icon.svg"; // ← a small, single-purpose asset
69
+
70
+ export default function Renderer() {
71
+ // Uses a font the template's theme already declares (`--tr-font-<name>`),
72
+ // which costs nothing because it is already loaded — no package font import.
73
+ return (
74
+ <h1 className="tr-font-display">
75
+ <img src={heroIcon} alt="" /> Welcome home
76
+ </h1>
77
+ );
78
+ }
79
+ ```
80
+
81
+ ## See also
82
+
83
+ - [`size-section-css`](size-section-css.md) — the stylesheet budget this rule was split
84
+ out of; a `url()`-referenced font now counts here, not there.
85
+ - [`size-renderer-bundle`](size-renderer-bundle.md) — the same idea applied to the
86
+ section's JavaScript.
87
+ - [Theme and CSS](../theme-and-css.md) — the fonts and tokens the page already loads,
88
+ which cost you nothing to reuse.