@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.
Files changed (110) hide show
  1. package/CHANGELOG.md +85 -0
  2. package/README.md +38 -27
  3. package/dist/base.css +6 -4
  4. package/dist/cli/dev/fill-state.js +5 -3
  5. package/dist/cli/link/overlay.js +1 -1
  6. package/dist/cli/new/scaffold/template/sections/hero/fixtures.ts +7 -7
  7. package/dist/contracts/slot-catalog.d.ts +2 -2
  8. package/dist/contracts/slot-catalog.js +3 -1
  9. package/dist/eslint/rules/no-raw-element.js +3 -2
  10. package/dist/fixtures/helpers.d.ts +23 -0
  11. package/dist/fixtures/helpers.js +50 -0
  12. package/dist/fixtures/sample-property.d.ts +7 -17
  13. package/dist/fixtures/sample-property.js +179 -57
  14. package/dist/fixtures/scenario.d.ts +20 -0
  15. package/dist/fixtures/scenarios/for-rent-condo.d.ts +5 -0
  16. package/dist/fixtures/scenarios/for-rent-condo.js +364 -0
  17. package/dist/fixtures/scenarios/index.d.ts +8 -0
  18. package/dist/fixtures/scenarios/index.js +13 -0
  19. package/dist/fixtures/scenarios/luxury-multi-unit.d.ts +5 -0
  20. package/dist/fixtures/scenarios/luxury-multi-unit.js +19 -0
  21. package/dist/fixtures/scenarios/sparse-single-family.d.ts +5 -0
  22. package/dist/fixtures/scenarios/sparse-single-family.js +233 -0
  23. package/dist/fixtures.d.ts +8 -2
  24. package/dist/fixtures.js +7 -2
  25. package/dist/index.d.ts +3 -2
  26. package/dist/index.js +2 -1
  27. package/dist/island-runtime.js +22 -7
  28. package/dist/package.js +1 -1
  29. package/dist/primitives/Image.d.ts +1 -1
  30. package/dist/primitives/Video.d.ts +38 -0
  31. package/dist/primitives/Video.js +63 -0
  32. package/dist/primitives/index.d.ts +2 -1
  33. package/dist/rules/registry.js +3 -3
  34. package/dist/schema/index.d.ts +2 -2
  35. package/dist/schema/runtime-values.d.ts +20 -1
  36. package/dist/schema/section-props.d.ts +8 -1
  37. package/dist/schema/section-schema.d.ts +2 -2
  38. package/dist/schema/slot-types.d.ts +15 -1
  39. package/dist/schema/slot-types.js +34 -0
  40. package/dist/schema/synthesize-nullable.js +1 -0
  41. package/{docs → guide}/INDEX.md +7 -6
  42. package/{docs → guide}/check.md +2 -2
  43. package/{docs → guide}/islands.md +6 -1
  44. package/{docs → guide}/llms.txt +34 -14
  45. package/{docs → guide}/overview.md +4 -4
  46. package/{docs → guide}/pack.md +2 -2
  47. package/{docs → guide}/primitives.md +59 -11
  48. package/{docs → guide}/recipes/INDEX.md +1 -0
  49. package/{docs → guide}/recipes/collection-slot.md +10 -8
  50. package/guide/recipes/fixture-states.md +109 -0
  51. package/{docs → guide}/recipes/image-slot-crop.md +5 -3
  52. package/guide/recipes/pick-a-scenario.md +119 -0
  53. package/{docs → guide}/rules/INDEX.md +2 -2
  54. package/{docs → guide}/rules/fixtures-invalid.md +2 -2
  55. package/{docs → guide}/rules/no-raw-element.md +13 -8
  56. package/{docs → guide}/rules/render-invariant.md +4 -3
  57. package/{docs → guide}/schema-system.md +97 -16
  58. package/{docs → guide}/theme-and-css.md +3 -2
  59. package/guide/upgrading.md +61 -0
  60. package/{docs → guide}/vocabulary.md +3 -1
  61. package/package.json +8 -3
  62. package/docs/recipes/fixture-states.md +0 -68
  63. /package/{docs → guide}/assets.md +0 -0
  64. /package/{docs → guide}/dev.md +0 -0
  65. /package/{docs → guide}/eslint.md +0 -0
  66. /package/{docs → guide}/new.md +0 -0
  67. /package/{docs → guide}/quickstart.md +0 -0
  68. /package/{docs → guide}/recipes/bind-property-fact.md +0 -0
  69. /package/{docs → guide}/recipes/fill-spec-decision.md +0 -0
  70. /package/{docs → guide}/recipes/interactive-island.md +0 -0
  71. /package/{docs → guide}/recipes/organize-section-folder.md +0 -0
  72. /package/{docs → guide}/recipes/prepare-submission.md +0 -0
  73. /package/{docs → guide}/recipes/second-template.md +0 -0
  74. /package/{docs → guide}/recipes/select-slot.md +0 -0
  75. /package/{docs → guide}/recipes/static-asset.md +0 -0
  76. /package/{docs → guide}/recipes/third-party-package.md +0 -0
  77. /package/{docs → guide}/rules/audit-severity.md +0 -0
  78. /package/{docs → guide}/rules/bundle-binary-asset.md +0 -0
  79. /package/{docs → guide}/rules/bundle-incomplete.md +0 -0
  80. /package/{docs → guide}/rules/css-reason.md +0 -0
  81. /package/{docs → guide}/rules/determinism-drift.md +0 -0
  82. /package/{docs → guide}/rules/image-bare-needs-reason.md +0 -0
  83. /package/{docs → guide}/rules/license-denied.md +0 -0
  84. /package/{docs → guide}/rules/lockfile-missing.md +0 -0
  85. /package/{docs → guide}/rules/lockfile-stale.md +0 -0
  86. /package/{docs → guide}/rules/manifest-invalid.md +0 -0
  87. /package/{docs → guide}/rules/missing-slot-marker.md +0 -0
  88. /package/{docs → guide}/rules/no-bare-css-import.md +0 -0
  89. /package/{docs → guide}/rules/no-client-directive-in-contract.md +0 -0
  90. /package/{docs → guide}/rules/no-client-runtime-in-server.md +0 -0
  91. /package/{docs → guide}/rules/no-css-import-from-render-path.md +0 -0
  92. /package/{docs → guide}/rules/no-hex.md +0 -0
  93. /package/{docs → guide}/rules/no-inline-style.md +0 -0
  94. /package/{docs → guide}/rules/no-nondeterminism.md +0 -0
  95. /package/{docs → guide}/rules/no-templates.md +0 -0
  96. /package/{docs → guide}/rules/parse-error.md +0 -0
  97. /package/{docs → guide}/rules/props-from-schema.md +0 -0
  98. /package/{docs → guide}/rules/schema-invalid.md +0 -0
  99. /package/{docs → guide}/rules/serializable-island-props.md +0 -0
  100. /package/{docs → guide}/rules/server-vs-client.md +0 -0
  101. /package/{docs → guide}/rules/sidebar-order.md +0 -0
  102. /package/{docs → guide}/rules/single-react.md +0 -0
  103. /package/{docs → guide}/rules/size-assets.md +0 -0
  104. /package/{docs → guide}/rules/size-island-bundle.md +0 -0
  105. /package/{docs → guide}/rules/size-renderer-bundle.md +0 -0
  106. /package/{docs → guide}/rules/size-section-css.md +0 -0
  107. /package/{docs → guide}/rules/slot-group-marker.md +0 -0
  108. /package/{docs → guide}/rules/slot-marker-literal.md +0 -0
  109. /package/{docs → guide}/rules/typecheck.md +0 -0
  110. /package/{docs → guide}/rules/unlayered-fence.md +0 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,90 @@
