@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.
Files changed (112) hide show
  1. package/CHANGELOG.md +61 -1
  2. package/README.md +2 -2
  3. package/dist/canvas-core/chrome-css.d.ts +24 -0
  4. package/dist/canvas-core/chrome-css.js +403 -0
  5. package/dist/canvas-core/chrome.d.ts +49 -0
  6. package/dist/canvas-core/chrome.js +147 -0
  7. package/dist/canvas-core/frame.d.ts +20 -0
  8. package/dist/canvas-core/frame.js +23 -0
  9. package/dist/canvas-core/hidden-tabs.d.ts +16 -0
  10. package/dist/canvas-core/hidden-tabs.js +84 -0
  11. package/dist/canvas-core/resolve-target.js +55 -0
  12. package/dist/canvas-core/scroll.d.ts +10 -0
  13. package/dist/canvas-core/scroll.js +15 -0
  14. package/dist/canvas-core/section-dom.d.ts +6 -0
  15. package/dist/canvas-core/section-dom.js +43 -0
  16. package/dist/canvas-core/srcdoc.d.ts +8 -0
  17. package/dist/canvas-core/srcdoc.js +77 -0
  18. package/dist/canvas-core/types.d.ts +76 -0
  19. package/dist/canvas-core.d.ts +9 -0
  20. package/dist/canvas-core.js +9 -0
  21. package/dist/cli/check/css.js +2 -2
  22. package/dist/cli/check/loader.js +1 -1
  23. package/dist/cli/check/stages/deps.js +1 -1
  24. package/dist/cli/check/stages/render.js +3 -2
  25. package/dist/cli/check/stages/size.js +2 -2
  26. package/dist/cli/check/stages/tree.js +1 -1
  27. package/dist/cli/check/stages/validate/index.js +1 -1
  28. package/dist/cli/check/stages/validate/nav-contract.js +4 -3
  29. package/dist/cli/check/stages/validate/orchestrator.js +1 -1
  30. package/dist/cli/check/workspace.js +2 -2
  31. package/dist/cli/dev/build-css.js +1 -1
  32. package/dist/cli/dev/discover.js +2 -2
  33. package/dist/cli/dev/inspect.js +1 -1
  34. package/dist/cli/dev/island-bootstrap.js +1 -1
  35. package/dist/cli/dev/island-canvas-bootstrap.js +37 -5
  36. package/dist/cli/dev/section-page.js +1 -1
  37. package/dist/cli/dev/server.js +12 -5
  38. package/dist/cli/dev/tailwind.js +1 -1
  39. package/dist/cli/dev/tsconfig-paths-plugin.js +1 -1
  40. package/dist/cli/dev/workspace.js +2 -2
  41. package/dist/cli/link/index.js +1 -1
  42. package/dist/cli/link/overlay.js +1 -1
  43. package/dist/cli/new/emit.js +1 -1
  44. package/dist/cli/new/index.js +1 -1
  45. package/dist/cli/new/scaffold/template/sections/hero/fixtures.ts +4 -4
  46. package/dist/cli/new/scaffold-assets.js +2 -2
  47. package/dist/cli/pack/collect.js +1 -1
  48. package/dist/cli/pack/guards.js +1 -1
  49. package/dist/cli/pack/index.js +1 -1
  50. package/dist/cli/theme/generate.js +1 -1
  51. package/dist/cli/theme/load-theme.js +1 -1
  52. package/dist/dev-client/assets/index-CJa9G1Na.css +1 -0
  53. package/dist/dev-client/assets/index-C_1atZs4.js +450 -0
  54. package/dist/dev-client/index.html +2 -2
  55. package/dist/fixtures/sample-property.d.ts +4 -7
  56. package/dist/fixtures/sample-property.js +41 -97
  57. package/dist/fixtures/scenario.d.ts +1 -1
  58. package/dist/fixtures/scenarios/for-rent-condo.js +24 -44
  59. package/dist/fixtures/scenarios/luxury-multi-unit.js +1 -1
  60. package/dist/fixtures/scenarios/sparse-single-family.js +14 -26
  61. package/dist/fixtures.d.ts +2 -2
  62. package/dist/fixtures.js +1 -2
  63. package/dist/islands/client-directive.d.ts +7 -0
  64. package/dist/{eslint/is-client-file.js → islands/client-directive.js} +6 -3
  65. package/dist/islands/detect.d.ts +1 -1
  66. package/dist/islands/detect.js +1 -1
  67. package/dist/islands/discover.js +1 -1
  68. package/dist/islands/esbuild-plugin.js +1 -1
  69. package/dist/islands/wrap.js +1 -1
  70. package/dist/manifest.json +940 -0
  71. package/dist/media/pack.d.ts +49 -0
  72. package/dist/media/pack.js +69 -0
  73. package/dist/media/resolve-fixture-media.js +73 -0
  74. package/dist/media/resolve-media.js +195 -0
  75. package/dist/package.js +1 -1
  76. package/dist/rules.d.ts +176 -0
  77. package/dist/rules.js +209 -0
  78. package/dist/schema/property-facts.d.ts +2 -2
  79. package/dist/ssr.d.ts +14 -2
  80. package/dist/ssr.js +28 -13
  81. package/dist/tokens.css +21 -0
  82. package/guide/INDEX.md +2 -2
  83. package/guide/check.md +5 -5
  84. package/guide/dev.md +39 -30
  85. package/guide/eslint.md +13 -10
  86. package/guide/llms.txt +4 -3
  87. package/guide/quickstart.md +1 -1
  88. package/guide/recipes/image-slot-crop.md +6 -6
  89. package/guide/recipes/pick-a-scenario.md +13 -5
  90. package/guide/rules/INDEX.md +3 -2
  91. package/guide/schema-system.md +36 -12
  92. package/guide/upgrading.md +18 -2
  93. package/package.json +8 -4
  94. package/tsconfig.json +1 -1
  95. package/dist/dev-client/assets/index-3Gn34X_P.js +0 -50
  96. package/dist/dev-client/assets/index-DxQeiHfu.css +0 -1
  97. package/dist/eslint/is-client-file.d.ts +0 -4
  98. package/dist/eslint/rules/image-bare-needs-reason.js +0 -43
  99. package/dist/eslint/rules/no-client-directive-in-contract.js +0 -31
  100. package/dist/eslint/rules/no-client-runtime-in-server.js +0 -103
  101. package/dist/eslint/rules/no-css-import-from-render-path.js +0 -29
  102. package/dist/eslint/rules/no-hex.js +0 -188
  103. package/dist/eslint/rules/no-inline-style.js +0 -27
  104. package/dist/eslint/rules/no-nondeterminism.js +0 -48
  105. package/dist/eslint/rules/no-raw-element.js +0 -38
  106. package/dist/eslint/rules/props-from-schema.js +0 -37
  107. package/dist/eslint/rules/require-island-editor.js +0 -74
  108. package/dist/eslint/rules/serializable-island-props.js +0 -112
  109. package/dist/eslint/rules/slot-marker-literal.js +0 -38
  110. package/dist/eslint.d.ts +0 -6
  111. package/dist/eslint.js +0 -80
  112. package/dist/rules/registry.js +0 -16
