@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.
- package/CHANGELOG.md +84 -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 +12 -0
- package/dist/islands/wrap.js +48 -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 +12 -3
- 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.
|