@homepages/template-kit 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/CHANGELOG.md +84 -0
  2. package/README.md +39 -4
  3. package/dist/asset-modules.d.ts +81 -0
  4. package/dist/cli/check/config.js +5 -1
  5. package/dist/cli/check/css.js +27 -13
  6. package/dist/cli/check/loader.js +4 -0
  7. package/dist/cli/check/section-assets.js +58 -0
  8. package/dist/cli/check/stages/size.js +124 -8
  9. package/dist/cli/check/stages/tree.js +26 -27
  10. package/dist/cli/dev/build-css.js +91 -0
  11. package/dist/cli/dev/catalog.js +35 -0
  12. package/dist/cli/dev/compose-template.js +38 -0
  13. package/dist/cli/dev/content-override.js +58 -0
  14. package/dist/cli/dev/discover.js +35 -0
  15. package/dist/cli/dev/fill-state.js +75 -0
  16. package/dist/cli/dev/index.js +32 -0
  17. package/dist/cli/dev/inspect.js +38 -0
  18. package/dist/cli/dev/island-bootstrap.js +38 -0
  19. package/dist/cli/dev/island-map.js +35 -0
  20. package/dist/cli/dev/island-transform.js +31 -0
  21. package/dist/cli/dev/manifest-instances.js +31 -0
  22. package/dist/cli/dev/render-section.js +24 -0
  23. package/dist/cli/dev/screenshot-target.js +24 -0
  24. package/dist/cli/dev/section-page.js +54 -0
  25. package/dist/cli/dev/server.js +480 -0
  26. package/dist/cli/dev/slot-schema.js +12 -0
  27. package/dist/cli/dev/structure-summary.js +118 -0
  28. package/dist/cli/dev/tailwind.js +32 -0
  29. package/dist/cli/dev/vite-server.js +33 -0
  30. package/dist/cli/dev/workspace.js +63 -0
  31. package/dist/cli/link/index.js +74 -0
  32. package/dist/cli/link/overlay.js +59 -0
  33. package/dist/cli/link/watch.js +25 -0
  34. package/dist/cli/new/emit.js +48 -0
  35. package/dist/cli/new/index.js +69 -0
  36. package/dist/cli/new/scaffold/section/_Renderer.tsx +20 -0
  37. package/dist/cli/new/scaffold/section/_fill-spec.ts +18 -0
  38. package/dist/cli/new/scaffold/section/_fixtures.ts +13 -0
  39. package/dist/cli/new/scaffold/section/_schema.ts +24 -0
  40. package/dist/cli/new/scaffold/template/_manifest.json +9 -0
  41. package/dist/cli/new/scaffold/template/sections/hero/ExpandableText.tsx +45 -0
  42. package/dist/cli/new/scaffold/template/sections/hero/Renderer.tsx +40 -0
  43. package/dist/cli/new/scaffold/template/sections/hero/components/Feature.tsx +14 -0
  44. package/dist/cli/new/scaffold/template/sections/hero/fill-spec.ts +29 -0
  45. package/dist/cli/new/scaffold/template/sections/hero/fixtures.ts +41 -0
  46. package/dist/cli/new/scaffold/template/sections/hero/schema.ts +60 -0
  47. package/dist/cli/new/scaffold/template/theme.css +24 -0
  48. package/dist/cli/new/scaffold/template/theme.ts +27 -0
  49. package/dist/cli/new/scaffold-assets.js +20 -0
  50. package/dist/cli/pack/collect.js +44 -0
  51. package/dist/cli/pack/guards.js +57 -0
  52. package/dist/cli/pack/index.js +66 -0
  53. package/dist/cli/pack/manifest.js +15 -0
  54. package/dist/cli/pack/zip.js +86 -0
  55. package/dist/cli.js +80 -14
  56. package/dist/dev-client/assets/index-DfEfrp6P.css +1 -0
  57. package/dist/dev-client/assets/index-bpMP0b0W.js +50 -0
  58. package/dist/dev-client/index.html +13 -0
  59. package/dist/eslint/is-client-file.d.ts +4 -0
  60. package/dist/index.d.ts +1 -0
  61. package/dist/islands/detect.d.ts +16 -0
  62. package/dist/islands/detect.js +22 -0
  63. package/dist/islands/discover.d.ts +5 -0
  64. package/dist/islands/discover.js +19 -0
  65. package/dist/islands/esbuild-plugin.d.ts +13 -0
  66. package/dist/islands/esbuild-plugin.js +39 -0
  67. package/dist/islands/wrap.d.ts +12 -0
  68. package/dist/islands/wrap.js +48 -0
  69. package/dist/package.js +1 -1
  70. package/dist/ssr.d.ts +31 -0
  71. package/dist/ssr.js +46 -0
  72. package/docs/INDEX.md +9 -1
  73. package/docs/assets.md +128 -0
  74. package/docs/check.md +6 -3
  75. package/docs/dev.md +187 -0
  76. package/docs/islands.md +5 -2
  77. package/docs/llms.txt +41 -3
  78. package/docs/new.md +27 -0
  79. package/docs/overview.md +57 -0
  80. package/docs/pack.md +46 -0
  81. package/docs/primitives.md +5 -6
  82. package/docs/quickstart.md +102 -0
  83. package/docs/recipes/INDEX.md +27 -0
  84. package/docs/recipes/bind-property-fact.md +58 -0
  85. package/docs/recipes/collection-slot.md +100 -0
  86. package/docs/recipes/fill-spec-decision.md +66 -0
  87. package/docs/recipes/fixture-states.md +68 -0
  88. package/docs/recipes/image-slot-crop.md +98 -0
  89. package/docs/recipes/interactive-island.md +100 -0
  90. package/docs/recipes/organize-section-folder.md +72 -0
  91. package/docs/recipes/prepare-submission.md +55 -0
  92. package/docs/recipes/second-template.md +49 -0
  93. package/docs/recipes/select-slot.md +59 -0
  94. package/docs/recipes/static-asset.md +103 -0
  95. package/docs/recipes/third-party-package.md +90 -0
  96. package/docs/rules/INDEX.md +4 -2
  97. package/docs/rules/bundle-binary-asset.md +33 -52
  98. package/docs/rules/bundle-incomplete.md +2 -2
  99. package/docs/rules/css-reason.md +3 -3
  100. package/docs/rules/no-bare-css-import.md +8 -8
  101. package/docs/rules/no-css-import-from-render-path.md +7 -8
  102. package/docs/rules/size-assets.md +88 -0
  103. package/docs/rules/size-island-bundle.md +123 -0
  104. package/docs/rules/size-section-css.md +19 -14
  105. package/docs/schema-system.md +28 -28
  106. package/docs/theme-and-css.md +2 -2
  107. package/docs/vocabulary.md +98 -0
  108. package/package.json +12 -3
  109. package/tsconfig.json +1 -1