package/dist/rules.js ADDED
@@ -0,0 +1,209 @@
1
+ //#region src/rules/registry.ts
2
+ const RULE_IDS = [
3
+ {
4
+ id: "no-nondeterminism",
5
+ venue: "eslint",
6
+ summary: "No clock or randomness reads in server-rendered code."
7
+ },
8
+ {
9
+ id: "no-client-runtime-in-server",
10
+ venue: "eslint",
11
+ summary: "No effects, state, event handlers, network, or browser globals in server-rendered code."
12
+ },
13
+ {
14
+ id: "serializable-island-props",
15
+ venue: "eslint",
16
+ summary: "Props handed to an island must be JSON-serializable."
17
+ },
18
+ {
19
+ id: "require-island-editor",
20
+ venue: "eslint",
21
+ summary: "Every island must declare whether it is live in the editor canvas."
22
+ },
23
+ {
24
+ id: "no-client-directive-in-contract",
25
+ venue: "eslint",
26
+ summary: "No \"use client\" in a section's four contract files."
27
+ },
28
+ {
29
+ id: "props-from-schema",
30
+ venue: "eslint",
31
+ summary: "A Renderer's Props type must be derived from schema.ts, not hand-written."
32
+ },
33
+ {
34
+ id: "no-inline-style",
35
+ venue: "eslint",
36
+ summary: "No inline style= in server-rendered JSX. Islands are exempt."
37
+ },
38
+ {
39
+ id: "no-raw-element",
40
+ venue: "eslint",
41
+ summary: "No raw `<section>`, `<img>` or `<video>` — use `<Section>`, `<Image>` and `<Video>`."
42
+ },
43
+ {
44
+ id: "image-bare-needs-reason",
45
+ venue: "eslint",
46
+ summary: "`<Image bare>` needs a `// bare:` comment saying why it skips the frame."
47
+ },
48
+ {
49
+ id: "slot-marker-literal",
50
+ venue: "eslint",
51
+ summary: "A slot-marker key must be a string literal, not an expression."
52
+ },
53
+ {
54
+ id: "no-hex",
55
+ venue: "eslint",
56
+ summary: "No hard-coded colours — the one legal colour function is a `color-mix()` over theme-derived arguments. Islands are NOT exempt."
57
+ },
58
+ {
59
+ id: "no-css-import-from-render-path",
60
+ venue: "eslint",
61
+ summary: "No relative/absolute .css import from a section's render path — a package's is the sanctioned channel."
62
+ },
63
+ {
64
+ id: "bundle-incomplete",
65
+ venue: "check",
66
+ summary: "A section folder is missing one of its four required files."
67
+ },
68
+ {
69
+ id: "bundle-binary-asset",
70
+ venue: "check",
71
+ summary: "An unreferenced binary asset sits in a section folder."
72
+ },
73
+ {
74
+ id: "schema-invalid",
75
+ venue: "check",
76
+ summary: "A section's schema.ts or fill-spec.ts fails a contract assertion, or cannot be loaded."
77
+ },
78
+ {
79
+ id: "fixtures-invalid",
80
+ venue: "check",
81
+ summary: "A section's fixtures.ts does not satisfy the fixture-module shape."
82
+ },
83
+ {
84
+ id: "manifest-invalid",
85
+ venue: "check",
86
+ summary: "A template's manifest.json is missing, unparseable, or violates the manifest contract."
87
+ },
88
+ {
89
+ id: "missing-slot-marker",
90
+ venue: "check",
91
+ summary: "An editable slot in schema.ts has no marker in the section's JSX."
92
+ },
93
+ {
94
+ id: "slot-group-marker",
95
+ venue: "check",
96
+ summary: "A group with two or more members has no `<SlotGroup>` wrapper."
97
+ },
98
+ {
99
+ id: "css-reason",
100
+ venue: "check",
101
+ summary: "A hand-written CSS file must open with a `css-reason:` comment."
102
+ },
103
+ {
104
+ id: "no-bare-css-import",
105
+ venue: "check",
106
+ summary: "No @import in hand-written CSS — the browser drops it silently."
107
+ },
108
+ {
109
+ id: "unlayered-fence",
110
+ venue: "check",
111
+ summary: "An `/* unlayered: */` escape hatch must be balanced and give a reason."
112
+ },
113
+ {
114
+ id: "sidebar-order",
115
+ venue: "check",
116
+ summary: "Schema slot order must match the order the page renders them in."
117
+ },
118
+ {
119
+ id: "typecheck",
120
+ venue: "check",
121
+ summary: "The template must compile under TypeScript."
122
+ },
123
+ {
124
+ id: "parse-error",
125
+ venue: "check",
126
+ summary: "A file could not be parsed at all — a syntax error, not a rule violation."
127
+ },
128
+ {
129
+ id: "determinism-drift",
130
+ venue: "check",
131
+ summary: "Rendering the same fixture twice must produce byte-identical HTML."
132
+ },
133
+ {
134
+ id: "render-invariant",
135
+ venue: "check",
136
+ summary: "Rendered output must carry no [object Object], bare null/undefined/NaN leaf, or src-less `<img>`."
137
+ },
138
+ {
139
+ id: "lockfile-missing",
140
+ venue: "check",
141
+ summary: "The workspace has no package-lock.json."
142
+ },
143
+ {
144
+ id: "lockfile-stale",
145
+ venue: "check",
146
+ summary: "The lockfile does not satisfy package.json — an `npm ci` would fail."
147
+ },
148
+ {
149
+ id: "single-react",
150
+ venue: "check",
151
+ summary: "The installed tree carries more than one copy of react or react-dom."
152
+ },
153
+ {
154
+ id: "audit-severity",
155
+ venue: "check",
156
+ summary: "A dependency carries a high or critical security advisory."
157
+ },
158
+ {
159
+ id: "license-denied",
160
+ venue: "check",
161
+ summary: "A production dependency's license is not on the allowlist."
162
+ },
163
+ {
164
+ id: "size-renderer-bundle",
165
+ venue: "check",
166
+ summary: "A section's renderer bundle exceeds its byte budget."
167
+ },
168
+ {
169
+ id: "size-section-css",
170
+ venue: "check",
171
+ summary: "A section's compiled CSS exceeds its byte budget."
172
+ },
173
+ {
174
+ id: "size-island-bundle",
175
+ venue: "check",
176
+ summary: "A section's island (browser) bundle exceeds its byte budget."
177
+ },
178
+ {
179
+ id: "size-assets",
180
+ venue: "check",
181
+ summary: "A section's static assets exceed their byte budget."
182
+ },
183
+ {
184
+ id: "no-templates",
185
+ venue: "check",
186
+ summary: "The workspace has no template to check at all."
187
+ }
188
+ ];
189
+ /** The namespace every id is printed under, in both venues. */
190
+ const RULE_NAMESPACE = "template-kit";
191
+ /** `no-hex` → `template-kit/no-hex`. */
192
+ function qualify(id) {
193
+ return `${RULE_NAMESPACE}/${id}`;
194
+ }
195
+ /**
196
+ * `no-hex` → `guide/rules/no-hex.md`.
197
+ *
198
+ * Package-relative on purpose: the caller decides what to resolve it against. A
199
+ * printer prefixes the resolved package root so an author sees a path they can open
200
+ * with no network (`node_modules/@homepages/template-kit/guide/rules/no-hex.md`);
201
+ * ESLint's `meta.docs.url` uses it as-is. This module never touches the filesystem —
202
+ * the ESLint preset imports it.
203
+ */
204
+ function docsPathForRule(id) {
205
+ return `guide/rules/${id}.md`;
206
+ }
207
+
208
+ //#endregion
209
+ export { RULE_IDS, RULE_NAMESPACE, docsPathForRule, qualify };
@@ -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;
package/dist/ssr.d.ts CHANGED
@@ -1,10 +1,11 @@
1
1
  import { ResolvedFixture } from "./schema/fixture-schema.js";
