@homepages/template-kit 0.2.0 → 0.3.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 (74) hide show
  1. package/CHANGELOG.md +151 -0
  2. package/README.md +61 -17
  3. package/dist/base.css +9 -0
  4. package/dist/cli/check/config.js +37 -0
  5. package/dist/cli/check/css.js +117 -0
  6. package/dist/cli/check/diagnostics.js +32 -0
  7. package/dist/cli/check/index.js +87 -0
  8. package/dist/cli/check/loader.js +142 -0
  9. package/dist/cli/check/paths.js +15 -0
  10. package/dist/cli/check/relativize.js +18 -0
  11. package/dist/cli/check/render-invariants.js +24 -0
  12. package/dist/cli/check/resolve-tool.js +38 -0
  13. package/dist/cli/check/stages/deps.js +337 -0
  14. package/dist/cli/check/stages/lint.js +46 -0
  15. package/dist/cli/check/stages/render.js +101 -0
  16. package/dist/cli/check/stages/size.js +158 -0
  17. package/dist/cli/check/stages/tree.js +207 -0
  18. package/dist/cli/check/stages/typecheck.js +46 -0
  19. package/dist/cli/check/stages/validate/catalog-exhaustiveness.js +18 -0
  20. package/dist/cli/check/stages/validate/facts.js +18 -0
  21. package/dist/cli/check/stages/validate/formatter-contract.js +14 -0
  22. package/dist/cli/check/stages/validate/group-contract.js +21 -0
  23. package/dist/cli/check/stages/validate/index.js +66 -0
  24. package/dist/cli/check/stages/validate/list-scalar-contract.js +22 -0
  25. package/dist/cli/check/stages/validate/nav-contract.js +93 -0
  26. package/dist/cli/check/stages/validate/nested-slot-contract.js +12 -0
  27. package/dist/cli/check/stages/validate/object-list-contract.js +31 -0
  28. package/dist/cli/check/stages/validate/orchestrator.js +241 -0
  29. package/dist/cli/check/stages/validate/produced-by-contract.js +18 -0
  30. package/dist/cli/check/stages/validate/row-contract.js +31 -0
  31. package/dist/cli/check/stages/validate/select-contract.js +13 -0
  32. package/dist/cli/check/stages/validate/source-contract.js +21 -0
  33. package/dist/cli/check/stages/validate/variant-contract.js +15 -0
  34. package/dist/cli/check/workspace.js +86 -0
  35. package/dist/cli/theme/generate.js +43 -0
  36. package/dist/cli/theme/index.js +33 -0
  37. package/dist/cli/theme/load-theme.js +69 -0
  38. package/dist/cli.js +22 -4
  39. package/dist/contracts/fill-treatments.js +47 -0
  40. package/dist/design-system/theme.d.ts +30 -300
  41. package/dist/design-system/theme.js +112 -90
  42. package/dist/eslint/rules/no-hex.js +78 -6
  43. package/dist/eslint.js +1 -1
  44. package/dist/index.d.ts +2 -2
  45. package/dist/index.js +2 -2
  46. package/dist/package.js +1 -1
  47. package/dist/primitives/Image.js +1 -1
  48. package/dist/schema/fill-spec.d.ts +80 -590
  49. package/dist/schema/fixture-schema.d.ts +5 -81
  50. package/dist/schema/manifest.d.ts +39 -475
  51. package/dist/schema/resolve-section-ref.js +10 -0
  52. package/dist/schema/rows.js +10 -0
  53. package/dist/schema/section-nav.js +9 -0
  54. package/dist/schema/section-schema.d.ts +51 -437
  55. package/dist/schema/slot-types.d.ts +12 -16
  56. package/dist/styles.css +31 -88
  57. package/docs/INDEX.md +4 -3
  58. package/docs/check.md +121 -0
  59. package/docs/eslint.md +18 -8
  60. package/docs/llms.txt +21 -9
  61. package/docs/primitives.md +5 -0
  62. package/docs/rules/INDEX.md +11 -4
  63. package/docs/rules/bundle-binary-asset.md +102 -0
  64. package/docs/rules/fixtures-invalid.md +96 -0
  65. package/docs/rules/license-denied.md +11 -3
  66. package/docs/rules/manifest-invalid.md +99 -0
  67. package/docs/rules/no-hex.md +76 -10
  68. package/docs/rules/no-templates.md +87 -0
  69. package/docs/rules/parse-error.md +76 -0
  70. package/docs/rules/schema-invalid.md +96 -0
  71. package/docs/rules/size-section-css.md +2 -2
  72. package/docs/theme-and-css.md +155 -90
  73. package/package.json +4 -9
  74. package/tsconfig.json +1 -1
