@homepages/template-kit 0.8.1-dev-20260718224903 → 0.9.0-dev-20260719224319
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 +61 -1
- package/README.md +2 -2
- package/dist/canvas-core/chrome-css.d.ts +24 -0
- package/dist/canvas-core/chrome-css.js +403 -0
- package/dist/canvas-core/chrome.d.ts +49 -0
- package/dist/canvas-core/chrome.js +147 -0
- package/dist/canvas-core/frame.d.ts +20 -0
- package/dist/canvas-core/frame.js +23 -0
- package/dist/canvas-core/hidden-tabs.d.ts +16 -0
- package/dist/canvas-core/hidden-tabs.js +84 -0
- package/dist/canvas-core/resolve-target.js +55 -0
- package/dist/canvas-core/scroll.d.ts +10 -0
- package/dist/canvas-core/scroll.js +15 -0
- package/dist/canvas-core/section-dom.d.ts +6 -0
- package/dist/canvas-core/section-dom.js +43 -0
- package/dist/canvas-core/srcdoc.d.ts +8 -0
- package/dist/canvas-core/srcdoc.js +77 -0
- package/dist/canvas-core/types.d.ts +76 -0
- package/dist/canvas-core.d.ts +9 -0
- package/dist/canvas-core.js +9 -0
- package/dist/cli/check/css.js +2 -2
- package/dist/cli/check/loader.js +1 -1
- package/dist/cli/check/stages/deps.js +1 -1
- package/dist/cli/check/stages/render.js +3 -2
- package/dist/cli/check/stages/size.js +2 -2
- package/dist/cli/check/stages/tree.js +1 -1
- package/dist/cli/check/stages/validate/index.js +1 -1
- package/dist/cli/check/stages/validate/nav-contract.js +4 -3
- package/dist/cli/check/stages/validate/orchestrator.js +1 -1
- package/dist/cli/check/workspace.js +2 -2
- package/dist/cli/dev/build-css.js +1 -1
- package/dist/cli/dev/discover.js +2 -2
- package/dist/cli/dev/inspect.js +1 -1
- package/dist/cli/dev/island-bootstrap.js +1 -1
- package/dist/cli/dev/island-canvas-bootstrap.js +37 -5
- package/dist/cli/dev/section-page.js +1 -1
- package/dist/cli/dev/server.js +12 -5
- package/dist/cli/dev/tailwind.js +1 -1
- package/dist/cli/dev/tsconfig-paths-plugin.js +1 -1
- package/dist/cli/dev/workspace.js +2 -2
- package/dist/cli/link/index.js +1 -1
- package/dist/cli/link/overlay.js +1 -1
- package/dist/cli/new/emit.js +1 -1
- package/dist/cli/new/index.js +1 -1
- package/dist/cli/new/scaffold/template/sections/hero/fixtures.ts +4 -4
- package/dist/cli/new/scaffold-assets.js +2 -2
- package/dist/cli/pack/collect.js +1 -1
- package/dist/cli/pack/guards.js +1 -1
- package/dist/cli/pack/index.js +1 -1
- package/dist/cli/theme/generate.js +1 -1
- package/dist/cli/theme/load-theme.js +1 -1
- package/dist/dev-client/assets/index-CJa9G1Na.css +1 -0
- package/dist/dev-client/assets/index-C_1atZs4.js +450 -0
- package/dist/dev-client/index.html +2 -2
- 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/islands/client-directive.d.ts +7 -0
- package/dist/{eslint/is-client-file.js → islands/client-directive.js} +6 -3
- package/dist/islands/detect.d.ts +1 -1
- package/dist/islands/detect.js +1 -1
- package/dist/islands/discover.js +1 -1
- package/dist/islands/esbuild-plugin.js +1 -1
- package/dist/islands/wrap.js +1 -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.js +73 -0
- package/dist/media/resolve-media.js +195 -0
- package/dist/package.js +1 -1
- package/dist/rules.d.ts +176 -0
- package/dist/rules.js +209 -0
- package/dist/schema/property-facts.d.ts +2 -2
- package/dist/ssr.d.ts +14 -2
- package/dist/ssr.js +28 -13
- package/dist/tokens.css +21 -0
- package/guide/INDEX.md +2 -2
- package/guide/check.md +5 -5
- package/guide/dev.md +39 -30
- package/guide/eslint.md +13 -10
- package/guide/llms.txt +4 -3
- package/guide/quickstart.md +1 -1
- package/guide/recipes/image-slot-crop.md +6 -6
- package/guide/recipes/pick-a-scenario.md +13 -5
- package/guide/rules/INDEX.md +3 -2
- package/guide/schema-system.md +36 -12
- package/guide/upgrading.md +18 -2
- package/package.json +8 -4
- package/tsconfig.json +1 -1
- package/dist/dev-client/assets/index-3Gn34X_P.js +0 -50
- package/dist/dev-client/assets/index-DxQeiHfu.css +0 -1
- package/dist/eslint/is-client-file.d.ts +0 -4
- package/dist/eslint/rules/image-bare-needs-reason.js +0 -43
- package/dist/eslint/rules/no-client-directive-in-contract.js +0 -31
- package/dist/eslint/rules/no-client-runtime-in-server.js +0 -103
- package/dist/eslint/rules/no-css-import-from-render-path.js +0 -29
- package/dist/eslint/rules/no-hex.js +0 -188
- package/dist/eslint/rules/no-inline-style.js +0 -27
- package/dist/eslint/rules/no-nondeterminism.js +0 -48
- package/dist/eslint/rules/no-raw-element.js +0 -38
- package/dist/eslint/rules/props-from-schema.js +0 -37
- package/dist/eslint/rules/require-island-editor.js +0 -74
- package/dist/eslint/rules/serializable-island-props.js +0 -112
- package/dist/eslint/rules/slot-marker-literal.js +0 -38
- package/dist/eslint.d.ts +0 -6
- package/dist/eslint.js +0 -80
- package/dist/rules/registry.js +0 -16
|
@@ -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,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
package/dist/rules.d.ts
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
//#region src/rules/registry.d.ts
|
|
2
|
+
type RuleVenue = "eslint" | "check";
|
|
3
|
+
interface RuleEntry {
|
|
4
|
+
/** Bare id — "no-hex". Never the qualified "template-kit/no-hex". */
|
|
5
|
+
id: string;
|
|
6
|
+
/** Which engine emits it. */
|
|
7
|
+
venue: RuleVenue;
|
|
8
|
+
/** One line, for the docs index. */
|
|
9
|
+
summary: string;
|
|
10
|
+
}
|
|
11
|
+
declare const RULE_IDS: readonly [{
|
|
12
|
+
readonly id: "no-nondeterminism";
|
|
13
|
+
readonly venue: "eslint";
|
|
14
|
+
readonly summary: "No clock or randomness reads in server-rendered code.";
|
|
15
|
+
}, {
|
|
16
|
+
readonly id: "no-client-runtime-in-server";
|
|
17
|
+
readonly venue: "eslint";
|
|
18
|
+
readonly summary: "No effects, state, event handlers, network, or browser globals in server-rendered code.";
|
|
19
|
+
}, {
|
|
20
|
+
readonly id: "serializable-island-props";
|
|
21
|
+
readonly venue: "eslint";
|
|
22
|
+
readonly summary: "Props handed to an island must be JSON-serializable.";
|
|
23
|
+
}, {
|
|
24
|
+
readonly id: "require-island-editor";
|
|
25
|
+
readonly venue: "eslint";
|
|
26
|
+
readonly summary: "Every island must declare whether it is live in the editor canvas.";
|
|
27
|
+
}, {
|
|
28
|
+
readonly id: "no-client-directive-in-contract";
|
|
29
|
+
readonly venue: "eslint";
|
|
30
|
+
readonly summary: "No \"use client\" in a section's four contract files.";
|
|
31
|
+
}, {
|
|
32
|
+
readonly id: "props-from-schema";
|
|
33
|
+
readonly venue: "eslint";
|
|
34
|
+
readonly summary: "A Renderer's Props type must be derived from schema.ts, not hand-written.";
|
|
35
|
+
}, {
|
|
36
|
+
readonly id: "no-inline-style";
|
|
37
|
+
readonly venue: "eslint";
|
|
38
|
+
readonly summary: "No inline style= in server-rendered JSX. Islands are exempt.";
|
|
39
|
+
}, {
|
|
40
|
+
readonly id: "no-raw-element";
|
|
41
|
+
readonly venue: "eslint";
|
|
42
|
+
readonly summary: "No raw `<section>`, `<img>` or `<video>` — use `<Section>`, `<Image>` and `<Video>`.";
|
|
43
|
+
}, {
|
|
44
|
+
readonly id: "image-bare-needs-reason";
|
|
45
|
+
readonly venue: "eslint";
|
|
46
|
+
readonly summary: "`<Image bare>` needs a `// bare:` comment saying why it skips the frame.";
|
|
47
|
+
}, {
|
|
48
|
+
readonly id: "slot-marker-literal";
|
|
49
|
+
readonly venue: "eslint";
|
|
50
|
+
readonly summary: "A slot-marker key must be a string literal, not an expression.";
|
|
51
|
+
}, {
|
|
52
|
+
readonly id: "no-hex";
|
|
53
|
+
readonly venue: "eslint";
|
|
54
|
+
readonly summary: "No hard-coded colours — the one legal colour function is a `color-mix()` over theme-derived arguments. Islands are NOT exempt.";
|
|
55
|
+
}, {
|
|
56
|
+
readonly id: "no-css-import-from-render-path";
|
|
57
|
+
readonly venue: "eslint";
|
|
58
|
+
readonly summary: "No relative/absolute .css import from a section's render path — a package's is the sanctioned channel.";
|
|
59
|
+
}, {
|
|
60
|
+
readonly id: "bundle-incomplete";
|
|
61
|
+
readonly venue: "check";
|
|
62
|
+
readonly summary: "A section folder is missing one of its four required files.";
|
|
63
|
+
}, {
|
|
64
|
+
readonly id: "bundle-binary-asset";
|
|
65
|
+
readonly venue: "check";
|
|
66
|
+
readonly summary: "An unreferenced binary asset sits in a section folder.";
|
|
67
|
+
}, {
|
|
68
|
+
readonly id: "schema-invalid";
|
|
69
|
+
readonly venue: "check";
|
|
70
|
+
readonly summary: "A section's schema.ts or fill-spec.ts fails a contract assertion, or cannot be loaded.";
|
|
71
|
+
}, {
|
|
72
|
+
readonly id: "fixtures-invalid";
|
|
73
|
+
readonly venue: "check";
|
|
74
|
+
readonly summary: "A section's fixtures.ts does not satisfy the fixture-module shape.";
|
|
75
|
+
}, {
|
|
76
|
+
readonly id: "manifest-invalid";
|
|
77
|
+
readonly venue: "check";
|
|
78
|
+
readonly summary: "A template's manifest.json is missing, unparseable, or violates the manifest contract.";
|
|
79
|
+
}, {
|
|
80
|
+
readonly id: "missing-slot-marker";
|
|
81
|
+
readonly venue: "check";
|
|
82
|
+
readonly summary: "An editable slot in schema.ts has no marker in the section's JSX.";
|
|
83
|
+
}, {
|
|
84
|
+
readonly id: "slot-group-marker";
|
|
85
|
+
readonly venue: "check";
|
|
86
|
+
readonly summary: "A group with two or more members has no `<SlotGroup>` wrapper.";
|
|
87
|
+
}, {
|
|
88
|
+
readonly id: "css-reason";
|
|
89
|
+
readonly venue: "check";
|
|
90
|
+
readonly summary: "A hand-written CSS file must open with a `css-reason:` comment.";
|
|
91
|
+
}, {
|
|
92
|
+
readonly id: "no-bare-css-import";
|
|
93
|
+
readonly venue: "check";
|
|
94
|
+
readonly summary: "No @import in hand-written CSS — the browser drops it silently.";
|
|
95
|
+
}, {
|
|
96
|
+
readonly id: "unlayered-fence";
|
|
97
|
+
readonly venue: "check";
|
|
98
|
+
readonly summary: "An `/* unlayered: */` escape hatch must be balanced and give a reason.";
|
|
99
|
+
}, {
|
|
100
|
+
readonly id: "sidebar-order";
|
|
101
|
+
readonly venue: "check";
|
|
102
|
+
readonly summary: "Schema slot order must match the order the page renders them in.";
|
|
103
|
+
}, {
|
|
104
|
+
readonly id: "typecheck";
|
|
105
|
+
readonly venue: "check";
|
|
106
|
+
readonly summary: "The template must compile under TypeScript.";
|
|
107
|
+
}, {
|
|
108
|
+
readonly id: "parse-error";
|
|
109
|
+
readonly venue: "check";
|
|
110
|
+
readonly summary: "A file could not be parsed at all — a syntax error, not a rule violation.";
|
|
111
|
+
}, {
|
|
112
|
+
readonly id: "determinism-drift";
|
|
113
|
+
readonly venue: "check";
|
|
114
|
+
readonly summary: "Rendering the same fixture twice must produce byte-identical HTML.";
|
|
115
|
+
}, {
|
|
116
|
+
readonly id: "render-invariant";
|
|
117
|
+
readonly venue: "check";
|
|
118
|
+
readonly summary: "Rendered output must carry no [object Object], bare null/undefined/NaN leaf, or src-less `<img>`.";
|
|
119
|
+
}, {
|
|
120
|
+
readonly id: "lockfile-missing";
|
|
121
|
+
readonly venue: "check";
|
|
122
|
+
readonly summary: "The workspace has no package-lock.json.";
|
|
123
|
+
}, {
|
|
124
|
+
readonly id: "lockfile-stale";
|
|
125
|
+
readonly venue: "check";
|
|
126
|
+
readonly summary: "The lockfile does not satisfy package.json — an `npm ci` would fail.";
|
|
127
|
+
}, {
|
|
128
|
+
readonly id: "single-react";
|
|
129
|
+
readonly venue: "check";
|
|
130
|
+
readonly summary: "The installed tree carries more than one copy of react or react-dom.";
|
|
131
|
+
}, {
|
|
132
|
+
readonly id: "audit-severity";
|
|
133
|
+
readonly venue: "check";
|
|
134
|
+
readonly summary: "A dependency carries a high or critical security advisory.";
|
|
135
|
+
}, {
|
|
136
|
+
readonly id: "license-denied";
|
|
137
|
+
readonly venue: "check";
|
|
138
|
+
readonly summary: "A production dependency's license is not on the allowlist.";
|
|
139
|
+
}, {
|
|
140
|
+
readonly id: "size-renderer-bundle";
|
|
141
|
+
readonly venue: "check";
|
|
142
|
+
readonly summary: "A section's renderer bundle exceeds its byte budget.";
|
|
143
|
+
}, {
|
|
144
|
+
readonly id: "size-section-css";
|
|
145
|
+
readonly venue: "check";
|
|
146
|
+
readonly summary: "A section's compiled CSS exceeds its byte budget.";
|
|
147
|
+
}, {
|
|
148
|
+
readonly id: "size-island-bundle";
|
|
149
|
+
readonly venue: "check";
|
|
150
|
+
readonly summary: "A section's island (browser) bundle exceeds its byte budget.";
|
|
151
|
+
}, {
|
|
152
|
+
readonly id: "size-assets";
|
|
153
|
+
readonly venue: "check";
|
|
154
|
+
readonly summary: "A section's static assets exceed their byte budget.";
|
|
155
|
+
}, {
|
|
156
|
+
readonly id: "no-templates";
|
|
157
|
+
readonly venue: "check";
|
|
158
|
+
readonly summary: "The workspace has no template to check at all.";
|
|
159
|
+
}];
|
|
160
|
+
type RuleId = (typeof RULE_IDS)[number]["id"];
|
|
161
|
+
/** The namespace every id is printed under, in both venues. */
|
|
162
|
+
declare const RULE_NAMESPACE = "template-kit";
|
|
163
|
+
/** `no-hex` → `template-kit/no-hex`. */
|
|
164
|
+
declare function qualify(id: RuleId): string;
|
|
165
|
+
/**
|
|
166
|
+
* `no-hex` → `guide/rules/no-hex.md`.
|
|
167
|
+
*
|
|
168
|
+
* Package-relative on purpose: the caller decides what to resolve it against. A
|
|
169
|
+
* printer prefixes the resolved package root so an author sees a path they can open
|
|
170
|
+
* with no network (`node_modules/@homepages/template-kit/guide/rules/no-hex.md`);
|
|
171
|
+
* ESLint's `meta.docs.url` uses it as-is. This module never touches the filesystem —
|
|
172
|
+
* the ESLint preset imports it.
|
|
173
|
+
*/
|
|
174
|
+
declare function docsPathForRule(id: RuleId): string;
|
|
175
|
+
//#endregion
|
|
176
|
+
export { RULE_IDS, RULE_NAMESPACE, RuleEntry, RuleId, RuleVenue, docsPathForRule, qualify };
|