2
2
  import "./schema/index.js";
3
- import { hasClientDirective } from "./eslint/is-client-file.js";
3
+ import { hasClientDirective, isClientFile, resetClientFileCache, resolveImportedFile } from "./islands/client-directive.js";
4
4
  import { isClientComponentFile, islandKey } from "./islands/detect.js";
5
5
  import { wrapClientModuleSource } from "./islands/wrap.js";
6
6
  import { findSectionIslands } from "./islands/discover.js";
7
7
  import { islandTransformEsbuildPlugin } from "./islands/esbuild-plugin.js";
8
+ import { DEV_MEDIA_URL_PREFIX, MediaAsset, MediaKind, getMediaAsset, listMediaAssets, mediaPackHasFile, packRelativeFromUrl } from "./media/pack.js";
8
9
  import { ComponentType } from "react";
9
10
  //#region src/ssr/index.d.ts
10
11
  type LoadedSection = {
@@ -16,6 +17,11 @@ type LoadedSection = {
16
17
  editable_by_user?: boolean;
17
18
  required?: boolean;
18
19
  }>;
20
+ unresolvedMedia: {
21
+ fixture: string;
22
+ field: string;
23
+ id: string;
24
+ }[];
19
25
  };
20
26
  type SectionModuleLoader = (dir: string, file: string) => Promise<unknown>;