package/dist/styles.css CHANGED
@@ -1,15 +1,26 @@
1
1
  /*
2
- * @homepages/template-kit — theme tokens + kit source registration.
2
+ * @homepages/template-kit — the global Tailwind preamble.
3
3
  *
4
- * Import this AFTER `@import "tailwindcss";` in your own CSS entry:
4
+ * Import this AFTER `@import "tailwindcss";` in your own CSS entry, and import your
5
+ * template's compiled theme after it:
5
6
  *
6
7
  * @import "tailwindcss";
7
8
  * @import "@homepages/template-kit/styles.css";
9
+ * @import "./theme.css"; // compileThemeToCss(yourTheme) output
8
10
  *
9
11
  * This file must NOT import Tailwind itself. Exactly one `@import "tailwindcss"` may
10
12
  * exist per build graph — your entry owns it. Two double-emit Tailwind's preflight
11
13
  * reset (box-sizing declared twice, ~4 KB of duplicate base layer).
12
14
  *
15
+ * WHAT IS HERE: only what is true of EVERY template — the breakpoint ladder, the
16
+ * spacing base unit, and the motion defaults.
17
+ *
18
+ * WHAT IS NOT: your design tokens. Colors, fonts, type, radii and shadows are yours.
19
+ * Declare them in `theme.ts` under names you choose, and `compileThemeToCss` generates
20
+ * the `@theme inline` alias layer that turns each one into a utility. Nothing about
21
+ * your visual system is shared, and there is no global file for you to edit to make a
22
+ * token of yours usable.
23
+ *
13
24
  * The @source below resolves relative to THIS FILE'S SHIPPED LOCATION (dist/styles.css),
14
25
  * registering the kit's compiled JS for class scanning — that is how the utility classes
15
26
  * baked into the kit's primitives reach your compiled CSS. It also overrides Tailwind's
@@ -23,107 +34,39 @@
23
34
  * utilities and 19 stray @property registrations, ~2.5 KB. If a primitive ever moves out
24
35
  * of primitives/, widen this — an unstyled <Image> in every template is the failure mode,
25
36
  * and scripts/verify-consumer.mjs is what catches it.
26
- *
27
- * The @theme block aliases Tailwind's utility namespace onto the `--tr-*` custom
28
- * properties your template's theme.ts supplies (`text-ink` → `var(--tr-color-ink)`).
29
- * The aliases are the cross-template contract; the values behind them are yours.
30
37
  */
31
38
  @source "./primitives/**/*.js";
32
39
 
