@homepages/template-kit 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/CHANGELOG.md +84 -0
  2. package/README.md +39 -4
  3. package/dist/asset-modules.d.ts +81 -0
  4. package/dist/cli/check/config.js +5 -1
  5. package/dist/cli/check/css.js +27 -13
  6. package/dist/cli/check/loader.js +4 -0
  7. package/dist/cli/check/section-assets.js +58 -0
  8. package/dist/cli/check/stages/size.js +124 -8
  9. package/dist/cli/check/stages/tree.js +26 -27
  10. package/dist/cli/dev/build-css.js +91 -0
  11. package/dist/cli/dev/catalog.js +35 -0
  12. package/dist/cli/dev/compose-template.js +38 -0
  13. package/dist/cli/dev/content-override.js +58 -0
  14. package/dist/cli/dev/discover.js +35 -0
  15. package/dist/cli/dev/fill-state.js +75 -0
  16. package/dist/cli/dev/index.js +32 -0
  17. package/dist/cli/dev/inspect.js +38 -0
  18. package/dist/cli/dev/island-bootstrap.js +38 -0
  19. package/dist/cli/dev/island-map.js +35 -0
  20. package/dist/cli/dev/island-transform.js +31 -0
  21. package/dist/cli/dev/manifest-instances.js +31 -0
  22. package/dist/cli/dev/render-section.js +24 -0
  23. package/dist/cli/dev/screenshot-target.js +24 -0
  24. package/dist/cli/dev/section-page.js +54 -0
  25. package/dist/cli/dev/server.js +480 -0
  26. package/dist/cli/dev/slot-schema.js +12 -0
  27. package/dist/cli/dev/structure-summary.js +118 -0
  28. package/dist/cli/dev/tailwind.js +32 -0
  29. package/dist/cli/dev/vite-server.js +33 -0
  30. package/dist/cli/dev/workspace.js +63 -0
  31. package/dist/cli/link/index.js +74 -0
  32. package/dist/cli/link/overlay.js +59 -0
  33. package/dist/cli/link/watch.js +25 -0
  34. package/dist/cli/new/emit.js +48 -0
  35. package/dist/cli/new/index.js +69 -0
  36. package/dist/cli/new/scaffold/section/_Renderer.tsx +20 -0
  37. package/dist/cli/new/scaffold/section/_fill-spec.ts +18 -0
  38. package/dist/cli/new/scaffold/section/_fixtures.ts +13 -0
  39. package/dist/cli/new/scaffold/section/_schema.ts +24 -0
  40. package/dist/cli/new/scaffold/template/_manifest.json +9 -0
  41. package/dist/cli/new/scaffold/template/sections/hero/ExpandableText.tsx +45 -0
  42. package/dist/cli/new/scaffold/template/sections/hero/Renderer.tsx +40 -0
  43. package/dist/cli/new/scaffold/template/sections/hero/components/Feature.tsx +14 -0
  44. package/dist/cli/new/scaffold/template/sections/hero/fill-spec.ts +29 -0
  45. package/dist/cli/new/scaffold/template/sections/hero/fixtures.ts +41 -0
  46. package/dist/cli/new/scaffold/template/sections/hero/schema.ts +60 -0
  47. package/dist/cli/new/scaffold/template/theme.css +24 -0
  48. package/dist/cli/new/scaffold/template/theme.ts +27 -0
  49. package/dist/cli/new/scaffold-assets.js +20 -0
  50. package/dist/cli/pack/collect.js +44 -0
  51. package/dist/cli/pack/guards.js +57 -0
  52. package/dist/cli/pack/index.js +66 -0
  53. package/dist/cli/pack/manifest.js +15 -0
  54. package/dist/cli/pack/zip.js +86 -0
  55. package/dist/cli.js +80 -14
  56. package/dist/dev-client/assets/index-DfEfrp6P.css +1 -0
  57. package/dist/dev-client/assets/index-bpMP0b0W.js +50 -0
  58. package/dist/dev-client/index.html +13 -0
  59. package/dist/eslint/is-client-file.d.ts +4 -0
  60. package/dist/index.d.ts +1 -0
  61. package/dist/islands/detect.d.ts +16 -0
  62. package/dist/islands/detect.js +22 -0
  63. package/dist/islands/discover.d.ts +5 -0
  64. package/dist/islands/discover.js +19 -0
  65. package/dist/islands/esbuild-plugin.d.ts +13 -0
  66. package/dist/islands/esbuild-plugin.js +39 -0
  67. package/dist/islands/wrap.d.ts +12 -0
  68. package/dist/islands/wrap.js +48 -0
  69. package/dist/package.js +1 -1
  70. package/dist/ssr.d.ts +31 -0
  71. package/dist/ssr.js +46 -0
  72. package/docs/INDEX.md +9 -1
  73. package/docs/assets.md +128 -0
  74. package/docs/check.md +6 -3
  75. package/docs/dev.md +187 -0
  76. package/docs/islands.md +5 -2
  77. package/docs/llms.txt +41 -3
  78. package/docs/new.md +27 -0
  79. package/docs/overview.md +57 -0
  80. package/docs/pack.md +46 -0
  81. package/docs/primitives.md +5 -6
  82. package/docs/quickstart.md +102 -0
  83. package/docs/recipes/INDEX.md +27 -0
  84. package/docs/recipes/bind-property-fact.md +58 -0
  85. package/docs/recipes/collection-slot.md +100 -0
  86. package/docs/recipes/fill-spec-decision.md +66 -0
  87. package/docs/recipes/fixture-states.md +68 -0
  88. package/docs/recipes/image-slot-crop.md +98 -0
  89. package/docs/recipes/interactive-island.md +100 -0
  90. package/docs/recipes/organize-section-folder.md +72 -0
  91. package/docs/recipes/prepare-submission.md +55 -0
  92. package/docs/recipes/second-template.md +49 -0
  93. package/docs/recipes/select-slot.md +59 -0
  94. package/docs/recipes/static-asset.md +103 -0
  95. package/docs/recipes/third-party-package.md +90 -0
  96. package/docs/rules/INDEX.md +4 -2
  97. package/docs/rules/bundle-binary-asset.md +33 -52
  98. package/docs/rules/bundle-incomplete.md +2 -2
  99. package/docs/rules/css-reason.md +3 -3
  100. package/docs/rules/no-bare-css-import.md +8 -8
  101. package/docs/rules/no-css-import-from-render-path.md +7 -8
  102. package/docs/rules/size-assets.md +88 -0
  103. package/docs/rules/size-island-bundle.md +123 -0
  104. package/docs/rules/size-section-css.md +19 -14
  105. package/docs/schema-system.md +28 -28
  106. package/docs/theme-and-css.md +2 -2
  107. package/docs/vocabulary.md +98 -0
  108. package/package.json +12 -3
  109. package/tsconfig.json +1 -1