@@ -0,0 +1,13 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>template-kit dev</title>
7
+ <script type="module" crossorigin src="/dev-client/assets/index-bpMP0b0W.js"></script>
8
+ <link rel="stylesheet" crossorigin href="/dev-client/assets/index-DfEfrp6P.css">
9
+ </head>
10
+ <body>
11
+ <div id="root"></div>
12
+ </body>
13
+ </html>
@@ -0,0 +1,4 @@
1
+ //#region src/eslint/is-client-file.d.ts
2
+ declare function hasClientDirective(text: string): boolean;
3
+ //#endregion
4
+ export { hasClientDirective };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { ResponsiveImage, ResponsiveSource } from "./contracts/image-descriptor.js";
2
2
  import { Coordinates, FloorplanAssignment, PropertyFacts, UnitRow } from "./schema/property-facts.js";
3
+ import "./asset-modules.js";
3
4
  import { COLLECTIONS, CONTENT_SLOT_TYPES, CollectionField, CollectionIdKey, CollectionOwnership, ContentSlotType, DECISION_TYPES, DERIVED_INPUT_FIELDS, DecisionType, DerivedInputName, FORMATTER_NAMES, FormatterName, REFERRAL_FIELDS, REFERRAL_IMAGE_FIELDS, REF_SLOT_TYPES, RefSlotType, ReferralField, ReferralImageField, SLOT_TYPES, SOURCE_FIELDS, SlotType, SourceFieldContainer, SourceFieldDescriptor, SourceFieldName, SourceFieldValueType, WritebackTarget, collectionForSlot } from "./contracts/slot-catalog.js";
