@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
package/docs/pack.md ADDED
@@ -0,0 +1,46 @@
1
+ ---
2
+ purpose: The `template-kit pack` command — what it produces, what it refuses, and why.
3
+ status: living
4
+ related: [INDEX.md, check.md]
5
+ updated: 2026-07-15
6
+ ---
7
+ # template-kit pack
8
+
9
+ Build a template's submission artifact: a zip of the template's source plus the
10
+ workspace lockfile, validated and stamped.
11
+
12
+ ## Usage
13
+
14
+ ```bash
15
+ template-kit pack <template>
16
+ ```
17
+
18
+ Run it from the workspace root, or from inside a template folder (the template is
19
+ inferred). In a workspace with several templates you must name one — the submission
20
+ unit is a single template.
21
+
22
+ ## What it produces
23
+
24
+ `<template>.zip` at the workspace root, containing:
25
+
26
+ - `pack-manifest.json` — the exact kit version the source was validated against, a
27
+ content hash of the template tree, the pack timestamp, and a `check` flag
28
+ recording that validation passed.
29
+ - `package.json` and `package-lock.json` from the workspace root, so the build is
30
+ reproducible with `npm ci`.
31
+ - `templates/<template>/` — the template's source only (sections, manifest, theme,
32
+ README). No `node_modules`, no build output, no other template.
33
+
34
+ ## What it refuses
35
+
36
+ `pack` will not produce a zip when:
37
+
38
+ - `check <template>` is not green — fix the reported problems first.
39
+ - the workspace has no `package-lock.json` — run `npm install` and commit it.
40
+ - a dependency uses a local `file:`/`link:` protocol — depend on published versions.
41
+
42
+ Fix the reported cause and run it again.
43
+
44
+ ## See also
45
+
46
+ - [Check](check.md) — the gate `pack` runs before it will produce a zip.
@@ -2,7 +2,7 @@
2
2
  purpose: The five contract primitives a section renderer must use, and the marker attributes they emit.
3
3
  status: living
4
4
  related: [schema-system.md, theme-and-css.md, eslint.md]
5
- updated: 2026-07-14
5
+ updated: 2026-07-15
6
6
  ---
7
7
  # Contract primitives
8
8
 
@@ -106,8 +106,8 @@ whole — no per-item selection, no per-item image cropping, no per-item live su
106
106
  `index` **must** be the item's zero-based position in the slot's array as rendered. The
107
107
  editor writes edits back by that index, so an index that doesn't match its position edits
108
108
  the wrong row. `index` is an array position, not a schema key, so unlike `Slot`'s and
109
- `SlotGroup`'s `id` it is exempt from the [`template-kit/slot-marker-literal`](eslint.md#markers-and-schema)
110
- lint rule.
109
+ `SlotGroup`'s `id` it is exempt from the
110
+ [`template-kit/slot-marker-literal`](rules/slot-marker-literal.md) lint rule.
111
111
 
112
112
  ## `SlotGroup` — slots edited as one unit
113
113
 
@@ -206,7 +206,6 @@ magic string.
206
206
  | `ATTR_SLOT_ITEM` | `data-slot-item` | one item of a collection slot |
207
207
  | `ATTR_SLOT_GROUP` | `data-slot-group` | the container wrapping a group's members |
208
208
  | `ATTR_SECTION_INSTANCE_ID` | `data-section-instance-id` | the section root — **written by the platform**, not by a primitive |
209
- | `ATTR_TR_ENHANCE` | `data-tr-enhance` | opt-in hook telling an enhancer to attach to this element |
210
209
 
211
210
  Changing what an attribute means is a platform contract break, so these names are stable
212
211
  across a major version of the kit.
@@ -215,5 +214,5 @@ across a major version of the kit.
215
214
 
216
215
  Renderers are pure functions of props: no fetches, no globals, no `useEffect` for data,
217
216
  no `Date.now()` / `Math.random()`. They are rendered to static markup by the publisher