40
+ /* `@theme inline` so a spacing/breakpoint value is substituted straight into the
41
+ * utilities Tailwind generates (`.gap-2 { gap: .5rem }`), rather than emitted as a
42
+ * `:root` custom property the utilities then read through `var()`. These are fixed
43
+ * global constants, not per-page-overridable tokens, so inlining is both correct and
44
+ * what keeps the compiled output stable. Nothing here may reference a token a theme.ts
45
+ * defines. */
33
46
  @theme inline {
34
- /* ---------- Colors (alias to the --tr-color-* values a theme.ts supplies) ---------- */
35
- --color-ink: var(--tr-color-ink);
36
- --color-ink-soft: var(--tr-color-ink-soft);
37
- --color-ink-mute: var(--tr-color-ink-mute);
38
- --color-background: var(--tr-color-background);
39
- --color-surface: var(--tr-color-surface);
40
- --color-surface-alt: var(--tr-color-surface-alt);
41
- --color-border: var(--tr-color-border);
42
- --color-border-strong: var(--tr-color-border-strong);
43
- --color-accent: var(--tr-color-accent);
44
- --color-accent-strong: var(--tr-color-accent-strong);
45
- --color-accent-contrast: var(--tr-color-accent-contrast);
46
- --color-primary: var(--tr-color-primary);
47
- --color-primary-fg: var(--tr-color-primary-fg);
48
- --color-primary-darker: var(--tr-color-primary-darker);
49
- --color-overlay: var(--tr-color-overlay);
50
- --color-overlay-strong: var(--tr-color-overlay-strong);
51
- --color-overlay-light: var(--tr-color-overlay-light);
52
- --color-footer-bg: var(--tr-color-footer-bg);
53
- --color-footer-fg: var(--tr-color-footer-fg);
54
- --color-footer-fg-mute: var(--tr-color-footer-fg-mute);
55
- --color-footer-divider: var(--tr-color-footer-divider);
56
-
57
- /* Decorative brand swatches — the open `palette` escape hatch in TokenTheme.
58
- * A theme that omits one leaves its utility resolving to an unset custom
59
- * property; only reach for these when a semantic role genuinely doesn't fit. */
60
- --color-yellow-deep: var(--tr-color-yellow-deep);
61
- --color-yellow-200: var(--tr-color-yellow-200);
62
- --color-yellow-100: var(--tr-color-yellow-100);
63
- --color-brown: var(--tr-color-brown);
64
- --color-green: var(--tr-color-green);
65
- --color-blue: var(--tr-color-blue);
66
- --color-rule: var(--tr-color-rule);
67
- --color-gray-light: var(--tr-color-gray-light);
68
- --color-team-gold: var(--tr-color-team-gold);
69
- --color-mute: var(--tr-color-mute);
70
- --color-scrim: var(--tr-color-scrim);
71
- --color-footer-line: var(--tr-color-footer-line);
72
- --color-footer-mute: var(--tr-color-footer-mute);
73
- --color-divider-40: var(--tr-color-divider-40);
74
- --color-contact-deep: var(--tr-color-contact-deep);
75
- --color-header-deep: var(--tr-color-header-deep);
76
-
77
- /* ---------- Fonts ---------- */
78
- --font-sans: var(--tr-font-sans);
79
- --font-display: var(--tr-font-sans);
80
- --font-serif: var(--tr-font-serif);
81
- --font-num: var(--tr-font-num);
82
- --font-label: var(--tr-font-label);
83
-
84
- /* ---------- Type scale (mobile baseline; layer up with md:/lg:) ---------- */
85
- --text-xs: var(--tr-text-xs);
86
- --text-sm: var(--tr-text-sm);
87
- --text-base: var(--tr-text-base);
88
- --text-lg: var(--tr-text-lg);
89
- --text-xl: var(--tr-text-xl);
90
- --text-2xl: var(--tr-text-2xl);
91
- --text-3xl: var(--tr-text-3xl);
92
- --text-4xl: var(--tr-text-4xl);
93
- --text-5xl: var(--tr-text-5xl);
94
- --text-display: var(--tr-text-display);
95
-
96
- /* ---------- Spacing (Tailwind's own scale — no custom aliases) ---------- */
47
+ /* ---------- Spacing ---------- */
97
48
  --spacing: 0.25rem; /* v4 base unit; Tailwind multiplies (e.g. p-4 = 1rem) */
98
49
 
99
- /* ---------- Radii ---------- */
100
- --radius-sm: var(--tr-radius-sm);
101
- --radius-md: var(--tr-radius-md);
102
- --radius-lg: var(--tr-radius-lg);
103
- --radius-full: var(--tr-radius-full);
104
-
105
- /* ---------- Shadows ---------- */
106
- --shadow-sm: var(--tr-shadow-sm);
107
- --shadow-md: var(--tr-shadow-md);
108
- --shadow-lg: var(--tr-shadow-lg);
109
-
110
- /* ---------- Motion ---------- */
50
+ /* ---------- Motion ----------
51
+ * The --tr-* values these read are defined in base.css, which every page loads. */
111
52
  --default-transition-duration: var(--tr-duration-fast);
112
53
  --default-transition-timing-function: var(--tr-ease);
113
54
 
114
55
  /* ---------- Canonical breakpoint ladder ----------
115
- * Eight canonical widths — the same set the preview grid and the screenshot
116
- * harness verify a section against (VERIFICATION_WIDTHS is the matching TS
117
- * constant). 350 is the mobile-first BASE (no prefix); the other seven are
118
- * named breakpoints:
56
+ * Eight canonical widths — the same set the preview grid and the screenshot harness
57
+ * verify a section against. `VERIFICATION_WIDTHS` (design-system/breakpoints.ts) is
58
+ * the matching TS constant; CSS cannot import it, so `src/css/styles.test.ts` fails
59
+ * the build if the two ever drift.
60
+ *
61
+ * 350 is the mobile-first BASE (no prefix); the other seven are named breakpoints:
119
62
  *
120
63
  * bp390 (390) · bp425 (425) · bp512 (512) · md (768) · lg (1024)
121
64
  * · bp1440 (1440) · bp1920 (1920)
122
65
  *
123
66
  * `md` and `lg` keep Tailwind's own names and values. The off-ladder defaults
124
- * (sm/xl/2xl) are reset to `initial` so a section cannot come to depend on a
125
- * width nobody screenshots. A swap-heavy section may still reach for a raw
126
- * `@media` micro-breakpoint at an off-ladder width (e.g. 640 / 769).
67
+ * (sm/xl/2xl) are reset to `initial` so a section cannot come to depend on a width
68
+ * nobody screenshots. A swap-heavy section may still reach for a raw `@media`
69
+ * micro-breakpoint at an off-ladder width (e.g. 640 / 769).
127
70
  */
128
71
  --breakpoint-sm: initial;
129
72
  --breakpoint-bp390: 390px;
package/docs/INDEX.md CHANGED
@@ -2,7 +2,7 @@
2
2
  purpose: Router for the docs corpus that ships inside the @homepages/template-kit npm package.
3
3
  status: living
4
4
  related: []
5
- updated: 2026-07-14
5
+ updated: 2026-07-15
6
6
  ---
7
7
  # template-kit docs — router
8
8
 
@@ -16,7 +16,8 @@ detail.
16
16
  | `llms.txt` | Machine-readable index of this corpus for an agent working inside an installed kit | orienting an agent in a consuming workspace |
17
17
  | `schema-system.md` | The section authoring contract: `schema.ts` / `fill-spec.ts` / `fixtures.ts` / `Renderer.tsx`, `SectionProps` inference, the closed source/transform vocabulary, and the golden property fixtures | authoring or changing any section |
18
18
  | `primitives.md` | The five contract primitives (`Section`, `Slot`, `SlotItem`, `SlotGroup`, `Image`) and the marker attributes they emit | writing a section's markup |
19
- | `theme-and-css.md` | The two CSS entries, the one-Tailwind-import rule, `TokenTheme`, the breakpoint ladder, and the opt-in `content-visibility` recipe for long, image-heavy pages | a template's theme, its CSS entry, or a long page with a live map or full-screen overlay |
19
+ | `theme-and-css.md` | The two CSS entries, the one-Tailwind-import rule, `TokenTheme` (your own token names → your own Tailwind utilities), the `template-kit theme` command that generates `theme.css` from `theme.ts`, the breakpoint ladder, and the opt-in `content-visibility` recipe for long, image-heavy pages | a template's theme, its CSS entry, or a long page with a live map or full-screen overlay |
20
20
  | `islands.md` | Interactivity: `"use client"` components, the serializable-props rule, editor options, the loader | anything interactive in a section |
21
- | `eslint.md` | The `@homepages/template-kit/eslint` preset: zero-config setup and its glob scope | wiring up lint in a template workspace |
21
+ | `eslint.md` | The `@homepages/template-kit/eslint` preset: the two-line config, the ESLint + parser install it expects, and its glob scope | wiring up lint in a template workspace |
22
+ | `check.md` | The `template-kit check` CLI command: the toolchain it resolves from your workspace, its stages, `--all`/`--json`, output shape, and exit code | running `check` locally or wiring it into CI |
22
23
  | `rules/` | One page per rule-id (`template-kit/*`), across both venues — the id printed by a lint or `check` failure resolves here. `rules/INDEX.md` routes them; `rules/server-vs-client.md` is the concept the server-rendering rules assume | any lint or `check` failure |
package/docs/check.md ADDED
@@ -0,0 +1,121 @@
1
+ ---
2
+ purpose: The `template-kit check` command — what it runs, its flags, its output shape, and its exit code.
3
+ status: living
4
+ related: [INDEX.md, rules/INDEX.md, eslint.md, theme-and-css.md]
5
+ updated: 2026-07-15
6
+ ---
7
+ # template-kit check
8
+
9
+ `template-kit check` is the one command that answers "will the platform accept this
10
+ template?" — the same gate you run locally and the one the registry re-runs when you
11
+ submit. Every failure prints a stable `template-kit/<id>`, a `file:line` where one
12
+ applies, and a one-line fix hint. The id resolves to its own page under
13
+ [`rules/`](rules/INDEX.md).
14
+
15
+ ## Usage
16
+
17
+ ```bash
18
+ template-kit check # infer the template from cwd, or the workspace's one template
19
+ template-kit check <template> # check one named template by its key
20
+ template-kit check --all # check every template in the workspace
21
+ template-kit check --json # machine-readable report (combine with any of the above)
22
+ ```
23
+
24
+ Run it from the workspace root, or from inside a template's own folder — a bare `check`
25
+ infers which template you mean. A workspace with more than one template must either name
26
+ one or pass `--all`.
27
+
28
+ ### What it needs installed
29
+
30
+ ```bash
31
+ npm i -D typescript eslint @typescript-eslint/parser esbuild
32
+ ```
33
+
34
+ `check` runs **your** toolchain, not a copy of its own: it resolves TypeScript, ESLint
35
+ and esbuild from your workspace, so the compiler that judges your types is the one you
36
+ pinned, and a linter and a bundler never end up in the production install of a package
37
+ whose job is to render sections. A missing one is reported by name, with the install
38
+ line to fix it.
39
+
40
+ ## What it runs
41
+
42
+ Two granularities, because a workspace has one `package.json` and one lockfile, but can
43
+ hold many templates.
44
+
45
+ **Six authoring stages**, run in order, once per selected template. None of them abort
46
+ the run — a template with several unrelated problems reports all of them in one pass, not
47
+ one class of fix at a time:
48
+
49
+ 1. **typecheck** — the workspace's own TypeScript compiles the template's files, judged
50
+ against the workspace's own `tsconfig.json`.
51
+ 2. **lint** — the workspace's own ESLint, running the kit's shipped preset over the
52
+ template's files. See [the ESLint preset](eslint.md) for the rules this stage shares
53
+ with your editor.
54
+ 3. **validate** — the schema, fill-spec, fixtures, and manifest contracts: every
55
+ cross-reference between a section's contract files, and a template's `manifest.json`
56
+ against the sections it composes.
57
+ 4. **tree** — the rules a one-file-at-a-time linter cannot express: a missing contract
58
+ file, a slot declared in `schema.ts` with no marker anywhere in the section's JSX, a
59
+ hand-written CSS file's own rules.
60
+ 5. **render** — a determinism probe (every fixture is rendered twice; the two outputs
61
+ must be byte-identical) and a scan of the rendered HTML for objective defects — a
62
+ stray `[object Object]`, a bare `null`/`undefined`/`NaN` leaf, a `src`-less `<img>` —
63
+ plus, for the `typical` fixture, that the editor's sidebar order matches the page's
64
+ real render order.
65
+ 6. **size** — every section's renderer bundle and compiled stylesheet, gzipped, against a
66
+ fixed byte budget.
67
+
68
+ **The dependency gates** run once per invocation, never once per template — they are
69
+ properties of the workspace's one lockfile and installed tree, not of any one template:
70
+ a lockfile present and satisfying `package.json`, exactly one copy of React installed,
71
+ no `npm audit` finding at high or critical severity, and every shipped dependency's
72
+ license on an allowlist.
73
+
74
+ A run that selects zero templates to check is itself a failure —
75
+ [`no-templates`](rules/no-templates.md) — never a silent pass.
76
+
77
+ ## Output
78
+
79
+ Human-readable output prints one line per template (`✓ <key>`, or `✗ <key> — N
80
+ problem(s)` followed by each finding), then any workspace-level findings from the
81
+ dependency gates, then `check passed` if everything passed.
82
+
83
+ `--json` prints one report object instead, keyed per template so a single-template
84
+ consumer (the registry, ingesting one submission) and an all-templates consumer (your
85
+ own CI) parse the same shape:
86
+
87
+ ```ts
88
+ {
89
+ ok: boolean,
90
+ kitVersion: string,
91
+ templates: {
92
+ [templateKey: string]: { ok: boolean, diagnostics: Diagnostic[] },
93
+ },
94
+ // Dependency-gate findings — evaluated once, not per template.
95
+ workspace: { ok: boolean, diagnostics: Diagnostic[] },
96
+ }
97
+ ```
98
+
99
+ Each `Diagnostic` carries a `ruleId` (`template-kit/<id>`), the `template` it belongs to,
100
+ an optional `section` and `file`/`line`, a `message`, and a `fix`.
101
+
102
+ ## Exit code
103
+
104
+ `0` iff every selected template and the workspace pass. `1` otherwise — including when
105
+ the run itself can't proceed at all (an unknown template name, no `templates/` folder
106
+ in the workspace).
107
+
108
+ ## Thresholds are not configurable
109
+
110
+ Every byte budget, severity floor, and license allowlist `check` enforces is a platform
111
+ acceptance criterion, not a per-workspace preference — there is no config file or flag
112
+ that raises them. If a template has a legitimate reason to need an exception, ask the
113
+ platform team; a workaround that quietly disables a gate will simply fail again at
114
+ submission.
115
+
116
+ ## See also
117
+
118
+ - [Rules](rules/INDEX.md) — every id `check` (and the lint preset) can print, one page
119
+ each, Rule / Reason / Fix.
120
+ - [ESLint preset](eslint.md) — the one-file-at-a-time half of the same rule set, run
121
+ directly by your editor, not only through `check`.
package/docs/eslint.md CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  purpose: The authoring-lint preset shipped at `@homepages/template-kit/eslint` — setup and scope. Per-rule reference lives in `rules/`.
3
3
  status: living
