@homepages/template-kit 0.3.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 (111) hide show
  1. package/CHANGELOG.md +71 -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 +11 -0
  68. package/dist/islands/wrap.js +48 -0
  69. package/dist/node_modules/@jridgewell/sourcemap-codec/dist/sourcemap-codec.js +77 -0
  70. package/dist/node_modules/magic-string/dist/magic-string.es.js +939 -0
  71. package/dist/package.js +1 -1
  72. package/dist/ssr.d.ts +31 -0
  73. package/dist/ssr.js +46 -0
  74. package/docs/INDEX.md +9 -1
  75. package/docs/assets.md +128 -0
  76. package/docs/check.md +6 -3
  77. package/docs/dev.md +187 -0
  78. package/docs/islands.md +5 -2
  79. package/docs/llms.txt +41 -3
  80. package/docs/new.md +27 -0
  81. package/docs/overview.md +57 -0
  82. package/docs/pack.md +46 -0
  83. package/docs/primitives.md +5 -6
  84. package/docs/quickstart.md +102 -0
  85. package/docs/recipes/INDEX.md +27 -0
  86. package/docs/recipes/bind-property-fact.md +58 -0
  87. package/docs/recipes/collection-slot.md +100 -0
  88. package/docs/recipes/fill-spec-decision.md +66 -0
  89. package/docs/recipes/fixture-states.md +68 -0
  90. package/docs/recipes/image-slot-crop.md +98 -0
  91. package/docs/recipes/interactive-island.md +100 -0
  92. package/docs/recipes/organize-section-folder.md +72 -0
  93. package/docs/recipes/prepare-submission.md +55 -0
  94. package/docs/recipes/second-template.md +49 -0
  95. package/docs/recipes/select-slot.md +59 -0
  96. package/docs/recipes/static-asset.md +103 -0
  97. package/docs/recipes/third-party-package.md +90 -0
  98. package/docs/rules/INDEX.md +4 -2
  99. package/docs/rules/bundle-binary-asset.md +33 -52
  100. package/docs/rules/bundle-incomplete.md +2 -2
  101. package/docs/rules/css-reason.md +3 -3
  102. package/docs/rules/no-bare-css-import.md +8 -8
  103. package/docs/rules/no-css-import-from-render-path.md +7 -8
  104. package/docs/rules/size-assets.md +88 -0
  105. package/docs/rules/size-island-bundle.md +123 -0
  106. package/docs/rules/size-section-css.md +19 -14
  107. package/docs/schema-system.md +28 -28
  108. package/docs/theme-and-css.md +2 -2
  109. package/docs/vocabulary.md +98 -0
  110. package/package.json +11 -3
  111. package/tsconfig.json +1 -1
