@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.
- package/CHANGELOG.md +60 -59
- package/README.md +14 -4
- package/dist/contracts/fill-treatments.d.ts +23 -0
- package/dist/fixtures/sample-property.d.ts +4 -7
- package/dist/fixtures/sample-property.js +41 -97
- package/dist/fixtures/scenario.d.ts +1 -1
- package/dist/fixtures/scenarios/for-rent-condo.js +24 -44
- package/dist/fixtures/scenarios/luxury-multi-unit.js +1 -1
- package/dist/fixtures/scenarios/sparse-single-family.js +14 -26
- package/dist/fixtures.d.ts +2 -2
- package/dist/fixtures.js +1 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/islands/discover.js +1 -1
- package/dist/islands/esbuild-plugin.js +1 -1
- package/dist/islands/wrap.d.ts +9 -1
- package/dist/manifest.json +940 -0
- package/dist/media/pack.d.ts +49 -0
- package/dist/media/pack.js +69 -0
- package/dist/media/resolve-fixture-media.d.ts +22 -0
- package/dist/media/resolve-fixture-media.js +73 -0
- package/dist/media/resolve-media.js +195 -0
- package/dist/package.js +1 -1
- package/dist/schema/property-facts.d.ts +2 -2
- package/dist/schema/resolve-section-ref.d.ts +4 -0
- package/dist/schema/rows.d.ts +9 -0
- package/dist/schema/section-nav.d.ts +5 -1
- package/dist/schema/slot-types.d.ts +1 -1
- package/dist/ssr.d.ts +19 -2
- package/dist/ssr.js +32 -13
- package/guide/INDEX.md +5 -0
- package/guide/check.md +1 -0
- package/guide/dev.md +1 -0
- package/guide/llms.txt +11 -3
- package/guide/recipes/image-slot-crop.md +6 -6
- package/guide/recipes/pick-a-scenario.md +13 -5
- package/guide/schema-system.md +36 -12
- package/guide/upgrading.md +12 -8
- package/package.json +4 -16
- package/dist/canvas-core/chrome-css.d.ts +0 -24
- package/dist/canvas-core/chrome-css.js +0 -403
- package/dist/canvas-core/chrome.d.ts +0 -49
- package/dist/canvas-core/chrome.js +0 -147
- package/dist/canvas-core/frame.d.ts +0 -20
- package/dist/canvas-core/frame.js +0 -23
- package/dist/canvas-core/hidden-tabs.d.ts +0 -16
- package/dist/canvas-core/hidden-tabs.js +0 -84
- package/dist/canvas-core/resolve-target.js +0 -55
- package/dist/canvas-core/scroll.d.ts +0 -10
- package/dist/canvas-core/scroll.js +0 -15
- package/dist/canvas-core/section-dom.d.ts +0 -6
- package/dist/canvas-core/section-dom.js +0 -43
- package/dist/canvas-core/srcdoc.d.ts +0 -8
- package/dist/canvas-core/srcdoc.js +0 -77
- package/dist/canvas-core/types.d.ts +0 -76
- package/dist/canvas-core.d.ts +0 -9
- package/dist/canvas-core.js +0 -9
- package/dist/cli/check/config.js +0 -41
- package/dist/cli/check/css.js +0 -131
- package/dist/cli/check/diagnostics.js +0 -37
- package/dist/cli/check/index.js +0 -94
- package/dist/cli/check/loader.js +0 -149
- package/dist/cli/check/paths.js +0 -15
- package/dist/cli/check/relativize.js +0 -18
- package/dist/cli/check/render-invariants.js +0 -24
- package/dist/cli/check/resolve-tool.js +0 -38
- package/dist/cli/check/section-assets.js +0 -58
- package/dist/cli/check/stages/deps.js +0 -337
- package/dist/cli/check/stages/lint.js +0 -46
- package/dist/cli/check/stages/render.js +0 -97
- package/dist/cli/check/stages/size.js +0 -274
- package/dist/cli/check/stages/tree.js +0 -206
- package/dist/cli/check/stages/typecheck.js +0 -46
- package/dist/cli/check/stages/validate/catalog-exhaustiveness.js +0 -18
- package/dist/cli/check/stages/validate/facts.js +0 -19
- package/dist/cli/check/stages/validate/formatter-contract.js +0 -14
- package/dist/cli/check/stages/validate/group-contract.js +0 -21
- package/dist/cli/check/stages/validate/index.js +0 -66
- package/dist/cli/check/stages/validate/list-scalar-contract.js +0 -22
- package/dist/cli/check/stages/validate/nav-contract.js +0 -92
- package/dist/cli/check/stages/validate/nested-slot-contract.js +0 -12
- package/dist/cli/check/stages/validate/object-list-contract.js +0 -31
- package/dist/cli/check/stages/validate/orchestrator.js +0 -241
- package/dist/cli/check/stages/validate/produced-by-contract.js +0 -18
- package/dist/cli/check/stages/validate/row-contract.js +0 -31
- package/dist/cli/check/stages/validate/select-contract.js +0 -13
- package/dist/cli/check/stages/validate/source-contract.js +0 -21
- package/dist/cli/check/stages/validate/variant-contract.js +0 -15
- package/dist/cli/check/workspace.js +0 -99
- package/dist/cli/dev/build-css.js +0 -97
- package/dist/cli/dev/catalog.js +0 -35
- package/dist/cli/dev/compose-template.js +0 -39
- package/dist/cli/dev/content-override.js +0 -58
- package/dist/cli/dev/diagnostics.js +0 -68
- package/dist/cli/dev/discover.js +0 -35
- package/dist/cli/dev/fill-state.js +0 -77
- package/dist/cli/dev/index.js +0 -42
- package/dist/cli/dev/inspect.js +0 -38
- package/dist/cli/dev/island-bootstrap.js +0 -38
- package/dist/cli/dev/island-canvas-bootstrap.js +0 -204
- package/dist/cli/dev/island-map.js +0 -35
- package/dist/cli/dev/island-transform.js +0 -31
- package/dist/cli/dev/manifest-instances.js +0 -31
- package/dist/cli/dev/render-section.js +0 -24
- package/dist/cli/dev/screenshot-target.js +0 -24
- package/dist/cli/dev/section-page.js +0 -91
- package/dist/cli/dev/server.js +0 -510
- package/dist/cli/dev/slot-schema.js +0 -12
- package/dist/cli/dev/structure-summary.js +0 -119
- package/dist/cli/dev/tailwind.js +0 -32
- package/dist/cli/dev/tsconfig-paths-plugin.js +0 -90
- package/dist/cli/dev/vite-server.js +0 -39
- package/dist/cli/dev/workspace.js +0 -63
- package/dist/cli/link/index.js +0 -74
- package/dist/cli/link/overlay.js +0 -59
- package/dist/cli/link/watch.js +0 -25
- package/dist/cli/new/emit.js +0 -48
- package/dist/cli/new/index.js +0 -69
- package/dist/cli/new/scaffold/section/_Renderer.tsx +0 -20
- package/dist/cli/new/scaffold/section/_fill-spec.ts +0 -18
- package/dist/cli/new/scaffold/section/_fixtures.ts +0 -13
- package/dist/cli/new/scaffold/section/_schema.ts +0 -24
- package/dist/cli/new/scaffold/template/_manifest.json +0 -9
- package/dist/cli/new/scaffold/template/sections/hero/ExpandableText.tsx +0 -45
- package/dist/cli/new/scaffold/template/sections/hero/Renderer.tsx +0 -40
- package/dist/cli/new/scaffold/template/sections/hero/components/Feature.tsx +0 -14
- package/dist/cli/new/scaffold/template/sections/hero/fill-spec.ts +0 -29
- package/dist/cli/new/scaffold/template/sections/hero/fixtures.ts +0 -41
- package/dist/cli/new/scaffold/template/sections/hero/schema.ts +0 -60
- package/dist/cli/new/scaffold/template/theme.css +0 -24
- package/dist/cli/new/scaffold/template/theme.ts +0 -27
- package/dist/cli/new/scaffold-assets.js +0 -20
- package/dist/cli/pack/collect.js +0 -44
- package/dist/cli/pack/guards.js +0 -57
- package/dist/cli/pack/index.js +0 -66
- package/dist/cli/pack/manifest.js +0 -15
- package/dist/cli/pack/zip.js +0 -86
- package/dist/cli/theme/generate.js +0 -43
- package/dist/cli/theme/index.js +0 -33
- package/dist/cli/theme/load-theme.js +0 -69
- package/dist/cli.d.ts +0 -1
- package/dist/cli.js +0 -138
- package/dist/dev-client/assets/index-CJa9G1Na.css +0 -1
- package/dist/dev-client/assets/index-C_1atZs4.js +0 -450
- package/dist/dev-client/index.html +0 -13
- 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
|
@@ -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:
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
package/guide/dev.md
CHANGED
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
|
|
5
|
-
>
|
|
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
|
-
|
|
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
|