4
- related: [INDEX.md, rules/INDEX.md, islands.md, theme-and-css.md, primitives.md]
4
+ related: [INDEX.md, rules/INDEX.md, check.md, islands.md, theme-and-css.md, primitives.md]
5
5
  updated: 2026-07-14
6
6
  ---
7
7
  # ESLint preset
@@ -12,6 +12,10 @@ rule namespace.
12
12
 
13
13
  ## Setup
14
14
 
15
+ ```bash
16
+ npm i -D eslint @typescript-eslint/parser
17
+ ```
18
+
15
19
  ```js
16
20
  // eslint.config.mjs
17
21
  import kit from "@homepages/template-kit/eslint";
@@ -19,9 +23,15 @@ export default kit;
19
23
  ```
20
24
 
21
25
  That is the whole setup — no plugin registration, no parser wiring, no tsconfig.
22
- The preset ships its own parser (`@typescript-eslint/parser`, a real `dependency`
23
- of the kit, not a devDependency) and sets `languageOptions` itself, so `npm i -D
24
- eslint` plus the three lines above is a complete consumer config.
26
+ The preset registers its rules and sets `languageOptions` (parser included) itself,
27
+ so those two lines are a complete config.
28
+
29
+ You install ESLint and the parser yourself because the kit deliberately does **not**
30
+ depend on them: a package whose main entry is a set of React primitives must not drag
31
+ a linter into the production install of every app that renders a template. The preset
32
+ resolves the parser from your workspace when you import it. If you see
33
+ `Cannot find package '@typescript-eslint/parser'`, that is the install line above
34
+ missing.
25
35
 