218
- and re-mounted constantly by the editor. Interactivity that needs the browser attaches
219
- through `data-tr-enhance`, not through render-time side effects.
217
+ and re-mounted constantly by the editor. Interactivity that needs the browser moves into
218
+ a `"use client"` island (see [Islands](islands.md)), never into render-time side effects.
@@ -0,0 +1,102 @@
1
+ ---
2
+ purpose: Zero to a rendered, checked section in under an hour, on the scaffolded starter.
3
+ status: living
4
+ related: [overview.md, dev.md, check.md, schema-system.md]
5
+ updated: 2026-07-15
6
+ ---
7
+ # Quickstart
8
+
9
+ This walks the scaffolded starter template end to end: create a workspace, see
10
+ its one section render, read how it's built, make one edit, and check it. It
11
+ does not re-teach the authoring contract — see [Overview](overview.md) for the
12
+ mental model and [Schema system](schema-system.md) for the four files in
13
+ depth.
14
+
15
+ ## Create your workspace
16
+
17
+ ```bash
18
+ npm create homepages-workspace@latest my-workspace
19
+ ```
20
+
21
+ This stamps a workspace with one template, `starter`, and one section in it,
22
+ `hero` — a working section built on the real contract, not a stub. See
23
+ [Overview](overview.md) for what a workspace, template, and section are.
24
+
25
+ ## See it render
26
+
27
+ ```bash
28
+ cd my-workspace
29
+ npm run dev
30
+ ```
31
+
32
+ This opens the **home launcher** in your browser: from there the **canvas
33
+ mirror** renders the template with `starter`'s `hero` in it, and the
34
+ full-template viewer shows the whole page. See [Dev](dev.md) for the launcher,
35
+ the canvas mirror, and live reload.
36
+
37
+ If you're an agent (or otherwise browser-less), hit the inspect endpoint
38
+ instead — it returns each slot's fill state and a text outline of the render:
39
+
40
+ ```bash
41
+ curl 'http://localhost:<port>/api/inspect?template=starter&section=hero&fixture=typical&format=text'
42
+ ```
43
+
44
+ `typical` is the starter hero's baseline fixture; it also has `left` and
45
+ `long-blurb` fixtures exercising an option and a content-length branch. See
46
+ [Dev](dev.md#agent-facing-endpoints) for `/api/inspect` and the other
47
+ agent-facing endpoints.
48
+
49
+ ## Read the worked example
50
+
51
+ Before changing anything, read the four files that make up the starter's
52
+ `hero` section — `templates/starter/sections/hero/`:
53
+
54
+ - `schema.ts` — the slots (`hero_image`, `headline`, `blurb`) and the `align`
55
+ option, each comment explaining why it's declared the way it is.
56
+ - `fill-spec.ts` — how AI fills `hero_image` and `blurb`; `headline` needs no
57
+ decision because it's fact-bound.
58
+ - `fixtures.ts` — the `typical` baseline plus the `left`/`long-blurb` states.
59
+ - `Renderer.tsx` — the markup, including the one interactive part: `blurb`
60
+ wrapped in a `"use client"` island.
61
+
62
+ Each file is annotated in place. For the concepts behind them, see
63
+ [Schema system](schema-system.md).
64
+
65
+ ## Make one edit
66
+
67
+ Open `templates/starter/sections/hero/fixtures.ts` and change the `headline`
68
+ value under `base`:
69
+
70
+ ```diff
71
+ - headline: "101 Queensway, Jamaica Plain, MA 02130",
72
+ + headline: "12 Elm Street, Jamaica Plain, MA 02130",
73
+ ```
74
+
75
+ Save it. If `npm run dev` is running, any open preview of the `hero` section
76
+ reloads with the new headline; re-running the `/api/inspect` call above also
77
+ shows the new value.
78
+
79
+ ## Check it
80
+
81
+ ```bash
82
+ npm run check
83
+ ```
84
+
85
+ ```
86
+ ✓ starter
87
+ check passed
88
+ ```
89
+
90
+ A green `check` is the acceptance gate — see [Check](check.md) for what its
91
+ six stages verify and what a failure looks like.
92
+
93
+ ## The author loop
94
+
95
+ Every section, and every change to one, runs the same three-step loop:
96
+
97
+ 1. **Edit** a section's files.
98
+ 2. **Preview** the result — `npm run dev` in a browser, or `/api/inspect` from
99
+ a terminal.
100
+ 3. **Check** it — `npm run check` — until it's green.
101
+
102
+ Every recipe in this corpus ends by running this loop.
@@ -0,0 +1,27 @@
1
+ ---
2
+ purpose: Task router — how do I… → the recipe.
3
+ status: living
4
+ related: []
5
+ updated: 2026-07-16
6
+ ---
7
+ # Recipes
8
+
9
+ Every recipe is a **delta from the starter's `hero` section** — never a
10
+ from-scratch file — ending in the same [author loop](../quickstart.md#the-author-loop)
11
+ as everything else in this corpus: edit, preview, check. Find your task below and
12
+ copy the shapes from its recipe, not from memory.
13
+
14
+ | How do I… | Recipe |
15
+ |---|---|
16
+ | add an image slot the editor can crop | [`image-slot-crop.md`](image-slot-crop.md) |
17
+ | fill a slot from a property fact without AI | [`bind-property-fact.md`](bind-property-fact.md) |
18
+ | give the user a closed set of choices | [`select-slot.md`](select-slot.md) |
19
+ | add a repeating list the user edits per item (unit table, cards, gallery) | [`collection-slot.md`](collection-slot.md) |
20
+ | have AI write a slot's content | [`fill-spec-decision.md`](fill-spec-decision.md) |
21
+ | preview a section's structural edge cases / states | [`fixture-states.md`](fixture-states.md) |
22
+ | make part of a section interactive (an island) | [`interactive-island.md`](interactive-island.md) |
23
+ | organize a section's files beyond the four reserved | [`organize-section-folder.md`](organize-section-folder.md) |
24
+ | ship a static asset (image/svg/font) with a section | [`static-asset.md`](static-asset.md) |
25
+ | use a third-party npm package in a section | [`third-party-package.md`](third-party-package.md) |
26
+ | add another template to the workspace | [`second-template.md`](second-template.md) |
27
+ | package a template for submission | [`prepare-submission.md`](prepare-submission.md) |
@@ -0,0 +1,58 @@
1
+ ---
2
+ purpose: Fill a slot from a known property fact, no AI.
3
+ status: living
4
+ related: [../vocabulary.md, ../schema-system.md, ../rules/schema-invalid.md]
5
+ updated: 2026-07-15
6
+ ---
7
+ # Bind a slot to a property fact
8
+
9
+ ## Goal
10
+
11
+ Fill a slot from a known property fact, no AI.
12
+
13
+ ## The delta
14
+
15
+ Give the slot a `source: direct("<field>")` instead of a `produced_by` — the
16
+ same pattern the starter's `headline` already uses
17
+ (`source: direct("address")`). Model it on an AI-produced slot you want to
18
+ make fact-bound instead: swap its `produced_by` for `source`, and drop the
19
+ matching decision from `fill-spec.ts`.
20
+
21
+ ```ts
22
+ // schema.ts — was produced_by: "blurb_text"
23
+ blurb: {
24
+ type: "text",
25
+ size: "long",
26
+ editable_by_user: true,
27
+ required: true,
28
+ source: direct("lot_size"),
29
+ display_name: "Blurb",
30
+ },
31
+ ```
32
+
33
+ Then delete the now-unused `blurb_text` decision from `fill-spec.ts`'s
34
+ `decisions` array — a `direct()`-bound slot needs none; the fact is baked in
35
+ at fill time, and the [`schema-invalid`](../rules/schema-invalid.md) check
36
+ rejects a slot that declares both `source` and `produced_by`.
37
+
38
+ The same pattern works for any slot type — pick a field whose runtime shape
39
+ matches. For a `number` slot, `source: direct("beds")`; for `text`,
40
+ `source: direct("address")` or any other text-shaped field in
41
+ [the closed vocabulary](../vocabulary.md#direct-sources).
42
+
43
+ ## Commands
44
+
45
+ None — this is a pure content edit. No CLI command beyond the author loop below.
46
+
47
+ ## Verify
48
+
49
+ Verify with [the author loop](../quickstart.md#the-author-loop).
50
+
51
+ ## See also
52
+
53
+ - [The closed vocabulary](../vocabulary.md#direct-sources) — every `direct()` field,
54
+ grouped by what it resolves against.
55
+ - [The schema system](../schema-system.md#binding-a-slot-to-a-property-fact) — the
56
+ `direct()`/`derived()` contract and why a bad name is a compile error.
57
+ - [`schema-invalid`](../rules/schema-invalid.md) — fires if a slot declares both
58
+ `source` and `produced_by`, or neither.
@@ -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.