4
5
  import { AddressParts, TRANSFORMS, TRANSFORM_NAMES, TransformDescriptor, TransformFn, TransformInput, TransformName, TransformUnit } from "./contracts/transforms.js";
5
6
  import { VariantSelectorFn, VariantsMeta, makeResolveVariants } from "./schema/resolve-variants.js";
@@ -0,0 +1,16 @@
1
+ import { hasClientDirective } from "../eslint/is-client-file.js";
2
+ //#region src/islands/detect.d.ts
3
+ /**
4
+ * True iff `file` is a client component — an on-disk source file whose directive
5
+ * prologue is `"use client"`. Returns false for an unreadable or absent path.
6
+ */
7
+ declare function isClientComponentFile(file: string): boolean;
8
+ /**
9
+ * Compose an island key: `<sectionKey>:<componentName>` (e.g.
10
+ * `spw-essential/floorplan:UnitFilter`). The key is what a marker in the DOM
11
+ * carries and what the browser loader resolves the client module by, so the
12
+ * emitter and the loader must derive it identically.
13
+ */
14
+ declare function islandKey(sectionKey: string, componentName: string): string;
15
+ //#endregion
16
+ export { hasClientDirective, isClientComponentFile, islandKey };
@@ -0,0 +1,22 @@
1
+ import { hasClientDirective, isClientFile } from "../eslint/is-client-file.js";
2
+
3
+ //#region src/islands/detect.ts
4
+ /**
5
+ * True iff `file` is a client component — an on-disk source file whose directive
6
+ * prologue is `"use client"`. Returns false for an unreadable or absent path.
7
+ */
8
+ function isClientComponentFile(file) {
9
+ return isClientFile(file);
10
+ }
11
+ /**
12
+ * Compose an island key: `<sectionKey>:<componentName>` (e.g.
13
+ * `spw-essential/floorplan:UnitFilter`). The key is what a marker in the DOM
14
+ * carries and what the browser loader resolves the client module by, so the
15
+ * emitter and the loader must derive it identically.
16
+ */
17
+ function islandKey(sectionKey, componentName) {
18
+ return `${sectionKey}:${componentName}`;
19
+ }
20
+
21
+ //#endregion
22
+ export { hasClientDirective, isClientComponentFile, islandKey };
@@ -0,0 +1,5 @@
1
+ //#region src/islands/discover.d.ts
2
+ /** Every `"use client"` component file anywhere under a section directory (recursive). */
3
+ declare function findSectionIslands(sectionDir: string): Promise<string[]>;
4
+ //#endregion
5
+ export { findSectionIslands };
@@ -0,0 +1,19 @@
1
+ import { isClientComponentFile } from "./detect.js";
2
+ import { extname, join } from "node:path";
3
+ import { readdir } from "node:fs/promises";
4
+
5
+ //#region src/islands/discover.ts
6
+ const COMPONENT_EXTS = /* @__PURE__ */ new Set([".tsx", ".jsx"]);
7
+ /** Every `"use client"` component file anywhere under a section directory (recursive). */
8
+ async function findSectionIslands(sectionDir) {
9
+ const out = [];
10
+ for (const entry of await readdir(sectionDir, { withFileTypes: true })) {
11
+ const full = join(sectionDir, entry.name);
12
+ if (entry.isDirectory()) out.push(...await findSectionIslands(full));
13
+ else if (COMPONENT_EXTS.has(extname(entry.name)) && isClientComponentFile(full)) out.push(full);
14
+ }
15
+ return out;
16
+ }
17
+
18
+ //#endregion
19
+ export { findSectionIslands };
@@ -0,0 +1,13 @@
1
+ import { Plugin } from "esbuild";
2
+ //#region src/islands/esbuild-plugin.d.ts
3
+ /**
4
+ * Wrap every `"use client"` `.tsx`/`.jsx` in the build graph so it renders through
5
+ * `IslandMarker`, keyed `islandKey(sectionKey, <file-basename>)`. `sectionKey` is
6
+ * fixed per bundle (one section per renderer build). Non-client modules and other
7
+ * extensions fall through to esbuild untouched.
8
+ */
9
+ declare function islandTransformEsbuildPlugin(opts: {
10
+ sectionKey: string;
11
+ }): Plugin;
12
+ //#endregion
13
+ export { islandTransformEsbuildPlugin };
@@ -0,0 +1,39 @@
1
+ import { islandKey } from "./detect.js";
2
+ import { wrapClientModuleSource } from "./wrap.js";
3
+ import { basename, extname } from "node:path";
4
+ import { readFile } from "node:fs/promises";
5
+
6
+ //#region src/islands/esbuild-plugin.ts
7
+ const LOADER = {
8
+ ".tsx": "tsx",
9
+ ".jsx": "jsx"
10
+ };
11
+ /**
12
+ * Wrap every `"use client"` `.tsx`/`.jsx` in the build graph so it renders through
13
+ * `IslandMarker`, keyed `islandKey(sectionKey, <file-basename>)`. `sectionKey` is
14
+ * fixed per bundle (one section per renderer build). Non-client modules and other
15
+ * extensions fall through to esbuild untouched.
16
+ */
17
+ function islandTransformEsbuildPlugin(opts) {
18
+ return {
19
+ name: "template-kit:island-transform-esbuild",
20
+ setup(b) {
21
+ b.onLoad({ filter: /\.(tsx|jsx)$/ }, async (args) => {
22
+ const code = await readFile(args.path, "utf8");
23
+ const wrapped = wrapClientModuleSource(code, islandKey(opts.sectionKey, basename(args.path, extname(args.path))));
24
+ const ext = extname(args.path);
25
+ if (wrapped === null) return {
26
+ contents: code,
27
+ loader: LOADER[ext]
28
+ };
29
+ return {
30
+ contents: wrapped,
31
+ loader: LOADER[ext]
32
+ };
33
+ });
34
+ }
35
+ };
36
+ }
37
+
38
+ //#endregion
39
+ export { islandTransformEsbuildPlugin };
@@ -0,0 +1,12 @@
1
+ import MagicString from "magic-string";
2
+ //#region src/islands/wrap.d.ts
3
+ /**
4
+ * Rewrite a client module's source so its default export renders through
5
+ * `IslandMarker` with `islandKey`. Returns null when `code` is not a client
6
+ * component (`hasClientDirective` false) or has no default export — the caller
7
+ * then leaves the module untouched. The rebound original is `__trIslandReal`;
8
+ * it still flows through the consumer's JSX/TS transform unchanged.
9
+ */
10
+ declare function wrapClientModuleSource(code: string, islandKey: string): string | null;
11
+ //#endregion
12
+ export { wrapClientModuleSource };
@@ -0,0 +1,48 @@
1
+ import { hasClientDirective } from "../eslint/is-client-file.js";
2
+ import MagicString from "magic-string";
3
+
4
+ //#region src/islands/wrap.ts
5
+ const DEFAULT_EXPORT = /export\s+default\s+/;
6
+ /**
7
+ * The codemod as a `MagicString`, so a caller (the Vite plugin) can emit a
8
+ * hi-res sourcemap alongside the rewritten code. Returns null on the same terms
9
+ * as `wrapClientModuleSource` — a non-client module or one with no default
10
+ * export. The rebound original is `__trIslandReal`; it still flows through the
11
+ * consumer's JSX/TS transform unchanged.
12
+ */
13
+ function wrapClientModuleMagic(code, islandKey) {
14
+ if (!hasClientDirective(code)) return null;
15
+ if (!DEFAULT_EXPORT.test(code)) return null;
16
+ const s = new MagicString(code);
17
+ s.replace(DEFAULT_EXPORT, "const __trIslandReal = ");
18
+ s.append(wrapperSource(islandKey));
19
+ return s;
20
+ }
21
+ /**
22
+ * Rewrite a client module's source so its default export renders through
23
+ * `IslandMarker` with `islandKey`. Returns null when `code` is not a client
24
+ * component (`hasClientDirective` false) or has no default export — the caller
25
+ * then leaves the module untouched. The rebound original is `__trIslandReal`;
26
+ * it still flows through the consumer's JSX/TS transform unchanged.
27
+ */
28
+ function wrapClientModuleSource(code, islandKey) {
29
+ return wrapClientModuleMagic(code, islandKey)?.toString() ?? null;
30
+ }
31
+ function wrapperSource(islandKey) {
32
+ return `
33
+ ;
34
+ import { createElement as __trCreateElement, useId as __trUseId } from "react";
35
+ import { IslandMarker as __TrIslandMarker } from "@homepages/template-kit";
36
+ export default function __trIslandWrapper(__trProps) {
37
+ const __trIslandId = __trUseId();
38
+ return __trCreateElement(
39
+ __TrIslandMarker,
40
+ { islandKey: ${JSON.stringify(islandKey)}, islandId: __trIslandId, props: __trProps },
41
+ __trCreateElement(__trIslandReal, __trProps),
42
+ );
43
+ }
44
+ `;
45
+ }
46
+
47
+ //#endregion
48
+ export { wrapClientModuleMagic, wrapClientModuleSource };
package/dist/package.js CHANGED
@@ -1,5 +1,5 @@
1
1
  //#region package.json