@@ -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.
@@ -0,0 +1,123 @@
1
+ ---
2
+ purpose: The template-kit/size-island-bundle authoring rule — what it enforces, why, and how to fix it.
3
+ status: living
4
+ related: [INDEX.md, size-renderer-bundle.md, size-section-css.md, server-vs-client.md]
5
+ updated: 2026-07-15
6
+ ---
7
+ # template-kit/size-island-bundle
8
+
9
+ > Each island's browser bundle must be **≤ 50 KB gzip**. The budget is not
10
+ > author-overridable, and it applies per island, not per section.
11
+
12
+ ## Rule
13
+
14
+ An island — a component whose file opens with the `"use client"` directive (see
15
+ [Server vs client](server-vs-client.md)) — ships to the browser as its own bundle,
16
+ separate from the section's server-rendered HTML. Each island is bundled with esbuild
17
+ and minified, with **React treated as external** (the page's runtime chunk supplies it
18
+ once, shared by every island on the page, and is not counted against any one of them).
19
+ The gzipped result must be **50 KB or less**.
20
+
21
+ A section can have more than one island. The budget is checked **per island**: a section
22
+ with three islands each at 40 KB passes; a section with one island at 60 KB fails, even
23
+ though the same section's other files are small.
24
+
25
+ The budget is **visible in `template-kit check`'s configuration and is not
26
+ author-overridable**. A template workspace cannot raise it, and there is no per-island
27
+ exemption.
28
+
29
+ ## Reason
30
+
31
+ An island is the part of the page that has to work on a phone before the visitor does
32
+ anything — a carousel, a filter, a form. Every one of its bytes is downloaded, parsed,
33
+ and executed on the client, on top of whatever the page's shared runtime already costs.
34
+ Unlike the section's server-rendered markup, which the browser only has to paint, an
35
+ island's code has to run.
36
+
37
+ The number is set to be generous, so it does not fight ordinary authoring — a real
38
+ interactive widget fits comfortably inside it. You hit the budget when something
39
+ **runaway** gets pulled into the client bundle:
40
+
41
+ - **a whole UI or icon library imported for one component**, which drags every export
42
+ into the bundle so the island can use a handful of them;
43
+ - **a heavy dependency that belongs on the server**, ported into the island because it
44
+ was easiest to reach for where the code was being written, not because the browser
45
+ needs it.
46
+
47
+ That is the shape of essentially every violation: not an island that grew, but a single
48
+ import that was more expensive than it looked.
49
+
50
+ ## Fix
51
+
52
+ Find the import that is not paying for itself, or narrow what ships to the browser at
53
+ all.
54
+
55
+ - **Import named symbols, never a namespace.** `import { ArrowRight } from "…"` is
56
+ tree-shakeable; `import * as Icons from "…"` is not — a namespace object has to contain
57
+ everything, so nothing can be dropped.
58
+ - **Prefer the package's narrow entry** if it offers one, instead of its default
59
+ "everything" bundle.
60
+ - **Move what doesn't need to be interactive out of the island.** If only a small part of
61
+ a component actually needs client-side behavior — an expand/collapse toggle inside an
62
+ otherwise static block — split the interactive part into its own small island and leave
63
+ the rest server-rendered. Server-rendered markup costs nothing against this budget.
64
+ - **Hand-write the residue.** If you use one function out of a large utility package, copy
65
+ the function instead of importing the package for it.
66
+
67
+ ### Before
68
+
69
+ ```tsx
70
+ // sections/gallery/Carousel.tsx
71
+ "use client";
72
+
73
+ import * as Icons from "lucide-react"; // ← the whole set: every icon, in this island's bundle
74
+ import { useState } from "react";
75
+
76
+ export default function Carousel({ slides }: { slides: { id: string; url: string }[] }) {
77
+ const [index, setIndex] = useState(0);
78
+ return (
79
+ <div>
80
+ <img src={slides[index]?.url} alt="" />
81
+ <button onClick={() => setIndex((i) => (i - 1 + slides.length) % slides.length)}>
82
+ <Icons.ChevronLeft aria-hidden />
83
+ </button>
84
+ <button onClick={() => setIndex((i) => (i + 1) % slides.length)}>
85
+ <Icons.ChevronRight aria-hidden />
86
+ </button>
87
+ </div>
88
+ );
89
+ }
90
+ ```
91
+
92
+ ### After
93
+
94
+ ```tsx
95
+ // sections/gallery/Carousel.tsx
96
+ "use client";
97
+
98
+ import { ChevronLeft, ChevronRight } from "lucide-react"; // ← two icons, and the bundler can drop the rest
99
+ import { useState } from "react";
100
+
101
+ export default function Carousel({ slides }: { slides: { id: string; url: string }[] }) {
102
+ const [index, setIndex] = useState(0);
103
+ return (
104
+ <div>
105
+ <img src={slides[index]?.url} alt="" />
106
+ <button onClick={() => setIndex((i) => (i - 1 + slides.length) % slides.length)}>
107
+ <ChevronLeft aria-hidden />
108
+ </button>
109
+ <button onClick={() => setIndex((i) => (i + 1) % slides.length)}>
110
+ <ChevronRight aria-hidden />
111
+ </button>
112
+ </div>
113
+ );
114
+ }
115
+ ```
116
+
117
+ ## See also
118
+
119
+ - [`size-renderer-bundle`](size-renderer-bundle.md) — the same budget on the section's
120
+ server-rendered bundle.
121
+ - [`size-section-css`](size-section-css.md) — the same budget on the section's stylesheet.
122
+ - [Server vs client](server-vs-client.md) — what makes a file an island in the first
123
+ place.
@@ -1,8 +1,8 @@
1
1
  ---
2
2
  purpose: The template-kit/size-section-css authoring rule — what it enforces, why, and how to fix it.
3
3
  status: living
4
- related: [INDEX.md, size-renderer-bundle.md, ../theme-and-css.md]
5
- updated: 2026-07-14
4
+ related: [INDEX.md, size-renderer-bundle.md, size-assets.md, ../theme-and-css.md]
5
+ updated: 2026-07-15
6
6
  ---
7
7
  # template-kit/size-section-css
8
8
 
@@ -16,7 +16,7 @@ everything that lands in the layer:
16
16
 
17
17
  - the section's hand-written CSS, and
18
18
  - **every package stylesheet its module graph imports** — the `import "swiper/css"` in a
19
- component, an island, or a marked enhancer.
19
+ component or an island.
20
20
 
21
21
  The budget is **visible in `template-kit check`'s configuration and is not
22
22
  author-overridable**. A workspace cannot raise it, and there is no per-section exemption.