26
36
  The preset is **syntactic only** — it never sets `parserOptions.project` or
27
37
  `projectService`. Nothing it enforces requires type information, which is what
@@ -36,7 +46,7 @@ three config entries, matched by glob rather than by directory depth:
36
46
 
37
47
  | Entry | Files | Rules |
38
48
  |---|---|---|
39
- | `template-kit/authoring` | `**/*.ts`, `**/*.tsx` | the nine file-scoped rules — see [`rules/`](rules/INDEX.md) |
49
+ | `template-kit/authoring` | `**/*.ts`, `**/*.tsx` | the eleven file-scoped rules — see [`rules/`](rules/INDEX.md) |
40
50
  | `template-kit/contract-files` | `**/Renderer.tsx`, `**/schema.ts`, `**/fill-spec.ts`, `**/fixtures.ts` | `no-client-directive-in-contract` |
41
51
  | `template-kit/renderer` | `**/Renderer.tsx` | `props-from-schema` |
42
52
 
@@ -50,6 +60,6 @@ Every rule the preset enforces — what it bans, why, and a before/after fix —
50
60
  documented once, per id, in [`rules/`](rules/INDEX.md). Rules scoped to a single
51
61
  file are enforced here, by this preset; rules scoped to the **tree** — a missing
52
62
  contract file, a slot declared in `schema.ts` but unmarked in `Renderer.tsx`, a CSS
