@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
@@ -0,0 +1,100 @@
1
+ ---
2
+ purpose: Make part of a section interactive with a client-hydrated island.
3
+ status: living
4
+ related: [../islands.md, ../rules/server-vs-client.md, ../rules/serializable-island-props.md]
5
+ updated: 2026-07-15
6
+ ---
7
+ # Make part of a section interactive
8
+
9
+ ## Goal
10
+
11
+ Make part of a section interactive.
12
+
13
+ ## The delta
14
+
15
+ Starting from the starter's `hero` section, add a `"use client"` file next to the
16
+ four contract files, matching the shape of the starter's own island,
17
+ `ExpandableText.tsx`: the directive as the file's first statement, a default-exported
18
+ function typed against plain props, and `useState` for the interactive part.
19
+
20
+ ```tsx
21
+ // sections/hero/ContactReveal.tsx — a new island, same shape as ExpandableText
22
+ "use client";
23
+
24
+ import { type ReactNode, useState } from "react";
25
+
26
+ import type { IslandEditorOptions } from "@homepages/template-kit";
27
+
28
+ export default function ContactReveal({
29
+ hiddenLabel,
30
+ revealedValue,
31
+ }: {
32
+ hiddenLabel: string;
33
+ revealedValue: string;
34
+ }): ReactNode {
35
+ const [revealed, setRevealed] = useState(false);
36
+ return revealed ? (
37
+ <p className="text-base font-medium text-ink">{revealedValue}</p>
38
+ ) : (
39
+ <button
40
+ type="button"
41
+ className="font-medium text-accent underline"
42
+ onClick={() => setRevealed(true)}
43
+ >
44
+ {hiddenLabel}
45
+ </button>
46
+ );
47
+ }
48
+
49
+ // Optional, as on ExpandableText: hydrate live in the editor canvas too.
50
+ export const editor: IslandEditorOptions = {
51
+ live: true,
52
+ };
53
+ ```
54
+
55
+ Render it from `Renderer.tsx` like any other component:
56
+
57
+ ```tsx
58
+ // Renderer.tsx
59
+ import ContactReveal from "./ContactReveal";
60
+ // …
61
+ <ContactReveal hiddenLabel="Show phone number" revealedValue="(555) 010-2020" />
62
+ ```
63
+
64
+ If the island renders a schema slot's own content — the way the starter's `blurb`
65
+ slot renders through `ExpandableText` — wrap it in `<Slot id="…">` with the matching
66
+ schema id, exactly as `Renderer.tsx` already does:
67
+
68
+ ```tsx
69
+ <Slot id="blurb">
70
+ <ExpandableText text={slots.blurb} collapsedChars={140} moreLabel="Read more" />
71
+ </Slot>
72
+ ```
73
+
74
+ A standalone island with no bound slot — like `ContactReveal` above — needs no
75
+ `<Slot>` wrapper; `<Slot>` marks an editable slot's DOM for the editor, not
76
+ "this is interactive."
77
+
78
+ Props crossing into an island must be JSON-serializable — strings, numbers,
79
+ booleans, and plain arrays/objects only, never a function, a `Date`, or a class
80
+ instance; the island owns its own event handlers instead of receiving one. This is
81
+ the only mechanism for interactivity in a section: a Renderer stays a pure function
82
+ of its props, and any state, effect, or event handler moves into a `"use client"`
83
+ island like the one above.
84
+
85
+ ## Commands
86
+
87
+ None — this is a pure content edit. No CLI command beyond the author loop below.
88
+
89
+ ## Verify
90
+
91
+ Verify with [the author loop](../quickstart.md#the-author-loop).
92
+
93
+ ## See also
94
+
95
+ - [Islands](../islands.md) — writing one, its props contract, editor options, and
96
+ the DOM contract a published page hydrates.
97
+ - [Server vs client](../rules/server-vs-client.md) — how a file is scoped server or
98
+ client, and the three rules an island is exempt from.
99
+ - [`serializable-island-props`](../rules/serializable-island-props.md) — the rule
100
+ that catches a non-serializable prop before the render ever throws.
@@ -0,0 +1,72 @@
1
+ ---
2
+ purpose: Structure a section's files beyond the four reserved contract files.
3
+ status: living
4
+ related: [../new.md, ../islands.md, ../pack.md, ../rules/bundle-binary-asset.md]
5
+ updated: 2026-07-16
6
+ ---
7
+ # Organize a section's files beyond the four reserved
8
+
9
+ ## Goal
10
+
11
+ Structure a section beyond the four files.
12
+
13
+ ## The delta
14
+
15
+ Starting from the starter's `hero` section, add a `components/` folder and pull a
16
+ piece of markup out into its own module — the same shape as the starter's own
17
+ `components/Feature.tsx`, which the starter's `Renderer.tsx` already imports and
18
+ renders.
19
+
20
+ ```tsx
21
+ // sections/hero/components/Feature.tsx
22
+ import type { ReactNode } from "react";
23
+
24
+ export function Feature({ label, value }: { label: string; value: string }): ReactNode {
25
+ return (
26
+ <div className="rounded-lg border border-ink/10 p-4">
27
+ <div className="text-sm text-ink/60">{label}</div>
28
+ <div className="text-lg font-semibold text-ink">{value}</div>
29
+ </div>
30
+ );
31
+ }
32
+ ```
33
+
34
+ ```tsx
35
+ // Renderer.tsx
36
+ import { Feature } from "./components/Feature";
37
+ // …
38
+ <Feature label="Status" value="For sale" />
39
+ ```
40
+
41
+ Exactly four files are reserved, and only at the section folder's root:
42
+ `schema.ts`, `fill-spec.ts`, `fixtures.ts`, `Renderer.tsx`. Everything else in the
43
+ folder is yours — `components/`, `hooks/`, `utils/`, stylesheets, any nesting, any
44
+ number of support modules, and `"use client"` islands. A binary asset (an image, a
45
+ font, an archive) may live under an `assets/` folder too, but only if something in
46
+ the section references it — see
47
+ [`bundle-binary-asset`](../rules/bundle-binary-asset.md) for the rule and what an
48
+ unreferenced one costs you.
49
+
50
+ Every file under `templates/<template>/` — not only the four reserved files — feeds
51
+ `pack`'s content hash of the template tree, stamped into `pack-manifest.json`.
52
+ Editing a component, a util, or even a comment inside a section folder changes that
53
+ hash on the next `pack`, exactly as editing `Renderer.tsx` would.
54
+
55
+ ## Commands
56
+
57
+ None — this is a pure content edit. No CLI command beyond the author loop below.
58
+
59
+ ## Verify
60
+
61
+ Verify with [the author loop](../quickstart.md#the-author-loop).
62
+
63
+ ## See also
64
+
65
+ - [Adding templates and sections](../new.md) — `template-kit new` scaffolds a
66
+ starter section that already includes a `components/` folder and an island.
67
+ - [Islands](../islands.md) — a `"use client"` file is one more kind of file the
68
+ section folder is free to hold.
69
+ - [`template-kit pack`](../pack.md) — what produces the content hash and what it
70
+ refuses to pack.
71
+ - [`bundle-binary-asset`](../rules/bundle-binary-asset.md) — when a binary file
72
+ in the folder must be referenced, and what happens if it isn't.
@@ -0,0 +1,55 @@
1
+ ---
2
+ purpose: Package a template for submission.
3
+ status: living
4
+ related: [../pack.md, ../check.md]
5
+ updated: 2026-07-15
6
+ ---
7
+ # Prepare a submission
8
+
9
+ ## Goal
10
+
11
+ Package a template for submission.
12
+
13
+ ## The delta
14
+
15
+ Get the template to a green `check`, then pack it:
16
+
17
+ ```bash
18
+ template-kit check <template>
19
+ template-kit pack <template>
20
+ ```
21
+
22
+ `pack` will not produce a zip until `check <template>` passes — it re-runs
23
+ the same gate itself, and a failing `check` reports every problem in one
24
+ pass, not one at a time, so a manual `check` first is just a faster feedback
25
+ loop, not a separate requirement.
26
+ A passing `pack` writes `<template>.zip` at the workspace root: the
27
+ template's source, `pack-manifest.json` (the kit version, a content hash,
28
+ and the `check` flag), and the workspace's `package.json` /
29
+ `package-lock.json` so the build reproduces with `npm ci`.
30
+
31
+ `pack` refuses to write a zip when:
32
+
33
+ - `check <template>` is not green — fix the reported problems first.
34
+ - the workspace has no `package-lock.json` — run `npm install` and commit it.
35
+ - a dependency uses a local `file:`/`link:` protocol — depend on a published
36
+ version instead.
37
+
38
+ Each refusal prints the cause; fix it and run `pack` again.
39
+
40
+ ## Commands
41
+
42
+ ```bash
43
+ template-kit check <template>
44
+ template-kit pack <template>
45
+ ```
46
+
47
+ ## Verify
48
+
49
+ Verify with [the author loop](../quickstart.md#the-author-loop) — `pack`
50
+ refuses to build the zip until `check` is green, so a clean loop is the gate.
51
+
52
+ ## See also
53
+
54
+ - [Pack](../pack.md) — what the zip contains and the full refusal list.
55
+ - [Check](../check.md) — the gate `pack` runs before it will produce a zip.
@@ -0,0 +1,49 @@
1
+ ---
2
+ purpose: Add another template to the workspace.
3
+ status: living
4
+ related: [../new.md, ../check.md]
5
+ updated: 2026-07-15
6
+ ---
7
+ # Add a second template
8
+
9
+ ## Goal
10
+
11
+ Add another template to the workspace.
12
+
13
+ ## The delta
14
+
15
+ ```bash
16
+ template-kit new <name>
17
+ ```
18
+
19
+ `new` creates `templates/<name>/` as a working starter template — the same
20
+ starter shape a fresh workspace ships (a text slot, an image slot, a
21
+ fact-bound slot, an option, fixtures, a fill spec, a `components/` folder, a
22
+ `"use client"` island, `theme.ts`/`theme.css`, `manifest.json`) — and it
23
+ already passes `check` as-is. Edit its sections from there.
24
+
25
+ A template is a plain folder under `templates/`. Adding one does not
26
+ duplicate anything: every template in the workspace shares the same kit
27
+ version, the same root `package.json`/lockfile, and the same
28
+ `tsconfig.json`/ESLint config. There is nothing per-template to configure
29
+ beyond the folder itself.
30
+
31
+ `new` refuses to overwrite an existing `templates/<name>/` folder.
32
+
33
+ ## Commands
34
+
35
+ ```bash
36
+ template-kit new <name>
37
+ template-kit check <name>
38
+ ```
39
+
40
+ ## Verify
41
+
42
+ Verify with [the author loop](../quickstart.md#the-author-loop) — a freshly
43
+ created template is check-clean before you change a line.
44
+
45
+ ## See also
46
+
47
+ - [Adding templates and sections](../new.md) — the full command, including
48
+ `new-section` for adding a section within a template.
49
+ - [Check](../check.md) — the gate every template (new or edited) must pass.
@@ -0,0 +1,59 @@
1
+ ---
2
+ purpose: Give the user a closed set of choices — an editable content slot, distinct from a behavioral enum option.
3
+ status: living
4
+ related: [../vocabulary.md, ../schema-system.md]
5
+ updated: 2026-07-15
6
+ ---
7
+ # A select slot — a closed set of choices
8
+
9
+ ## Goal
10
+
11
+ Give the user a closed set of choices — an editable content slot, distinct from
12
+ a behavioral `enum` option.
13
+
14
+ ## The delta
15
+
16
+ A `select` slot still needs a `source` or a `produced_by`, exactly like every
17
+ other slot type — `values` alone only declares the closed set the editor's
18
+ dropdown offers. The cleanest no-AI fill for a closed set is usually a
19
+ `direct()` fact that already resolves to one of a closed set of strings:
20
+
21
+ ```ts
22
+ // schema.ts — add a select slot, seeded from a closed-vocabulary fact
23
+ property_type: {
24
+ type: "select",
25
+ editable_by_user: true,
26
+ required: false,
27
+ values: ["single_family", "condo_townhome", "multi_family", "investment"],
28
+ source: direct("property_type"),
29
+ display_name: "Property type",
30
+ },
31
+ ```
32
+
33
+ ```tsx
34
+ // Renderer.tsx — read it like any other text-shaped slot
35
+ <Slot id="property_type" textLeaf>
36
+ <span>{slots.property_type}</span>
37
+ </Slot>
38
+ ```
39
+
40
+ `slots.property_type` is `string | null` (required is `false`). The general
41
+ `source`/`produced_by` contract still applies to a `select` slot, but no
42
+ fill-spec decision type constrains its output to the closed `values` set —
43
+ so `source: direct(...)` (a fact that already resolves within the set) is
44
+ the reliable way to fill one.
45
+
46
+ ## Commands
47
+
48
+ None — this is a pure content edit. No CLI command beyond the author loop below.
49
+
50
+ ## Verify
51
+
52
+ Verify with [the author loop](../quickstart.md#the-author-loop).
53
+
54
+ ## See also
55
+
56
+ - [The closed vocabulary](../vocabulary.md#slot-types) — the slot-type table, including
57
+ where `select` sits next to `text`/`number`/etc.
58
+ - [The schema system](../schema-system.md) — the full `schema.ts` contract, including
59
+ `direct()`/`derived()` binding.
@@ -0,0 +1,103 @@
1
+ ---
2
+ purpose: Add a graphic — an icon, a divider, a photo, a font — to a section.
3
+ status: living
4
+ related: [../assets.md, ../check.md, ../rules/bundle-binary-asset.md, image-slot-crop.md, ../theme-and-css.md]
5
+ updated: 2026-07-16
6
+ ---
7
+ # Add a static asset (image, icon, font)
8
+
9
+ ## Goal
10
+
11
+ Add a graphic — an icon, a divider mark, a photo, a custom font — to a section.
12
+
13
+ ## The delta
14
+
15
+ A binary asset — an image, a font, a video, an archive — may live in a section's own
16
+ `sections/<section>/assets/` folder, but only if the section references it: imported in
17
+ `Renderer.tsx` (`import crest from "./assets/crest.svg"`) or `url()`-referenced from one
18
+ of the section's CSS files. An unreferenced binary in the folder is flagged; see
19
+ [`bundle-binary-asset`](../rules/bundle-binary-asset.md) for the rule and why.
20
+
21
+ A file shared across sections — a logo, a brand webfont, a texture — lives at the
22
+ **template level** instead, referenced the same way with a `../../assets/…` path. See
23
+ [Static assets](../assets.md) for both paths and the full reference contract. What you
24
+ reach for depends on what the graphic is:
25
+
26
+ **A vector graphic — an icon, a divider, a decorative mark used only here.** Inline it as
27
+ SVG markup directly in the JSX that draws it. It is text, ships with the rest of your
28
+ component, and needs no file or import at all:
29
+
30
+ ```tsx
31
+ // sections/hero/components/ScrollArrow.tsx
32
+ export function ScrollArrow() {
33
+ return (
34
+ <svg viewBox="0 0 16 16" className="h-4 w-4" fill="none" aria-hidden="true">
35
+ <path d="M8 3v10M3 9l5 4 5-4" stroke="currentColor" strokeWidth="1.5" />
36
+ </svg>
37
+ );
38
+ }
39
+ ```
40
+
41
+ ```tsx
42
+ // sections/hero/Renderer.tsx
43
+ import { ScrollArrow } from "./components/ScrollArrow";
44
+ // …
45
+ <ScrollArrow />
46
+ ```
47
+
48
+ That markup's bytes ship inside the section's renderer bundle, so they count
49
+ toward the same byte budget as the rest of your JSX — see the size stage in
50
+ [Check](../check.md).
51
+
52
+ **A raster image or watermark used only in this section.** Add it under
53
+ `sections/<section>/assets/` and import it into the component:
54
+
55
+ ```tsx
56
+ // sections/hero/Renderer.tsx
57
+ import { Image } from "@homepages/template-kit";
58
+ import crest from "./assets/crest.svg";
59
+
60
+ export default function Renderer() {
61
+ return <Image src={crest} alt="" />;
62
+ }
63
+ ```
64
+
65
+ Or reference it from the section's CSS with `url()` instead of importing it — see
66
+ [Static assets](../assets.md) for both forms. Either way the file must be referenced:
67
+ an asset nothing imports or `url()`-references is flagged by
68
+ [`bundle-binary-asset`](../rules/bundle-binary-asset.md).
69
+
70
+ **A photo.** Not a section asset at all — a section never ships a photo file. A
71
+ photo arrives at runtime through an `image` slot: declare it in `schema.ts`,
72
+ produce it with an `image-assign` decision in `fill-spec.ts`, and render it
73
+ through the `<Image>` primitive, which reads `url`, `alt`, and `responsive` off
74
+ the slot's resolved value. See
75
+ [Image slot with a locked crop](image-slot-crop.md) for the full shape.
76
+
77
+ **A custom font shared across the template.** Declared once, for the whole
78
+ template, in the theme's `fonts` and optional `fontFaces` block. See
79
+ [Theme and CSS](../theme-and-css.md). A font only one section uses can instead
80
+ live in that section's `assets/` folder and be declared with a `@font-face` in
81
+ the section's own CSS — see [Static assets](../assets.md).
82
+
83
+ ## Commands
84
+
85
+ None — inlining SVG markup and declaring a slot are content edits. No CLI
86
+ command beyond the author loop below.
87
+
88
+ ## Verify
89
+
90
+ Verify with [the author loop](../quickstart.md#the-author-loop).
91
+
92
+ ## See also
93
+
94
+ - [Static assets](../assets.md) — the full reference/import contract, template
95
+ level vs. section folder.
96
+ - [`bundle-binary-asset`](../rules/bundle-binary-asset.md) — the rule that
97
+ flags an unreferenced binary in a section folder, and the SVG-inlining fix.
98
+ - [Check](../check.md) — the size stage a large inlined SVG or a bloated
99
+ renderer bundle counts against.
100
+ - [Image slot with a locked crop](image-slot-crop.md) — the real channel for a
101
+ photo: an `image` slot, an `image-assign` decision, and the `<Image>`
102
+ primitive.
103
+ - [Theme and CSS](../theme-and-css.md) — where a template's fonts are declared.
@@ -0,0 +1,90 @@
1
+ ---
2
+ purpose: Use a third-party npm package in a section.
3
+ status: living
4
+ related: [../islands.md, ../pack.md, ../rules/single-react.md, ../rules/no-bare-css-import.md, ../rules/no-css-import-from-render-path.md]
5
+ updated: 2026-07-15
6
+ ---
7
+ # Use a third-party npm package in a section
8
+
9
+ ## Goal
10
+
11
+ Use an npm package in a section.
12
+
13
+ ## The delta
14
+
15
+ Install the package like any npm dependency:
16
+
17
+ ```bash
18
+ npm i swiper
19
+ ```
20
+
21
+ **Only a published version.** `pack` refuses to produce a submission zip when a
22
+ dependency uses a local `file:`/`link:` protocol — depend on a real published
23
+ version, never a path on your own machine.
24
+
25
+ **Where you import it from matters.** A client library — a carousel, a map SDK,
26
+ anything that touches `window` or the DOM — is imported inside a `"use client"`
27
+ island, the same as any other browser-only code:
28
+
29
+ ```tsx
30
+ // sections/gallery/Carousel.tsx
31
+ "use client";
32
+
33
+ import Swiper from "swiper";
34
+
35
+ export default function Carousel({ slides }: { slides: { id: string; url: string }[] }) {
36
+ // …
37
+ }
38
+ ```
39
+
40
+ A server-side import — one that lands in the Renderer or a plain component it
41
+ imports, not behind `"use client"` — must stay pure and deterministic: no reads
42
+ of the clock, randomness, or the network at render time. A package built for a
43
+ browser runtime (it touches `window`, schedules a timer, generates an id) does
44
+ not belong there; move the usage into an island.
45
+
46
+ **A package that ships its own stylesheet** is imported by its bare specifier,
47
+ from the module that renders it — the Renderer's import graph (the components
48
+ and islands it imports, transitively) is what the CSS build bundles and
49
+ harvests for the section's stylesheet:
50
+
51
+ ```tsx
52
+ // sections/gallery/Carousel.tsx
53
+ "use client";
54
+
55
+ import Swiper from "swiper";
56
+ import "swiper/css"; // ← the sanctioned channel: a bare specifier reaches this section's layer
57
+ ```
58
+
59
+ A **local** `.css` import from that same render path is a different, banned case
60
+ — see [`no-css-import-from-render-path`](../rules/no-css-import-from-render-path.md)
61
+ for the full distinction between the two, and
62
+ [`no-bare-css-import`](../rules/no-bare-css-import.md) for why a hand-written
63
+ stylesheet can't reach the package's CSS with its own `@import` instead.
64
+
65
+ ## Commands
66
+
67
+ ```bash
68
+ npm i <package>
69
+ ```
70
+
71
+ ## Verify
72
+
73
+ Verify with [the author loop](../quickstart.md#the-author-loop) — a green
74
+ `check` after the install confirms the package resolves, its CSS (if any)
75
+ landed in the section's layer, and the workspace still carries exactly one copy
76
+ of React.
77
+
78
+ ## See also
79
+
80
+ - [Islands](../islands.md) — writing a `"use client"` island; a third-party
81
+ client library is fine inside one, never in a Renderer.
82
+ - [`template-kit pack`](../pack.md) — why a `file:`/`link:` dependency blocks a
83
+ submission.
84
+ - [`single-react`](../rules/single-react.md) — a package that pulls its own
85
+ copy of React breaks hydration; the installed tree may carry only one.
86
+ - [`no-css-import-from-render-path`](../rules/no-css-import-from-render-path.md) —
87
+ bare package specifier (legal) vs. a local `.css` import (banned) from a
88
+ section's render path.
89
+ - [`no-bare-css-import`](../rules/no-bare-css-import.md) — why a hand-written
90
+ stylesheet can't reach a package's CSS with its own `@import` either.
@@ -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