@@ -29,14 +29,15 @@ CSS blocks the first paint.
29
29
 
30
30
  "Including package CSS" is not a technicality; it is the failure this rule exists to catch.
31
31
  Your own stylesheet is almost never the problem — it is a few kilobytes of overrides. The
32
- package's is. And the reason it is invisible to you is the mechanism: a package stylesheet's
33
- `url()` references its icon sprites, its webfont are **inlined as `data:` URIs** into the
34
- compiled stylesheet. A 300 KB font file arrives in your published CSS without you ever
35
- having seen a 300 KB file. Nothing in your source looks large. Nothing in the package's
36
- `dist` listing looks alarming. The compiled layer is 400 KB.
32
+ package's is. A one-line `import "some-ui/dist.css"` can drag in thousands of rules for
33
+ components you never render resets, theme variants, every widget's styles and they all
34
+ land in your compiled layer. Nothing in your source looks large; the compiled layer is
35
+ 400 KB.
37
36
 
38
37
  So the number this check reports is frequently the first time anyone finds out what a
39
- one-line CSS import actually cost.
38
+ one-line CSS import actually cost. (What this budget does **not** count is asset bytes: a
39
+ `url()`-referenced webfont or icon file resolves to a cached URL and is weighed separately —
40
+ see [`size-assets`](size-assets.md).)
40
41
 
41
42
  ## Fix
42
43
 
@@ -45,10 +46,11 @@ Three moves, in order of how often they work:
45
46
  1. **Import the slimmest entry the package offers.** Most CSS-shipping packages have a core
46
47
  stylesheet plus opt-in modules, *and* a fat "everything" bundle that is the one you get
47
48
  from copying the quickstart. Take the core, plus only the modules you use.
48
- 2. **Cut the webfont.** If the package's stylesheet drags in a font for its icons, and you
49
- are using three of those icons, replace them with inline SVG — or with a family the
50
- template's theme already declares in `fonts` (`--tr-font-<name>`), which costs nothing
51
- because it is already loaded.
49
+ 2. **Drop the icon font.** If the package's stylesheet declares an icon font and you use
50
+ three of its glyphs, replace them with inline SVG — or with a family the template's theme
51
+ already declares in `fonts` (`--tr-font-<name>`), which costs nothing because it is
52
+ already loaded. (The font *file* is weighed under [`size-assets`](size-assets.md), but its
53
+ `@font-face` rules and every `.icon-*` class still land in this layer.)
52
54
  3. **Drop the package and hand-write the residue.** If you use one component from a large
53
55
  widget library, its stylesheet is paying for components you never render.
54
56
 
