@homepages/template-kit 0.4.1 → 0.5.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 +85 -0
- package/README.md +38 -27
- package/dist/base.css +6 -4
- package/dist/cli/dev/fill-state.js +5 -3
- package/dist/cli/link/overlay.js +1 -1
- package/dist/cli/new/scaffold/template/sections/hero/fixtures.ts +7 -7
- package/dist/contracts/slot-catalog.d.ts +2 -2
- package/dist/contracts/slot-catalog.js +3 -1
- package/dist/eslint/rules/no-raw-element.js +3 -2
- package/dist/fixtures/helpers.d.ts +23 -0
- package/dist/fixtures/helpers.js +50 -0
- package/dist/fixtures/sample-property.d.ts +7 -17
- package/dist/fixtures/sample-property.js +179 -57
- package/dist/fixtures/scenario.d.ts +20 -0
- package/dist/fixtures/scenarios/for-rent-condo.d.ts +5 -0
- package/dist/fixtures/scenarios/for-rent-condo.js +364 -0
- package/dist/fixtures/scenarios/index.d.ts +8 -0
- package/dist/fixtures/scenarios/index.js +13 -0
- package/dist/fixtures/scenarios/luxury-multi-unit.d.ts +5 -0
- package/dist/fixtures/scenarios/luxury-multi-unit.js +19 -0
- package/dist/fixtures/scenarios/sparse-single-family.d.ts +5 -0
- package/dist/fixtures/scenarios/sparse-single-family.js +233 -0
- package/dist/fixtures.d.ts +8 -2
- package/dist/fixtures.js +7 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -1
- package/dist/island-runtime.js +22 -7
- package/dist/package.js +1 -1
- package/dist/primitives/Image.d.ts +1 -1
- package/dist/primitives/Video.d.ts +38 -0
- package/dist/primitives/Video.js +63 -0
- package/dist/primitives/index.d.ts +2 -1
- package/dist/rules/registry.js +3 -3
- package/dist/schema/index.d.ts +2 -2
- package/dist/schema/runtime-values.d.ts +20 -1
- package/dist/schema/section-props.d.ts +8 -1
- package/dist/schema/section-schema.d.ts +2 -2
- package/dist/schema/slot-types.d.ts +15 -1
- package/dist/schema/slot-types.js +34 -0
- package/dist/schema/synthesize-nullable.js +1 -0
- package/{docs → guide}/INDEX.md +7 -6
- package/{docs → guide}/check.md +2 -2
- package/{docs → guide}/islands.md +6 -1
- package/{docs → guide}/llms.txt +34 -14
- package/{docs → guide}/overview.md +4 -4
- package/{docs → guide}/pack.md +2 -2
- package/{docs → guide}/primitives.md +59 -11
- package/{docs → guide}/recipes/INDEX.md +1 -0
- package/{docs → guide}/recipes/collection-slot.md +10 -8
- package/guide/recipes/fixture-states.md +109 -0
- package/{docs → guide}/recipes/image-slot-crop.md +5 -3
- package/guide/recipes/pick-a-scenario.md +119 -0
- package/{docs → guide}/rules/INDEX.md +2 -2
- package/{docs → guide}/rules/fixtures-invalid.md +2 -2
- package/{docs → guide}/rules/no-raw-element.md +13 -8
- package/{docs → guide}/rules/render-invariant.md +4 -3
- package/{docs → guide}/schema-system.md +97 -16
- package/{docs → guide}/theme-and-css.md +3 -2
- package/guide/upgrading.md +61 -0
- package/{docs → guide}/vocabulary.md +3 -1
- package/package.json +8 -3
- package/docs/recipes/fixture-states.md +0 -68
- /package/{docs → guide}/assets.md +0 -0
- /package/{docs → guide}/dev.md +0 -0
- /package/{docs → guide}/eslint.md +0 -0
- /package/{docs → guide}/new.md +0 -0
- /package/{docs → guide}/quickstart.md +0 -0
- /package/{docs → guide}/recipes/bind-property-fact.md +0 -0
- /package/{docs → guide}/recipes/fill-spec-decision.md +0 -0
- /package/{docs → guide}/recipes/interactive-island.md +0 -0
- /package/{docs → guide}/recipes/organize-section-folder.md +0 -0
- /package/{docs → guide}/recipes/prepare-submission.md +0 -0
- /package/{docs → guide}/recipes/second-template.md +0 -0
- /package/{docs → guide}/recipes/select-slot.md +0 -0
- /package/{docs → guide}/recipes/static-asset.md +0 -0
- /package/{docs → guide}/recipes/third-party-package.md +0 -0
- /package/{docs → guide}/rules/audit-severity.md +0 -0
- /package/{docs → guide}/rules/bundle-binary-asset.md +0 -0
- /package/{docs → guide}/rules/bundle-incomplete.md +0 -0
- /package/{docs → guide}/rules/css-reason.md +0 -0
- /package/{docs → guide}/rules/determinism-drift.md +0 -0
- /package/{docs → guide}/rules/image-bare-needs-reason.md +0 -0
- /package/{docs → guide}/rules/license-denied.md +0 -0
- /package/{docs → guide}/rules/lockfile-missing.md +0 -0
- /package/{docs → guide}/rules/lockfile-stale.md +0 -0
- /package/{docs → guide}/rules/manifest-invalid.md +0 -0
- /package/{docs → guide}/rules/missing-slot-marker.md +0 -0
- /package/{docs → guide}/rules/no-bare-css-import.md +0 -0
- /package/{docs → guide}/rules/no-client-directive-in-contract.md +0 -0
- /package/{docs → guide}/rules/no-client-runtime-in-server.md +0 -0
- /package/{docs → guide}/rules/no-css-import-from-render-path.md +0 -0
- /package/{docs → guide}/rules/no-hex.md +0 -0
- /package/{docs → guide}/rules/no-inline-style.md +0 -0
- /package/{docs → guide}/rules/no-nondeterminism.md +0 -0
- /package/{docs → guide}/rules/no-templates.md +0 -0
- /package/{docs → guide}/rules/parse-error.md +0 -0
- /package/{docs → guide}/rules/props-from-schema.md +0 -0
- /package/{docs → guide}/rules/schema-invalid.md +0 -0
- /package/{docs → guide}/rules/serializable-island-props.md +0 -0
- /package/{docs → guide}/rules/server-vs-client.md +0 -0
- /package/{docs → guide}/rules/sidebar-order.md +0 -0
- /package/{docs → guide}/rules/single-react.md +0 -0
- /package/{docs → guide}/rules/size-assets.md +0 -0
- /package/{docs → guide}/rules/size-island-bundle.md +0 -0
- /package/{docs → guide}/rules/size-renderer-bundle.md +0 -0
- /package/{docs → guide}/rules/size-section-css.md +0 -0
- /package/{docs → guide}/rules/slot-group-marker.md +0 -0
- /package/{docs → guide}/rules/slot-marker-literal.md +0 -0
- /package/{docs → guide}/rules/typecheck.md +0 -0
- /package/{docs → guide}/rules/unlayered-fence.md +0 -0
package/{docs → guide}/pack.md
RENAMED
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
---
|
|
2
|
-
purpose: The
|
|
2
|
+
purpose: The six contract primitives a section renderer must use, and the marker attributes they emit.
|
|
3
3
|
status: living
|
|
4
|
-
related: [schema-system.md, theme-and-css.md, eslint.md]
|
|
5
|
-
updated: 2026-07-
|
|
4
|
+
related: [schema-system.md, theme-and-css.md, eslint.md, upgrading.md]
|
|
5
|
+
updated: 2026-07-16
|
|
6
6
|
---
|
|
7
7
|
# Contract primitives
|
|
8
8
|
|
|
9
9
|
Everything here is imported from the package root:
|
|
10
10
|
|
|
11
11
|
```tsx
|
|
12
|
-
import { Image, Section, Slot, SlotGroup, SlotItem } from "@homepages/template-kit";
|
|
12
|
+
import { Image, Section, Slot, SlotGroup, SlotItem, Video } from "@homepages/template-kit";
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
The kit ships **
|
|
15
|
+
The kit ships **six components, and nothing else**. That is deliberate: a primitive
|
|
16
16
|
earns its place only by emitting something the platform breaks without — the section
|
|
17
|
-
box model, the editor's marker attributes, the responsive-image machinery
|
|
17
|
+
box model, the editor's marker attributes, the responsive-image machinery, the
|
|
18
|
+
poster/playback contract for hosted video.
|
|
18
19
|
Presentation is yours. Headings, links, layout wells, icons, lists, cards: write them
|
|
19
20
|
as JSX in your section, or reach for a package.
|
|
20
21
|
|
|
@@ -22,9 +23,10 @@ as JSX in your section, or reach for a package.
|
|
|
22
23
|
|---|---|---|
|
|
23
24
|
| `Section` | `class="tr-section"` on the section root | always — every renderer roots at exactly one |
|
|
24
25
|
| `Slot` | `data-slot-id` (+ optional `data-slot-text-leaf`) | around every editable slot's outer DOM |
|
|
25
|
-
| `SlotItem` | `data-slot-item` | around each item of an `object_list` / `image_collection` slot |
|
|
26
|
+
| `SlotItem` | `data-slot-item` | around each item of an `object_list` / `image_collection` / `video_collection` slot |
|
|
26
27
|
| `SlotGroup` | `data-slot-group` | around the members of a group declared in `meta.groups` |
|
|
27
28
|
| `Image` | `<picture>` / `srcset` / the empty-state box | every image — sections never write a raw `<img>` |
|
|
29
|
+
| `Video` | the poster frame / the playback config / the empty-state box | every video — sections never write a raw `<video>` |
|
|
28
30
|
|
|
29
31
|
A slot with no marker in the DOM **cannot be selected or edited**. The markers are the
|
|
30
32
|
only thing connecting the pixels a user clicks to the schema key behind them.
|
|
@@ -86,8 +88,8 @@ needs no wrapper. `<Slot>` is for everything else.
|
|
|
86
88
|
|
|
87
89
|
## `SlotItem` — the per-item handle in a collection
|
|
88
90
|
|
|
89
|
-
A collection slot (`object_list`, `image_collection`) needs **both**
|
|
90
|
-
marks the whole array, `<SlotItem>` marks one row of it.
|
|
91
|
+
A collection slot (`object_list`, `image_collection`, `video_collection`) needs **both**
|
|
92
|
+
markers: `<Slot>` marks the whole array, `<SlotItem>` marks one row of it.
|
|
91
93
|
|
|
92
94
|
```tsx
|
|
93
95
|
<Slot id="amenities">
|
|
@@ -193,6 +195,51 @@ inside its own aspect-ratio parent). It deliberately sets no `object-fit`, so an
|
|
|
193
195
|
`object-contain` you pass isn't fighting a built-in `object-cover`. It forfeits the
|
|
194
196
|
empty-state box — reach for it only when the parent guarantees the box.
|
|
195
197
|
|
|
198
|
+
## `Video` — the hosted-video primitive
|
|
199
|
+
|
|
200
|
+
Sections never write a raw `<video>`. `Video` owns the poster frame, the author-declared
|
|
201
|
+
playback config, and the same layout box that survives a missing source.
|
|
202
|
+
|
|
203
|
+
```tsx
|
|
204
|
+
<Video
|
|
205
|
+
slotId="tour_video"
|
|
206
|
+
src={slots.tour_video.url}
|
|
207
|
+
poster={slots.tour_video.poster}
|
|
208
|
+
alt={slots.tour_video.alt}
|
|
209
|
+
playback={schema.slots.tour_video.playback}
|
|
210
|
+
aspectRatio="16 / 9"
|
|
211
|
+
/>
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
| Prop | Meaning |
|
|
215
|
+
|---|---|
|
|
216
|
+
| `src` | The rendition URL. Required prop; `null`/empty is a supported state, not a bug. |
|
|
217
|
+
| `poster` | Poster-frame URL. The first paint, and the entire render while `src` is empty. |
|
|
218
|
+
| `alt` | Required. Empty string for a purely decorative loop. |
|
|
219
|
+
| `slotId` | Emits `data-slot-id` on the wrapper — a video slot needs no `<Slot>` around it. |
|
|
220
|
+
| `aspectRatio` | CSS `aspect-ratio` reserved on the wrapper. Survives a missing `src`. |
|
|
221
|
+
| `playback` | The slot's [`playback` config](schema-system.md#video-slots). Absent ⇒ a plain controls player. |
|
|
222
|
+
| `width` / `height` | Intrinsic rendition dimensions (`VideoValue.width`/`height`) — reserve the box before metadata loads. |
|
|
223
|
+
|
|
224
|
+
**Framed, always.** There is no `bare` escape hatch: a wrapper `div` carries the slot
|
|
225
|
+
marker and the aspect-ratio box, and absolutely-positions the inner `<video>` with
|
|
226
|
+
`object-cover`. When `src` is empty the wrapper renders the poster alone — or a neutral
|
|
227
|
+
`role="img"` rect if there is no poster — keeping the layout box and the slot selectable.
|
|
228
|
+
Both empty states are routine: an unfilled video slot is a thin listing, and a poster with
|
|
229
|
+
no rendition is what a video still transcoding looks like.
|
|
230
|
+
|
|
231
|
+
The placeholder fill rides on the same `.tr-image-frame` class `<Image>` uses (from
|
|
232
|
+
[`base.css`](theme-and-css.md#basecss-requires-a-tailwind-entry)) — one neutral rect, one
|
|
233
|
+
`--tr-image-frame-bg` token, so a template retints both frames once.
|
|
234
|
+
|
|
235
|
+
**Playback is passed through verbatim** — the primitive infers nothing from the asset.
|
|
236
|
+
The config is authored on the slot, not chosen here; see
|
|
237
|
+
[Video slots](schema-system.md#video-slots) for the fields and the `autoplay`-requires-`muted`
|
|
238
|
+
rule.
|
|
239
|
+
|
|
240
|
+
There is no cropping and no responsive ladder: one progressive MP4 is served per video,
|
|
241
|
+
and the poster is derived upstream.
|
|
242
|
+
|
|
196
243
|
## The marker contract
|
|
197
244
|
|
|
198
245
|
The primitives are the sanctioned way to produce these attributes. The constants are
|
|
@@ -201,14 +248,15 @@ magic string.
|
|
|
201
248
|
|
|
202
249
|
| Constant | Attribute | On |
|
|
203
250
|
|---|---|---|
|
|
204
|
-
| `ATTR_SLOT_ID` | `data-slot-id` | the `Slot` wrapper / `Image`'s framed wrapper |
|
|
251
|
+
| `ATTR_SLOT_ID` | `data-slot-id` | the `Slot` wrapper / `Image`'s and `Video`'s framed wrapper |
|
|
205
252
|
| `ATTR_SLOT_TEXT_LEAF` | `data-slot-text-leaf` | the text leaf inside a text slot |
|
|
206
253
|
| `ATTR_SLOT_ITEM` | `data-slot-item` | one item of a collection slot |
|
|
207
254
|
| `ATTR_SLOT_GROUP` | `data-slot-group` | the container wrapping a group's members |
|
|
208
255
|
| `ATTR_SECTION_INSTANCE_ID` | `data-section-instance-id` | the section root — **written by the platform**, not by a primitive |
|
|
209
256
|
|
|
210
257
|
Changing what an attribute means is a platform contract break, so these names are stable
|
|
211
|
-
across a
|
|
258
|
+
across patch releases of the kit; a rename is exactly the kind of breaking change that
|
|
259
|
+
ships as a minor bump (see [Upgrading the kit](upgrading.md)).
|
|
212
260
|
|
|
213
261
|
## Renderers stay pure
|
|
214
262
|
|
|
@@ -18,6 +18,7 @@ copy the shapes from its recipe, not from memory.
|
|
|
18
18
|
| give the user a closed set of choices | [`select-slot.md`](select-slot.md) |
|
|
19
19
|
| add a repeating list the user edits per item (unit table, cards, gallery) | [`collection-slot.md`](collection-slot.md) |
|
|
20
20
|
| have AI write a slot's content | [`fill-spec-decision.md`](fill-spec-decision.md) |
|
|
21
|
+
| source a fixture from a real property, not invented values | [`pick-a-scenario.md`](pick-a-scenario.md) |
|
|
21
22
|
| preview a section's structural edge cases / states | [`fixture-states.md`](fixture-states.md) |
|
|
22
23
|
| make part of a section interactive (an island) | [`interactive-island.md`](interactive-island.md) |
|
|
23
24
|
| organize a section's files beyond the four reserved | [`organize-section-folder.md`](organize-section-folder.md) |
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
purpose: Add a repeating list whose items the editor selects and edits one at a time.
|
|
3
3
|
status: living
|
|
4
|
-
related: [../primitives.md, ../vocabulary.md, ../schema-system.md, bind-property-fact.md]
|
|
4
|
+
related: [../primitives.md, ../vocabulary.md, ../schema-system.md, bind-property-fact.md, pick-a-scenario.md]
|
|
5
5
|
updated: 2026-07-16
|
|
6
6
|
---
|
|
7
7
|
# A collection slot — a repeating, per-item list
|
|
@@ -58,10 +58,10 @@ type UnitRow = { unit_label: string; beds: number; baths: number; sqft: number;
|
|
|
58
58
|
```
|
|
59
59
|
|
|
60
60
|
```ts
|
|
61
|
-
// fixtures.ts — supply the array from
|
|
62
|
-
import {
|
|
61
|
+
// fixtures.ts — supply the array from a scenario's facts
|
|
62
|
+
import { scenarios } from "@homepages/template-kit/fixtures";
|
|
63
63
|
// ...in base.slots:
|
|
64
|
-
units:
|
|
64
|
+
units: scenarios.luxuryMultiUnit.facts.units,
|
|
65
65
|
```
|
|
66
66
|
|
|
67
67
|
Two contracts to know before you widen this:
|
|
@@ -73,9 +73,10 @@ Two contracts to know before you widen this:
|
|
|
73
73
|
- **An `item` needs more than one field, or a non-scalar one** — a lone scalar
|
|
74
74
|
field is rejected; use a `list` slot for a flat list of scalars.
|
|
75
75
|
|
|
76
|
-
`object_list` is one of
|
|
77
|
-
`image_collection`,
|
|
78
|
-
`poi_list` — same `<SlotItem>` render pattern,
|
|
76
|
+
`object_list` is one of five collection types. A gallery of framed photos is an
|
|
77
|
+
`image_collection`, several hosted videos are a `video_collection`, a flat scalar list is
|
|
78
|
+
a `list`, neighborhood points are a `poi_list` — same `<SlotItem>` render pattern,
|
|
79
|
+
different value shape. See
|
|
79
80
|
[the slot types](../vocabulary.md#slot-types) for which to pick.
|
|
80
81
|
|
|
81
82
|
## Commands
|
|
@@ -92,7 +93,8 @@ Verify with [the author loop](../quickstart.md#the-author-loop).
|
|
|
92
93
|
the `SlotItem` / `SlotGroup` contract: both markers a collection needs, and why
|
|
93
94
|
`index` must be the row's array position.
|
|
94
95
|
- [The closed vocabulary](../vocabulary.md#slot-types) — every collection slot type
|
|
95
|
-
(`object_list` / `image_collection` / `poi_list` / `list`) and the
|
|
96
|
+
(`object_list` / `image_collection` / `video_collection` / `poi_list` / `list`) and the
|
|
97
|
+
`direct()` List sources.
|
|
96
98
|
- [The schema system](../schema-system.md#fill-spects--how-ai-fills-the-slots) — the
|
|
97
99
|
`produced_by` decision types (`structured-list`, `list-extract`, `image-collection`)
|
|
98
100
|
for a collection AI fills instead of binding to a fact.
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
---
|
|
2
|
+
purpose: Preview a section's structural edge cases.
|
|
3
|
+
status: living
|
|
4
|
+
related: [../schema-system.md, ../dev.md, pick-a-scenario.md]
|
|
5
|
+
updated: 2026-07-16
|
|
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 case, 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 12-unit mansard Victorian 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 12-unit mansard Victorian moments from the Emerald Necklace, with a " +
|
|
39
|
+
"terrazzo lobby, an on-site gym, quartz kitchens throughout, and a " +
|
|
40
|
+
"top-floor penthouse with a private rooftop terrace and skyline views.",
|
|
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 state; 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
|
+
### Swap the whole scenario
|
|
55
|
+
|
|
56
|
+
A state can also be designed to look different against a *sparser property* — fewer
|
|
57
|
+
units, no logo, no video — rather than a content-length or option branch. Source that
|
|
58
|
+
kind of state from a different scenario, not from hand-picked overrides:
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
// fixtures.ts — a `sparse` state sourced from a different scenario
|
|
62
|
+
import { scenarios } from "@homepages/template-kit/fixtures";
|
|
63
|
+
|
|
64
|
+
const luxury = scenarios.luxuryMultiUnit;
|
|
65
|
+
const sparse = scenarios.sparseSingleFamily;
|
|
66
|
+
|
|
67
|
+
const fixtures: FixtureModule = {
|
|
68
|
+
base: {
|
|
69
|
+
slots: {
|
|
70
|
+
headline: luxury.facts.address,
|
|
71
|
+
summary: "A 12-unit mansard Victorian moments from the Emerald Necklace.",
|
|
72
|
+
},
|
|
73
|
+
options: { align: "center" },
|
|
74
|
+
},
|
|
75
|
+
states: {
|
|
76
|
+
sparse: {
|
|
77
|
+
slots: {
|
|
78
|
+
headline: sparse.facts.address,
|
|
79
|
+
summary:
|
|
80
|
+
"A classic center-hall colonial with an updated kitchen and original " +
|
|
81
|
+
"hardwood floors throughout.",
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Fill every slot in a scenario-swapped state the same way you filled `base` — by the
|
|
89
|
+
slot's fill-spec source, never with a placeholder. See
|
|
90
|
+
[the fill-by-source rule](../schema-system.md#fill-every-slot-from-the-scenario-by-its-fill-spec-source)
|
|
91
|
+
and [Pick a scenario](pick-a-scenario.md) for the picker and helper cheat-sheet.
|
|
92
|
+
|
|
93
|
+
## Commands
|
|
94
|
+
|
|
95
|
+
None — this is a pure content edit. No CLI command beyond the author loop below.
|
|
96
|
+
|
|
97
|
+
## Verify
|
|
98
|
+
|
|
99
|
+
Verify with [the author loop](../quickstart.md#the-author-loop).
|
|
100
|
+
|
|
101
|
+
## See also
|
|
102
|
+
|
|
103
|
+
- [The schema system](../schema-system.md#fixturests--content-for-local-preview) —
|
|
104
|
+
the `fixtures.ts` contract: `base`, `states`, the scenario breadth table, and the
|
|
105
|
+
fill-by-source rule.
|
|
106
|
+
- [Pick a scenario](pick-a-scenario.md) — the scenario picker and helper cheat-sheet a
|
|
107
|
+
scenario-swapped state builds on.
|
|
108
|
+
- [`template-kit dev`](../dev.md) — the preview server that renders each
|
|
109
|
+
fixture state across the breakpoint ladder.
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
purpose: Add an image slot the editor crops to a fixed ratio.
|
|
3
3
|
status: living
|
|
4
|
-
related: [../schema-system.md, ../primitives.md, ../rules/image-bare-needs-reason.md]
|
|
5
|
-
updated: 2026-07-
|
|
4
|
+
related: [../schema-system.md, ../primitives.md, ../rules/image-bare-needs-reason.md, pick-a-scenario.md]
|
|
5
|
+
updated: 2026-07-16
|
|
6
6
|
---
|
|
7
7
|
# Image slot with a locked crop
|
|
8
8
|
|
|
@@ -52,7 +52,9 @@ interior_image: {
|
|
|
52
52
|
|
|
53
53
|
```ts
|
|
54
54
|
// fixtures.ts — a photo + a matching mkResponsive() height
|
|
55
|
-
|
|
55
|
+
import { mkResponsive, scenarios } from "@homepages/template-kit/fixtures";
|
|
56
|
+
|
|
57
|
+
const interiorImage = scenarios.luxuryMultiUnit.photos[3]!;
|
|
56
58
|
// ...
|
|
57
59
|
interior_image: {
|
|
58
60
|
photo_id: interiorImage.id,
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
---
|
|
2
|
+
purpose: Source a fixture's slots from real property data, by the fill-by-source rule.
|
|
3
|
+
status: living
|
|
4
|
+
related: [../schema-system.md, fixture-states.md, ../vocabulary.md]
|
|
5
|
+
updated: 2026-07-16
|
|
6
|
+
---
|
|
7
|
+
# Pick a scenario for a fixture
|
|
8
|
+
|
|
9
|
+
## Goal
|
|
10
|
+
|
|
11
|
+
Fill a section's `fixtures.ts` from the kit's golden scenarios instead of inventing
|
|
12
|
+
values, so preview stays honest and every fixture is grounded in a real,
|
|
13
|
+
self-consistent property.
|
|
14
|
+
|
|
15
|
+
## The delta
|
|
16
|
+
|
|
17
|
+
### Pick a scenario
|
|
18
|
+
|
|
19
|
+
Import `scenarios` from the fixtures entry — never the root barrel; dev-only sample
|
|
20
|
+
data must never reach a published renderer bundle's import graph — and pick the one
|
|
21
|
+
whose breadth matches the state you're building:
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { scenarios } from "@homepages/template-kit/fixtures";
|
|
25
|
+
|
|
26
|
+
const luxury = scenarios.luxuryMultiUnit; // 12 units, 3 contacts, both logos, video + 3D tour
|
|
27
|
+
const sparse = scenarios.sparseSingleFamily; // 1 unit, 1 contact, no logos, no media
|
|
28
|
+
const rent = scenarios.forRentCondo; // 1 unit, 2 contacts, both logos, video only
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`base` almost always builds on `luxuryMultiUnit` — it's the breadth ceiling, so a
|
|
32
|
+
layout that survives it survives everything smaller. Reach for `sparseSingleFamily`
|
|
33
|
+
or `forRentCondo` for a [`states`](fixture-states.md) entry designed to look
|
|
34
|
+
different against a thinner property.
|
|
35
|
+
|
|
36
|
+
| Scenario | units | listing_intent | property_type | contacts | pois | photos | contact logos | media |
|
|
37
|
+
|---|---|---|---|---|---|---|---|---|
|
|
38
|
+
| `luxuryMultiUnit` | 12 | for_sale | multi_family | 3 | 24 | 20 | both logos | video + 3D tour |
|
|
39
|
+
| `sparseSingleFamily` | 1 | for_sale | single_family | 1 | 4 | 5 | none (null) | none (null) |
|
|
40
|
+
| `forRentCondo` | 1 | for_rent | condo_townhome | 2 | 8 | 10 | both logos | video only |
|
|
41
|
+
|
|
42
|
+
### Fill every slot by its fill-spec source
|
|
43
|
+
|
|
44
|
+
Don't invent a value. Every slot in `schema.ts` already declares how it's filled in
|
|
45
|
+
production — `fixtures.ts` mirrors that:
|
|
46
|
+
|
|
47
|
+
| Slot's fill-spec source | How the fixture fills it |
|
|
48
|
+
|---|---|
|
|
49
|
+
| direct (a fact) | read it off the scenario — `s.facts.address`, `primaryContact(s).fields.phone` |
|
|
50
|
+
| derived | a helper — `propertyMetrics(s)`, `poisByCategory(s)`, `contactCards(s)` |
|
|
51
|
+
| ai | a short sample fill **grounded in the scenario's facts** — a headline about *this* property, never lorem ipsum or invented facts |
|
|
52
|
+
|
|
53
|
+
A fixture that gets this wrong is worse than a missing one: a slot bound
|
|
54
|
+
`direct("beds")` whose fixture value doesn't match `s.facts.units[0].beds` teaches
|
|
55
|
+
nothing about the layout it's supposed to preview.
|
|
56
|
+
|
|
57
|
+
### The helper cheat-sheet
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
import {
|
|
61
|
+
contactCards,
|
|
62
|
+
mkResponsive,
|
|
63
|
+
poisByCategory,
|
|
64
|
+
primaryContact,
|
|
65
|
+
propertyMetrics,
|
|
66
|
+
scenarios,
|
|
67
|
+
} from "@homepages/template-kit/fixtures";
|
|
68
|
+
|
|
69
|
+
const s = scenarios.luxuryMultiUnit;
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
| Helper | Returns |
|
|
73
|
+
|---|---|
|
|
74
|
+
| `primaryContact(s)` | The scenario's primary contact (`ContactFixture`, position 0 by construction) — throws if the scenario has none |
|
|
75
|
+
| `contactCards(s)` | Every attached contact, in order (`ContactFixture[]`) |
|
|
76
|
+
| `poisByCategory(s)` | The scenario's POIs bucketed by category (`Record<PoiFixture["category"], PoiFixture[]>`) |
|
|
77
|
+
| `propertyMetrics(s)` | `beds`/`baths`/`sqft`/`price` off the lowest-position unit, `bedsMin`/`bedsMax`/`bathsMin`/`bathsMax`/`sqftMax`/`priceStart` across all units, and `addressLine1`/`addressLine2` |
|
|
78
|
+
| `mkResponsive(url, width, height)` | A deterministic `ResponsiveImage` descriptor so `<Image responsive>` can be previewed and snapshotted |
|
|
79
|
+
|
|
80
|
+
`propertyMetrics` returns kit fact vocabulary — `addressLine1`/`addressLine2`, not a
|
|
81
|
+
template's own slot names, so the kit never learns what a section calls its slots. Map
|
|
82
|
+
it to your slots at the call site:
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
const m = propertyMetrics(s);
|
|
86
|
+
// ...in base.slots:
|
|
87
|
+
address_street: m.addressLine1,
|
|
88
|
+
address_locality: m.addressLine2,
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Fields the helpers don't cover are readable straight off the scenario: `s.photos`,
|
|
92
|
+
`s.pois`, `s.amenities`, `s.contacts`, `s.media`, and `s.facts` (the raw
|
|
93
|
+
`PropertyFacts`). A contact's org name and logos (`logo_light`/`logo_dark`) live on
|
|
94
|
+
`ContactFixture["fields"]`, not on a sibling "brand" object — read them off
|
|
95
|
+
`primaryContact(s).fields` or `s.contacts`.
|
|
96
|
+
|
|
97
|
+
### Add a scenario-driven state
|
|
98
|
+
|
|
99
|
+
A `states` entry designed to look different against a sparser property sources from a
|
|
100
|
+
different scenario, not from overridden literals — see
|
|
101
|
+
[Preview a section's structural edge cases](fixture-states.md#swap-the-whole-scenario)
|
|
102
|
+
for the full pattern.
|
|
103
|
+
|
|
104
|
+
## Commands
|
|
105
|
+
|
|
106
|
+
None — this is a pure content edit. No CLI command beyond the author loop below.
|
|
107
|
+
|
|
108
|
+
## Verify
|
|
109
|
+
|
|
110
|
+
Verify with [the author loop](../quickstart.md#the-author-loop).
|
|
111
|
+
|
|
112
|
+
## See also
|
|
113
|
+
|
|
114
|
+
- [The schema system](../schema-system.md#fixturests--content-for-local-preview) —
|
|
115
|
+
the `fixtures.ts` contract, the scenario breadth table, and the fill-by-source rule.
|
|
116
|
+
- [Preview a section's structural edge cases](fixture-states.md) — `states`, including
|
|
117
|
+
a scenario-swapped state.
|
|
118
|
+
- [The closed vocabulary](../vocabulary.md#direct-sources) — the `direct()` fields a
|
|
119
|
+
scenario's `facts` supplies.
|
|
@@ -7,7 +7,7 @@ updated: 2026-07-15
|
|
|
7
7
|
# Rules
|
|
8
8
|
|
|
9
9
|
Every authoring rule the kit enforces is printed under one namespace, `template-kit/<id>`,
|
|
10
|
-
and resolves to exactly one page here: `
|
|
10
|
+
and resolves to exactly one page here: `guide/rules/<id>.md`.
|
|
11
11
|
|
|
12
12
|
**One namespace, two venues.** Some rules are properties of **one file** (a `Date.now()`
|
|
13
13
|
in a Renderer) and are enforced by the ESLint preset. Others are properties of the
|
|
@@ -28,7 +28,7 @@ and it is explained there rather than on each of them.
|
|
|
28
28
|
| [`no-client-directive-in-contract`](no-client-directive-in-contract.md) | eslint | No "use client" in a section's four contract files. |
|
|
29
29
|
| [`props-from-schema`](props-from-schema.md) | eslint | A Renderer's Props type must be derived from schema.ts, not hand-written. |
|
|
30
30
|
| [`no-inline-style`](no-inline-style.md) | eslint | No inline style= in server-rendered JSX. Islands are exempt. |
|
|
31
|
-
| [`no-raw-element`](no-raw-element.md) | eslint | No raw `<section>` or `<
|
|
31
|
+
| [`no-raw-element`](no-raw-element.md) | eslint | No raw `<section>`, `<img>` or `<video>` — use `<Section>`, `<Image>` and `<Video>`. |
|
|
32
32
|
| [`image-bare-needs-reason`](image-bare-needs-reason.md) | eslint | `<Image bare>` needs a `// bare:` comment saying why it skips the frame. |
|
|
33
33
|
| [`slot-marker-literal`](slot-marker-literal.md) | eslint | A slot-marker key must be a string literal, not an expression. |
|
|
34
34
|
| [`no-hex`](no-hex.md) | eslint | No hard-coded colours — the one legal colour function is a `color-mix()` over theme-derived arguments. Islands are NOT exempt. |
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
purpose: The template-kit/fixtures-invalid authoring rule — what it enforces, why, and how to fix it.
|
|
3
3
|
status: living
|
|
4
4
|
related: [INDEX.md, ../schema-system.md, schema-invalid.md]
|
|
5
|
-
updated: 2026-07-
|
|
5
|
+
updated: 2026-07-16
|
|
6
6
|
---
|
|
7
7
|
# template-kit/fixtures-invalid
|
|
8
8
|
|
|
@@ -21,7 +21,7 @@ The default export of `fixtures.ts` must satisfy the fixture-module shape:
|
|
|
21
21
|
variants: { /* optional */ },
|
|
22
22
|
},
|
|
23
23
|
option_matrix: [ /* optional: { label?, options } entries */ ],
|
|
24
|
-
states: { /* optional: named
|
|
24
|
+
states: { /* optional: named state → { label?, slots?, options?, anchors?, variants? } */ },
|
|
25
25
|
}
|
|
26
26
|
```
|
|
27
27
|
|
|
@@ -6,28 +6,29 @@ updated: 2026-07-14
|
|
|
6
6
|
---
|
|
7
7
|
# template-kit/no-raw-element
|
|
8
8
|
|
|
9
|
-
> **Exactly
|
|
10
|
-
> yours.
|
|
9
|
+
> **Exactly three** raw elements are banned: `<section>`, `<img>` and `<video>`. Every
|
|
10
|
+
> other tag is yours.
|
|
11
11
|
|
|
12
12
|
## Rule
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
Three tags, and no others:
|
|
15
15
|
|
|
16
16
|
| Raw tag | Use instead |
|
|
17
17
|
|---|---|
|
|
18
18
|
| `<section>` | `<Section>` |
|
|
19
19
|
| `<img>` | `<Image>` |
|
|
20
|
+
| `<video>` | `<Video>` |
|
|
20
21
|
|
|
21
22
|
That is the entire rule. **`<a>`, `<h1>`–`<h6>`, `<div>`, `<ul>`, `<button>`, `<picture>`,
|
|
22
23
|
`<figure>` — every other element is yours to write.** Presentation is the template's own
|
|
23
24
|
JSX; the kit is not trying to own your markup. If you have concluded "the kit bans raw
|
|
24
25
|
HTML", that is exactly backwards.
|
|
25
26
|
|
|
26
|
-
There is no island exemption: the
|
|
27
|
+
There is no island exemption: the three tags are banned in a `"use client"` file too.
|
|
27
28
|
|
|
28
29
|
## Reason
|
|
29
30
|
|
|
30
|
-
Each of the
|
|
31
|
+
Each of the three banned tags has a **contract-bearing primitive that must emit it**, and
|
|
31
32
|
writing the raw tag skips the contract.
|
|
32
33
|
|
|
33
34
|
- **`<Section>`** emits `<section class="tr-section">`, which is full-bleed by contract:
|
|
@@ -40,9 +41,13 @@ writing the raw tag skips the contract.
|
|
|
40
41
|
or a broken-image icon on a real customer's page — and a missing image is a **supported
|
|
41
42
|
state**, not a bug: it happens whenever a listing is thin. `<Image slotId>` also emits
|
|
42
43
|
the slot marker itself, so the image stays selectable in the editor.
|
|
44
|
+
- **`<Video>`** owns the poster frame, the author-declared playback config, and the same
|
|
45
|
+
fallback rect — a video slot may be empty (a thin listing) or still transcoding, and
|
|
46
|
+
neither may collapse the layout. A raw `<video>` also skips the slot marker, so the
|
|
47
|
+
video stops being selectable in the editor.
|
|
43
48
|
|
|
44
|
-
The
|
|
45
|
-
Everything else carries no contract, so nothing is gained by policing it.
|
|
49
|
+
The three tags are banned because they are the three the platform has to be able to
|
|
50
|
+
trust. Everything else carries no contract, so nothing is gained by policing it.
|
|
46
51
|
|
|
47
52
|
## Fix
|
|
48
53
|
|
|
@@ -95,6 +100,6 @@ The `<h2>` stays exactly as it was. `Image` takes `src`/`alt` directly — a `nu
|
|
|
95
100
|
|
|
96
101
|
## See also
|
|
97
102
|
|
|
98
|
-
- [Contract primitives](../primitives.md) —
|
|
103
|
+
- [Contract primitives](../primitives.md) — every primitive and the markers they emit.
|
|
99
104
|
- [`image-bare-needs-reason`](image-bare-needs-reason.md) — the escape hatch that drops
|
|
100
105
|
`<Image>`'s frame, and what it costs.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
purpose: The template-kit/render-invariant authoring rule — what it enforces, why, and how to fix it.
|
|
3
3
|
status: living
|
|
4
4
|
related: [INDEX.md, ../primitives.md, ../schema-system.md]
|
|
5
|
-
updated: 2026-07-
|
|
5
|
+
updated: 2026-07-16
|
|
6
6
|
---
|
|
7
7
|
# template-kit/render-invariant
|
|
8
8
|
|
|
@@ -51,7 +51,8 @@ routine, not exceptional** — and your markup has to say what happens then.
|
|
|
51
51
|
Worth being precise, because the obvious guesses are wrong. Rendering a nullish slot as a
|
|
52
52
|
**child** is safe: React renders `null` and `undefined` as nothing, so `<p>{slots.year_built}</p>`
|
|
53
53
|
on a thin listing is an empty `<p>`, not the text `undefined`. And a list slot is **never
|
|
54
|
-
`null`** — `list`, `object_list`, `poi_list` and `
|
|
54
|
+
`null`** — `list`, `object_list`, `poi_list`, `image_collection` and `video_collection`
|
|
55
|
+
always arrive as an array
|
|
55
56
|
(empty when absent), so `.map` cannot throw and `?? []` buys you nothing.
|
|
56
57
|
|
|
57
58
|
The defects come from three narrower places:
|
|
@@ -107,7 +108,7 @@ export function Renderer({ slots }: Props) {
|
|
|
107
108
|
}
|
|
108
109
|
```
|
|
109
110
|
|
|
110
|
-
On the golden
|
|
111
|
+
On the richest golden scenario this renders perfectly. On a listing with no year, an unpriced unit,
|
|
111
112
|
and no floor plan it ships `built null`, `2 Bed · undefined`, `NaN sq ft heated`, and
|
|
112
113
|
`<img src="">`.
|
|
113
114
|
|