@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.
- package/CHANGELOG.md +71 -0
- package/README.md +39 -4
- package/dist/asset-modules.d.ts +81 -0
- package/dist/cli/check/config.js +5 -1
- package/dist/cli/check/css.js +27 -13
- package/dist/cli/check/loader.js +4 -0
- package/dist/cli/check/section-assets.js +58 -0
- package/dist/cli/check/stages/size.js +124 -8
- package/dist/cli/check/stages/tree.js +26 -27
- package/dist/cli/dev/build-css.js +91 -0
- package/dist/cli/dev/catalog.js +35 -0
- package/dist/cli/dev/compose-template.js +38 -0
- package/dist/cli/dev/content-override.js +58 -0
- package/dist/cli/dev/discover.js +35 -0
- package/dist/cli/dev/fill-state.js +75 -0
- package/dist/cli/dev/index.js +32 -0
- package/dist/cli/dev/inspect.js +38 -0
- package/dist/cli/dev/island-bootstrap.js +38 -0
- package/dist/cli/dev/island-map.js +35 -0
- package/dist/cli/dev/island-transform.js +31 -0
- package/dist/cli/dev/manifest-instances.js +31 -0
- package/dist/cli/dev/render-section.js +24 -0
- package/dist/cli/dev/screenshot-target.js +24 -0
- package/dist/cli/dev/section-page.js +54 -0
- package/dist/cli/dev/server.js +480 -0
- package/dist/cli/dev/slot-schema.js +12 -0
- package/dist/cli/dev/structure-summary.js +118 -0
- package/dist/cli/dev/tailwind.js +32 -0
- package/dist/cli/dev/vite-server.js +33 -0
- package/dist/cli/dev/workspace.js +63 -0
- package/dist/cli/link/index.js +74 -0
- package/dist/cli/link/overlay.js +59 -0
- package/dist/cli/link/watch.js +25 -0
- package/dist/cli/new/emit.js +48 -0
- package/dist/cli/new/index.js +69 -0
- package/dist/cli/new/scaffold/section/_Renderer.tsx +20 -0
- package/dist/cli/new/scaffold/section/_fill-spec.ts +18 -0
- package/dist/cli/new/scaffold/section/_fixtures.ts +13 -0
- package/dist/cli/new/scaffold/section/_schema.ts +24 -0
- package/dist/cli/new/scaffold/template/_manifest.json +9 -0
- package/dist/cli/new/scaffold/template/sections/hero/ExpandableText.tsx +45 -0
- package/dist/cli/new/scaffold/template/sections/hero/Renderer.tsx +40 -0
- package/dist/cli/new/scaffold/template/sections/hero/components/Feature.tsx +14 -0
- package/dist/cli/new/scaffold/template/sections/hero/fill-spec.ts +29 -0
- package/dist/cli/new/scaffold/template/sections/hero/fixtures.ts +41 -0
- package/dist/cli/new/scaffold/template/sections/hero/schema.ts +60 -0
- package/dist/cli/new/scaffold/template/theme.css +24 -0
- package/dist/cli/new/scaffold/template/theme.ts +27 -0
- package/dist/cli/new/scaffold-assets.js +20 -0
- package/dist/cli/pack/collect.js +44 -0
- package/dist/cli/pack/guards.js +57 -0
- package/dist/cli/pack/index.js +66 -0
- package/dist/cli/pack/manifest.js +15 -0
- package/dist/cli/pack/zip.js +86 -0
- package/dist/cli.js +80 -14
- package/dist/dev-client/assets/index-DfEfrp6P.css +1 -0
- package/dist/dev-client/assets/index-bpMP0b0W.js +50 -0
- package/dist/dev-client/index.html +13 -0
- package/dist/eslint/is-client-file.d.ts +4 -0
- package/dist/index.d.ts +1 -0
- package/dist/islands/detect.d.ts +16 -0
- package/dist/islands/detect.js +22 -0
- package/dist/islands/discover.d.ts +5 -0
- package/dist/islands/discover.js +19 -0
- package/dist/islands/esbuild-plugin.d.ts +13 -0
- package/dist/islands/esbuild-plugin.js +39 -0
- package/dist/islands/wrap.d.ts +11 -0
- package/dist/islands/wrap.js +48 -0
- package/dist/node_modules/@jridgewell/sourcemap-codec/dist/sourcemap-codec.js +77 -0
- package/dist/node_modules/magic-string/dist/magic-string.es.js +939 -0
- package/dist/package.js +1 -1
- package/dist/ssr.d.ts +31 -0
- package/dist/ssr.js +46 -0
- package/docs/INDEX.md +9 -1
- package/docs/assets.md +128 -0
- package/docs/check.md +6 -3
- package/docs/dev.md +187 -0
- package/docs/islands.md +5 -2
- package/docs/llms.txt +41 -3
- package/docs/new.md +27 -0
- package/docs/overview.md +57 -0
- package/docs/pack.md +46 -0
- package/docs/primitives.md +5 -6
- package/docs/quickstart.md +102 -0
- package/docs/recipes/INDEX.md +27 -0
- package/docs/recipes/bind-property-fact.md +58 -0
- package/docs/recipes/collection-slot.md +100 -0
- package/docs/recipes/fill-spec-decision.md +66 -0
- package/docs/recipes/fixture-states.md +68 -0
- package/docs/recipes/image-slot-crop.md +98 -0
- package/docs/recipes/interactive-island.md +100 -0
- package/docs/recipes/organize-section-folder.md +72 -0
- package/docs/recipes/prepare-submission.md +55 -0
- package/docs/recipes/second-template.md +49 -0
- package/docs/recipes/select-slot.md +59 -0
- package/docs/recipes/static-asset.md +103 -0
- package/docs/recipes/third-party-package.md +90 -0
- package/docs/rules/INDEX.md +4 -2
- package/docs/rules/bundle-binary-asset.md +33 -52
- package/docs/rules/bundle-incomplete.md +2 -2
- package/docs/rules/css-reason.md +3 -3
- package/docs/rules/no-bare-css-import.md +8 -8
- package/docs/rules/no-css-import-from-render-path.md +7 -8
- package/docs/rules/size-assets.md +88 -0
- package/docs/rules/size-island-bundle.md +123 -0
- package/docs/rules/size-section-css.md +19 -14
- package/docs/schema-system.md +28 -28
- package/docs/theme-and-css.md +2 -2
- package/docs/vocabulary.md +98 -0
- package/package.json +11 -3
- 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.
|
package/docs/primitives.md
CHANGED
|
@@ -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-
|
|
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
|
|
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
|
|
219
|
-
|
|
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§ion=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.
|