@@ -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.
@@ -2,7 +2,7 @@
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
4
  related: [server-vs-client.md, ../eslint.md, ../check.md, ../INDEX.md]
5
- updated: 2026-07-14
5
+ updated: 2026-07-15
6
6
  ---
7
7
  # Rules
8
8
 
@@ -34,7 +34,7 @@ and it is explained there rather than on each of them.
34
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. |
37
+ | [`bundle-binary-asset`](bundle-binary-asset.md) | check | An unreferenced binary asset sits in a section folder. |
38
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
39
  | [`fixtures-invalid`](fixtures-invalid.md) | check | A section's fixtures.ts does not satisfy the fixture-module shape. |
40
40
  | [`manifest-invalid`](manifest-invalid.md) | check | A template's manifest.json is missing, unparseable, or violates the manifest contract. |
@@ -55,6 +55,8 @@ and it is explained there rather than on each of them.
55
55
  | [`license-denied`](license-denied.md) | check | A production dependency's license is not on the allowlist. |
56
56
  | [`size-renderer-bundle`](size-renderer-bundle.md) | check | A section's renderer bundle exceeds its byte budget. |
57
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. |
58
60
  | [`no-templates`](no-templates.md) | check | The workspace has no template to check at all. |
59
61
 
60
62
  ## See also
@@ -1,44 +1,43 @@
1
1
  ---
2
2
  purpose: The template-kit/bundle-binary-asset authoring rule — what it enforces, why, and how to fix it.
3
3
  status: living
4
- related: [INDEX.md, bundle-incomplete.md, ../primitives.md]
5
- updated: 2026-07-14
4
+ related: [INDEX.md, bundle-incomplete.md, ../primitives.md, ../assets.md]
5
+ updated: 2026-07-16
6
6
  ---
7
7
  # template-kit/bundle-binary-asset
8
8
 
9
- > A section folder holds code, not static files. A binary asset dropped into it has no
10
- > build meaning yet.
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
11
 
12
12
  ## Rule
13
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:
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:
16
18
 
17
19
  - **By extension** — `.svg`, `.png`, `.jpg`/`.jpeg`, `.gif`, `.webp`, `.avif`, `.ico`,
18
20
  `.woff`/`.woff2`, `.ttf`, `.otf`, `.eot`, `.mp4`, `.webm`, `.mov`, `.mp3`, `.wav`,
19
21
  `.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.
22
+ - **By content** — any other file whose first bytes contain a NUL byte.
26
23
 
27
24
  ## Reason
28
25
 
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.
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.
37
31
 
38
32
  ## Fix
39
33
 
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.
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.
42
41
 
43
42
  ### Before
44
43
 
@@ -49,32 +48,25 @@ sections/hero/
49
48
  fill-spec.ts
50
49
  fixtures.ts
51
50
  assets/
52
- arrow.svg ← binary asset in the bundle
51
+ crest.svg ← nothing imports or url()-references this file
53
52
  ```
54
53
 
54
+ ### After
55
+
55
56
  ```tsx
56
57
  // sections/hero/Renderer.tsx
57
- import arrowUrl from "./assets/arrow.svg";
58
+ import { Image } from "@homepages/template-kit";
59
+ import crest from "./assets/crest.svg";
58
60
 
59
- export function Renderer() {
60
- return <img src={arrowUrl} alt="" className="h-4 w-4" />;
61
+ export default function Renderer() {
62
+ return <Image src={crest} alt="" />;
61
63
  }
62
64
  ```
63
65
 
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
- ```
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:
75
68
 
76
69
  ```tsx
77
- // sections/hero/components/ScrollArrow.tsx
78
70
  export function ScrollArrow() {
79
71
  return (
80
72
  <svg viewBox="0 0 16 16" className="h-4 w-4" fill="none" aria-hidden="true">
@@ -84,19 +76,8 @@ export function ScrollArrow() {
84
76
  }
85
77
  ```
86
78
 
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
79
  ## See also
97
80
 
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.
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 {
@@ -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
 
@@ -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.