53
- budget — cannot be expressed one-file-at-a-time and belong to `template-kit check`
54
- under the same `template-kit/<id>` namespace. One namespace, two venues: the id
55
- printed by either failure resolves to exactly one page.
63
+ budget — cannot be expressed one-file-at-a-time and belong to
64
+ [`template-kit check`](check.md) under the same `template-kit/<id>` namespace. One
65
+ namespace, two venues: the id printed by either failure resolves to exactly one page.
package/docs/llms.txt CHANGED
@@ -4,9 +4,11 @@
4
4
  > inside the npm package so an agent can read it locally, versioned in lockstep
5
5
  > with the code it describes.
6
6
 
7
- Status: 0.0.x. The schema system, the contract primitives, and the theme tokens are
7
+ Status: 0.0.x. The schema system, the contract primitives, and the theme system are
8
8
  complete — a section is authorable end to end against the kit. The `template-kit`
9
- CLI's commands arrive with the surfaces they drive.
9
+ CLI's `check` and `theme` commands are implemented see [Check](check.md) and
10
+ [Theme and CSS](theme-and-css.md#generating-themecss); its other commands arrive with
11
+ the surfaces they drive.
10
12
 
11
13
  Everything an author writes a section against is imported from the package root —
12
14
  `@homepages/template-kit`, no subpath imports. The two CSS entries
@@ -28,15 +30,25 @@ Everything an author writes a section against is imported from the package root
28
30
  contract (`ATTR_SLOT_ID`, `ATTR_SLOT_ITEM`, …). Presentation is the template's own JSX.
29
31
  - [Theme and CSS](theme-and-css.md): the two CSS entries and how they load; import
30
32
  Tailwind exactly once; `base.css` ships no reset of its own — it requires the
31
- preflight your Tailwind entry brings; `TokenTheme` + `compileThemeToCss`; the breakpoint ladder
32
- (base 350, `bp390` `bp425` `bp512` `md` `lg` `bp1440` `bp1920`); the opt-in
33
- `content-visibility` recipe for long, image-heavy pages, and why it requires a
34
- full-screen overlay to be appended to `document.body` rather than rendered inside
35
- the section.
33
+ preflight your Tailwind entry brings; `TokenTheme` a template declares its own
34
+ tokens under names it chooses (`colors` `fonts` `type` `radii` `shadows` `layout`,
35
+ kebab-case, emitted verbatim) and each becomes a Tailwind utility; the `template-kit
36
+ theme` CLI command that compiles `theme.ts` to the generated `theme.css` the
37
+ Tailwind entry `@import`s after the kit's `styles.css`; the required `document`
38
+ block; the breakpoint ladder (base 350, `bp390` `bp425` `bp512` `md` `lg` `bp1440`
39
+ `bp1920`); the opt-in `content-visibility` recipe for long, image-heavy pages, and
40
+ why it requires a full-screen overlay to be appended to `document.body` rather than
41
+ rendered inside the section.
36
42
  - [README](../README.md): install, exports, and the release flow.
37
43
  - [Islands](islands.md): interactivity — client components, the serializable-props contract, editor options, and the browser loader.
38
- - [ESLint preset](eslint.md): `@homepages/template-kit/eslint`, the zero-config flat-config
39
- preset setup and its three-entry glob scope. Per-rule reference lives in `rules/`.
44
+ - [ESLint preset](eslint.md): `@homepages/template-kit/eslint`, the flat-config preset —
45
+ a two-line config with no parser wiring, over an ESLint + `@typescript-eslint/parser`
46
+ install of your own (the kit depends on neither — both resolve from your workspace).
47
+ Its three-entry glob scope. Per-rule reference lives in `rules/`.
48
+ - [Check](check.md): the `template-kit check` CLI command — the toolchain it runs (your
49
+ TypeScript, ESLint and esbuild, resolved from your workspace), its six per-template
50
+ stages, the once-per-invocation dependency gates, `--all`/`--json`, the report shape,
51
+ and the exit code. Thresholds are fixed platform acceptance criteria, not configurable.
40
52
  - [Rules](rules/INDEX.md): every rule-id a lint or `check` failure can print, under the
41
53
  `template-kit/<id>` namespace, one page each at `docs/rules/<id>.md` — Rule / Reason /
42
54
  Fix / Before→After. **The loop:** see an id in a failure → read `docs/rules/<id>.md` →
@@ -176,6 +176,11 @@ When `src` is empty the wrapper renders alone as a neutral `role="img"` rect, ke
176
176
  layout box and the slot selectable. **That deterministic empty state is the reason the
177
177
  frame exists**, so pass `aspectRatio` rather than sizing the image from its own content.
178
178
 
179
+ The frame's placeholder fill rides on `.tr-image-frame` (from
180
+ [`base.css`](theme-and-css.md#basecss-requires-a-tailwind-entry)), not on a theme utility:
181
+ you name your own tokens, so no primitive may assume a colour of yours exists. Retint it
182
+ per template by setting `--tr-image-frame-bg`; it defaults to a neutral grey.
183
+
179
184
  **Responsive.** Given a `responsive` descriptor the `<img>` is wrapped in `<picture>` with
180
185
  per-format AVIF/WebP `<source srcset>` (the base raster stays on the `<img>`), plus a
181
186
  media-gated mobile crop when `responsiveMobile` + `mobileBreakpoint` are set, and carries
@@ -1,7 +1,7 @@
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]
4
+ related: [server-vs-client.md, ../eslint.md, ../check.md, ../INDEX.md]
5
5
  updated: 2026-07-14
6
6
  ---
7
7
  # Rules
@@ -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 | A binary asset file sits in a section folder — not supported yet. |
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,11 @@ 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
+ | [`no-templates`](no-templates.md) | check | The workspace has no template to check at all. |
53
59
 
54
60
  ## See also
55
61
 
56
62
  - [Server vs client](server-vs-client.md) — how a file is scoped, and which rules stop at
57
63
  the hydration boundary.
58
64
  - [ESLint preset](../eslint.md) — setting the preset up in a template workspace.
65
+ - [Check](../check.md) — running `template-kit check`, its flags, output shape, and exit code.
@@ -0,0 +1,102 @@
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]
5
+ updated: 2026-07-14
6
+ ---
7
+ # template-kit/bundle-binary-asset
8
+
9
+ > A section folder holds code, not static files. A binary asset dropped into it has no
10
+ > build meaning yet.
11
+
12
+ ## Rule
13
+
14
+ No file inside a section folder may be a binary asset: an image, a font, a video, an
15
+ audio file, an archive, a document. The check looks two ways:
16
+
17
+ - **By extension** — `.svg`, `.png`, `.jpg`/`.jpeg`, `.gif`, `.webp`, `.avif`, `.ico`,
18
+ `.woff`/`.woff2`, `.ttf`, `.otf`, `.eot`, `.mp4`, `.webm`, `.mov`, `.mp3`, `.wav`,
19
+ `.pdf`, `.zip`.
20
+ - **By content** — any other file whose first bytes contain a NUL byte, catching an
21
+ asset type the extension list doesn't name.
22
+
23
+ This is scoped to the section folder itself (`Renderer.tsx`, `schema.ts`, `components/`,
24
+ everything under it) — not to the photos a listing supplies. Those arrive at runtime
25
+ through a slot's `src`, never as a file that ships inside your section.
26
+
27
+ ## Reason
28
+
29
+ A section folder is published as source: its content hash covers every file in it, and
30
+ the platform has no pipeline yet to fingerprint, optimize, or serve a binary file that
31
+ lives alongside your code. A `.png` committed today has no URL tomorrow — there is
32
+ nowhere for it to be requested from.
33
+
34
+ Everything a section needs to draw — an icon, a divider graphic, a background pattern —
35
+ is expressible as markup instead: an inline `<svg>`, or a shape built from CSS. That
36
+ markup is text, ships with the rest of your JSX, and needs no pipeline at all.
37
+
38
+ ## Fix
39
+
40
+ Delete the binary file and inline its content as SVG markup — either directly in the
41
+ JSX that used it, or as its own small component if more than one place needs it.
42
+
43
+ ### Before
44
+
45
+ ```text
46
+ sections/hero/
47
+ Renderer.tsx
48
+ schema.ts
49
+ fill-spec.ts
50
+ fixtures.ts
51
+ assets/
52
+ arrow.svg ← binary asset in the bundle
53
+ ```
54
+
55
+ ```tsx
56
+ // sections/hero/Renderer.tsx
57
+ import arrowUrl from "./assets/arrow.svg";
58
+
59
+ export function Renderer() {
60
+ return <img src={arrowUrl} alt="" className="h-4 w-4" />;
61
+ }
62
+ ```
63
+
64
+ ### After
65
+
66
+ ```text
67
+ sections/hero/
68
+ Renderer.tsx
69
+ schema.ts
70
+ fill-spec.ts
71
+ fixtures.ts
72
+ components/
73
+ ScrollArrow.tsx ← markup, not a binary file
74
+ ```
75
+
76
+ ```tsx
77
+ // sections/hero/components/ScrollArrow.tsx
78
+ export function ScrollArrow() {
79
+ return (
80
+ <svg viewBox="0 0 16 16" className="h-4 w-4" fill="none" aria-hidden="true">
81
+ <path d="M8 3v10M3 9l5 4 5-4" stroke="currentColor" strokeWidth="1.5" />
82
+ </svg>
83
+ );
84
+ }
85
+ ```
86
+
87
+ ```tsx
88
+ // sections/hero/Renderer.tsx
89
+ import { ScrollArrow } from "./components/ScrollArrow.js";
90
+
91
+ export function Renderer() {
92
+ return <ScrollArrow />;
93
+ }
94
+ ```
95
+
96
+ ## See also
97
+
98
+ - [`bundle-incomplete`](bundle-incomplete.md) — the other rule on the folder's contents,
99
+ checked by the same pass over the section anatomy.
100
+ - [Contract primitives](../primitives.md) — `Image`, the primitive that owns every photo
101
+ a listing supplies. It reads a `src` at runtime; it is not a way to ship a binary file
102
+ inside your section.