21
27
  declare const nativeSectionLoader: SectionModuleLoader;
@@ -23,6 +29,12 @@ declare const nativeSectionLoader: SectionModuleLoader;
23
29
  * Load a bundle's Renderer plus its resolved snapshot and invariant fixture
24
30
  * sets from the bundle directory `dir`. `load` resolves each of the four
25
31
  * contract files; it defaults to native dynamic import.
32
+ *
33
+ * Media references resolve here, against the dev media pack: fixtures state the
34
+ * asset id `content_doc` holds, and a Renderer must receive the post-resolution
35
+ * `url`. This is the single fixture loader, so every Node-side caller — the
36
+ * registry's render + invariant gates, the dev server — gets resolved media
37
+ * without asking for it.
26
38
  */
27
39
  declare function loadSectionBundle(dir: string, load?: SectionModuleLoader): Promise<LoadedSection>;
28
40
  /**
@@ -31,4 +43,4 @@ declare function loadSectionBundle(dir: string, load?: SectionModuleLoader): Pro
31
43
  */
32
44
  declare function renderSectionHtml(renderer: ComponentType<Record<string, unknown>>, props: Record<string, unknown>): string;
33
45
  //#endregion
34
- export { LoadedSection, SectionModuleLoader, findSectionIslands, hasClientDirective, isClientComponentFile, islandKey, islandTransformEsbuildPlugin, loadSectionBundle, nativeSectionLoader, renderSectionHtml, wrapClientModuleSource };
46
+ export { DEV_MEDIA_URL_PREFIX, LoadedSection, type MediaAsset, type MediaKind, SectionModuleLoader, findSectionIslands, getMediaAsset, hasClientDirective, isClientComponentFile, isClientFile, islandKey, islandTransformEsbuildPlugin, listMediaAssets, loadSectionBundle, mediaPackHasFile, nativeSectionLoader, packRelativeFromUrl, renderSectionHtml, resetClientFileCache, resolveImportedFile, wrapClientModuleSource };
package/dist/ssr.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import { FixtureModuleShape, buildFixtureSet, buildInvariantFixtures } from "./schema/fixture-schema.js";
2
- import { hasClientDirective } from "./eslint/is-client-file.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";
4
+ import { hasClientDirective, isClientFile, resetClientFileCache, resolveImportedFile } from "./islands/client-directive.js";
3
5
  import { isClientComponentFile, islandKey } from "./islands/detect.js";
4
6
  import { wrapClientModuleSource } from "./islands/wrap.js";
5
7
  import { findSectionIslands } from "./islands/discover.js";