1
1
  # @homepages/template-kit
2
2
 
3
+ ## 0.5.1
4
+
5
+ ## 0.5.0
6
+
7
+ ### Minor Changes
8
+
9
+ - ac0d622: Fixtures: replace the single `sampleProperty` dataset with three named scenarios
10
+ (`scenarios.luxuryMultiUnit` / `sparseSingleFamily` / `forRentCondo`) plus fact-level
11
+ helpers (`primaryContact`, `contactCards`, `poisByCategory`, `propertyMetrics`). The raw
12
+ single-property exports (`sampleProperty`, `allPhotos`, `samplePois`, `sampleContacts`,
13
+ `sampleAmenities`, `sampleHeroSlots`) are removed — build fixtures from a scenario.
14
+ `mkResponsive` and the data types are unchanged.
15
+
16
+ A scenario's org name and logos live on `ContactFixture["fields"]`
17
+ (`org_name`/`logo_light`/`logo_dark`), matching where the real facts schema and
18
+ `SOURCE_FIELDS` (`logo_dark`/`logo_light`, `container: "contact"`) put them — there is
19
+ no separate `brand` field or `BrandFixture` type. `propertyMetrics`'s cross-unit
20
+ aggregates (`bedsMin`/`bedsMax`/`bathsMin`/`bathsMax`/`sqftMax`) are `string | null`,
21
+ matching the canonical `units.bedsMin`/etc. transforms' formatted output (e.g. `"3,800"`,
22
+ not `3800`); `beds`/`baths`/`sqft`/`price` (the lowest-position unit's raw figures) are
23
+ unchanged.
24
+
25
+ - 0907653: The shipped authoring corpus moved from `docs/` to `guide/` inside the published
26
+ package (e.g. `node_modules/@homepages/template-kit/guide/quickstart.md`). Every
27
+ rule-id doc path (`docsPathForRule`, printed in `check`/lint failures) and the
28
+ `link`/`unlink`/`status` overlay now resolve against `guide/` as well. Update any
29
+ tooling or bookmarks that hardcoded the old `docs/` path.
30
+ - a0712da: Add the `video` and `video_collection` slot types and the `<Video>` primitive — hosted
31
+ video a section declares like an image, not a link to a third-party player.
32
+
33
+ A `video` slot's value is `{ video_id, url, poster, alt, width?, height? }`; a
34
+ `video_collection` is an array of those. `url` always points at a single transcoded
35
+ progressive MP4 rendition, so — unlike `ImageValue` — there is no `responsive` ladder and
36
+ no `mobile` variant. Video slots have **no cropping or editing of any kind**: a video is
37
+ selected, never edited, so none of `ImageValue`'s `rect`/`ar`/`crop_key` re-edit state
38
+ exists, and authoring `crop` on a video slot is rejected. Defaults are non-null
39
+ (`{ url: "", poster: "", alt: "" }` and `[]`) so a renderer with a non-nullable slot
40
+ cannot crash on `slots.x.url`.
41
+
42
+ Playback is **author-declared per slot** via a `playback` config alongside `frame` —
43
+ a silent hero loop and a narrated tour need opposite behavior and the platform cannot
44
+ infer which from the asset:
45
+
46
+ ```ts
47
+ hero_loop: {
48
+ type: "video",
49
+ frame: { breakpoint: 768, desktop: { aspect: 16 / 9 } },
50
+ playback: { autoplay: true, muted: true, loop: true, controls: false, preload: "auto" },
51
+ }
52
+ ```
53
+
54
+ **`autoplay` requires `muted`** — every browser blocks unmuted autoplay, so declaring one
55
+ without the other fails `check` rather than shipping a video that silently never plays.
56
+ `preload: "none"` is the bandwidth lever for a below-fold video most visitors never reach.
57
+
58
+ **Upgrading: raw `<video>` is now banned.** `<Video>` is the sole lint-legal emitter of
59
+ the tag — `no-raw-element` bans it exactly as it does `<img>`, with no island exemption.
60
+ If a section of yours writes a raw `<video>`, `check` will fail until it moves to
61
+ `<Video>`; that is the one backward-incompatible part of this release.
62
+
63
+ `<Video>` renders the poster, passes playback through verbatim,
64
+ and — like `<Image>` — keeps its aspect-ratio box and fallback rect when `url` is empty,
65
+ which is what both an unfilled slot and a still-transcoding video look like. With no
66
+ `playback` declared it falls back to a plain controls player.
67
+
68
+ ### Patch Changes
69
+
70
+ - aaa2e2f: Islands: `hydrateIslands({ applyEditorProps: true })` now hydrates an island's
71
+ server-rendered HTML instead of discarding it. Applying `editor.props` previously issued a
72
+ root-level re-render before hydration had flushed, which made React drop the SSR tree,
73
+ client-render the island from scratch, and log `This root received an early update, before
74
+ anything was able hydrate` straight to the console — where the loader's `onError` could not
75
+ intercept it. The props are now merged in a layout effect after the hydration commit, so
76
+ the SSR tree survives and the console stays clean.
77
+
78
+ Because the island is now hydrated rather than replaced, `editor.props` arrive as an update
79
+ after mount rather than as part of the initial mount. A prop read only at mount time — a
80
+ `useState` initializer, an `<input defaultValue>` — must be read during render instead, or
81
+ the editor-only value will not apply.
82
+
83
+ - c79d75a: Adds a guide page on upgrading: what a patch vs a minor release means while the kit
84
+ is pre-1.0, why a 0.x caret cannot cross a minor on its own, and how long submissions
85
+ built on the previous kit line keep being accepted. `npm create homepages-workspace` is
86
+ now published, and always carries the same version as the kit it scaffolds against.
87
+
3
88
  ## 0.4.1
4
89
 
5
90
  ### Patch Changes
package/README.md CHANGED
@@ -9,7 +9,7 @@ are complete — a section is authorable end to end against the kit. The `templa
9
9
  CLI's `check`, `theme`, `dev`, and `pack` commands are implemented — see
10
10
  [Checking a workspace](#checking-a-workspace), [Generating a theme](#generating-a-theme),
11
11
  and [Previewing a workspace](#previewing-a-workspace) below — and `new` / `new-section`
12
- scaffold a template or a section into a workspace (see [docs/new.md](docs/new.md)).
12
+ scaffold a template or a section into a workspace (see [guide/new.md](guide/new.md)).
13
13
 
14
14
  ## Quick start
15
15
 
@@ -24,7 +24,7 @@ npm run dev # preview the starter; the island hydrates
24
24
  ```
25
25
 
26
26
  Add more templates with `npx template-kit new <template>` and sections with
27
- `npx template-kit new-section <template>/<section>` — see [docs/new.md](docs/new.md).
27
+ `npx template-kit new-section <template>/<section>` — see [guide/new.md](guide/new.md).
28
28
  Prefer to wire a workspace by hand? See **Install** below.
29
29
 
30
30
  ## Install
@@ -37,7 +37,7 @@ npm install -D typescript eslint @typescript-eslint/parser esbuild # the autho
37
37
  React 19 is a **peer dependency** — the kit never bundles its own copy.
38
38
 
39
39
  The kit has exactly **one runtime dependency** (`zod`). The authoring toolchain the
40
- [`check` command](docs/check.md) and the [ESLint preset](docs/eslint.md) run on is
40
+ [`check` command](guide/check.md) and the [ESLint preset](guide/eslint.md) run on is
41
41
  yours: both resolve TypeScript, ESLint, the parser and esbuild from your workspace, so
42
42
  your pinned versions are the ones that judge your code. The kit does not depend on them
43
43
  at all — which is why a linter never lands in the production install of an app that
@@ -65,30 +65,34 @@ export type Props = SectionProps<typeof schema>; // slots.headline is `string`
65
65
 
66
66
  Property facts come from a closed vocabulary, so `direct("listing_intentt")` is a
67
67
  compile error, not a runtime surprise. Full contract:
68
- **[docs/schema-system.md](docs/schema-system.md)**.
68
+ **[guide/schema-system.md](guide/schema-system.md)**.
69
69
 
70
- Your fixtures build on the kit's golden property dataset (`sampleProperty`, `allPhotos`,
71
- `samplePois`, …) a realistic 12-unit building, long labels and missing values included.
70
+ Your fixtures build on the kit's golden scenarios (`scenarios.luxuryMultiUnit`,
71
+ `sparseSingleFamily`, `forRentCondo`, plus helpers like `propertyMetrics()`) — real,
72
+ self-consistent properties spanning multi/single unit and sale/rent, long labels and
73
+ missing values included.
72
74
 
