@homepages/template-kit 0.9.0-dev-20260719205406 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/CHANGELOG.md +60 -59
  2. package/README.md +14 -4
  3. package/dist/contracts/fill-treatments.d.ts +23 -0
  4. package/dist/fixtures/sample-property.d.ts +4 -7
  5. package/dist/fixtures/sample-property.js +41 -97
  6. package/dist/fixtures/scenario.d.ts +1 -1
  7. package/dist/fixtures/scenarios/for-rent-condo.js +24 -44
  8. package/dist/fixtures/scenarios/luxury-multi-unit.js +1 -1
  9. package/dist/fixtures/scenarios/sparse-single-family.js +14 -26
  10. package/dist/fixtures.d.ts +2 -2
  11. package/dist/fixtures.js +1 -2
  12. package/dist/index.d.ts +2 -2
  13. package/dist/index.js +2 -2
  14. package/dist/islands/discover.js +1 -1
  15. package/dist/islands/esbuild-plugin.js +1 -1
  16. package/dist/islands/wrap.d.ts +9 -1
  17. package/dist/manifest.json +940 -0
  18. package/dist/media/pack.d.ts +49 -0
  19. package/dist/media/pack.js +69 -0
  20. package/dist/media/resolve-fixture-media.d.ts +22 -0
  21. package/dist/media/resolve-fixture-media.js +73 -0
  22. package/dist/media/resolve-media.js +195 -0
  23. package/dist/package.js +1 -1
  24. package/dist/schema/property-facts.d.ts +2 -2
  25. package/dist/schema/resolve-section-ref.d.ts +4 -0
  26. package/dist/schema/rows.d.ts +9 -0
  27. package/dist/schema/section-nav.d.ts +5 -1
  28. package/dist/schema/slot-types.d.ts +1 -1
  29. package/dist/ssr.d.ts +19 -2
  30. package/dist/ssr.js +32 -13
  31. package/guide/INDEX.md +5 -0
  32. package/guide/check.md +1 -0
  33. package/guide/dev.md +1 -0
  34. package/guide/llms.txt +11 -3
  35. package/guide/recipes/image-slot-crop.md +6 -6
  36. package/guide/recipes/pick-a-scenario.md +13 -5
  37. package/guide/schema-system.md +36 -12
  38. package/guide/upgrading.md +12 -8
  39. package/package.json +4 -16
  40. package/dist/canvas-core/chrome-css.d.ts +0 -24
  41. package/dist/canvas-core/chrome-css.js +0 -403
  42. package/dist/canvas-core/chrome.d.ts +0 -49
  43. package/dist/canvas-core/chrome.js +0 -147
  44. package/dist/canvas-core/frame.d.ts +0 -20
  45. package/dist/canvas-core/frame.js +0 -23
  46. package/dist/canvas-core/hidden-tabs.d.ts +0 -16
  47. package/dist/canvas-core/hidden-tabs.js +0 -84
  48. package/dist/canvas-core/resolve-target.js +0 -55
  49. package/dist/canvas-core/scroll.d.ts +0 -10
  50. package/dist/canvas-core/scroll.js +0 -15
  51. package/dist/canvas-core/section-dom.d.ts +0 -6
  52. package/dist/canvas-core/section-dom.js +0 -43
  53. package/dist/canvas-core/srcdoc.d.ts +0 -8
  54. package/dist/canvas-core/srcdoc.js +0 -77
  55. package/dist/canvas-core/types.d.ts +0 -76
  56. package/dist/canvas-core.d.ts +0 -9
  57. package/dist/canvas-core.js +0 -9
  58. package/dist/cli/check/config.js +0 -41
  59. package/dist/cli/check/css.js +0 -131
  60. package/dist/cli/check/diagnostics.js +0 -37
  61. package/dist/cli/check/index.js +0 -94
  62. package/dist/cli/check/loader.js +0 -149
  63. package/dist/cli/check/paths.js +0 -15
  64. package/dist/cli/check/relativize.js +0 -18
  65. package/dist/cli/check/render-invariants.js +0 -24
  66. package/dist/cli/check/resolve-tool.js +0 -38
  67. package/dist/cli/check/section-assets.js +0 -58
  68. package/dist/cli/check/stages/deps.js +0 -337
  69. package/dist/cli/check/stages/lint.js +0 -46
  70. package/dist/cli/check/stages/render.js +0 -97
  71. package/dist/cli/check/stages/size.js +0 -274
  72. package/dist/cli/check/stages/tree.js +0 -206
  73. package/dist/cli/check/stages/typecheck.js +0 -46
  74. package/dist/cli/check/stages/validate/catalog-exhaustiveness.js +0 -18
  75. package/dist/cli/check/stages/validate/facts.js +0 -19
  76. package/dist/cli/check/stages/validate/formatter-contract.js +0 -14
  77. package/dist/cli/check/stages/validate/group-contract.js +0 -21
  78. package/dist/cli/check/stages/validate/index.js +0 -66
  79. package/dist/cli/check/stages/validate/list-scalar-contract.js +0 -22
  80. package/dist/cli/check/stages/validate/nav-contract.js +0 -92
  81. package/dist/cli/check/stages/validate/nested-slot-contract.js +0 -12
  82. package/dist/cli/check/stages/validate/object-list-contract.js +0 -31
  83. package/dist/cli/check/stages/validate/orchestrator.js +0 -241
  84. package/dist/cli/check/stages/validate/produced-by-contract.js +0 -18
  85. package/dist/cli/check/stages/validate/row-contract.js +0 -31
  86. package/dist/cli/check/stages/validate/select-contract.js +0 -13
  87. package/dist/cli/check/stages/validate/source-contract.js +0 -21
  88. package/dist/cli/check/stages/validate/variant-contract.js +0 -15
  89. package/dist/cli/check/workspace.js +0 -99
  90. package/dist/cli/dev/build-css.js +0 -97
  91. package/dist/cli/dev/catalog.js +0 -35
  92. package/dist/cli/dev/compose-template.js +0 -39
  93. package/dist/cli/dev/content-override.js +0 -58
  94. package/dist/cli/dev/diagnostics.js +0 -68
  95. package/dist/cli/dev/discover.js +0 -35
  96. package/dist/cli/dev/fill-state.js +0 -77
  97. package/dist/cli/dev/index.js +0 -42
  98. package/dist/cli/dev/inspect.js +0 -38
  99. package/dist/cli/dev/island-bootstrap.js +0 -38
  100. package/dist/cli/dev/island-canvas-bootstrap.js +0 -204
  101. package/dist/cli/dev/island-map.js +0 -35
  102. package/dist/cli/dev/island-transform.js +0 -31
  103. package/dist/cli/dev/manifest-instances.js +0 -31
  104. package/dist/cli/dev/render-section.js +0 -24
  105. package/dist/cli/dev/screenshot-target.js +0 -24
  106. package/dist/cli/dev/section-page.js +0 -91
  107. package/dist/cli/dev/server.js +0 -510
  108. package/dist/cli/dev/slot-schema.js +0 -12
  109. package/dist/cli/dev/structure-summary.js +0 -119
  110. package/dist/cli/dev/tailwind.js +0 -32
  111. package/dist/cli/dev/tsconfig-paths-plugin.js +0 -90
  112. package/dist/cli/dev/vite-server.js +0 -39
  113. package/dist/cli/dev/workspace.js +0 -63
  114. package/dist/cli/link/index.js +0 -74
  115. package/dist/cli/link/overlay.js +0 -59
  116. package/dist/cli/link/watch.js +0 -25
  117. package/dist/cli/new/emit.js +0 -48
  118. package/dist/cli/new/index.js +0 -69
  119. package/dist/cli/new/scaffold/section/_Renderer.tsx +0 -20
  120. package/dist/cli/new/scaffold/section/_fill-spec.ts +0 -18
  121. package/dist/cli/new/scaffold/section/_fixtures.ts +0 -13
  122. package/dist/cli/new/scaffold/section/_schema.ts +0 -24
  123. package/dist/cli/new/scaffold/template/_manifest.json +0 -9
  124. package/dist/cli/new/scaffold/template/sections/hero/ExpandableText.tsx +0 -45
  125. package/dist/cli/new/scaffold/template/sections/hero/Renderer.tsx +0 -40
  126. package/dist/cli/new/scaffold/template/sections/hero/components/Feature.tsx +0 -14
  127. package/dist/cli/new/scaffold/template/sections/hero/fill-spec.ts +0 -29
  128. package/dist/cli/new/scaffold/template/sections/hero/fixtures.ts +0 -41
  129. package/dist/cli/new/scaffold/template/sections/hero/schema.ts +0 -60
  130. package/dist/cli/new/scaffold/template/theme.css +0 -24
  131. package/dist/cli/new/scaffold/template/theme.ts +0 -27
  132. package/dist/cli/new/scaffold-assets.js +0 -20
  133. package/dist/cli/pack/collect.js +0 -44
  134. package/dist/cli/pack/guards.js +0 -57
  135. package/dist/cli/pack/index.js +0 -66
  136. package/dist/cli/pack/manifest.js +0 -15
  137. package/dist/cli/pack/zip.js +0 -86
  138. package/dist/cli/theme/generate.js +0 -43
  139. package/dist/cli/theme/index.js +0 -33
  140. package/dist/cli/theme/load-theme.js +0 -69
  141. package/dist/cli.d.ts +0 -1
  142. package/dist/cli.js +0 -138
  143. package/dist/dev-client/assets/index-CJa9G1Na.css +0 -1
  144. package/dist/dev-client/assets/index-C_1atZs4.js +0 -450
  145. package/dist/dev-client/index.html +0 -13
  146. package/dist/tokens.css +0 -21