2
- var version = "0.3.0";
2
+ var version = "0.4.1";
3
3
 
4
4
  //#endregion
5
5
  export { version };
package/dist/ssr.d.ts ADDED
@@ -0,0 +1,31 @@
1
+ import { ResolvedFixture } from "./schema/fixture-schema.js";
2
+ import "./schema/index.js";
3
+ import { hasClientDirective } from "./eslint/is-client-file.js";
4
+ import { isClientComponentFile, islandKey } from "./islands/detect.js";
5
+ import { wrapClientModuleSource } from "./islands/wrap.js";
6
+ import { findSectionIslands } from "./islands/discover.js";
7
+ import { islandTransformEsbuildPlugin } from "./islands/esbuild-plugin.js";
8
+ import { ComponentType } from "react";
9
+ //#region src/ssr/index.d.ts
10
+ type LoadedSection = {
11
+ Renderer: ComponentType<Record<string, unknown>>;
12
+ snapshotFixtures: ResolvedFixture[];
13
+ invariantFixtures: ResolvedFixture[];
14
+ schemaSlots: Record<string, {
15
+ type: string;
16
+ editable_by_user?: boolean;
17
+ required?: boolean;
18
+ }>;
19
+ };
20
+ type SectionModuleLoader = (dir: string, file: string) => Promise<unknown>;
21
+ declare const nativeSectionLoader: SectionModuleLoader;
22
+ /**
23
+ * Load a bundle's Renderer plus its resolved snapshot and invariant fixture
24
+ * sets from the bundle directory `dir`. `load` resolves each of the four
25
+ * contract files; it defaults to native dynamic import.
26
+ */
27
+ declare function loadSectionBundle(dir: string, load?: SectionModuleLoader): Promise<LoadedSection>;
28
+ /** Render a resolved fixture's props to static markup — the byte-for-byte page render. */
29
+ declare function renderSectionHtml(renderer: ComponentType<Record<string, unknown>>, props: Record<string, unknown>): string;
30
+ //#endregion
31
+ export { LoadedSection, SectionModuleLoader, findSectionIslands, hasClientDirective, isClientComponentFile, islandKey, islandTransformEsbuildPlugin, loadSectionBundle, nativeSectionLoader, renderSectionHtml, wrapClientModuleSource };
package/dist/ssr.js ADDED
@@ -0,0 +1,46 @@
1
+ import { FixtureModuleShape, buildFixtureSet, buildInvariantFixtures } from "./schema/fixture-schema.js";
2
+ import { hasClientDirective } from "./eslint/is-client-file.js";
3
+ import { isClientComponentFile, islandKey } from "./islands/detect.js";
4
+ import { findSectionIslands } from "./islands/discover.js";
5
+ import { wrapClientModuleSource } from "./islands/wrap.js";
6
+ import { islandTransformEsbuildPlugin } from "./islands/esbuild-plugin.js";
7
+ import { createElement } from "react";
8
+ import { basename, join } from "node:path";
9
+ import { pathToFileURL } from "node:url";
10
+ import { renderToStaticMarkup } from "react-dom/server";
11
+
12
+ //#region src/ssr/index.ts
13
+ const nativeSectionLoader = (dir, file) => import(pathToFileURL(join(dir, file)).href);
14
+ /**
15
+ * Load a bundle's Renderer plus its resolved snapshot and invariant fixture
16
+ * sets from the bundle directory `dir`. `load` resolves each of the four
17
+ * contract files; it defaults to native dynamic import.
18
+ */
19
+ async function loadSectionBundle(dir, load = nativeSectionLoader) {
20
+ const rendererMod = await load(dir, "Renderer.tsx");
21
+ const fixturesMod = await load(dir, "fixtures.ts");
22
+ const schemaMod = await load(dir, "schema.ts");
23
+ const parsed = FixtureModuleShape.parse(fixturesMod.default);
24
+ const selfAnchor = schemaMod.schema.meta?.anchor ?? basename(dir);
25
+ return {
26
+ 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
38
+ };
39
+ }
40
+ /** Render a resolved fixture's props to static markup — the byte-for-byte page render. */
41
+ function renderSectionHtml(renderer, props) {
42
+ return renderToStaticMarkup(createElement(renderer, props));
43
+ }
44
+
45
+ //#endregion
46
+ export { findSectionIslands, hasClientDirective, isClientComponentFile, islandKey, islandTransformEsbuildPlugin, loadSectionBundle, nativeSectionLoader, renderSectionHtml, wrapClientModuleSource };
package/docs/INDEX.md CHANGED
@@ -2,7 +2,7 @@
2
2
  purpose: Router for the docs corpus that ships inside the @homepages/template-kit npm package.
