@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,100 @@
1
+ ---
2
+ purpose: Add a repeating list whose items the editor selects and edits one at a time.
3
+ status: living
4
+ related: [../primitives.md, ../vocabulary.md, ../schema-system.md, bind-property-fact.md]
5
+ updated: 2026-07-16
6
+ ---
7
+ # A collection slot — a repeating, per-item list
8
+
9
+ ## Goal
10
+
11
+ Add a repeating list — a unit table, agent cards, a feature list, a gallery —
12
+ whose items the editor selects and edits one at a time.
13
+
14
+ ## The delta
15
+
16
+ Starting from the starter's `hero` section, add an `object_list` slot: a row of
17
+ nested `item` slots that repeats. Bind the whole array to a list fact with
18
+ `direct("units")` (a unit table) — the array is baked from the property, so it
19
+ needs no `fill-spec.ts` decision, exactly like a fact-bound scalar
20
+ ([bind-property-fact.md](bind-property-fact.md)). Each item field binds to a
21
+ per-row fact with its own `direct(...)`.
22
+
23
+ ```ts
24
+ // schema.ts — a repeating list of units, after the `blurb` slot
25
+ units: {
26
+ type: "object_list",
27
+ source: direct("units"),
28
+ editable_by_user: true,
29
+ required: true,
30
+ source_title_field: "unit_label",
31
+ display_name: "Units",
32
+ item: {
33
+ unit_label: { type: "text", size: "short", editable_by_user: true, display_name: "Unit", source: direct("unit_label") },
34
+ beds: { type: "number", editable_by_user: true, display_name: "Beds", source: direct("beds") },
35
+ baths: { type: "number", editable_by_user: true, display_name: "Baths", source: direct("baths") },
36
+ sqft: { type: "number", editable_by_user: true, display_name: "Sqft", source: direct("sqft") },
37
+ price: { type: "text", size: "short", format: "currency", editable_by_user: true, display_name: "Price", source: direct("price") },
38
+ },
39
+ },
40
+ ```
41
+
42
+ ```tsx
43
+ // Renderer.tsx — <Slot> marks the whole array, <SlotItem> marks each row
44
+ import { Image, Section, Slot, SlotItem } from "@homepages/template-kit";
45
+
46
+ // SectionProps types an object_list as Record<string, unknown>[], so narrow
47
+ // each row to your item shape at the map site.
48
+ type UnitRow = { unit_label: string; beds: number; baths: number; sqft: number; price: string };
49
+
50
+ <Slot id="units" as="ul" className="mt-4 flex w-full flex-col divide-y">
51
+ {(slots.units as UnitRow[]).map((unit, i) => (
52
+ <SlotItem key={unit.unit_label} index={i} as="li" className="flex justify-between py-2">
53
+ <span>{unit.unit_label}</span>
54
+ <span>{unit.beds} bd · {unit.baths} ba · {unit.sqft} sqft · {unit.price}</span>
55
+ </SlotItem>
56
+ ))}
57
+ </Slot>
58
+ ```
59
+
60
+ ```ts
61
+ // fixtures.ts — supply the array from the golden property
62
+ import { allPhotos, mkResponsive, sampleProperty } from "@homepages/template-kit/fixtures";
63
+ // ...in base.slots:
64
+ units: sampleProperty.units,
65
+ ```
66
+
67
+ Two contracts to know before you widen this:
68
+
69
+ - **`direct()` binds only a registry collection** — `units` or `contacts` (the
70
+ [List sources](../vocabulary.md#direct-sources)). For any other repeating data,
71
+ drop the `source` and give the slot a `produced_by` decision instead (see the
72
+ [fill-spec decision types](../schema-system.md#fill-spects--how-ai-fills-the-slots)).
73
+ - **An `item` needs more than one field, or a non-scalar one** — a lone scalar
74
+ field is rejected; use a `list` slot for a flat list of scalars.
75
+
76
+ `object_list` is one of four collection types. A gallery of framed photos is an
77
+ `image_collection`, a flat scalar list is a `list`, neighborhood points are a
78
+ `poi_list` — same `<SlotItem>` render pattern, different value shape. See
79
+ [the slot types](../vocabulary.md#slot-types) for which to pick.
80
+
81
+ ## Commands
82
+
83
+ None — this is a pure content edit. No CLI command beyond the author loop below.
84
+
85
+ ## Verify
86
+
87
+ Verify with [the author loop](../quickstart.md#the-author-loop).
88
+
89
+ ## See also
90
+
91
+ - [Contract primitives](../primitives.md#slotitem--the-per-item-handle-in-a-collection) —
92
+ the `SlotItem` / `SlotGroup` contract: both markers a collection needs, and why
93
+ `index` must be the row's array position.
94
+ - [The closed vocabulary](../vocabulary.md#slot-types) — every collection slot type
95
+ (`object_list` / `image_collection` / `poi_list` / `list`) and the `direct()` List sources.
96
+ - [The schema system](../schema-system.md#fill-spects--how-ai-fills-the-slots) — the
97
+ `produced_by` decision types (`structured-list`, `list-extract`, `image-collection`)
98
+ for a collection AI fills instead of binding to a fact.
99
+ - [Bind a slot to a property fact](bind-property-fact.md) — the `direct()` pattern this
100
+ extends from a scalar to a whole array.
@@ -0,0 +1,66 @@
1
+ ---
2
+ purpose: Have AI write a slot's content.
3
+ status: living
4
+ related: [../schema-system.md]
5
+ updated: 2026-07-15
6
+ ---
7
+ # Have AI write a slot's content
8
+
9
+ ## Goal
10
+
11
+ Have AI write a slot's content.
12
+
13
+ ## The delta
14
+
15
+ Add a `decisions` entry to `fill-spec.ts` and point the slot's `produced_by`
16
+ at its `id` — the same pattern the starter's `blurb` slot already uses. The
17
+ `text-block` type is the common case: it fills a `text` slot with generated
18
+ prose.
19
+
20
+ ```ts
21
+ // fill-spec.ts — a text-block decision, same shape as the starter's blurb_text
22
+ {
23
+ id: "summary_text",
24
+ type: "text-block",
25
+ produces: ["summary"],
26
+ length_cap: 200,
27
+ // `structure` enum is single_sentence | paragraph | bullet_list.
28
+ structure: "paragraph",
29
+ perspective: "third",
30
+ voice_prompt: "Write a warm, concrete paragraph a buyer would want to read.",
31
+ content_prompt: "Summarize the property's standout feature in 1–2 sentences.",
32
+ },
33
+ ```
34
+
35
+ ```ts
36
+ // schema.ts — the slot names the decision by id
37
+ summary: {
38
+ type: "text",
39
+ size: "long",
40
+ editable_by_user: true,
41
+ required: true,
42
+ produced_by: "summary_text",
43
+ display_name: "Summary",
44
+ },
45
+ ```
46
+
47
+ `text-block` is one of several decision types — the others include
48
+ `image-assign`, `image-collection`, `text-seed`, `list-extract`, and
49
+ `structured-list`, each producing a differently-shaped slot value. This
50
+ recipe covers the common text case; see [the schema
51
+ system](../schema-system.md#fill-spects--how-ai-fills-the-slots) for the
52
+ full decision-type contract before reaching for one of the others.
53
+
54
+ ## Commands
55
+
56
+ None — this is a pure content edit. No CLI command beyond the author loop below.
57
+
58
+ ## Verify
59
+
60
+ Verify with [the author loop](../quickstart.md#the-author-loop).
61
+
62
+ ## See also
63
+
64
+ - [The schema system](../schema-system.md#fill-spects--how-ai-fills-the-slots) —
65
+ the full `fill-spec.ts` contract, every decision type, and how `produces`
66
+ maps back to `produced_by`.
@@ -0,0 +1,68 @@
1
+ ---
2
+ purpose: Preview a section's structural edge cases.
3
+ status: living
4
+ related: [../schema-system.md, ../dev.md]
5
+ updated: 2026-07-15
6
+ ---
7
+ # Preview a section's structural edge cases
8
+
9
+ ## Goal
10
+
11
+ Preview a section's structural edge cases.
12
+
13
+ ## The delta
14
+
15
+ Add entries under `states` in `fixtures.ts` — each one overrides part of
16
+ `base`, the same pattern the starter's `left` and `long-blurb` states use.
17
+ A `states` entry is for a DESIGNED scenario, not raw coverage: nullability
18
+ and empty-slot coverage (every optional slot rendered empty) is derived
19
+ automatically from `schema.ts`, so a fixture state is for a case the
20
+ section renders *differently* — a content-length branch, an option branch,
21
+ or several overrides combined.
22
+
23
+ ```ts
24
+ // fixtures.ts — a content-length case and an option-branch case
25
+ const fixtures: FixtureModule = {
26
+ base: {
27
+ slots: {
28
+ headline: "101 Queensway, Jamaica Plain, MA 02130",
29
+ summary: "A bright corner home moments from the Emerald Necklace.",
30
+ },
31
+ options: { align: "center" },
32
+ },
33
+ states: {
34
+ // Content-length branch: exercises a long value's wrap/expand behavior.
35
+ "long-summary": {
36
+ slots: {
37
+ summary:
38
+ "A bright corner home moments from the Emerald Necklace, with a " +
39
+ "chef's kitchen, three sun-filled bedrooms, off-street parking, and " +
40
+ "a landscaped yard that backs onto a quiet dead-end street.",
41
+ },
42
+ },
43
+ // Option branch: exercises the layout under the other enum value.
44
+ left: { options: { align: "left" } },
45
+ },
46
+ };
47
+ ```
48
+
49
+ Each `states` key names a scenario; its value overrides `slots` and/or
50
+ `options` on top of `base` — omit either key to inherit `base`'s value
51
+ unchanged. Each state is a selectable fixture in the dev preview, so every one
52
+ you add is there to render and review alongside `base`.
53
+
54
+ ## Commands
55
+
56
+ None — this is a pure content edit. No CLI command beyond the author loop below.
57
+
58
+ ## Verify
59
+
60
+ Verify with [the author loop](../quickstart.md#the-author-loop).
61
+
62
+ ## See also
63
+
64
+ - [The schema system](../schema-system.md#fixturests--content-for-local-preview) —
65
+ the `fixtures.ts` contract: `base`, `states`, and the golden-property
66
+ fixture exports.
67
+ - [`template-kit dev`](../dev.md) — the preview server that renders each
68
+ fixture state across the breakpoint ladder.
@@ -0,0 +1,98 @@
1
+ ---
2
+ purpose: Add an image slot the editor crops to a fixed ratio.
3
+ status: living
4
+ related: [../schema-system.md, ../primitives.md, ../rules/image-bare-needs-reason.md]
5
+ updated: 2026-07-15
6
+ ---
7
+ # Image slot with a locked crop
8
+
9
+ ## Goal
10
+
11
+ Add an image slot the editor crops to a fixed ratio.
12
+
13
+ ## The delta
14
+
15
+ Starting from the starter's `hero` section, add a second `image` slot shaped
16
+ like `hero_image` — a `crop` config, an `image-assign` decision, an `<Image>`
17
+ in the Renderer, and a fixture value.
18
+
19
+ ```ts
20
+ // schema.ts — a second image slot, same shape as hero_image, own aspect
21
+ interior_image: {
22
+ type: "image",
23
+ editable_by_user: true,
24
+ required: true,
25
+ produced_by: "interior_image_assign",
26
+ display_name: "Interior image",
27
+ crop: { mode: "locked", aspect: 4 / 3, aspectLabel: "4:3" },
28
+ },
29
+ ```
30
+
31
+ ```ts
32
+ // fill-spec.ts — the decision producing it
33
+ {
34
+ id: "interior_image_assign",
35
+ type: "image-assign",
36
+ produces: ["interior_image"],
37
+ tag_query: "category:interior",
38
+ selection_prompt: "Pick a bright, decluttered interior shot.",
39
+ },
40
+ ```
41
+
42
+ ```tsx
43
+ // Renderer.tsx — copy the starter's <Image> usage for hero_image
44
+ <Image
45
+ slotId="interior_image"
46
+ src={slots.interior_image.url}
47
+ responsive={slots.interior_image.responsive}
48
+ alt={slots.interior_image.alt}
49
+ className="w-full"
50
+ />
51
+ ```
52
+
53
+ ```ts
54
+ // fixtures.ts — a photo + a matching mkResponsive() height
55
+ const interiorImage = allPhotos[3]!;
56
+ // ...
57
+ interior_image: {
58
+ photo_id: interiorImage.id,
59
+ url: interiorImage.url,
60
+ alt: interiorImage.alt,
61
+ // Height set to a 4:3 ratio to match the slot's locked crop.
62
+ responsive: mkResponsive(interiorImage.url, 1600, 1200),
63
+ },
64
+ ```
65
+
66
+ An image slot supports two independent geometry configs — use one, not both, per
67
+ slot:
68
+
69
+ - **`crop`** — steers the editor's cropper only: `mode: "locked"` fixes an
70
+ `aspect` (w/h) and shows the `aspectLabel` pill; `mode: "free"` lets the user
71
+ resize any edge. This is what the recipe above uses.
72
+ - **`frame`** — a per-breakpoint aspect (`desktop`/`mobile`, switching at a
73
+ declared `breakpoint`) that is the single source of truth for BOTH the
74
+ cropper's target ratio and the Renderer's own layout box (imported as a
75
+ shared constant on both sides — see [schema-system.md](../schema-system.md)).
76
+ Reach for `frame` instead of `crop` when the section's markup itself needs
77
+ to know the ratio (e.g. to size a wrapper), not just the editor.
78
+
79
+ Either way, framing is baked into the served asset — the Renderer never
80
+ applies an aspect ratio to the pixels itself; it only reads `url`, `alt`,
81
+ `responsive` (and `mobile`) off the slot's `ImageValue`.
82
+
83
+ ## Commands
84
+
85
+ None — this is a pure content edit. No CLI command beyond the author loop below.
86
+
87
+ ## Verify
88
+
89
+ Verify with [the author loop](../quickstart.md#the-author-loop).
90
+
91
+ ## See also
92
+
93
+ - [The schema system](../schema-system.md) — `ImageValue`, the fields a Renderer reads
94
+ vs. only carries, and `frame`.
95
+ - [Contract primitives](../primitives.md#image--the-defensive-image-primitive) — the
96
+ `Image` primitive's full prop table.
97
+ - [`image-bare-needs-reason`](../rules/image-bare-needs-reason.md) — the rule that fires
98
+ if you reach for `<Image bare>` instead of the framed default this recipe uses.
@@ -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.