@@ -0,0 +1,49 @@
1
+ //#region src/media/pack.d.ts
2
+ /** The four library kinds the editor's media picker lists. */
3
+ type MediaKind = "photo" | "plan" | "library" | "video";
4
+ type MediaAsset = {
5
+ id: string;
6
+ kind: MediaKind;
7
+ name: string;
8
+ caption: string;
9
+ tags: Record<string, string>;
10
+ /** Pack-relative path to the source bytes. */
11
+ file: string;
12
+ /** True intrinsic dimensions of `file`. Absent for vector assets. */
13
+ width?: number;
14
+ height?: number;
15
+ /** `video` only. */
16
+ poster?: string;
17
+ duration_ms?: number;
18
+ };
19
+ /**
20
+ * URL prefix every resolved media URL carries. Root-relative so a section page at
21
+ * any depth resolves it identically, and so the dev server can mount the pack under
22
+ * one known path. Kept in sync with the dev server's static mount.
23
+ */
24
+ declare const DEV_MEDIA_URL_PREFIX = "/__dev-media/";
25
+ /** The pack entry for an asset id, or null when the id names nothing in the pack. */
26
+ declare function getMediaAsset(id: string): MediaAsset | null;
27
+ /**
28
+ * Every asset the pack ships, in manifest order.
29
+ *
30
+ * Exposes the asset list and NOT the whole manifest on purpose. The manifest's
31
+ * `sets` are generated FROM the scenario ownership a consumer derives out of the
32
+ * kit fixtures, so a consumer that read `sets` back to compute ownership would be
33
+ * checking its own output.
34
+ */
35
+ declare function listMediaAssets(): MediaAsset[];
36
+ /**
37
+ * Inverse of `mediaUrl` — the pack-relative path a served URL names, or null when
38
+ * the URL is not a pack URL. Lets a consumer (render-invariants) check a rendered
39
+ * `src` back against the manifest.
40
+ */
41
+ declare function packRelativeFromUrl(url: string): string | null;
42
+ /**
43
+ * Whether a pack-relative path names real bytes in the pack. Pair with
44
+ * `packRelativeFromUrl` to check a rendered `src`/`srcset` back against the
45
+ * manifest — asserting a media URL RESOLVES rather than merely being non-empty.
46
+ */
47
+ declare function mediaPackHasFile(packRelative: string): boolean;
48
+ //#endregion
49
+ export { DEV_MEDIA_URL_PREFIX, MediaAsset, MediaKind, getMediaAsset, listMediaAssets, mediaPackHasFile, packRelativeFromUrl };
@@ -0,0 +1,69 @@
1
+ import { fileURLToPath } from "node:url";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+
4
+ //#region src/media/pack.ts
5
+ /**
6
+ * URL prefix every resolved media URL carries. Root-relative so a section page at
7
+ * any depth resolves it identically, and so the dev server can mount the pack under
8
+ * one known path. Kept in sync with the dev server's static mount.
9
+ */
10
+ const DEV_MEDIA_URL_PREFIX = "/__dev-media/";
11
+ const MANIFEST_CANDIDATES = ["../manifest.json", "../../../dev-media/manifest.json"];
12
+ function loadManifest() {
13
+ for (const candidate of MANIFEST_CANDIDATES) {
14
+ const path = fileURLToPath(new URL(candidate, import.meta.url));
15
+ if (existsSync(path)) return JSON.parse(readFileSync(path, "utf8"));
16
+ }
17
+ throw new Error("dev media manifest not found — expected it beside the kit build (dist/manifest.json) or in the workspace sibling (packages/dev-media/manifest.json)");
18
+ }
19
+ let cached = null;
20
+ /** The pack index. Parsed once per process. */
21
+ function mediaManifest() {
22
+ cached ??= loadManifest();
23
+ return cached;
24
+ }
25
+ let byId = null;
26
+ /** The pack entry for an asset id, or null when the id names nothing in the pack. */
27
+ function getMediaAsset(id) {
28
+ byId ??= new Map(mediaManifest().assets.map((asset) => [asset.id, asset]));
29
+ return byId.get(id) ?? null;
30
+ }
31
+ /**
32
+ * Every asset the pack ships, in manifest order.
33
+ *
34
+ * Exposes the asset list and NOT the whole manifest on purpose. The manifest's
35
+ * `sets` are generated FROM the scenario ownership a consumer derives out of the
36
+ * kit fixtures, so a consumer that read `sets` back to compute ownership would be
37
+ * checking its own output.
38
+ */
39
+ function listMediaAssets() {
40
+ return mediaManifest().assets;
41
+ }
42
+ /**
43
+ * The served URL for a pack-relative reference (an asset's `file` or `poster`).
44
+ * This is the ONLY place the pack's on-disk layout becomes a URL.
45
+ */
46
+ function mediaUrl(packRelative) {
47
+ return `${DEV_MEDIA_URL_PREFIX}${packRelative}`;
48
+ }
49
+ /**
50
+ * Inverse of `mediaUrl` — the pack-relative path a served URL names, or null when
51
+ * the URL is not a pack URL. Lets a consumer (render-invariants) check a rendered
52
+ * `src` back against the manifest.
53
+ */
54
+ function packRelativeFromUrl(url) {
55
+ return url.startsWith("/__dev-media/") ? url.slice(13) : null;
56
+ }
57
+ let files = null;
58
+ /**
59
+ * Whether a pack-relative path names real bytes in the pack. Pair with
60
+ * `packRelativeFromUrl` to check a rendered `src`/`srcset` back against the
61
+ * manifest — asserting a media URL RESOLVES rather than merely being non-empty.
62
+ */
63
+ function mediaPackHasFile(packRelative) {
64
+ files ??= new Set(mediaManifest().assets.flatMap((asset) => asset.poster ? [asset.file, asset.poster] : [asset.file]));
65
+ return files.has(packRelative);
66
+ }
67
+
68
+ //#endregion
69
+ export { DEV_MEDIA_URL_PREFIX, getMediaAsset, listMediaAssets, mediaManifest, mediaPackHasFile, mediaUrl, packRelativeFromUrl };
@@ -0,0 +1,22 @@
1
+ //#region src/media/resolve-fixture-media.d.ts
2
+ /**
3
+ * `resolveFixtureMedia` over a built fixture set — the form every node-only caller
4
+ * (the SSR loader, `template-kit check`) actually needs.
5
+ */
6
+ declare function resolveFixtureSetMedia<T extends {
7
+ props: unknown;
8
+ }>(fixtures: T[]): T[];
9
+ /** A fixture stating a media asset id the pack does not carry. */
10
+ type UnresolvedMedia = {
11
+ /** Where the id sits — `photo_id`, `video_id`, `floor_plans[].id`. */
12
+ field: string;
13
+ id: string;
14
+ };
15
+ /**
16
+ * Every media asset id a fixture states that the pack does not carry. Run over
17
+ * PRE-resolution props — after resolution the id is gone, replaced by the empty
18
+ * default.
19
+ */
20
+ declare function findUnresolvedMedia(props: unknown): UnresolvedMedia[];
21
+ //#endregion
22
+ export { UnresolvedMedia, findUnresolvedMedia, resolveFixtureSetMedia };
@@ -0,0 +1,73 @@
1
+ import { getMediaAsset } from "./pack.js";
2
+ import { resolveFloorplan, resolveImageValue, resolveVideoValue } from "./resolve-media.js";
3
+
4
+ //#region src/media/resolve-fixture-media.ts
5
+ function isPlainObject(value) {
6
+ return typeof value === "object" && value !== null && !Array.isArray(value);
7
+ }
8
+ function hasStringKey(value, key) {
9
+ return typeof value[key] === "string";
10
+ }
11
+ function resolveValue(value, key) {
12
+ if (Array.isArray(value)) {
13
+ const entries = value;
14
+ if (key === "floor_plans") return entries.map((entry) => isPlainObject(entry) && typeof entry.id === "string" ? resolveFloorplan(entry) : entry);
15
+ return entries.map((entry) => resolveValue(entry, null));
16
+ }
17
+ if (!isPlainObject(value)) return value;
18
+ if (hasStringKey(value, "video_id")) return resolveVideoValue(value);
19
+ if (hasStringKey(value, "photo_id") || hasStringKey(value, "asset_id")) return resolveImageValue(value);
20
+ return Object.fromEntries(Object.entries(value).map(([childKey, childValue]) => [childKey, resolveValue(childValue, childKey)]));
21
+ }
22
+ /**
23
+ * Resolve every media reference in a fixture's props against the dev media pack,
24
+ * returning a new props object. Props with no media pass through unchanged in
25
+ * value, though the object identity is not preserved.
26
+ */
27
+ function resolveFixtureMedia(props) {
28
+ return resolveValue(props, null);
29
+ }
30
+ /**
31
+ * `resolveFixtureMedia` over a built fixture set — the form every node-only caller
32
+ * (the SSR loader, `template-kit check`) actually needs.
33
+ */
34
+ function resolveFixtureSetMedia(fixtures) {
35
+ return fixtures.map((fixture) => ({
36
+ ...fixture,
37
+ props: resolveFixtureMedia(fixture.props)
38
+ }));
39
+ }
40
+ function collectUnresolved(value, key, out) {
41
+ if (Array.isArray(value)) {
42
+ for (const entry of value) if (key === "floor_plans" && isPlainObject(entry) && typeof entry.id === "string") {
43
+ if (entry.id && !getMediaAsset(entry.id)) out.set(entry.id, "floor_plans[].id");
44
+ } else collectUnresolved(entry, null, out);
45
+ return;
46
+ }
47
+ if (!isPlainObject(value)) return;
48
+ for (const field of [
49
+ "photo_id",
50
+ "asset_id",
51
+ "video_id"
52
+ ]) {
53
+ const id = value[field];
54
+ if (typeof id === "string" && id && !getMediaAsset(id)) out.set(id, field);
55
+ }
56
+ for (const [childKey, childValue] of Object.entries(value)) collectUnresolved(childValue, childKey, out);
57
+ }
58
+ /**
59
+ * Every media asset id a fixture states that the pack does not carry. Run over
60
+ * PRE-resolution props — after resolution the id is gone, replaced by the empty
61
+ * default.
62
+ */
63
+ function findUnresolvedMedia(props) {
64
+ const found = /* @__PURE__ */ new Map();
65
+ collectUnresolved(props, null, found);
66
+ return [...found].map(([id, field]) => ({
67
+ field,
68
+ id
69
+ }));
70
+ }
71
+
72
+ //#endregion
73
+ export { findUnresolvedMedia, resolveFixtureMedia, resolveFixtureSetMedia };
@@ -0,0 +1,195 @@
1
+ import { getMediaAsset, mediaUrl } from "./pack.js";
2
+
3
+ //#region src/media/resolve-media.ts
4
+ const EMPTY_IMAGE = {
5
+ photo_id: "",
6
+ url: "",
7
+ alt: ""
8
+ };
9
+ const EMPTY_VIDEO = {
10
+ video_id: "",
11
+ url: "",
12
+ poster: "",
13
+ alt: ""
14
+ };
15
+ const RUNG_WIDTHS = [
16
+ 320,
17
+ 640,
18
+ 960,
19
+ 1200,
20
+ 1600,
21
+ 2048
22
+ ];
23
+ const MIME_BY_EXTENSION = {
24
+ avif: "image/avif",
25
+ jpeg: "image/jpeg",
26
+ jpg: "image/jpeg",
27
+ png: "image/png",
28
+ webp: "image/webp"
29
+ };
30
+ function mimeFor(file) {
31
+ return MIME_BY_EXTENSION[file.split(".").pop()?.toLowerCase() ?? ""] ?? null;
32
+ }
33
+ /**
34
+ * The responsive descriptor for a pack asset, or null when the asset cannot carry
35
+ * one (a vector, or a raster whose intrinsic dimensions the manifest omits).
36
+ *
37
+ * TWO DELIBERATE DIVERGENCES FROM PRODUCTION, both recorded in the design record:
38
+ *
39
+ * 1. Every rung points at the SAME source file. Production pre-encodes one file per
40
+ * rung; generating those here would put `sharp` — a platform-specific native
41
+ * binary — into every agency install and make output vary by encoder version.
42
+ * The manifest still declares each asset's TRUE dimensions, so layout and
43
+ * authoring behave exactly as in production; only transferred bytes differ.
44
+ * 2. One source, carrying the file's REAL mime type, where production emits an
45
+ * avif/webp/jpeg triple. Claiming `image/avif` over PNG bytes would not be a
46
+ * harmless fiction — the browser would select that source and fail to decode it.
47
+ */
48
+ function responsiveFor(asset) {
49
+ const type = mimeFor(asset.file);
50
+ if (!type || !asset.width || !asset.height) return null;
51
+ const url = mediaUrl(asset.file);
52
+ const widths = RUNG_WIDTHS.filter((w) => w <= asset.width);
53
+ if (widths.length === 0) widths.push(asset.width);
54
+ return {
55
+ sources: [{
56
+ type,
57
+ srcset: widths.map((w) => `${url} ${w}w`).join(", ")
58
+ }],
59
+ width: asset.width,
60
+ height: asset.height
61
+ };
62
+ }
63
+ function clamp01(n) {
64
+ return n < 0 ? 0 : n > 1 ? 1 : n;
65
+ }
66
+ function normalizeRect(v) {
67
+ const nx = typeof v.nx === "number" ? v.nx : 0;
68
+ const ny = typeof v.ny === "number" ? v.ny : 0;
69
+ const nw = typeof v.nw === "number" ? v.nw : 1;
70
+ const nh = typeof v.nh === "number" ? v.nh : 1;
71
+ if (![
72
+ nx,
73
+ ny,
74
+ nw,
75
+ nh
76
+ ].every(Number.isFinite)) return null;
77
+ const r = {
78
+ nx: clamp01(nx),
79
+ ny: clamp01(ny),
80
+ nw: clamp01(nw),
81
+ nh: clamp01(nh)
82
+ };
83
+ if (r.nx === 0 && r.ny === 0 && r.nw === 1 && r.nh === 1) return null;
84
+ return r;
85
+ }
86
+ /**
87
+ * Resolve one image-slot value against the pack.
88
+ *
89
+ * `null` passes through as `null`, so an optional image slot stays null and the
90
+ * renderer skips it. Every other miss — an unknown id, an object with neither id
91
+ * nor url — degrades to the empty default rather than throwing: image resolution
92
+ * must degrade soft, and the empty `url` is what makes a dead id VISIBLE to
93
+ * render-invariants instead of silently plausible.
94
+ */
95
+ function resolveImageValue(value) {
96
+ if (value === null) return null;
97
+ if (typeof value !== "object") return { ...EMPTY_IMAGE };
98
+ const v = value;
99
+ const rect = v.rect && typeof v.rect === "object" ? normalizeRect(v.rect) : null;
100
+ const ar = typeof v.ar === "number" ? v.ar : null;
101
+ const cropKey = typeof v.crop_key === "string" && v.crop_key.length > 0 ? v.crop_key : null;
102
+ const photoId = typeof v.photo_id === "string" && v.photo_id ? v.photo_id : typeof v.asset_id === "string" && v.asset_id ? v.asset_id : "";
103
+ let base;
104
+ let cropKeyForAttach;
105
+ let source;
106
+ if (cropKey && photoId) {
107
+ source = getMediaAsset(cropKey);
108
+ if (!source) return { ...EMPTY_IMAGE };
109
+ base = {
110
+ photo_id: photoId,
111
+ url: mediaUrl(source.file),
112
+ alt: source.caption
113
+ };
114
+ cropKeyForAttach = cropKey;
115
+ } else if (photoId) {
116
+ source = getMediaAsset(photoId);
117
+ if (!source) return { ...EMPTY_IMAGE };
118
+ base = {
119
+ photo_id: photoId,
120
+ url: mediaUrl(source.file),
121
+ alt: source.caption
122
+ };
123
+ cropKeyForAttach = null;
124
+ } else if (typeof v.url === "string" && v.url.length > 0) {
125
+ base = {
126
+ photo_id: "",
127
+ url: v.url,
128
+ alt: typeof v.alt === "string" ? v.alt : ""
129
+ };
130
+ cropKeyForAttach = null;
131
+ source = null;
132
+ } else return { ...EMPTY_IMAGE };
133
+ if (rect) base.rect = rect;
134
+ if (ar) base.ar = ar;
135
+ if (cropKeyForAttach) base.crop_key = cropKeyForAttach;
136
+ const responsive = source ? responsiveFor(source) : null;
137
+ if (responsive) base.responsive = responsive;
138
+ return base;
139
+ }
140
+ /**
141
+ * Attach the render-time fields to one floorplan ref. A saved plan ref carries only
142
+ * id/upload_id/floor_label/position; url, alt, and the responsive descriptor appear
143
+ * only after resolution. An unknown id yields an empty url — the same visible miss
144
+ * an unknown image id produces.
145
+ */
146
+ function resolveFloorplan(ref) {
147
+ const asset = getMediaAsset(ref.id);
148
+ if (!asset) return {
149
+ ...ref,
150
+ url: "",
151
+ alt: ""
152
+ };
153
+ const responsive = responsiveFor(asset);
154
+ return {
155
+ ...ref,
156
+ url: mediaUrl(asset.file),
157
+ alt: asset.caption,
158
+ ...responsive ? { responsive } : {}
159
+ };
160
+ }
161
+ /**
162
+ * Resolve one video-slot value against the pack. There is exactly one progressive
163
+ * rendition per video, so there is no ladder and no mobile variant; the poster is
164
+ * always present because the transcode derives one.
165
+ */
166
+ function resolveVideoValue(value) {
167
+ if (value === null) return null;
168
+ if (typeof value !== "object") return { ...EMPTY_VIDEO };
169
+ const v = value;
170
+ const videoId = typeof v.video_id === "string" && v.video_id ? v.video_id : typeof v.asset_id === "string" && v.asset_id ? v.asset_id : "";
171
+ if (!videoId) {
172
+ if (typeof v.url === "string" && v.url.length > 0) return {
173
+ video_id: "",
174
+ url: v.url,
175
+ poster: typeof v.poster === "string" ? v.poster : "",
176
+ alt: typeof v.alt === "string" ? v.alt : ""
177
+ };
178
+ return { ...EMPTY_VIDEO };
179
+ }
180
+ const asset = getMediaAsset(videoId);
181
+ if (!asset) return { ...EMPTY_VIDEO };
182
+ return {
183
+ video_id: videoId,
184
+ url: mediaUrl(asset.file),
185
+ poster: asset.poster ? mediaUrl(asset.poster) : "",
186
+ alt: asset.caption,
187
+ ...asset.width && asset.height ? {
188
+ width: asset.width,
189
+ height: asset.height
190
+ } : {}
191
+ };
192
+ }
193
+
194
+ //#endregion
195
+ export { resolveFloorplan, resolveImageValue, resolveVideoValue };
package/dist/package.js CHANGED
@@ -1,5 +1,5 @@
1
1
  //#region package.json