73
75
  ## The primitives
74
76
 
75
- The kit ships **five components and nothing else**: `Section` (the full-bleed section
76
- root), `Slot` and `SlotItem` and `SlotGroup` (the editor's marker attributes), and
77
- `Image` (responsive `<picture>` + a deterministic empty-state box). Each earns its place
77
+ The kit ships **six components and nothing else**: `Section` (the full-bleed section
78
+ root), `Slot` and `SlotItem` and `SlotGroup` (the editor's marker attributes),
79
+ `Image` (responsive `<picture>` + a deterministic empty-state box), and `Video` (poster
80
+ frame + author-declared playback + the same empty-state box). Each earns its place
78
81
  by emitting something the platform breaks without. Presentation — headings, links,
79
82
  layout, icons — is your template's own JSX.
80
83
 
81
84
  ```tsx
82
- import { Image, Section, Slot, SlotItem } from "@homepages/template-kit";
85
+ import { Image, Section, Slot, SlotItem, Video } from "@homepages/template-kit";
83
86
  ```
84
87
 
85
- Full contract: **[docs/primitives.md](docs/primitives.md)**.
88
+ Full contract: **[guide/primitives.md](guide/primitives.md)**.
86
89
 
87
90
  ## Exports
88
91
 
89
92
  | Entry | Use |
90
93
  |---|---|
91
- | `@homepages/template-kit` | Authoring API — schema system, primitives, marker contract, theme types, and the golden fixtures |
94
+ | `@homepages/template-kit` | Authoring API — schema system, primitives, marker contract, theme types |
95
+ | `@homepages/template-kit/fixtures` | The golden scenarios + helpers for `fixtures.ts` — a separate entry so dev-only sample data never reaches a published renderer bundle |
92
96
  | `@homepages/template-kit/island-runtime` | Browser loader that hydrates a page's islands |
93
97
  | `@homepages/template-kit/browser` | Browser helpers for published pages (site config, signed ingest) |
94
98
  | `@homepages/template-kit/styles.css` | The global Tailwind preamble: the breakpoint ladder, the spacing base unit, the motion defaults, kit source registration |
@@ -96,8 +100,10 @@ Full contract: **[docs/primitives.md](docs/primitives.md)**.
96
100
  | `@homepages/template-kit/eslint` | ESLint flat-config preset |
97
101
  | `@homepages/template-kit/tsconfig` | tsconfig preset (`"extends"`) |
98
102
 
99
- The authoring API has **no subpath imports** one specifier for everything you
100
- write a section against. The two CSS entries are the deliberate exception.
103
+ One specifier for everything you write a section against. The exceptions are
104
+ deliberate: the two CSS entries, and `/fixtures` — the golden scenarios, which only
105
+ `fixtures.ts` imports, kept off the root barrel so dev-only sample data can never
106
+ reach a published renderer's import graph.
101
107
 
102
108
  `styles.css` does **not** import Tailwind. Import it *after* Tailwind in your entry, then
103
109
  your generated theme after that:
@@ -113,15 +119,15 @@ belongs in this entry and nowhere else: it carries an `@theme` block, a Tailwind
113
119
  that is inert outside the build graph. `base.css` is not part of that entry — it loads as
114
120
  its own stylesheet on the page. `base.css` ships no reset of its own — it depends on the
115
121
  preflight your entry's `@import "tailwindcss"` brings. Both must be on the page. Full
116
- contract: **[docs/theme-and-css.md](docs/theme-and-css.md)**.
122
+ contract: **[guide/theme-and-css.md](guide/theme-and-css.md)**.
117
123
 
118
124
  ## Rules
119
125
 
120
126
  The lint preset and the `template-kit check` CLI share one id namespace,
121
127
  `template-kit/<id>`. A rule-id printed by either resolves to a page —
122
- `docs/rules/<id>.md` — giving the rule, the reason it exists, and a before/after fix.
123
- Start at **[docs/rules/INDEX.md](docs/rules/INDEX.md)**; the full doc set is routed
124
- from **[docs/INDEX.md](docs/INDEX.md)**.
128
+ `guide/rules/<id>.md` — giving the rule, the reason it exists, and a before/after fix.
129
+ Start at **[guide/rules/INDEX.md](guide/rules/INDEX.md)**; the full doc set is routed
130
+ from **[guide/INDEX.md](guide/INDEX.md)**.
125
131
 
126
132
  ## Generating a theme
127
133
 
@@ -133,7 +139,7 @@ template-kit theme --all # generate every template's theme.css
133
139
  `theme` compiles a template's `theme.ts` into `templates/<key>/theme.css` — the file your
134
140
  Tailwind entry imports. The output is generated: don't hand-edit it, and re-run the
135
141
  command whenever `theme.ts` changes. Full contract:
136
- **[docs/theme-and-css.md](docs/theme-and-css.md#generating-themecss)**.
142
+ **[guide/theme-and-css.md](guide/theme-and-css.md#generating-themecss)**.
137
143
 
138
144
  ## Previewing a workspace
139
145
 
@@ -152,7 +158,7 @@ section also has a standalone page, and so does the whole template at
152
158
  payload), and `/api/screenshot` (a full-page PNG, section or whole template). Like
153
159
  `check`, it runs **your** toolchain — Vite and Tailwind resolved from your workspace,
154
160
  installed with `npm i -D vite @vitejs/plugin-react tailwindcss @tailwindcss/cli` — not
155
- shipped by the kit. Full contract: **[docs/dev.md](docs/dev.md)**.
161
+ shipped by the kit. Full contract: **[guide/dev.md](guide/dev.md)**.
156
162
 
157
163
  ## Checking a workspace
158
164
 
@@ -167,7 +173,7 @@ contracts, runs a determinism render probe, and enforces per-section byte budget
167
173
  plus workspace-wide dependency gates (lockfile health, a single React, license and
168
174
  vulnerability checks) evaluated once per run. It exits non-zero on any failure and is
169
175
  the same gate the platform re-runs when you submit a template. Full contract:
170
- **[docs/check.md](docs/check.md)**.
176
+ **[guide/check.md](guide/check.md)**.
171
177
 
172
178
  ## Development
173
179
 
@@ -199,9 +205,13 @@ token anywhere, and adding one would silently disable OIDC.
199
205
  ```bash
200
206
  npm run changeset
201
207
  ```
202
- Pick the bump (patch / minor / major) and write a human-readable summary —
203
- it becomes the changelog entry that template authors read. Commit the
204
- generated file in `.changeset/`.
208
+ The kit is pre-1.0, so **minor is the breaking axis** everything
209
+ non-breaking, features and fixes alike, is a **patch**. Never pick major
210
+ while 0.x: changesets offers it in its prompt, but taking it cuts 1.0.0, a
211
+ deliberate decision with its own issue, never a side effect of a
212
+ changeset. Write a human-readable summary — it becomes the changelog
213
+ entry that template authors read. Commit the generated file in
214
+ `.changeset/`.
205
215
 
206
216
  2. **Merge to `main`.** The release workflow opens (or updates) a
207
217
  **"Version Packages"** PR that consumes the changeset files, bumps
@@ -210,9 +220,10 @@ token anywhere, and adding one would silently disable OIDC.
210
220
  3. **Merge the Version Packages PR.** That merge publishes to npm.
211
221
 
212
222
  Semver contract: the platform vocabulary (slot types, sources, transforms) is
213
- closed and versioned. A breaking change to it is a **major**; a new optional
214
- capability is a **minor**. Design tokens are *not* part of that vocabulary
215
- your template names its own.
223
+ closed and versioned, and the kit is pre-1.0, so **minor is the breaking
224
+ axis** a breaking change to the vocabulary is a **minor**; a new optional
225
+ capability, and any bug fix, is a **patch**. Design tokens are *not* part of
226
+ that vocabulary — your template names its own.
216
227
 
217
228
  ### Bumping the version by hand
218
229
 
package/dist/base.css CHANGED
@@ -9,7 +9,7 @@
9
9
  * - The cascade-layer order declaration above — the one place it is stated
10
10
  * - The three reset gaps Tailwind's preflight leaves (below)
11
11
  * - The section box model (.tr-section, emitted by the <Section> primitive)
12
- * - The <Image> frame's placeholder fill (.tr-image-frame)
12
+ * - The <Image>/<Video> frame's placeholder fill (.tr-image-frame)
13
13
  * - Universal tokens: motion
14
14
  *
15
15
  * REQUIRES A TAILWIND ENTRY ON THE PAGE. Your entry's `@import "tailwindcss"` brings
@@ -55,9 +55,11 @@
55
55
  margin: 0;
56
56
  }
57
57
 
58
- /* The <Image> frame's placeholder fill, showing while an image is absent or loading.
59
- * It is a plain class rather than a `bg-*` utility on the primitive because a template
60
- * names its own tokens — there is no colour token this primitive may assume exists.
58
+ /* The <Image>/<Video> frame's placeholder fill, showing while an asset is absent or
59
+ * loading. It is a plain class rather than a `bg-*` utility on the primitive because a
60
+ * template names its own tokens — there is no colour token these primitives may assume
61
+ * exists. Both frames share this one class (and one token) deliberately: the fill is the
62
+ * same neutral rect, so a template retints it once.
61
63
  * Retint it by setting --tr-image-frame-bg in your theme's layout tokens. */
62
64
  .tr-image-frame {
63
65
  background-color: var(--tr-image-frame-bg, oklch(0.97 0 0));
@@ -3,13 +3,15 @@ const LIST_TYPES = /* @__PURE__ */ new Set([
3
3
  "list",
4
4
  "poi_list",
5
5
  "image_collection",
6
+ "video_collection",
6
7
  "object_list"
7
8
  ]);
9
+ const ASSET_TYPES = /* @__PURE__ */ new Set(["image", "video"]);
8
10
  /** Whether a resolved slot value carries real content for its type. */
9
11
  function slotHasContent(value, type) {
10
12
  if (value === null || value === void 0) return false;
11
13
  if (LIST_TYPES.has(type)) return Array.isArray(value) && value.length > 0;
12
- if (type === "image") return typeof value === "object" && Boolean(value.url);
14
+ if (ASSET_TYPES.has(type)) return typeof value === "object" && Boolean(value.url);
13
15
  if (typeof value === "string") return value.trim().length > 0;
14
16
  return true;
15
17
  }
@@ -44,9 +46,9 @@ function truncate(s, n = 80) {
44
46
  }
45
47
  function valuePreview(value, type) {
46
48
  if (LIST_TYPES.has(type)) return Array.isArray(value) ? `[${value.length} items]` : void 0;
47
- if (type === "image") {
49
+ if (ASSET_TYPES.has(type)) {
48
50
  const url = value?.url;
49
- return url ? `image:${truncate(url)}` : void 0;
51
+ return url ? `${type}:${truncate(url)}` : void 0;
50
52
  }
51
53
  if (typeof value === "string") return truncate(value);
52
54
  if (typeof value === "number" || typeof value === "boolean") return String(value);
@@ -7,7 +7,7 @@ const KIT_PKG_SUBPATH = join("node_modules", "@homepages", "template-kit");
7
7
  const LINK_MARKER_NAME = ".template-kit-link.json";
8
8
  const PUBLISHABLE = [
9
9
  "dist",
10
- "docs",
10
+ "guide",
11
11
  "package.json",
12
12
  "tsconfig.json",
13
13
  "tsconfig.base.json",
@@ -1,17 +1,17 @@
1
1
  import type { FixtureModule } from "@homepages/template-kit";
2
- import { allPhotos, mkResponsive } from "@homepages/template-kit/fixtures";
2
+ import { luxuryMultiUnit, mkResponsive } from "@homepages/template-kit/fixtures";
3
3
 
4
4
  // Base content the section is designed for. Nullability coverage (each optional
5
5
  // slot rendered empty) is derived automatically from schema.ts — no fixture
6
6
  // needed for it. Add a `states` entry only for a designed scenario that
7
7
  // combines overrides at once (a content-length or option branch the section
8
8
  // renders differently).
9
- const heroImage = allPhotos[19]!; // exterior facade shot
9
+ const heroImage = luxuryMultiUnit.photos[19]!; // exterior facade shot
10
10
 
11
11
  const fixtures: FixtureModule = {
12
12
  base: {
13
13
  slots: {
14
- headline: "101 Queensway, Jamaica Plain, MA 02130",
14
+ headline: luxuryMultiUnit.facts.address,
15
15
  hero_image: {
16
16
  photo_id: heroImage.id,
17
17
  url: heroImage.url,
@@ -19,7 +19,7 @@ const fixtures: FixtureModule = {
19
19
  // Height set to a 16:9 ratio to match the slot's locked crop.
20
20
  responsive: mkResponsive(heroImage.url, 2048, 1152),
21
21
  },
22
- blurb: "A bright corner home moments from the Emerald Necklace.",
22
+ blurb: "A 12-unit mansard Victorian in Jamaica Plain, moments from the Emerald Necklace.",
23
23
  },
24
24
  options: { align: "center" },
25
25
  },
@@ -30,9 +30,9 @@ const fixtures: FixtureModule = {
30
30
  "long-blurb": {
31
31
  slots: {
32
32
  blurb:
33
- "A bright corner home moments from the Emerald Necklace, with a chef's " +
34
- "kitchen, three sun-filled bedrooms, off-street parking, and a landscaped " +
35
- "yard that backs onto a quiet dead-end street perfect for morning coffee.",
33
+ "A 12-unit mansard Victorian in Jamaica Plain, with a terrazzo lobby, an " +
34
+ "on-site gym, quartz kitchens throughout, and a top-floor penthouse with a " +
35
+ "private rooftop terrace and skyline views all moments from the Emerald Necklace.",
36
36
  },
37
37
  },
38
38
  },
@@ -1,7 +1,7 @@
1
1
  //#region src/contracts/slot-catalog.d.ts
2
- declare const CONTENT_SLOT_TYPES: readonly ["text", "number", "select", "image", "image_collection", "list", "object_list", "poi_list", "url"];
2
+ declare const CONTENT_SLOT_TYPES: readonly ["text", "number", "select", "image", "image_collection", "list", "object_list", "poi_list", "url", "video", "video_collection"];
3
3
  declare const REF_SLOT_TYPES: readonly [];
4
- declare const SLOT_TYPES: readonly ["text", "number", "select", "image", "image_collection", "list", "object_list", "poi_list", "url"];
4
+ declare const SLOT_TYPES: readonly ["text", "number", "select", "image", "image_collection", "list", "object_list", "poi_list", "url", "video", "video_collection"];
5
5
  type ContentSlotType = (typeof CONTENT_SLOT_TYPES)[number];
6
6
  type RefSlotType = (typeof REF_SLOT_TYPES)[number];
7
7
  type SlotType = (typeof SLOT_TYPES)[number];
@@ -8,7 +8,9 @@ const CONTENT_SLOT_TYPES = [
8
8
  "list",
9
9
  "object_list",
10
10
  "poi_list",
11
- "url"
11
+ "url",
12
+ "video",
13
+ "video_collection"
12
14
  ];
13
15
  const REF_SLOT_TYPES = [];
14
16
  const SLOT_TYPES = [...CONTENT_SLOT_TYPES, ...REF_SLOT_TYPES];
@@ -3,13 +3,14 @@ import { docsPathForRule } from "../../rules/registry.js";
3
3
  //#region src/eslint/rules/no-raw-element.ts
4
4
  const FORBIDDEN = {
5
5
  section: "<Section>",
6
- img: "<Image>"
6
+ img: "<Image>",
7
+ video: "<Video>"
7
8
  };
8
9
  const noRawElement = {
9
10
  meta: {
10
11
  type: "problem",
11
12
  docs: {
12
- description: "Ban raw <section> and <img> in section JSX",
13
+ description: "Ban raw <section>, <img> and <video> in section JSX",
13
14
  url: docsPathForRule("no-raw-element")
14
15
  },
15
16
  schema: [],
@@ -0,0 +1,23 @@
1
+ import { ContactFixture, PoiFixture } from "./sample-property.js";
2
+ import { Scenario } from "./scenario.js";
3
+ //#region src/fixtures/helpers.d.ts
4
+ declare function primaryContact(s: Scenario): ContactFixture;
5
+ declare function contactCards(s: Scenario): ContactFixture[];
6
+ declare function poisByCategory(s: Scenario): Record<PoiFixture["category"], PoiFixture[]>;
7
+ type PropertyMetrics = {
8
+ beds: number | null;
9
+ baths: number | null;
10
+ sqft: number | null;
11
+ price: string | null;
12
+ bedsMin: string | null;
13
+ bedsMax: string | null;
14
+ bathsMin: string | null;
15
+ bathsMax: string | null;
16
+ sqftMax: string | null;
17
+ priceStart: string | null;
18
+ addressLine1: string;
19
+ addressLine2: string;
20
+ };
21
+ declare function propertyMetrics(s: Scenario): PropertyMetrics;
22
+ //#endregion
23
+ export { PropertyMetrics, contactCards, poisByCategory, primaryContact, propertyMetrics };
@@ -0,0 +1,50 @@
1
+ import { TRANSFORMS } from "../contracts/transforms.js";
2
+
3
+ //#region src/fixtures/helpers.ts
4
+ const POI_CATEGORIES = [
5
+ "restaurant",
6
+ "park",
7
+ "transit",
8
+ "shopping",
9
+ "coffee",
10
+ "fitness"
11
+ ];
12
+ function primaryContact(s) {
13
+ const first = s.contacts[0];
14
+ if (!first) throw new Error(`scenario ${s.key} has no contacts`);
15
+ return first;
16
+ }
17
+ function contactCards(s) {
18
+ return s.contacts;
19
+ }
20
+ function poisByCategory(s) {
21
+ const out = Object.fromEntries(POI_CATEGORIES.map((c) => [c, []]));
22
+ for (const poi of s.pois) out[poi.category].push(poi);
23
+ return out;
24
+ }
25
+ function propertyMetrics(s) {
26
+ const units = s.facts.units;
27
+ const lines = s.facts.address_lines ?? [s.facts.address, ""];
28
+ const primary = [...units].sort((a, b) => a.position - b.position)[0] ?? null;
29
+ const fmt = (name) => {
30
+ const v = TRANSFORMS[name].impl({ units });
31
+ return v == null ? null : String(v);
32
+ };
33
+ return {
34
+ beds: primary?.beds ?? null,
35
+ baths: primary?.baths ?? null,
36
+ sqft: primary?.sqft ?? null,
37
+ price: primary?.price ?? null,
38
+ bedsMin: fmt("units.bedsMin"),
39
+ bedsMax: fmt("units.bedsMax"),
40
+ bathsMin: fmt("units.bathsMin"),
41
+ bathsMax: fmt("units.bathsMax"),
42
+ sqftMax: fmt("units.sqftMax"),
43
+ priceStart: fmt("units.priceStart"),
44
+ addressLine1: lines[0] ?? "",
45
+ addressLine2: lines[1] ?? ""
46
+ };
47
+ }
48
+
49
+ //#endregion
50
+ export { contactCards, poisByCategory, primaryContact, propertyMetrics };
@@ -1,5 +1,4 @@
1
1
  import { ResponsiveImage } from "../contracts/image-descriptor.js";
2
- import { PropertyFacts } from "../schema/property-facts.js";
3
2
  //#region src/fixtures/sample-property.d.ts
4
3
  type PhotoFixture = {
5
4
  id: string;
@@ -10,18 +9,19 @@ type PhotoFixture = {
10
9
  category?: string;
11
10
  };
12
11
  };
13
- declare const allPhotos: PhotoFixture[];
14
12
  declare function mkResponsive(url: string, width: number, height: number): ResponsiveImage;
15
13
  type PoiFixture = {
16
14
  id: string;
17
15
  category: "restaurant" | "park" | "transit" | "shopping" | "coffee" | "fitness";
18
16
  name: string;
17
+ place_id: string;
18
+ address: string;
19
19
  distance: string;
20
+ distance_m: number;
20
21
  lat: number;
21
22
  lng: number;
22
23
  url: string;
23
24
  };
24
- declare const samplePois: PoiFixture[];
25
25
  type ContactFixture = {
26
26
  library_item_id: string;
27
27
  category: "contact";
@@ -36,13 +36,15 @@ type ContactFixture = {
36
36
  email: string;
37
37
  phone: string;
38
38
  address: string;
39
+ org_name: string;
40
+ logo_light: string | null;
41
+ logo_dark: string | null;
39
42
  socials: Array<{
40
43
  platform: "instagram" | "linkedin" | "x" | "facebook";
41
44
  url: string;
42
45
  }>;
43
46
  };
44
47
  };
45
- declare const sampleContacts: ContactFixture[];
46
48
  type AmenityItem = {
47
49
  id: string;
48
50
  label: string;
@@ -51,17 +53,5 @@ type AmenityItem = {
51
53
  description: string;
52
54
  items: string[];
53
55
  };
54
- declare const sampleAmenities: AmenityItem[];
55
- declare const sampleProperty: PropertyFacts;
56
- declare const sampleHeroSlots: {
57
- readonly headline: "A mansard victorian single-family home in the heart of Jamaica Plain";
58
- readonly hero_image: {
59
- readonly photo_id: string;
60
- readonly url: string;
61
- readonly alt: string;
62
- };
63
- readonly brand_logo: null;
64
- readonly listing_status: "for_sale";
65
- };
66
56
  //#endregion
67
- export { AmenityItem, ContactFixture, PhotoFixture, PoiFixture, allPhotos, mkResponsive, sampleAmenities, sampleContacts, sampleHeroSlots, samplePois, sampleProperty };
57
+ export { AmenityItem, ContactFixture, PhotoFixture, PoiFixture, mkResponsive };