@@ -83,12 +85,15 @@ export default function Carousel({ slides }: { slides: { id: string; url: string
83
85
  ```
84
86
 
85
87
  The arrows and dots that came from the bundle's icon font are drawn as inline SVG in the
86
- component instead, so nothing pulls a webfont into the layer.
88
+ component instead, so the layer no longer carries that font's `@font-face` and icon-class
89
+ rules (and the font file no longer counts against [`size-assets`](size-assets.md) either).
87
90
 
88
91
  ## See also
89
92
 
90
93
  - [`size-renderer-bundle`](size-renderer-bundle.md) — the same budget on the section's
91
94
  JavaScript.
95
+ - [`size-assets`](size-assets.md) — the separate budget on the static asset *files*
96
+ (fonts, images) a stylesheet or component references.
92
97
  - [`no-bare-css-import`](no-bare-css-import.md) — why a package's stylesheet is imported from
93
98
  code and never `@import`ed from CSS.
94
99
  - [Theme and CSS](../theme-and-css.md) — the fonts and tokens the page already loads, which
@@ -1,8 +1,8 @@
1
1
  ---
2
2
  purpose: The section authoring contract — schema.ts, fill-spec.ts, fixtures.ts, and the closed source/transform vocabulary.
3
3
  status: living
4
- related: [primitives.md, theme-and-css.md]
5
- updated: 2026-07-14
4
+ related: [primitives.md, theme-and-css.md, vocabulary.md]
5
+ updated: 2026-07-16
6
6
  ---
7
7
  # The schema system
8
8
 
@@ -32,22 +32,17 @@ from. Without it, `SectionProps` cannot tell a required slot from an optional on
32
32
  and your renderer props collapse to a useless widened shape.
33
33
 
34
34
  ```ts
35
- import { direct, type FrameConfig, type SectionProps, type SectionSchemaShape } from "@homepages/template-kit";
36
-
37
- export const HERO_FRAME = {
38
- breakpoint: 425,
39
- desktop: { aspect: 5 / 3 },
40
- } as const satisfies FrameConfig;
35
+ import { direct, type SectionProps, type SectionSchemaShape } from "@homepages/template-kit";
41
36
 
42
37
  export const schema = {
43
38
  meta: { displayName: "Hero" },
44
39
  slots: {
45
- headline: { type: "text", size: "medium", required: true, produced_by: "headline_text" },
46
- listing_status: { type: "select", required: false, source: direct("listing_intent") },
47
- hero_image: { type: "image", required: true, produced_by: "hero_image_assign", frame: HERO_FRAME },
40
+ hero_image: { type: "image", required: true, produced_by: "hero_image_assign", crop: { mode: "locked", aspect: 16 / 9 } },
41
+ headline: { type: "text", size: "medium", required: true, source: direct("address") },
42
+ blurb: { type: "text", size: "long", required: true, produced_by: "blurb_text" },
48
43
  },
49
44
  options: {
50
- align: { type: "enum", values: ["left", "center"], default: "left" },
45
+ align: { type: "enum", values: ["left", "center"], default: "center" },
51
46
  },
52
47
  } as const satisfies SectionSchemaShape;
53
48
 
@@ -65,14 +60,20 @@ binds it to a computed transform. Both take names from a **closed vocabulary**,
65
60
  a typo is a compile error at the call site — not a runtime surprise:
66
61
 
67
62
  ```ts
68
- source: direct("listing_intent") // ✅
69
- source: direct("listing_intentt") // ✗ compile error: not a SOURCE_FIELDS key
63
+ source: direct("address") // ✅
64
+ source: direct("addresss") // ✗ compile error: not a SOURCE_FIELDS key
70
65
  ```
71
66
 
72
- The vocabulary is the kit's, not yours. Read it off the exported tables
73
- `SOURCE_FIELDS` (facts `direct()` accepts), `TRANSFORMS` (`derived()`), and
74
- `SLOT_TYPE_DESCRIPTORS` (the slot types) or let your editor autocomplete it.
75
- Adding to it is a kit release, not a change you can make in a template.
67
+ The vocabulary is the kit's, not yours. Adding to it is a kit release, not a
68
+ change you can make in a template. See [the closed vocabulary](vocabulary.md)
69
+ for the sets and when to pick each; your editor autocompletes the exact fields.
70
+
71
+ A `select` slot binds the same way as any other type — here fact-bound rather
72
+ than AI-produced, with `values` declaring the closed set the fact resolves to:
73
+
74
+ ```ts
75
+ listing_status: { type: "select", required: false, source: direct("listing_intent"), values: ["for_sale", "for_rent"] },
76
+ ```
76
77
 
77
78
  ### Grouping slots into one editor card
78
79
 
@@ -104,7 +105,6 @@ import type { Props } from "./schema.js";
104
105
 
105
106
  export default function Hero({ slots, options, nav }: Props) {
106
107
  // headline is `string` — the schema says required: true
107
- // listing_status is `string | null` — required: false
108
108
  // options.align is `"left" | "center"` — the enum's declared values
109
109
  return (
110
110
  <Section id={nav.selfAnchor}>
@@ -153,12 +153,12 @@ export const fillSpec = {
153
153
  decisions: [
154
154
  { id: "hero_image_assign", type: "image-assign", produces: ["hero_image"], tag_query: "category:exterior" },
155
155
  {
156
- id: "headline_text",
156
+ id: "blurb_text",
157
157
  type: "text-block",
158
- produces: ["headline"],
159
- length_cap: 60,
160
- structure: "single_sentence",
161
- voice_prompt: "Confident premium-listing voice. Title Case.",
158
+ produces: ["blurb"],
159
+ length_cap: 400,
160
+ structure: "paragraph",
161
+ voice_prompt: "Warm, concrete paragraph a buyer would want to read.",
162
162
  },
163
163
  ],
164
164
  } as const satisfies FillSpecShape;
@@ -177,14 +177,14 @@ import type { FixtureModule } from "@homepages/template-kit";
177
177
  const fixtures: FixtureModule = {
178
178
  base: {
179
179
  slots: {
180
- headline: "A mansard Victorian in the heart of Jamaica Plain",
181
- listing_status: "for_sale",
180
+ headline: "101 Queensway, Jamaica Plain, MA 02130",
182
181
  hero_image: { photo_id: "p1", url: "https://example.com/a.jpg", alt: "Front elevation" },
182
+ blurb: "A bright corner home moments from the Emerald Necklace.",
183
183
  },
184
- options: { align: "left" },
184
+ options: { align: "center" },
185
185
  },
186
186
  states: {
187
- centered: { label: "Centered", options: { align: "center" } },
187
+ left: { label: "Left-aligned", options: { align: "left" } },
188
188
  },
189
189
  };
190
190