2
- var version = "0.9.0-dev-20260719205406";
2
+ var version = "0.9.0";
3
3
 
4
4
  //#endregion
5
5
  export { version };
@@ -7,8 +7,8 @@ type Coordinates = {
7
7
  type FloorplanAssignment = {
8
8
  id: string;
9
9
  upload_id?: string;
10
- url: string;
11
- alt: string;
10
+ url?: string;
11
+ alt?: string;
12
12
  floor_label: string | null;
13
13
  position: number;
14
14
  responsive?: ResponsiveImage | null;
@@ -0,0 +1,4 @@
1
+ //#region src/schema/resolve-section-ref.d.ts
2
+ declare function resolveSectionRef(templateKey: string, ref: string): string;
3
+ //#endregion
4
+ export { resolveSectionRef };
@@ -0,0 +1,9 @@
1
+ //#region src/schema/rows.d.ts
2
+ type LayoutEntry = string | readonly [string, string];
3
+ declare function flattenLayout(layout: readonly LayoutEntry[]): string[];
4
+ declare function isShortSlot(slot: {
5
+ type: string;
6
+ size?: string;
7
+ }): boolean;
8
+ //#endregion
9
+ export { LayoutEntry, flattenLayout, isShortSlot };
@@ -3,5 +3,9 @@ type SectionNav = {
3
3
  selfAnchor: string;
4
4
  anchors: Record<string, string>;
5
5
  };
6
+ declare function resolveAnchorMap(sections: ReadonlyArray<{
7
+ key: string;
8
+ anchor: string;
9
+ }>): Record<string, string>;
6
10
  //#endregion
7
- export { SectionNav };
11
+ export { SectionNav, resolveAnchorMap };
@@ -11,8 +11,8 @@ declare function slotDefTypes(): SlotType[];
11
11
  declare const ContentSlot: z.ZodObject<{
12
12
  type: z.ZodEnum<{
13
13
  number: "number";
14
- select: "select";
15
14
  text: "text";
15
+ select: "select";
16
16
  image: "image";
17
17
  image_collection: "image_collection";
18
18
  list: "list";
package/dist/ssr.d.ts CHANGED
@@ -1,10 +1,16 @@
1
+ import { resolveAnchorMap } from "./schema/section-nav.js";
1
2
  import { ResolvedFixture } from "./schema/fixture-schema.js";
2
3
  import "./schema/index.js";
3
4
  import { hasClientDirective, isClientFile, resetClientFileCache, resolveImportedFile } from "./islands/client-directive.js";
4
5
  import { isClientComponentFile, islandKey } from "./islands/detect.js";
5
- import { wrapClientModuleSource } from "./islands/wrap.js";
6
+ import { wrapClientModuleMagic, wrapClientModuleSource } from "./islands/wrap.js";
6
7
  import { findSectionIslands } from "./islands/discover.js";
7
8
  import { islandTransformEsbuildPlugin } from "./islands/esbuild-plugin.js";
9
+ import { DEV_MEDIA_URL_PREFIX, MediaAsset, MediaKind, getMediaAsset, listMediaAssets, mediaPackHasFile, packRelativeFromUrl } from "./media/pack.js";
10
+ import { UnresolvedMedia, findUnresolvedMedia, resolveFixtureSetMedia } from "./media/resolve-fixture-media.js";
11
+ import { Treatment, parseTreatment } from "./contracts/fill-treatments.js";
12
+ import { LayoutEntry, flattenLayout, isShortSlot } from "./schema/rows.js";
13
+ import { resolveSectionRef } from "./schema/resolve-section-ref.js";
8
14
  import { ComponentType } from "react";
9
15
  //#region src/ssr/index.d.ts
10
16
  type LoadedSection = {
@@ -16,6 +22,11 @@ type LoadedSection = {
16
22
  editable_by_user?: boolean;
17
23
  required?: boolean;
18
24
  }>;
25
+ unresolvedMedia: {
26
+ fixture: string;
27
+ field: string;
28
+ id: string;
29
+ }[];
19
30
  };
20
31
  type SectionModuleLoader = (dir: string, file: string) => Promise<unknown>;
21
32
  declare const nativeSectionLoader: SectionModuleLoader;
@@ -23,6 +34,12 @@ declare const nativeSectionLoader: SectionModuleLoader;
23
34
  * Load a bundle's Renderer plus its resolved snapshot and invariant fixture
24
35
  * sets from the bundle directory `dir`. `load` resolves each of the four
25
36
  * contract files; it defaults to native dynamic import.
37
+ *
38
+ * Media references resolve here, against the dev media pack: fixtures state the
39
+ * asset id `content_doc` holds, and a Renderer must receive the post-resolution
40
+ * `url`. This is the single fixture loader, so every Node-side caller — the
41
+ * registry's render + invariant gates, the dev server — gets resolved media
42
+ * without asking for it.
26
43
  */
27
44
  declare function loadSectionBundle(dir: string, load?: SectionModuleLoader): Promise<LoadedSection>;
28
45
  /**
@@ -31,4 +48,4 @@ declare function loadSectionBundle(dir: string, load?: SectionModuleLoader): Pro
31
48
  */
32
49
  declare function renderSectionHtml(renderer: ComponentType<Record<string, unknown>>, props: Record<string, unknown>): string;
33
50
  //#endregion
34
- export { LoadedSection, SectionModuleLoader, findSectionIslands, hasClientDirective, isClientComponentFile, isClientFile, islandKey, islandTransformEsbuildPlugin, loadSectionBundle, nativeSectionLoader, renderSectionHtml, resetClientFileCache, resolveImportedFile, wrapClientModuleSource };
51
+ export { DEV_MEDIA_URL_PREFIX, type LayoutEntry, LoadedSection, type MediaAsset, type MediaKind, SectionModuleLoader, type Treatment, type UnresolvedMedia, findSectionIslands, findUnresolvedMedia, flattenLayout, getMediaAsset, hasClientDirective, isClientComponentFile, isClientFile, isShortSlot, islandKey, islandTransformEsbuildPlugin, listMediaAssets, loadSectionBundle, mediaPackHasFile, nativeSectionLoader, packRelativeFromUrl, parseTreatment, renderSectionHtml, resetClientFileCache, resolveAnchorMap, resolveFixtureSetMedia, resolveImportedFile, resolveSectionRef, wrapClientModuleMagic, wrapClientModuleSource };
package/dist/ssr.js CHANGED
@@ -1,9 +1,15 @@
1
1
  import { FixtureModuleShape, buildFixtureSet, buildInvariantFixtures } from "./schema/fixture-schema.js";
2
+ import { DEV_MEDIA_URL_PREFIX, getMediaAsset, listMediaAssets, mediaPackHasFile, packRelativeFromUrl } from "./media/pack.js";
3
+ import { findUnresolvedMedia, resolveFixtureSetMedia } from "./media/resolve-fixture-media.js";
2
4
  import { hasClientDirective, isClientFile, resetClientFileCache, resolveImportedFile } from "./islands/client-directive.js";
3
5
  import { isClientComponentFile, islandKey } from "./islands/detect.js";
4
- import { wrapClientModuleSource } from "./islands/wrap.js";
6
+ import { wrapClientModuleMagic, wrapClientModuleSource } from "./islands/wrap.js";
5
7
  import { findSectionIslands } from "./islands/discover.js";
6
8
  import { islandTransformEsbuildPlugin } from "./islands/esbuild-plugin.js";
9
+ import { parseTreatment } from "./contracts/fill-treatments.js";
10
+ import { flattenLayout, isShortSlot } from "./schema/rows.js";
11
+ import { resolveSectionRef } from "./schema/resolve-section-ref.js";
12
+ import { resolveAnchorMap } from "./schema/section-nav.js";
7
13
  import { createElement } from "react";
8
14
  import { basename, join } from "node:path";
9
15
  import { pathToFileURL } from "node:url";
@@ -15,6 +21,12 @@ const nativeSectionLoader = (dir, file) => import(pathToFileURL(join(dir, file))
15
21
  * Load a bundle's Renderer plus its resolved snapshot and invariant fixture
16
22
  * sets from the bundle directory `dir`. `load` resolves each of the four
17
23
  * contract files; it defaults to native dynamic import.
24
+ *
25
+ * Media references resolve here, against the dev media pack: fixtures state the
26
+ * asset id `content_doc` holds, and a Renderer must receive the post-resolution
27
+ * `url`. This is the single fixture loader, so every Node-side caller — the
28
+ * registry's render + invariant gates, the dev server — gets resolved media
29
+ * without asking for it.
18
30
  */
19
31
  async function loadSectionBundle(dir, load = nativeSectionLoader) {
20
32
  const rendererMod = await load(dir, "Renderer.tsx");
@@ -22,19 +34,26 @@ async function loadSectionBundle(dir, load = nativeSectionLoader) {
22
34
  const schemaMod = await load(dir, "schema.ts");
23
35
  const parsed = FixtureModuleShape.parse(fixturesMod.default);
24
36
  const selfAnchor = schemaMod.schema.meta?.anchor ?? basename(dir);
37
+ const snapshot = buildFixtureSet({
38
+ schema: schemaMod.schema,
39
+ module: parsed,
40
+ selfAnchor
41
+ });
42
+ const invariant = buildInvariantFixtures({
43
+ schema: schemaMod.schema,
44
+ module: parsed,
45
+ selfAnchor
46
+ });
47
+ const unresolvedMedia = invariant.flatMap((fixture) => findUnresolvedMedia(fixture.props).map((miss) => ({
48
+ fixture: fixture.id,
49
+ ...miss
50
+ })));
25
51
  return {
26
52
  Renderer: rendererMod.default,
27
- snapshotFixtures: buildFixtureSet({
28
- schema: schemaMod.schema,
29
- module: parsed,
30
- selfAnchor
31
- }),
32
- invariantFixtures: buildInvariantFixtures({
33
- schema: schemaMod.schema,
34
- module: parsed,
35
- selfAnchor
36
- }),
37
- schemaSlots: schemaMod.schema.slots
53
+ snapshotFixtures: resolveFixtureSetMedia(snapshot),
54
+ invariantFixtures: resolveFixtureSetMedia(invariant),
55
+ schemaSlots: schemaMod.schema.slots,
56
+ unresolvedMedia
38
57
  };
39
58
  }
40
59
  /**
@@ -46,4 +65,4 @@ function renderSectionHtml(renderer, props) {
46
65
  }
47
66
 
48
67
  //#endregion
49
- export { findSectionIslands, hasClientDirective, isClientComponentFile, isClientFile, islandKey, islandTransformEsbuildPlugin, loadSectionBundle, nativeSectionLoader, renderSectionHtml, resetClientFileCache, resolveImportedFile, wrapClientModuleSource };
68
+ export { DEV_MEDIA_URL_PREFIX, findSectionIslands, findUnresolvedMedia, flattenLayout, getMediaAsset, hasClientDirective, isClientComponentFile, isClientFile, isShortSlot, islandKey, islandTransformEsbuildPlugin, listMediaAssets, loadSectionBundle, mediaPackHasFile, nativeSectionLoader, packRelativeFromUrl, parseTreatment, renderSectionHtml, resetClientFileCache, resolveAnchorMap, resolveFixtureSetMedia, resolveImportedFile, resolveSectionRef, wrapClientModuleMagic, wrapClientModuleSource };
package/guide/INDEX.md CHANGED
@@ -11,6 +11,11 @@ This tree **ships in the npm tarball** (`guide/` is listed in `package.json`'s
11
11
  LLM agents, reading it from inside `node_modules`. Keep it free of internal-only
12
12
  detail.
13
13
 
14
+ Every `template-kit <command>` on these pages is spelled exactly as you type it.
15
+ The package that provides that command is `@homepages/template-cli`, a
16
+ devDependency alongside the kit — see [Upgrading](upgrading.md) for its separate
17
+ version line.
18
+
14
19
  | Doc | What it covers | Read when you're touching... |
15
20
  |---|---|---|
16
21
  | `quickstart.md` | The cold-start tutorial: create a workspace, preview the starter's `hero` section, read its four files, make one edit, and check it — owns the author-loop anchor every recipe links to | your first section, end to end |
package/guide/check.md CHANGED
@@ -28,6 +28,7 @@ one or pass `--all`.
28
28
  ### What it needs installed
29
29
 
30
30
  ```bash
31
+ npm i -D @homepages/template-cli # provides the `template-kit` command
31
32
  npm i -D typescript eslint @typescript-eslint/parser esbuild @homepages/eslint-plugin-template
32
33
  ```
33
34
 
package/guide/dev.md CHANGED
@@ -28,6 +28,7 @@ command runs until you interrupt it (Ctrl-C).
28
28
  ### What it needs installed
29
29
 
30
30
  ```bash
31
+ npm i -D @homepages/template-cli # provides the `template-kit` command
31
32
  npm i -D vite @vitejs/plugin-react tailwindcss @tailwindcss/cli
32
33
  ```
33
34
 
package/guide/llms.txt CHANGED
@@ -1,8 +1,8 @@
1
1
  # @homepages/template-kit
2
2
 
3
3
  > The authoring kit for HomePages marketing-section templates. This corpus ships
4
- > inside the npm package so an agent can read it locally, versioned in lockstep
5
- > with the code it describes.
4
+ > inside the npm package so an agent can read it locally, versioned with the
5
+ > authoring contract it describes.
6
6
 
7
7
  Status: 0.x — pre-1.0, so a minor is the breaking axis and a patch carries features
8
8
  as well as fixes; see [Upgrading](upgrading.md). The schema system, the contract
@@ -13,6 +13,12 @@ implemented — see [Check](check.md), [Theme and CSS](theme-and-css.md#generati
13
13
  [Dev](dev.md), [Pack](pack.md), and [Adding templates](new.md); its remaining
14
14
  commands arrive with the surfaces they drive.
15
15
 
16
+ The **command is `template-kit`; the package that provides it is
17
+ `@homepages/template-cli`**, a devDependency alongside the kit. It versions on its
18
+ own line and declares the kit as a peer dependency, so a workspace resolves exactly
19
+ one kit. Every command written in this corpus is spelled the same either way — only
20
+ the install line names the CLI package.
21
+
16
22
  Everything an author writes a section against is imported from the package root —
17
23
  `@homepages/template-kit`. The exceptions are deliberate: the two CSS entries
18
24
  (`./styles.css`, `./base.css`), and `./fixtures` — the golden scenarios, which only
@@ -102,7 +108,9 @@ and [Pick a scenario](recipes/pick-a-scenario.md).
102
108
  patch (0.4.0 → 0.4.1) carries features AND fixes and `npm update` takes it, while
103
109
  a minor (0.4.x → 0.5.0) is breaking and needs an explicit
104
110
  `npm install @homepages/template-kit@^0.5.0` — a 0.x caret never crosses a minor.
105
- Submissions are accepted against the current kit line and the one before it.
111
+ `@homepages/template-cli` and `@homepages/eslint-plugin-template` each version on
112
+ their own line and update separately. Submissions are accepted against the current
113
+ kit line and the one before it.
106
114
  - [Adding templates](new.md): the `template-kit new <template>` and
107
115
  `template-kit new-section <template>/<section>` CLI commands — scaffolding a new
108
116
  template with a working starter section, or adding a bare section (the four