@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,49 @@
1
+ ---
2
+ purpose: Add another template to the workspace.
3
+ status: living
4
+ related: [../new.md, ../check.md]
5
+ updated: 2026-07-15
6
+ ---
7
+ # Add a second template
8
+
9
+ ## Goal
10
+
11
+ Add another template to the workspace.
12
+
13
+ ## The delta
14
+
15
+ ```bash
16
+ template-kit new <name>
17
+ ```
18
+
19
+ `new` creates `templates/<name>/` as a working starter template — the same
20
+ starter shape a fresh workspace ships (a text slot, an image slot, a
21
+ fact-bound slot, an option, fixtures, a fill spec, a `components/` folder, a
22
+ `"use client"` island, `theme.ts`/`theme.css`, `manifest.json`) — and it
23
+ already passes `check` as-is. Edit its sections from there.
24
+
25
+ A template is a plain folder under `templates/`. Adding one does not
26
+ duplicate anything: every template in the workspace shares the same kit
27
+ version, the same root `package.json`/lockfile, and the same
28
+ `tsconfig.json`/ESLint config. There is nothing per-template to configure
29
+ beyond the folder itself.
30
+
31
+ `new` refuses to overwrite an existing `templates/<name>/` folder.
32
+
33
+ ## Commands
34
+
35
+ ```bash
36
+ template-kit new <name>
37
+ template-kit check <name>
38
+ ```
39
+
40
+ ## Verify
41
+
42
+ Verify with [the author loop](../quickstart.md#the-author-loop) — a freshly
43
+ created template is check-clean before you change a line.
44
+
45
+ ## See also
46
+
47
+ - [Adding templates and sections](../new.md) — the full command, including
48
+ `new-section` for adding a section within a template.
49
+ - [Check](../check.md) — the gate every template (new or edited) must pass.
@@ -0,0 +1,59 @@
1
+ ---
2
+ purpose: Give the user a closed set of choices — an editable content slot, distinct from a behavioral enum option.
3
+ status: living
4
+ related: [../vocabulary.md, ../schema-system.md]
5
+ updated: 2026-07-15
6
+ ---
7
+ # A select slot — a closed set of choices
8
+
9
+ ## Goal
10
+
11
+ Give the user a closed set of choices — an editable content slot, distinct from
12
+ a behavioral `enum` option.
13
+
14
+ ## The delta
15
+
16
+ A `select` slot still needs a `source` or a `produced_by`, exactly like every
17
+ other slot type — `values` alone only declares the closed set the editor's
18
+ dropdown offers. The cleanest no-AI fill for a closed set is usually a
19
+ `direct()` fact that already resolves to one of a closed set of strings:
20
+
21
+ ```ts
22
+ // schema.ts — add a select slot, seeded from a closed-vocabulary fact
23
+ property_type: {
24
+ type: "select",
25
+ editable_by_user: true,
26
+ required: false,
27
+ values: ["single_family", "condo_townhome", "multi_family", "investment"],
28
+ source: direct("property_type"),
29
+ display_name: "Property type",
30
+ },
31
+ ```
32
+
33
+ ```tsx
34
+ // Renderer.tsx — read it like any other text-shaped slot
35
+ <Slot id="property_type" textLeaf>
36
+ <span>{slots.property_type}</span>
37
+ </Slot>
38
+ ```
39
+
40
+ `slots.property_type` is `string | null` (required is `false`). The general
41
+ `source`/`produced_by` contract still applies to a `select` slot, but no
42
+ fill-spec decision type constrains its output to the closed `values` set —
43
+ so `source: direct(...)` (a fact that already resolves within the set) is
44
+ the reliable way to fill one.
45
+
46
+ ## Commands
47
+
48
+ None — this is a pure content edit. No CLI command beyond the author loop below.
49
+
50
+ ## Verify
51
+
52
+ Verify with [the author loop](../quickstart.md#the-author-loop).
53
+
54
+ ## See also
55
+
56
+ - [The closed vocabulary](../vocabulary.md#slot-types) — the slot-type table, including
57
+ where `select` sits next to `text`/`number`/etc.
58
+ - [The schema system](../schema-system.md) — the full `schema.ts` contract, including
59
+ `direct()`/`derived()` binding.
@@ -0,0 +1,103 @@
1
+ ---
2
+ purpose: Add a graphic — an icon, a divider, a photo, a font — to a section.
3
+ status: living
4
+ related: [../assets.md, ../check.md, ../rules/bundle-binary-asset.md, image-slot-crop.md, ../theme-and-css.md]
5
+ updated: 2026-07-16
6
+ ---
7
+ # Add a static asset (image, icon, font)
8
+
9
+ ## Goal
10
+
11
+ Add a graphic — an icon, a divider mark, a photo, a custom font — to a section.
12
+
13
+ ## The delta
14
+
15
+ A binary asset — an image, a font, a video, an archive — may live in a section's own
16
+ `sections/<section>/assets/` folder, but only if the section references it: imported in
17
+ `Renderer.tsx` (`import crest from "./assets/crest.svg"`) or `url()`-referenced from one
18
+ of the section's CSS files. An unreferenced binary in the folder is flagged; see
19
+ [`bundle-binary-asset`](../rules/bundle-binary-asset.md) for the rule and why.
20
+
21
+ A file shared across sections — a logo, a brand webfont, a texture — lives at the
22
+ **template level** instead, referenced the same way with a `../../assets/…` path. See
23
+ [Static assets](../assets.md) for both paths and the full reference contract. What you
24
+ reach for depends on what the graphic is:
25
+
26
+ **A vector graphic — an icon, a divider, a decorative mark used only here.** Inline it as
27
+ SVG markup directly in the JSX that draws it. It is text, ships with the rest of your
28
+ component, and needs no file or import at all:
29
+
30
+ ```tsx
31
+ // sections/hero/components/ScrollArrow.tsx
32
+ export function ScrollArrow() {
33
+ return (
34
+ <svg viewBox="0 0 16 16" className="h-4 w-4" fill="none" aria-hidden="true">
35
+ <path d="M8 3v10M3 9l5 4 5-4" stroke="currentColor" strokeWidth="1.5" />
36
+ </svg>
37
+ );
38
+ }
39
+ ```
40
+
41
+ ```tsx
42
+ // sections/hero/Renderer.tsx
43
+ import { ScrollArrow } from "./components/ScrollArrow";
44
+ // …
45
+ <ScrollArrow />
46
+ ```
47
+
48
+ That markup's bytes ship inside the section's renderer bundle, so they count
49
+ toward the same byte budget as the rest of your JSX — see the size stage in
50
+ [Check](../check.md).
51
+
52
+ **A raster image or watermark used only in this section.** Add it under
53
+ `sections/<section>/assets/` and import it into the component:
54
+
55
+ ```tsx
56
+ // sections/hero/Renderer.tsx
57
+ import { Image } from "@homepages/template-kit";
58
+ import crest from "./assets/crest.svg";
59
+
60
+ export default function Renderer() {
61
+ return <Image src={crest} alt="" />;
62
+ }
63
+ ```
64
+
65
+ Or reference it from the section's CSS with `url()` instead of importing it — see
66
+ [Static assets](../assets.md) for both forms. Either way the file must be referenced:
67
+ an asset nothing imports or `url()`-references is flagged by
68
+ [`bundle-binary-asset`](../rules/bundle-binary-asset.md).
69
+
70
+ **A photo.** Not a section asset at all — a section never ships a photo file. A
71
+ photo arrives at runtime through an `image` slot: declare it in `schema.ts`,
72
+ produce it with an `image-assign` decision in `fill-spec.ts`, and render it
73
+ through the `<Image>` primitive, which reads `url`, `alt`, and `responsive` off
74
+ the slot's resolved value. See
75
+ [Image slot with a locked crop](image-slot-crop.md) for the full shape.
76
+
77
+ **A custom font shared across the template.** Declared once, for the whole
78
+ template, in the theme's `fonts` and optional `fontFaces` block. See
79
+ [Theme and CSS](../theme-and-css.md). A font only one section uses can instead
80
+ live in that section's `assets/` folder and be declared with a `@font-face` in
81
+ the section's own CSS — see [Static assets](../assets.md).
82
+
83
+ ## Commands
84
+
85
+ None — inlining SVG markup and declaring a slot are content edits. No CLI
86
+ command beyond the author loop below.
87
+
88
+ ## Verify
89
+
90
+ Verify with [the author loop](../quickstart.md#the-author-loop).
91
+
92
+ ## See also
93
+
94
+ - [Static assets](../assets.md) — the full reference/import contract, template
95
+ level vs. section folder.
96
+ - [`bundle-binary-asset`](../rules/bundle-binary-asset.md) — the rule that
97
+ flags an unreferenced binary in a section folder, and the SVG-inlining fix.
98
+ - [Check](../check.md) — the size stage a large inlined SVG or a bloated
99
+ renderer bundle counts against.
100
+ - [Image slot with a locked crop](image-slot-crop.md) — the real channel for a
101
+ photo: an `image` slot, an `image-assign` decision, and the `<Image>`
102
+ primitive.
103
+ - [Theme and CSS](../theme-and-css.md) — where a template's fonts are declared.
@@ -0,0 +1,90 @@
1
+ ---
2
+ purpose: Use a third-party npm package in a section.
3
+ status: living
4
+ related: [../islands.md, ../pack.md, ../rules/single-react.md, ../rules/no-bare-css-import.md, ../rules/no-css-import-from-render-path.md]
5
+ updated: 2026-07-15
6
+ ---
7
+ # Use a third-party npm package in a section
8
+
9
+ ## Goal
10
+
11
+ Use an npm package in a section.
12
+
13
+ ## The delta
14
+
15
+ Install the package like any npm dependency:
16
+
17
+ ```bash
18
+ npm i swiper
19
+ ```
20
+
21
+ **Only a published version.** `pack` refuses to produce a submission zip when a
22
+ dependency uses a local `file:`/`link:` protocol — depend on a real published
23
+ version, never a path on your own machine.
24
+
25
+ **Where you import it from matters.** A client library — a carousel, a map SDK,
26
+ anything that touches `window` or the DOM — is imported inside a `"use client"`
27
+ island, the same as any other browser-only code:
28
+
29
+ ```tsx
30
+ // sections/gallery/Carousel.tsx
31
+ "use client";
32
+
33
+ import Swiper from "swiper";
34
+
35
+ export default function Carousel({ slides }: { slides: { id: string; url: string }[] }) {
36
+ // …
37
+ }
38
+ ```
39
+
40
+ A server-side import — one that lands in the Renderer or a plain component it
41
+ imports, not behind `"use client"` — must stay pure and deterministic: no reads
42
+ of the clock, randomness, or the network at render time. A package built for a
43
+ browser runtime (it touches `window`, schedules a timer, generates an id) does
44
+ not belong there; move the usage into an island.
45
+
46
+ **A package that ships its own stylesheet** is imported by its bare specifier,
47
+ from the module that renders it — the Renderer's import graph (the components
48
+ and islands it imports, transitively) is what the CSS build bundles and
49
+ harvests for the section's stylesheet:
50
+
51
+ ```tsx
52
+ // sections/gallery/Carousel.tsx
53
+ "use client";
54
+
55
+ import Swiper from "swiper";
56
+ import "swiper/css"; // ← the sanctioned channel: a bare specifier reaches this section's layer
57
+ ```
58
+
59
+ A **local** `.css` import from that same render path is a different, banned case
60
+ — see [`no-css-import-from-render-path`](../rules/no-css-import-from-render-path.md)
61
+ for the full distinction between the two, and
62
+ [`no-bare-css-import`](../rules/no-bare-css-import.md) for why a hand-written
63
+ stylesheet can't reach the package's CSS with its own `@import` instead.
64
+
65
+ ## Commands
66
+
67
+ ```bash
68
+ npm i <package>
69
+ ```
70
+
71
+ ## Verify
72
+
73
+ Verify with [the author loop](../quickstart.md#the-author-loop) — a green
74
+ `check` after the install confirms the package resolves, its CSS (if any)
75
+ landed in the section's layer, and the workspace still carries exactly one copy
76
+ of React.
77
+
78
+ ## See also
79
+
80
+ - [Islands](../islands.md) — writing a `"use client"` island; a third-party
81
+ client library is fine inside one, never in a Renderer.
82
+ - [`template-kit pack`](../pack.md) — why a `file:`/`link:` dependency blocks a
83
+ submission.
84
+ - [`single-react`](../rules/single-react.md) — a package that pulls its own
85
+ copy of React breaks hydration; the installed tree may carry only one.
86
+ - [`no-css-import-from-render-path`](../rules/no-css-import-from-render-path.md) —
87
+ bare package specifier (legal) vs. a local `.css` import (banned) from a
88
+ section's render path.
89
+ - [`no-bare-css-import`](../rules/no-bare-css-import.md) — why a hand-written
90
+ stylesheet can't reach a package's CSS with its own `@import` either.
@@ -1,8 +1,8 @@
1
1
  ---
2
2
  purpose: Router for the rule corpus — every template-kit/<id> an author can hit, and the page that explains it.
3
3
  status: living
4
- related: [server-vs-client.md, ../eslint.md, ../INDEX.md]
5
- updated: 2026-07-14
4
+ related: [server-vs-client.md, ../eslint.md, ../check.md, ../INDEX.md]
5
+ updated: 2026-07-15
6
6
  ---
7
7
  # Rules
8
8
 
@@ -13,8 +13,8 @@ and resolves to exactly one page here: `docs/rules/<id>.md`.
13
13
  in a Renderer) and are enforced by the ESLint preset. Others are properties of the
14
14
  **tree** (a slot declared in `schema.ts` but never marked in the JSX, a stylesheet, a
15
15
  dependency, a byte budget) and cannot be expressed one file at a time; those are enforced
16
- by `template-kit check`. You never need to know which engine produced a violation — only
17
- the id, which names its page either way.
16
+ by [`template-kit check`](../check.md). You never need to know which engine produced a
17
+ violation — only the id, which names its page either way.
18
18
 
19
19
  Read [server vs client](server-vs-client.md) first if you are here for any rule that
20
20
  mentions "server-rendered code" or an island. It is the one concept those rules assume,
@@ -31,9 +31,13 @@ and it is explained there rather than on each of them.
31
31
  | [`no-raw-element`](no-raw-element.md) | eslint | No raw `<section>` or `<img>` — use `<Section>` and `<Image>`. |
32
32
  | [`image-bare-needs-reason`](image-bare-needs-reason.md) | eslint | `<Image bare>` needs a `// bare:` comment saying why it skips the frame. |
33
33
  | [`slot-marker-literal`](slot-marker-literal.md) | eslint | A slot-marker key must be a string literal, not an expression. |
34
- | [`no-hex`](no-hex.md) | eslint | No hard-coded colours. Islands are NOT exempt. |
34
+ | [`no-hex`](no-hex.md) | eslint | No hard-coded colours — the one legal colour function is a `color-mix()` over theme-derived arguments. Islands are NOT exempt. |
35
35
  | [`no-css-import-from-render-path`](no-css-import-from-render-path.md) | eslint | No relative/absolute .css import from a section's render path — a package's is the sanctioned channel. |
36
36
  | [`bundle-incomplete`](bundle-incomplete.md) | check | A section folder is missing one of its four required files. |
37
+ | [`bundle-binary-asset`](bundle-binary-asset.md) | check | An unreferenced binary asset sits in a section folder. |
38
+ | [`schema-invalid`](schema-invalid.md) | check | A section's schema.ts or fill-spec.ts fails a contract assertion, or cannot be loaded. |
39
+ | [`fixtures-invalid`](fixtures-invalid.md) | check | A section's fixtures.ts does not satisfy the fixture-module shape. |
40
+ | [`manifest-invalid`](manifest-invalid.md) | check | A template's manifest.json is missing, unparseable, or violates the manifest contract. |
37
41
  | [`missing-slot-marker`](missing-slot-marker.md) | check | An editable slot in schema.ts has no marker in the section's JSX. |
38
42
  | [`slot-group-marker`](slot-group-marker.md) | check | A group with two or more members has no `<SlotGroup>` wrapper. |
39
43
  | [`css-reason`](css-reason.md) | check | A hand-written CSS file must open with a `css-reason:` comment. |
@@ -41,6 +45,7 @@ and it is explained there rather than on each of them.
41
45
  | [`unlayered-fence`](unlayered-fence.md) | check | An `/* unlayered: */` escape hatch must be balanced and give a reason. |
42
46
  | [`sidebar-order`](sidebar-order.md) | check | Schema slot order must match the order the page renders them in. |
43
47
  | [`typecheck`](typecheck.md) | check | The template must compile under TypeScript. |
48
+ | [`parse-error`](parse-error.md) | check | A file could not be parsed at all — a syntax error, not a rule violation. |
44
49
  | [`determinism-drift`](determinism-drift.md) | check | Rendering the same fixture twice must produce byte-identical HTML. |
45
50
  | [`render-invariant`](render-invariant.md) | check | Rendered output must carry no [object Object], bare null/undefined/NaN leaf, or src-less `<img>`. |
46
51
  | [`lockfile-missing`](lockfile-missing.md) | check | The workspace has no package-lock.json. |
@@ -50,9 +55,13 @@ and it is explained there rather than on each of them.
50
55
  | [`license-denied`](license-denied.md) | check | A production dependency's license is not on the allowlist. |
51
56
  | [`size-renderer-bundle`](size-renderer-bundle.md) | check | A section's renderer bundle exceeds its byte budget. |
52
57
  | [`size-section-css`](size-section-css.md) | check | A section's compiled CSS exceeds its byte budget. |
58
+ | [`size-island-bundle`](size-island-bundle.md) | check | A section's island (browser) bundle exceeds its byte budget. |
59
+ | [`size-assets`](size-assets.md) | check | A section's static assets exceed their byte budget. |
60
+ | [`no-templates`](no-templates.md) | check | The workspace has no template to check at all. |
53
61
 
54
62
  ## See also
55
63
 
56
64
  - [Server vs client](server-vs-client.md) — how a file is scoped, and which rules stop at
57
65
  the hydration boundary.
58
66
  - [ESLint preset](../eslint.md) — setting the preset up in a template workspace.
67
+ - [Check](../check.md) — running `template-kit check`, its flags, output shape, and exit code.
@@ -0,0 +1,83 @@
1
+ ---
2
+ purpose: The template-kit/bundle-binary-asset authoring rule — what it enforces, why, and how to fix it.
3
+ status: living
4
+ related: [INDEX.md, bundle-incomplete.md, ../primitives.md, ../assets.md]
5
+ updated: 2026-07-16
6
+ ---
7
+ # template-kit/bundle-binary-asset
8
+
9
+ > A static asset may live in a section folder — but only if something references it. An
10
+ > unreferenced binary is dead weight and can never be served.
11
+
12
+ ## Rule
13
+
14
+ A binary asset inside a section folder is allowed **only when the section references it** —
15
+ imported in `Renderer.tsx` (`import crest from "./assets/crest.svg"`) or `url()`-referenced
16
+ from one of the section's CSS files. A binary that nothing references is flagged. The check
17
+ looks two ways to find candidates:
18
+
19
+ - **By extension** — `.svg`, `.png`, `.jpg`/`.jpeg`, `.gif`, `.webp`, `.avif`, `.ico`,
20
+ `.woff`/`.woff2`, `.ttf`, `.otf`, `.eot`, `.mp4`, `.webm`, `.mov`, `.mp3`, `.wav`,
21
+ `.pdf`, `.zip`.
22
+ - **By content** — any other file whose first bytes contain a NUL byte.
23
+
24
+ ## Reason
25
+
26
+ A referenced asset is first-class: the build content-hashes it to a stable
27
+ `/section-assets/` URL and the publish pipeline uploads it — identical in the `dev`
28
+ preview, the editor, and the published page. An *unreferenced* binary has no such URL: it
29
+ is never emitted or uploaded, yet it still counts toward the section's content hash (its
30
+ identity). So it is pure dead weight — a file that ships nowhere.
31
+
32
+ ## Fix
33
+
34
+ Either reference the asset, or delete it.
35
+
36
+ - **Reference it from a component:** `import crest from "./assets/crest.svg"` and render it
37
+ with the `Image` primitive — `<Image src={crest} alt="…" />`. See [Static assets](../assets.md).
38
+ - **Reference it from CSS:** `background-image: url("./assets/crest.svg")` (or a
39
+ `@font-face` `src`) in the section's `styles.css`.
40
+ - **Delete it** if it is left over and unused.
41
+
42
+ ### Before
43
+
44
+ ```text
45
+ sections/hero/
46
+ Renderer.tsx
47
+ schema.ts
48
+ fill-spec.ts
49
+ fixtures.ts
50
+ assets/
51
+ crest.svg ← nothing imports or url()-references this file
52
+ ```
53
+
54
+ ### After
55
+
56
+ ```tsx
57
+ // sections/hero/Renderer.tsx
58
+ import { Image } from "@homepages/template-kit";
59
+ import crest from "./assets/crest.svg";
60
+
61
+ export default function Renderer() {
62
+ return <Image src={crest} alt="" />;
63
+ }
64
+ ```
65
+
66
+ For a tiny glyph or divider, prefer inline `<svg>` markup over a file at all — it ships with
67
+ your JSX, needs no asset at all, and costs no extra request:
68
+
69
+ ```tsx
70
+ export function ScrollArrow() {
71
+ return (
72
+ <svg viewBox="0 0 16 16" className="h-4 w-4" fill="none" aria-hidden="true">
73
+ <path d="M8 3v10M3 9l5 4 5-4" stroke="currentColor" strokeWidth="1.5" />
74
+ </svg>
75
+ );
76
+ }
77
+ ```
78
+
79
+ ## See also
80
+
81
+ - [Static assets](../assets.md) — where an asset may live and how to reference it.
82
+ - [`bundle-incomplete`](bundle-incomplete.md) — the other rule on the folder's contents.
83
+ - [Contract primitives](../primitives.md) — `Image`, which renders an asset URL you import.
@@ -2,7 +2,7 @@
2
2
  purpose: The template-kit/bundle-incomplete authoring rule — what it enforces, why, and how to fix it.
3
3
  status: living
4
4
  related: [INDEX.md, ../schema-system.md]
5
- updated: 2026-07-14
5
+ updated: 2026-07-15
6
6
  ---
7
7
  # template-kit/bundle-incomplete
8
8
 
@@ -30,7 +30,7 @@ Two near-misses do **not** satisfy it:
30
30
  searched for down the tree.
31
31
 
32
32
  Everything else in the folder is unconstrained. `components/`, `hooks/`, `utils/`, any
33
- nesting, any number of support modules, islands, stylesheets, enhancers: the check has no
33
+ nesting, any number of support modules, islands, stylesheets: the check has no
34
34
  opinion about them, and neither does the rest of the kit.
35
35
 
36
36
  ## Reason
@@ -2,7 +2,7 @@
2
2
  purpose: The template-kit/css-reason 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/css-reason
8
8
 
@@ -36,7 +36,7 @@ and cannot leak into another section on the same page. Hand-written CSS gives al
36
36
  up.
37
37
 
38
38
  There is exactly one thing utilities cannot do: style **DOM that Tailwind's scanner never
39
- sees**. Markup an enhancer builds at runtime has no class in your source for the scanner
39
+ sees**. Markup an island builds at runtime has no class in your source for the scanner
40
40
  to find, and a third-party widget's own DOM was never yours to put classes on. Those are
41
41
  the cases hand-CSS exists for.
42
42
 
@@ -67,7 +67,7 @@ classes on the markup.
67
67
  ### After
68
68
 
69
69
  ```css
70
- /* css-reason: styles the marker DOM the neighborhood-map enhancer builds at runtime —
70
+ /* css-reason: styles the marker DOM the neighborhood-map island builds at runtime —
71
71
  the nodes are created by the map library after hydration, so Tailwind's scanner never
72
72
  sees a class to compile. */
73
73
  .tr-map-marker {
@@ -0,0 +1,96 @@
1
+ ---
2
+ purpose: The template-kit/fixtures-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]
5
+ updated: 2026-07-14
6
+ ---
7
+ # template-kit/fixtures-invalid
8
+
9
+ > `fixtures.ts` is the one contract file nothing else parses for you. This is that parse.
10
+
11
+ ## Rule
12
+
13
+ The default export of `fixtures.ts` must satisfy the fixture-module shape:
14
+
15
+ ```ts
16
+ {
17
+ base: {
18
+ slots: { /* every required slot, with a valid value */ },
19
+ options: { /* optional */ },
20
+ anchors: { /* optional */ },
21
+ variants: { /* optional */ },
22
+ },
23
+ option_matrix: [ /* optional: { label?, options } entries */ ],
24
+ states: { /* optional: named scenario → { label?, slots?, options?, anchors?, variants? } */ },
25
+ }
26
+ ```
27
+
28
+ `base` is required and must be an object holding a `slots` record — every other key is
29
+ optional. Each violation names the exact path inside the module that's wrong (`"base"`,
30
+ `"base.slots"`, `"states.empty-address"`, …) and what shape was expected there instead.
31
+
32
+ ## Reason
33
+
34
+ `schema.ts` and `fill-spec.ts` are both Zod-parsed on the way into every other check, so a
35
+ malformed one is caught immediately. `fixtures.ts` used to be the exception: nothing
36
+ touched it until the render stage read it directly, so a wrong shape there surfaced as a
37
+ raw crash mid-render, blamed on whatever line happened to dereference the missing field —
38
+ never on the fixture that was actually wrong.
39
+
40
+ This gate exists to move that failure to where the mistake was made, with a message about
41
+ the fixture module instead of a stack trace from three files away.
42
+
43
+ ## Fix
44
+
45
+ Read the message's path — it points at the exact key that's wrong — and bring that key's
46
+ shape in line with what's documented above.
47
+
48
+ ### Before
49
+
50
+ ```ts
51
+ // sections/hero/fixtures.ts
52
+ import type { FixtureModule } from "@homepages/template-kit";
53
+
54
+ const fixtures: FixtureModule = {
55
+ base: {
56
+ slots: { headline: "A mansard Victorian in the heart of Jamaica Plain" },
57
+ },
58
+ states: {
59
+ "no-photo": { slots: { hero_image: null }, label: 12 },
60
+ // ^ label must be a string
61
+ },
62
+ };
63
+
64
+ export default fixtures;
65
+ ```
66
+
67
+ ```text
68
+ Section "hero": fixtures.ts — "states.no-photo.label": Expected string, received number.
69
+ ```
70
+
71
+ ### After
72
+
73
+ ```ts
74
+ // sections/hero/fixtures.ts
75
+ import type { FixtureModule } from "@homepages/template-kit";
76
+
77
+ const fixtures: FixtureModule = {
78
+ base: {
79
+ slots: { headline: "A mansard Victorian in the heart of Jamaica Plain" },
80
+ },
81
+ states: {
82
+ "no-photo": { slots: { hero_image: null }, label: "No hero photo" },
83
+ },
84
+ };
85
+
86
+ export default fixtures;
87
+ ```
88
+
89
+ ## See also
90
+
91
+ - [The schema system](../schema-system.md) — `fixtures.ts`'s place among the section's
92
+ four contract files.
93
+ - [`schema-invalid`](schema-invalid.md) — the sibling gate on `schema.ts` and
94
+ `fill-spec.ts`.
95
+ - [`bundle-incomplete`](bundle-incomplete.md) — reported instead of this id when
96
+ `fixtures.ts` is missing outright, rather than present and wrong.
@@ -2,7 +2,7 @@
2
2
  purpose: The template-kit/license-denied authoring rule — what it enforces, why, and how to fix it.
3
3
  status: living
4
4
  related: [INDEX.md, audit-severity.md]
5
- updated: 2026-07-14
5
+ updated: 2026-07-15
6
6
  ---
7
7
  # template-kit/license-denied
8
8
 
@@ -31,8 +31,16 @@ Two cases people expect to slide through, and neither does:
31
31
  - **A missing `license` field is a denial**, not a pass. An unlicensed package is not a
32
32
  permissive one — by default, no rights are granted at all.
33
33
 
34
- **Scope: the production tree only.** `devDependencies` are not evaluated — your bundler,
35
- your test runner, and your formatter do not ship to anyone.
34
+ **Scope: the production tree only.** "Production tree" means exactly what
35
+ `npm ci --omit=dev` installs. `devDependencies` are not evaluated your bundler, your
36
+ test runner, and your formatter do not ship to anyone.
37
+
38
+ **An optional peer you have anyway counts.** If a production package declares an
39
+ optional `peerDependency` and your workspace has that package for any reason (even as a
40
+ `devDependency`), npm resolves the peer and keeps it in the `--omit=dev` install — so it
41
+ is in the production tree and its license is checked, transitively. An optional peer you
42
+ do **not** have is never installed and is not evaluated. Run `npm ci --omit=dev` and look
43
+ at the resulting `node_modules` if you want the exact set the check sees.
36
44
 
37
45
  **There is no per-package override.** No allowlist entry you can add, no waiver comment. By
38
46
  design.