3
3
  status: living
4
4
  related: []
5
- updated: 2026-07-15
5
+ updated: 2026-07-16
6
6
  ---
7
7
  # template-kit docs — router
8
8
 
@@ -13,11 +13,19 @@ detail.
13
13
 
14
14
  | Doc | What it covers | Read when you're touching... |
15
15
  |---|---|---|
16
+ | `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 |
17
+ | `overview.md` | The one-page mental model: workspace → templates → sections, the four reserved files, and the server-vs-client mapping | orienting yourself before your first section |
16
18
  | `llms.txt` | Machine-readable index of this corpus for an agent working inside an installed kit | orienting an agent in a consuming workspace |
17
19
  | `schema-system.md` | The section authoring contract: `schema.ts` / `fill-spec.ts` / `fixtures.ts` / `Renderer.tsx`, `SectionProps` inference, the closed source/transform vocabulary, and the golden property fixtures | authoring or changing any section |
20
+ | `vocabulary.md` | The closed authoring vocabulary — the 9 slot types, the `direct()` source fields, and the `derived()` transforms, with selection semantics | choosing which slot type / source / transform to use |
18
21
  | `primitives.md` | The five contract primitives (`Section`, `Slot`, `SlotItem`, `SlotGroup`, `Image`) and the marker attributes they emit | writing a section's markup |
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 |
19
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 |
20
24
  | `islands.md` | Interactivity: `"use client"` components, the serializable-props rule, editor options, the loader | anything interactive in a section |
21
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 |
22
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
+ | `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
+ | `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 |
29
+ | `pack.md` | The `template-kit pack` CLI command: builds a template's submission zip — template source, the workspace lockfile, and a stamped `pack-manifest.json`; refuses on a failing `check`, a missing lockfile, or a local-protocol dependency | packaging a template for submission |
23
30
  | `rules/` | One page per rule-id (`template-kit/*`), across both venues — the id printed by a lint or `check` failure resolves here. `rules/INDEX.md` routes them; `rules/server-vs-client.md` is the concept the server-rendering rules assume | any lint or `check` failure |
31
+ | `recipes/` | Task recipes: how do I add a slot, bind a fact, add an island, package a template… Each is a minimal delta from the starter's `hero` section, ending in the author loop. `recipes/INDEX.md` routes them by task | doing something specific and not sure which file to touch |
package/docs/assets.md ADDED
@@ -0,0 +1,128 @@
1
+ ---
2
+ purpose: Authoring static assets (svg, images, fonts) at the template level or in a section folder, and how they differ from content images.
3
+ status: living
4
+ related: [INDEX.md, primitives.md, theme-and-css.md, rules/size-assets.md, rules/bundle-binary-asset.md, rules/no-raw-element.md]
5
+ updated: 2026-07-16
6
+ ---
7
+ # Static assets
8
+
9
+ A **static asset** is a file that is part of the template itself — a logo, a
10
+ background texture, an icon, a brand webfont. It is authored once, is the
11
+ **same for every deliverable** rendered from the template, and is referenced
12
+ either from your CSS or from a component.
13
+
14
+ ## Where an asset lives
15
+
16
+ An asset lives in one of two places:
17
+
18
+ - **Template level** — e.g. `templates/<template>/assets/` — shared across every
19
+ section in the template. Reference it from a section's CSS with a `../../assets/…`
20
+ path (below).
21
+ - **Section folder** — e.g. `sections/<section>/assets/` — local to that one
22
+ section. A section-folder asset must be *referenced*: imported in `Renderer.tsx`
23
+ or `url()`-referenced from one of the section's CSS files. An unreferenced binary
24
+ in a section folder is flagged by [`bundle-binary-asset`](rules/bundle-binary-asset.md) —
25
+ it has no build meaning and can never be served.
26
+
27
+ Keep a logo, texture, or font that several sections share in the template-level
28
+ folder; keep one that only a single section uses alongside that section.
29
+
30
+ ## Supported files
31
+
32
+ ```
33
+ .svg .png .jpg .jpeg .gif .webp .avif .ico images
34
+ .woff .woff2 .ttf .otf .eot fonts
35
+ .mp4 .webm .mov .mp3 .wav audio / video
36
+ .pdf .zip documents
37
+ ```
38
+
39
+ ## Referencing an asset — from CSS with `url()`
40
+
41
+ Reference a template-level asset from a section's `styles.css` with `url()` —
42
+ a `@font-face` `src` or a `background-image` resolves to the asset's built URL,
43
+ identical in the `template-kit dev` preview, in the editor, and on the published
44
+ page. Hand-CSS opens with a `css-reason:` comment (see
45
+ [`css-reason`](rules/css-reason.md)):
46
+
47
+ ```css
48
+ /* css-reason: brand webfont + crest watermark, both template assets */
49
+ @font-face {
50
+ font-family: "Brand Serif";
51
+ src: url("../../assets/fonts/brand.woff2") format("woff2");
52
+ font-display: swap;
53
+ }
54
+
55
+ .hero-crest {
56
+ background-image: url("../../assets/crest.svg");
57
+ }
58
+ ```
59
+
60
+ The `../../assets/…` path climbs out of the section folder to the template-level
61
+ `assets/` directory. A font you declare here is available to the whole section.
62
+ If the family you want is one the template's theme already loads, reference that
63
+ instead — see [Theme and CSS](theme-and-css.md); a font the page already has
64
+ costs nothing to reuse.
65
+
66
+ A **section-folder** asset is referenced the same way, with a section-relative
67
+ path instead — no climbing out of the folder:
68
+
69
+ ```css
70
+ /* css-reason: hero watermark, local to this section */
71
+ .hero-crest {
72
+ background-image: url("./assets/crest.svg");
73
+ }
74
+ ```
75
+
76
+ ## Referencing an asset — from a component with `import`
77
+
78
+ An asset you render as an element — a logo, a crest — can be imported in the component and
79
+ handed to the `Image` primitive, whose `src` accepts a URL string:
80
+
81
+ ```tsx
82
+ import { Image } from "@homepages/template-kit";
83
+ import crest from "./assets/crest.svg";
84
+
85
+ export default function Renderer() {
86
+ return <Image src={crest} alt="" />;
87
+ }
88
+ ```
89
+
90
+ The import resolves to the asset's built `/section-assets/…` URL — the same URL in the
91
+ `dev` preview, the editor, and the published page. The kit ships an ambient TypeScript
92
+ declaration that types every supported asset import as that URL string, so the import
93
+ typechecks with no per-workspace setup. Use `alt=""` for a purely decorative
94
+ asset. (Never a raw `<img>` — `Image` carries the responsive/fallback contract; a raw
95
+ `<img>` is rejected by [`no-raw-element`](rules/no-raw-element.md).)
96
+
97
+ ## Which to use
98
+
99
+ - CSS decoration (backgrounds, `@font-face`, masks) → `url()`.
100
+ - An asset rendered as an element → `import` + `<Image>`.
101
+ - A tiny glyph or icon → inline `<svg>` markup — no file at all.
102
+
103
+ ## Assets vs. content images
104
+
105
+ These are two different things; keep them apart.
106
+
107
+ - A **static asset** is authored template decoration — a logo, a texture, a brand
108
+ font. It ships inside the template, is the same on every deliverable, and you
109
+ reference it from your CSS with `url()` or by importing it into a component (above).
110
+ - A **content image** is the property's own content — listing photos, floor
111
+ plans, a headshot. It differs on every deliverable and flows through the
112
+ section's slots via the [`Image`](primitives.md) descriptor, never as a file in
113
+ your template. Declare an image slot in your schema and render it with the
114
+ `Image` primitive; the platform supplies the actual photo per deliverable.
115
+
116
+ Rule of thumb: if the picture is the same for every home the template ever
117
+ renders, it's an asset — place it at the template level or in the section folder
118
+ and reference it from CSS or a component `import`. If it changes with the
119
+ property, it's content — give it a slot. Never hard-code a listing photo as an
120
+ asset.
121
+
122
+ ## Keep them lean
123
+
124
+ Every asset a section pulls in counts toward its asset budget — see
125
+ [`size-assets`](rules/size-assets.md). Fonts and images are already compressed, so
126
+ the budget is raw bytes: subset a webfont to the weights and characters you use,
127
+ export images at the size they render, and drop anything a section doesn't
128
+ actually reference.
package/docs/check.md CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
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
- related: [INDEX.md, rules/INDEX.md, eslint.md, theme-and-css.md]
4
+ related: [INDEX.md, rules/INDEX.md, eslint.md, theme-and-css.md, dev.md]
5
5
  updated: 2026-07-15
6
6
  ---
7
7
  # template-kit check
@@ -62,8 +62,9 @@ one class of fix at a time:
62
62
  stray `[object Object]`, a bare `null`/`undefined`/`NaN` leaf, a `src`-less `<img>` —
63
63
  plus, for the `typical` fixture, that the editor's sidebar order matches the page's
64
64
  real render order.
65
- 6. **size** — every section's renderer bundle and compiled stylesheet, gzipped, against a
66
- fixed byte budget.
65
+ 6. **size** — every section's renderer bundle, compiled stylesheet, and largest island
66
+ (browser) bundle, gzipped, against a fixed byte budget; and its static assets
67
+ (svg/images/fonts), budgeted by raw bytes since they are already compressed.
67
68
 
68
69
  **The dependency gates** run once per invocation, never once per template — they are
69
70
  properties of the workspace's one lockfile and installed tree, not of any one template:
@@ -119,3 +120,5 @@ submission.
119
120
  each, Rule / Reason / Fix.
120
121
  - [ESLint preset](eslint.md) — the one-file-at-a-time half of the same rule set, run
121
122
  directly by your editor, not only through `check`.
123
+ - [Dev](dev.md) — the other workspace command: preview sections in a browser as you
124
+ author, before running `check` as the acceptance gate.