@@ -15,6 +17,12 @@ const nativeSectionLoader = (dir, file) => import(pathToFileURL(join(dir, file))
15
17
  * Load a bundle's Renderer plus its resolved snapshot and invariant fixture
16
18
  * sets from the bundle directory `dir`. `load` resolves each of the four
17
19
  * contract files; it defaults to native dynamic import.
20
+ *
21
+ * Media references resolve here, against the dev media pack: fixtures state the
22
+ * asset id `content_doc` holds, and a Renderer must receive the post-resolution
23
+ * `url`. This is the single fixture loader, so every Node-side caller — the
24
+ * registry's render + invariant gates, the dev server — gets resolved media
25
+ * without asking for it.
18
26
  */
19
27
  async function loadSectionBundle(dir, load = nativeSectionLoader) {
20
28
  const rendererMod = await load(dir, "Renderer.tsx");
@@ -22,19 +30,26 @@ async function loadSectionBundle(dir, load = nativeSectionLoader) {
22
30
  const schemaMod = await load(dir, "schema.ts");
23
31
  const parsed = FixtureModuleShape.parse(fixturesMod.default);
24
32
  const selfAnchor = schemaMod.schema.meta?.anchor ?? basename(dir);
33
+ const snapshot = buildFixtureSet({
34
+ schema: schemaMod.schema,
35
+ module: parsed,
36
+ selfAnchor
37
+ });
38
+ const invariant = buildInvariantFixtures({
39
+ schema: schemaMod.schema,
40
+ module: parsed,
41
+ selfAnchor
42
+ });
43
+ const unresolvedMedia = invariant.flatMap((fixture) => findUnresolvedMedia(fixture.props).map((miss) => ({
44
+ fixture: fixture.id,
45
+ ...miss
46
+ })));
25
47
  return {
26
48
  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
49
+ snapshotFixtures: resolveFixtureSetMedia(snapshot),
50
+ invariantFixtures: resolveFixtureSetMedia(invariant),
51
+ schemaSlots: schemaMod.schema.slots,
52
+ unresolvedMedia
38
53
  };
39
54
  }
40
55
  /**
@@ -46,4 +61,4 @@ function renderSectionHtml(renderer, props) {
46
61
  }
47
62
 
48
63
  //#endregion
49
- export { findSectionIslands, hasClientDirective, isClientComponentFile, islandKey, islandTransformEsbuildPlugin, loadSectionBundle, nativeSectionLoader, renderSectionHtml, wrapClientModuleSource };
64
+ export { DEV_MEDIA_URL_PREFIX, findSectionIslands, getMediaAsset, hasClientDirective, isClientComponentFile, isClientFile, islandKey, islandTransformEsbuildPlugin, listMediaAssets, loadSectionBundle, mediaPackHasFile, nativeSectionLoader, packRelativeFromUrl, renderSectionHtml, resetClientFileCache, resolveImportedFile, wrapClientModuleSource };
@@ -0,0 +1,21 @@
1
+ /* Design tokens the editor surface around the canvas frame reads.
2
+ The kernel itself reads none of these — the iframe is a srcdoc document
3
+ that cannot inherit the host's Tailwind, so its own chrome CSS is built
4
+ from literals on purpose. These are shipped beside the kernel so a
5
+ vendored copy's host stylesheet resolves them identically instead of
6
+ degrading silently to an inherited value.
7
+
8
+ The application's global stylesheet is the source of truth; this partial
9
+ is the shipped mirror and a colocated test fails on any drift between
10
+ them. Change a value there, not here. */
11
+
12
+ @theme inline {
13
+ --color-canvas: #ffffff;
14
+ --color-line: #ebebe6;
15
+ }
16
+
17
+ :root {
18
+ --frame: clamp(1px, calc(1px + (100vw - 1536px) * 0.0006076), 1.41667px);
19
+ --content-follow: 0.25;
20
+ --content: calc(1px + (var(--frame) - 1px) * var(--content-follow));
21
+ }
package/guide/INDEX.md CHANGED
@@ -2,7 +2,7 @@
2
2
  purpose: Router for the guide corpus that ships inside the @homepages/template-kit npm package.
3
3
  status: living
4
4
  related: []
5
- updated: 2026-07-16
5
+ updated: 2026-07-19
6
6
  ---
7
7
  # template-kit guide — router
8
8
 
@@ -22,7 +22,7 @@ detail.
22
22
  | `assets.md` | Static assets (svg/images/fonts): where they live (template level or a section folder, if referenced), referencing them from a section's CSS with `url()`/`@font-face` or importing them into a component, and the asset-vs-content-image distinction | shipping a logo, texture, icon, or brand font with a template |
23
23
  | `theme-and-css.md` | The two CSS entries, the one-Tailwind-import rule, `TokenTheme` (your own token names → your own Tailwind utilities), the `template-kit theme` command that generates `theme.css` from `theme.ts`, the breakpoint ladder, and the opt-in `content-visibility` recipe for long, image-heavy pages | a template's theme, its CSS entry, or a long page with a live map or full-screen overlay |
24
24
  | `islands.md` | Interactivity: `"use client"` components, the serializable-props rule, editor options, the loader | anything interactive in a section |
25
- | `eslint.md` | The `@homepages/template-kit/eslint` preset: the two-line config, the ESLint + parser install it expects, and its glob scope | wiring up lint in a template workspace |
25
+ | `eslint.md` | The `@homepages/eslint-plugin-template` preset: the two-line config, the ESLint + parser install it expects, and its glob scope | wiring up lint in a template workspace |
26
26
  | `check.md` | The `template-kit check` CLI command: the toolchain it resolves from your workspace, its stages, `--all`/`--json`, output shape, and exit code | running `check` locally or wiring it into CI |
27
27
  | `dev.md` | The `template-kit dev` CLI command: the local preview server, its home launcher and two tool cards (full template viewer, canvas mirror), the canvas mirror's selection overlay and per-slot edit panel, live reload, the content-override protocol, and the render/inspect/screenshot endpoints | previewing sections as you author them |
28
28
  | `new.md` | The `template-kit new` / `new-section` CLI commands: scaffolding a new template with a working starter section, or adding a bare section to an existing template | adding a template or a section to a workspace |
package/guide/check.md CHANGED
@@ -2,7 +2,7 @@
2
2
  purpose: The `template-kit check` command — what it runs, its flags, its output shape, and its exit code.
3
3
  status: living
4
4
  related: [INDEX.md, rules/INDEX.md, eslint.md, theme-and-css.md, dev.md, upgrading.md]
5
- updated: 2026-07-16
5
+ updated: 2026-07-19
6
6
  ---
7
7
  # template-kit check
8
8
 
@@ -28,7 +28,7 @@ one or pass `--all`.
28
28
  ### What it needs installed
29
29
 
30
30
  ```bash
31
- npm i -D typescript eslint @typescript-eslint/parser esbuild
31
+ npm i -D typescript eslint @typescript-eslint/parser esbuild @homepages/eslint-plugin-template
32
32
  ```
33
33
 
34
34
  `check` runs **your** toolchain, not a copy of its own: it resolves TypeScript, ESLint
@@ -48,9 +48,9 @@ one class of fix at a time:
48
48
 
49
49
  1. **typecheck** — the workspace's own TypeScript compiles the template's files, judged
50
50
  against the workspace's own `tsconfig.json`.
51
- 2. **lint** — the workspace's own ESLint, running the kit's shipped preset over the
52
- template's files. See [the ESLint preset](eslint.md) for the rules this stage shares
53
- with your editor.
51
+ 2. **lint** — the workspace's own ESLint, running your own `eslint.config.mjs` and
52
+ so the [authoring preset](eslint.md) it imports — over the template's files. See
53
+ that page for the rules this stage shares with your editor.
54
54
  3. **validate** — the schema, fill-spec, fixtures, and manifest contracts: every
55
55
  cross-reference between a section's contract files, and a template's `manifest.json`
56
56
  against the sections it composes.
package/guide/dev.md CHANGED
@@ -2,7 +2,7 @@
2
2
  purpose: The `template-kit dev` command — the local preview server, what it serves, its URLs, and the agent-facing render endpoints.
3
3
  status: living
4
4
  related: [INDEX.md, theme-and-css.md, schema-system.md, check.md, islands.md]
5
- updated: 2026-07-18
5
+ updated: 2026-07-19
6
6
  ---
7
7
  # template-kit dev
8
8
 
@@ -134,9 +134,14 @@ own right.
134
134
  ## Canvas mirror
135
135
 
136
136
  `/canvas/<template>` — open it from the home launcher's **Canvas mirror** card,
137
- or directly — mirrors the whole template in an iframe: every section instance
138
- from `manifest.json`, stacked in one scroll container, exactly as the deliverable
139
- renders. A paint-only overlay sits on top of it, scoped per section instance —
137
+ or directly — renders the template inside the platform's own editor canvas
138
+ frame: a rounded page card that auto-scales to fit the available width, on the
139
+ same dotted canvas surface and with the same selection chrome the platform's
140
+ content editor uses. Each manifest section instance loads independently and is
141
+ injected into the page in manifest order as it arrives, so instances can appear
142
+ progressively rather than all at once; one instance failing to render shows an
143
+ error placeholder in its own spot without blocking the rest. A paint-only
144
+ overlay sits on top of the page, scoped per section instance —
140
145
  the same slot name in two different sections selects independently. Click a slot
141
146
  to select it (a gold ring); hover to preview one (a blue ring). Only slots that
142
147
  instance's schema marks `editable_by_user: true` select individually — clicking
@@ -152,38 +157,42 @@ The current template and selection are encoded in the URL as
152
157
  a repeating slot (`<instanceId>~<slotId>~<itemIndex>`) — so a canvas link
153
158
  reloads and shares exactly the view you're looking at.
154
159
 
155
- Selecting a slot or a section instance docks an **edit panel** beside the
156
- iframe. A slot selection's panel additionally shows a slot editor: one JSON
157
- editor handles every slot typeyou edit the slot's value in its own JSON
158
- shape rather than switching between per-type widgets alongside a
159
- **fill-state badge** (filled / empty / missing) for the selected slot and a
160
- per-instance **fixture picker**. The picker lists that section's whole
161
- fixture corpus, including its synthetic empty-content fixtures, so you can
162
- drive a slot into an empty or missing state without touching `fixtures.ts`.
163
- Applying a JSON edit, switching the fixture, or resetting the instance
164
- re-renders that section instance alone, the same way it renders in the
165
- stack, and swaps its HTML into the iframe in place — no page reload, no
166
- other instance disturbed then re-anchors the selection ring to the
167
- refreshed markup, and re-hydrates that instance's islands. The kit dev
168
- server is the harness's only backend, so this loop never touches fixture
169
- files, a database, or another service. Edit state lives only in the tab:
170
- nothing is written to disk, and reloading the page loses it. A section
171
- instance selection has no slot editor to dock, since there's no single slot
172
- to edit.
160
+ A **floating edit panel**, a fixed-width card anchored over the canvas's right
161
+ edge, is always open and shows one section instance at a time: whichever
162
+ instance is currently selected, orbefore any selection, or after Escape
163
+ clears the ring the last instance shown, defaulting to the template's first
164
+ manifest instance on initial load. The panel lists every one of that
165
+ instance's `editable_by_user: true` slots at once, each as its own row: one
166
+ JSON editor handles every slot type you edit the slot's value in its own
167
+ JSON shape rather than switching between per-type widgets alongside a
168
+ **fill-state badge** (filled / empty / missing). Selecting a slot scrolls its
169
+ row into view and highlights it. Above the slot rows sits a per-instance
170
+ **fixture picker**: it lists that section's whole fixture corpus, including
171
+ its synthetic empty-content fixtures, so you can drive a slot into an empty
172
+ or missing state without touching `fixtures.ts`. Applying a JSON edit,
173
+ switching the fixture, or resetting the instance re-renders that section
174
+ instance alone, the same way it renders in the stack, and swaps its HTML into
175
+ the iframe in place no page reload, no other instance disturbed — then
176
+ re-anchors the selection ring to the refreshed markup, and re-hydrates that
177
+ instance's islands. The kit dev server is the harness's only backend, so this
178
+ loop never touches fixture files, a database, or another service. Edit state
179
+ lives only in the tab: nothing is written to disk, and reloading the page
180
+ loses it.
173
181
 
174
182
  An island whose component declares `editor.live: true` (see [Islands](islands.md))
175
183
  hydrates in the canvas the same way it does on a published page, but behind a
176
184
  click-through shield until it's selected: the first click on a live island
177
185
  selects its owning slot or section instance instead of reaching the
178
186
  component, dropping the shield; a second click reaches the component itself.
179
- Pressing Escape clears the selection and re-arms every shield. An island with
180
- no `editor` export, or `editor.live: false`, stays static markup in the
181
- canvas same as a published page's non-live islands. Some islands own no
182
- slot of their own (e.g. a slider or map sitting beside a `<Slot>` rather than
183
- wrapped by one); their shield only ever drops under a section instance
184
- selection. Selecting a slot or a section instance docks a read-only
185
- **Islands** list in the edit panel showing every top-level island in that
186
- section instance, whether it's live or static, its shield placement, and
187
+ Pressing Escape clears the select ring and re-arms every shield; it does not
188
+ close the edit panel, which keeps showing the instance it was last showing.
189
+ An island with no `editor` export, or `editor.live: false`, stays static
190
+ markup in the canvas same as a published page's non-live islands. Some
191
+ islands own no slot of their own (e.g. a slider or map sitting beside a
192
+ `<Slot>` rather than wrapped by one); their shield only ever drops under a
193
+ section instance selection. The edit panel always includes a read-only
194
+ **Islands** list for its current instance, showing every top-level island in
195
+ that section instance, whether it's live or static, its shield placement, and
187
196
  whether it's currently interactive or shielded.
188
197
 
189
198
  ## Agent-facing endpoints
package/guide/eslint.md CHANGED
@@ -1,24 +1,24 @@
1
1
  ---
2
- purpose: The authoring-lint preset shipped at `@homepages/template-kit/eslint` — setup and scope. Per-rule reference lives in `rules/`.
2
+ purpose: The authoring-lint preset shipped as `@homepages/eslint-plugin-template` — setup and scope. Per-rule reference lives in `rules/`.
3
3
  status: living
4
- related: [INDEX.md, rules/INDEX.md, check.md, islands.md, theme-and-css.md, primitives.md]
5
- updated: 2026-07-14
4
+ related: [INDEX.md, rules/INDEX.md, check.md, islands.md, theme-and-css.md, primitives.md, upgrading.md]
5
+ updated: 2026-07-19
6
6
  ---
7
7
  # ESLint preset
8
8
 
9
- `@homepages/template-kit/eslint` is a flat-config ESLint preset carrying every
9
+ `@homepages/eslint-plugin-template` is a flat-config ESLint preset carrying every
10
10
  authoring rule the kit enforces one file at a time, under the `template-kit/*`
11
11
  rule namespace.
12
12
 
13
13
  ## Setup
14
14
 
15
15
  ```bash
16
- npm i -D eslint @typescript-eslint/parser
16
+ npm i -D @homepages/eslint-plugin-template eslint @typescript-eslint/parser
17
17
  ```
18
18
 
19
19
  ```js
20
20
  // eslint.config.mjs
21
- import kit from "@homepages/template-kit/eslint";
21
+ import kit from "@homepages/eslint-plugin-template";
22
22
  export default kit;
23
23
  ```
24
24
 
@@ -26,10 +26,13 @@ That is the whole setup — no plugin registration, no parser wiring, no tsconfi
26
26
  The preset registers its rules and sets `languageOptions` (parser included) itself,
27
27
  so those two lines are a complete config.
28
28
 
29
- You install ESLint and the parser yourself because the kit deliberately does **not**
30
- depend on them: a package whose main entry is a set of React primitives must not drag
31
- a linter into the production install of every app that renders a template. The preset
32
- resolves the parser from your workspace when you import it. If you see
29
+ The preset ships as its own package rather than a kit subpath because it needs a
30
+ different dependency tree: ESLint and the parser, not React. That keeps a linter out
31
+ of the production install of every app that renders a template the kit's main entry
32
+ is a set of React primitives, and it must not drag one in.
33
+
34
+ ESLint and the parser are peer dependencies, so you install them yourself; the preset
35
+ resolves them from your workspace. If you see
33
36
  `Cannot find package '@typescript-eslint/parser'`, that is the install line above
34
37
  missing.
35
38
 
package/guide/llms.txt CHANGED
@@ -80,10 +80,11 @@ and [Pick a scenario](recipes/pick-a-scenario.md).
80
80
  template — where an asset file lives (template level, or a section folder if the
81
81
  section references it), referencing it from a section's CSS with `url()`/`@font-face`
82
82
  or importing it into a component, and the asset-vs-content-image distinction.
83
- - [ESLint preset](eslint.md): `@homepages/template-kit/eslint`, the flat-config preset —
83
+ - [ESLint preset](eslint.md): `@homepages/eslint-plugin-template`, the flat-config preset —
84
84
  a two-line config with no parser wiring, over an ESLint + `@typescript-eslint/parser`
85
- install of your own (the kit depends on neither both resolve from your workspace).
86
- Its three-entry glob scope. Per-rule reference lives in `rules/`.
85
+ install of your own (both are peer dependencies of the preset and resolve from your
86
+ workspace; the kit itself depends on neither). Its own version line, separate from the
87
+ kit's. Its three-entry glob scope. Per-rule reference lives in `rules/`.
87
88
  - [Check](check.md): the `template-kit check` CLI command — the toolchain it runs (your
88
89
  TypeScript, ESLint and esbuild, resolved from your workspace), its six per-template
89
90
  stages, the once-per-invocation dependency gates, `--all`/`--json`, the report shape,
@@ -15,7 +15,7 @@ depth.
15
15
  ## Create your workspace
16
16
 
17
17
  ```bash
18
- npm create homepages-workspace@latest my-workspace
18
+ npm create @homepages/workspace@latest my-workspace
19
19
  ```
20
20
 
21
21
  This stamps a workspace with one template, `starter`, and one section in it,
@@ -2,7 +2,7 @@
2
2
  purpose: Add an image slot the editor crops to a fixed ratio.
3
3
  status: living
4
4
  related: [../schema-system.md, ../primitives.md, ../rules/image-bare-needs-reason.md, pick-a-scenario.md]
5
- updated: 2026-07-16
5
+ updated: 2026-07-19
6
6
  ---
7
7
  # Image slot with a locked crop
8
8
 
@@ -51,20 +51,20 @@ interior_image: {
51
51
  ```
52
52
 
53
53
  ```ts
54
- // fixtures.ts — a photo + a matching mkResponsive() height
55
- import { mkResponsive, scenarios } from "@homepages/template-kit/fixtures";
54
+ // fixtures.ts — the photo's asset id, exactly as a saved page states it
55
+ import { scenarios } from "@homepages/template-kit/fixtures";
56
56
 
57
57
  const interiorImage = scenarios.luxuryMultiUnit.photos[3]!;
58
58
  // ...
59
59
  interior_image: {
60
60
  photo_id: interiorImage.id,
61
- url: interiorImage.url,
62
61
  alt: interiorImage.alt,
63
- // Height set to a 4:3 ratio to match the slot's locked crop.
64
- responsive: mkResponsive(interiorImage.url, 1600, 1200),
65
62
  },
66
63
  ```
67
64
 
65
+ The `url` and the responsive ladder the Renderer reads are attached when the fixture
66
+ loads — see [Media is stated by id](../schema-system.md#media-is-stated-by-id-never-by-url).
67
+
68
68
  An image slot supports two independent geometry configs — use one, not both, per
69
69
  slot:
70
70