@homepages/template-kit 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +222 -0
- package/README.md +96 -17
- package/dist/asset-modules.d.ts +81 -0
- package/dist/base.css +9 -0
- package/dist/cli/check/config.js +41 -0
- package/dist/cli/check/css.js +131 -0
- package/dist/cli/check/diagnostics.js +32 -0
- package/dist/cli/check/index.js +87 -0
- package/dist/cli/check/loader.js +146 -0
- package/dist/cli/check/paths.js +15 -0
- package/dist/cli/check/relativize.js +18 -0
- package/dist/cli/check/render-invariants.js +24 -0
- package/dist/cli/check/resolve-tool.js +38 -0
- package/dist/cli/check/section-assets.js +58 -0
- package/dist/cli/check/stages/deps.js +337 -0
- package/dist/cli/check/stages/lint.js +46 -0
- package/dist/cli/check/stages/render.js +101 -0
- package/dist/cli/check/stages/size.js +274 -0
- package/dist/cli/check/stages/tree.js +206 -0
- package/dist/cli/check/stages/typecheck.js +46 -0
- package/dist/cli/check/stages/validate/catalog-exhaustiveness.js +18 -0
- package/dist/cli/check/stages/validate/facts.js +18 -0
- package/dist/cli/check/stages/validate/formatter-contract.js +14 -0
- package/dist/cli/check/stages/validate/group-contract.js +21 -0
- package/dist/cli/check/stages/validate/index.js +66 -0
- package/dist/cli/check/stages/validate/list-scalar-contract.js +22 -0
- package/dist/cli/check/stages/validate/nav-contract.js +93 -0
- package/dist/cli/check/stages/validate/nested-slot-contract.js +12 -0
- package/dist/cli/check/stages/validate/object-list-contract.js +31 -0
- package/dist/cli/check/stages/validate/orchestrator.js +241 -0
- package/dist/cli/check/stages/validate/produced-by-contract.js +18 -0
- package/dist/cli/check/stages/validate/row-contract.js +31 -0
- package/dist/cli/check/stages/validate/select-contract.js +13 -0
- package/dist/cli/check/stages/validate/source-contract.js +21 -0
- package/dist/cli/check/stages/validate/variant-contract.js +15 -0
- package/dist/cli/check/workspace.js +86 -0
- package/dist/cli/dev/build-css.js +91 -0
- package/dist/cli/dev/catalog.js +35 -0
- package/dist/cli/dev/compose-template.js +38 -0
- package/dist/cli/dev/content-override.js +58 -0
- package/dist/cli/dev/discover.js +35 -0
- package/dist/cli/dev/fill-state.js +75 -0
- package/dist/cli/dev/index.js +32 -0
- package/dist/cli/dev/inspect.js +38 -0
- package/dist/cli/dev/island-bootstrap.js +38 -0
- package/dist/cli/dev/island-map.js +35 -0
- package/dist/cli/dev/island-transform.js +31 -0
- package/dist/cli/dev/manifest-instances.js +31 -0
- package/dist/cli/dev/render-section.js +24 -0
- package/dist/cli/dev/screenshot-target.js +24 -0
- package/dist/cli/dev/section-page.js +54 -0
- package/dist/cli/dev/server.js +480 -0
- package/dist/cli/dev/slot-schema.js +12 -0
- package/dist/cli/dev/structure-summary.js +118 -0
- package/dist/cli/dev/tailwind.js +32 -0
- package/dist/cli/dev/vite-server.js +33 -0
- package/dist/cli/dev/workspace.js +63 -0
- package/dist/cli/link/index.js +74 -0
- package/dist/cli/link/overlay.js +59 -0
- package/dist/cli/link/watch.js +25 -0
- package/dist/cli/new/emit.js +48 -0
- package/dist/cli/new/index.js +69 -0
- package/dist/cli/new/scaffold/section/_Renderer.tsx +20 -0
- package/dist/cli/new/scaffold/section/_fill-spec.ts +18 -0
- package/dist/cli/new/scaffold/section/_fixtures.ts +13 -0
- package/dist/cli/new/scaffold/section/_schema.ts +24 -0
- package/dist/cli/new/scaffold/template/_manifest.json +9 -0
- package/dist/cli/new/scaffold/template/sections/hero/ExpandableText.tsx +45 -0
- package/dist/cli/new/scaffold/template/sections/hero/Renderer.tsx +40 -0
- package/dist/cli/new/scaffold/template/sections/hero/components/Feature.tsx +14 -0
- package/dist/cli/new/scaffold/template/sections/hero/fill-spec.ts +29 -0
- package/dist/cli/new/scaffold/template/sections/hero/fixtures.ts +41 -0
- package/dist/cli/new/scaffold/template/sections/hero/schema.ts +60 -0
- package/dist/cli/new/scaffold/template/theme.css +24 -0
- package/dist/cli/new/scaffold/template/theme.ts +27 -0
- package/dist/cli/new/scaffold-assets.js +20 -0
- package/dist/cli/pack/collect.js +44 -0
- package/dist/cli/pack/guards.js +57 -0
- package/dist/cli/pack/index.js +66 -0
- package/dist/cli/pack/manifest.js +15 -0
- package/dist/cli/pack/zip.js +86 -0
- package/dist/cli/theme/generate.js +43 -0
- package/dist/cli/theme/index.js +33 -0
- package/dist/cli/theme/load-theme.js +69 -0
- package/dist/cli.js +100 -16
- package/dist/contracts/fill-treatments.js +47 -0
- package/dist/design-system/theme.d.ts +30 -300
- package/dist/design-system/theme.js +112 -90
- package/dist/dev-client/assets/index-DfEfrp6P.css +1 -0
- package/dist/dev-client/assets/index-bpMP0b0W.js +50 -0
- package/dist/dev-client/index.html +13 -0
- package/dist/eslint/is-client-file.d.ts +4 -0
- package/dist/eslint/rules/no-hex.js +78 -6
- package/dist/eslint.js +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -2
- package/dist/islands/detect.d.ts +16 -0
- package/dist/islands/detect.js +22 -0
- package/dist/islands/discover.d.ts +5 -0
- package/dist/islands/discover.js +19 -0
- package/dist/islands/esbuild-plugin.d.ts +13 -0
- package/dist/islands/esbuild-plugin.js +39 -0
- package/dist/islands/wrap.d.ts +11 -0
- package/dist/islands/wrap.js +48 -0
- package/dist/node_modules/@jridgewell/sourcemap-codec/dist/sourcemap-codec.js +77 -0
- package/dist/node_modules/magic-string/dist/magic-string.es.js +939 -0
- package/dist/package.js +1 -1
- package/dist/primitives/Image.js +1 -1
- package/dist/schema/fill-spec.d.ts +80 -590
- package/dist/schema/fixture-schema.d.ts +5 -81
- package/dist/schema/manifest.d.ts +39 -475
- package/dist/schema/resolve-section-ref.js +10 -0
- package/dist/schema/rows.js +10 -0
- package/dist/schema/section-nav.js +9 -0
- package/dist/schema/section-schema.d.ts +51 -437
- package/dist/schema/slot-types.d.ts +12 -16
- package/dist/ssr.d.ts +31 -0
- package/dist/ssr.js +46 -0
- package/dist/styles.css +31 -88
- package/docs/INDEX.md +12 -3
- package/docs/assets.md +128 -0
- package/docs/check.md +124 -0
- package/docs/dev.md +187 -0
- package/docs/eslint.md +18 -8
- package/docs/islands.md +5 -2
- package/docs/llms.txt +59 -9
- package/docs/new.md +27 -0
- package/docs/overview.md +57 -0
- package/docs/pack.md +46 -0
- package/docs/primitives.md +10 -6
- package/docs/quickstart.md +102 -0
- package/docs/recipes/INDEX.md +27 -0
- package/docs/recipes/bind-property-fact.md +58 -0
- package/docs/recipes/collection-slot.md +100 -0
- package/docs/recipes/fill-spec-decision.md +66 -0
- package/docs/recipes/fixture-states.md +68 -0
- package/docs/recipes/image-slot-crop.md +98 -0
- package/docs/recipes/interactive-island.md +100 -0
- package/docs/recipes/organize-section-folder.md +72 -0
- package/docs/recipes/prepare-submission.md +55 -0
- package/docs/recipes/second-template.md +49 -0
- package/docs/recipes/select-slot.md +59 -0
- package/docs/recipes/static-asset.md +103 -0
- package/docs/recipes/third-party-package.md +90 -0
- package/docs/rules/INDEX.md +14 -5
- package/docs/rules/bundle-binary-asset.md +83 -0
- package/docs/rules/bundle-incomplete.md +2 -2
- package/docs/rules/css-reason.md +3 -3
- package/docs/rules/fixtures-invalid.md +96 -0
- package/docs/rules/license-denied.md +11 -3
- package/docs/rules/manifest-invalid.md +99 -0
- package/docs/rules/no-bare-css-import.md +8 -8
- package/docs/rules/no-css-import-from-render-path.md +7 -8
- package/docs/rules/no-hex.md +76 -10
- package/docs/rules/no-templates.md +87 -0
- package/docs/rules/parse-error.md +76 -0
- package/docs/rules/schema-invalid.md +96 -0
- package/docs/rules/size-assets.md +88 -0
- package/docs/rules/size-island-bundle.md +123 -0
- package/docs/rules/size-section-css.md +19 -14
- package/docs/schema-system.md +28 -28
- package/docs/theme-and-css.md +157 -92
- package/docs/vocabulary.md +98 -0
- package/package.json +14 -11
- package/tsconfig.json +1 -1
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
---
|
|
2
|
+
purpose: Add another template to the workspace.
|
|
3
|
+
status: living
|
|
4
|
+
related: [../new.md, ../check.md]
|
|
5
|
+
updated: 2026-07-15
|
|
6
|
+
---
|
|
7
|
+
# Add a second template
|
|
8
|
+
|
|
9
|
+
## Goal
|
|
10
|
+
|
|
11
|
+
Add another template to the workspace.
|
|
12
|
+
|
|
13
|
+
## The delta
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
template-kit new <name>
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`new` creates `templates/<name>/` as a working starter template — the same
|
|
20
|
+
starter shape a fresh workspace ships (a text slot, an image slot, a
|
|
21
|
+
fact-bound slot, an option, fixtures, a fill spec, a `components/` folder, a
|
|
22
|
+
`"use client"` island, `theme.ts`/`theme.css`, `manifest.json`) — and it
|
|
23
|
+
already passes `check` as-is. Edit its sections from there.
|
|
24
|
+
|
|
25
|
+
A template is a plain folder under `templates/`. Adding one does not
|
|
26
|
+
duplicate anything: every template in the workspace shares the same kit
|
|
27
|
+
version, the same root `package.json`/lockfile, and the same
|
|
28
|
+
`tsconfig.json`/ESLint config. There is nothing per-template to configure
|
|
29
|
+
beyond the folder itself.
|
|
30
|
+
|
|
31
|
+
`new` refuses to overwrite an existing `templates/<name>/` folder.
|
|
32
|
+
|
|
33
|
+
## Commands
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
template-kit new <name>
|
|
37
|
+
template-kit check <name>
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Verify
|
|
41
|
+
|
|
42
|
+
Verify with [the author loop](../quickstart.md#the-author-loop) — a freshly
|
|
43
|
+
created template is check-clean before you change a line.
|
|
44
|
+
|
|
45
|
+
## See also
|
|
46
|
+
|
|
47
|
+
- [Adding templates and sections](../new.md) — the full command, including
|
|
48
|
+
`new-section` for adding a section within a template.
|
|
49
|
+
- [Check](../check.md) — the gate every template (new or edited) must pass.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
---
|
|
2
|
+
purpose: Give the user a closed set of choices — an editable content slot, distinct from a behavioral enum option.
|
|
3
|
+
status: living
|
|
4
|
+
related: [../vocabulary.md, ../schema-system.md]
|
|
5
|
+
updated: 2026-07-15
|
|
6
|
+
---
|
|
7
|
+
# A select slot — a closed set of choices
|
|
8
|
+
|
|
9
|
+
## Goal
|
|
10
|
+
|
|
11
|
+
Give the user a closed set of choices — an editable content slot, distinct from
|
|
12
|
+
a behavioral `enum` option.
|
|
13
|
+
|
|
14
|
+
## The delta
|
|
15
|
+
|
|
16
|
+
A `select` slot still needs a `source` or a `produced_by`, exactly like every
|
|
17
|
+
other slot type — `values` alone only declares the closed set the editor's
|
|
18
|
+
dropdown offers. The cleanest no-AI fill for a closed set is usually a
|
|
19
|
+
`direct()` fact that already resolves to one of a closed set of strings:
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
// schema.ts — add a select slot, seeded from a closed-vocabulary fact
|
|
23
|
+
property_type: {
|
|
24
|
+
type: "select",
|
|
25
|
+
editable_by_user: true,
|
|
26
|
+
required: false,
|
|
27
|
+
values: ["single_family", "condo_townhome", "multi_family", "investment"],
|
|
28
|
+
source: direct("property_type"),
|
|
29
|
+
display_name: "Property type",
|
|
30
|
+
},
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
// Renderer.tsx — read it like any other text-shaped slot
|
|
35
|
+
<Slot id="property_type" textLeaf>
|
|
36
|
+
<span>{slots.property_type}</span>
|
|
37
|
+
</Slot>
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`slots.property_type` is `string | null` (required is `false`). The general
|
|
41
|
+
`source`/`produced_by` contract still applies to a `select` slot, but no
|
|
42
|
+
fill-spec decision type constrains its output to the closed `values` set —
|
|
43
|
+
so `source: direct(...)` (a fact that already resolves within the set) is
|
|
44
|
+
the reliable way to fill one.
|
|
45
|
+
|
|
46
|
+
## Commands
|
|
47
|
+
|
|
48
|
+
None — this is a pure content edit. No CLI command beyond the author loop below.
|
|
49
|
+
|
|
50
|
+
## Verify
|
|
51
|
+
|
|
52
|
+
Verify with [the author loop](../quickstart.md#the-author-loop).
|
|
53
|
+
|
|
54
|
+
## See also
|
|
55
|
+
|
|
56
|
+
- [The closed vocabulary](../vocabulary.md#slot-types) — the slot-type table, including
|
|
57
|
+
where `select` sits next to `text`/`number`/etc.
|
|
58
|
+
- [The schema system](../schema-system.md) — the full `schema.ts` contract, including
|
|
59
|
+
`direct()`/`derived()` binding.
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
---
|
|
2
|
+
purpose: Add a graphic — an icon, a divider, a photo, a font — to a section.
|
|
3
|
+
status: living
|
|
4
|
+
related: [../assets.md, ../check.md, ../rules/bundle-binary-asset.md, image-slot-crop.md, ../theme-and-css.md]
|
|
5
|
+
updated: 2026-07-16
|
|
6
|
+
---
|
|
7
|
+
# Add a static asset (image, icon, font)
|
|
8
|
+
|
|
9
|
+
## Goal
|
|
10
|
+
|
|
11
|
+
Add a graphic — an icon, a divider mark, a photo, a custom font — to a section.
|
|
12
|
+
|
|
13
|
+
## The delta
|
|
14
|
+
|
|
15
|
+
A binary asset — an image, a font, a video, an archive — may live in a section's own
|
|
16
|
+
`sections/<section>/assets/` folder, but only if the section references it: imported in
|
|
17
|
+
`Renderer.tsx` (`import crest from "./assets/crest.svg"`) or `url()`-referenced from one
|
|
18
|
+
of the section's CSS files. An unreferenced binary in the folder is flagged; see
|
|
19
|
+
[`bundle-binary-asset`](../rules/bundle-binary-asset.md) for the rule and why.
|
|
20
|
+
|
|
21
|
+
A file shared across sections — a logo, a brand webfont, a texture — lives at the
|
|
22
|
+
**template level** instead, referenced the same way with a `../../assets/…` path. See
|
|
23
|
+
[Static assets](../assets.md) for both paths and the full reference contract. What you
|
|
24
|
+
reach for depends on what the graphic is:
|
|
25
|
+
|
|
26
|
+
**A vector graphic — an icon, a divider, a decorative mark used only here.** Inline it as
|
|
27
|
+
SVG markup directly in the JSX that draws it. It is text, ships with the rest of your
|
|
28
|
+
component, and needs no file or import at all:
|
|
29
|
+
|
|
30
|
+
```tsx
|
|
31
|
+
// sections/hero/components/ScrollArrow.tsx
|
|
32
|
+
export function ScrollArrow() {
|
|
33
|
+
return (
|
|
34
|
+
<svg viewBox="0 0 16 16" className="h-4 w-4" fill="none" aria-hidden="true">
|
|
35
|
+
<path d="M8 3v10M3 9l5 4 5-4" stroke="currentColor" strokeWidth="1.5" />
|
|
36
|
+
</svg>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
```tsx
|
|
42
|
+
// sections/hero/Renderer.tsx
|
|
43
|
+
import { ScrollArrow } from "./components/ScrollArrow";
|
|
44
|
+
// …
|
|
45
|
+
<ScrollArrow />
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
That markup's bytes ship inside the section's renderer bundle, so they count
|
|
49
|
+
toward the same byte budget as the rest of your JSX — see the size stage in
|
|
50
|
+
[Check](../check.md).
|
|
51
|
+
|
|
52
|
+
**A raster image or watermark used only in this section.** Add it under
|
|
53
|
+
`sections/<section>/assets/` and import it into the component:
|
|
54
|
+
|
|
55
|
+
```tsx
|
|
56
|
+
// sections/hero/Renderer.tsx
|
|
57
|
+
import { Image } from "@homepages/template-kit";
|
|
58
|
+
import crest from "./assets/crest.svg";
|
|
59
|
+
|
|
60
|
+
export default function Renderer() {
|
|
61
|
+
return <Image src={crest} alt="" />;
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Or reference it from the section's CSS with `url()` instead of importing it — see
|
|
66
|
+
[Static assets](../assets.md) for both forms. Either way the file must be referenced:
|
|
67
|
+
an asset nothing imports or `url()`-references is flagged by
|
|
68
|
+
[`bundle-binary-asset`](../rules/bundle-binary-asset.md).
|
|
69
|
+
|
|
70
|
+
**A photo.** Not a section asset at all — a section never ships a photo file. A
|
|
71
|
+
photo arrives at runtime through an `image` slot: declare it in `schema.ts`,
|
|
72
|
+
produce it with an `image-assign` decision in `fill-spec.ts`, and render it
|
|
73
|
+
through the `<Image>` primitive, which reads `url`, `alt`, and `responsive` off
|
|
74
|
+
the slot's resolved value. See
|
|
75
|
+
[Image slot with a locked crop](image-slot-crop.md) for the full shape.
|
|
76
|
+
|
|
77
|
+
**A custom font shared across the template.** Declared once, for the whole
|
|
78
|
+
template, in the theme's `fonts` and optional `fontFaces` block. See
|
|
79
|
+
[Theme and CSS](../theme-and-css.md). A font only one section uses can instead
|
|
80
|
+
live in that section's `assets/` folder and be declared with a `@font-face` in
|
|
81
|
+
the section's own CSS — see [Static assets](../assets.md).
|
|
82
|
+
|
|
83
|
+
## Commands
|
|
84
|
+
|
|
85
|
+
None — inlining SVG markup and declaring a slot are content edits. No CLI
|
|
86
|
+
command beyond the author loop below.
|
|
87
|
+
|
|
88
|
+
## Verify
|
|
89
|
+
|
|
90
|
+
Verify with [the author loop](../quickstart.md#the-author-loop).
|
|
91
|
+
|
|
92
|
+
## See also
|
|
93
|
+
|
|
94
|
+
- [Static assets](../assets.md) — the full reference/import contract, template
|
|
95
|
+
level vs. section folder.
|
|
96
|
+
- [`bundle-binary-asset`](../rules/bundle-binary-asset.md) — the rule that
|
|
97
|
+
flags an unreferenced binary in a section folder, and the SVG-inlining fix.
|
|
98
|
+
- [Check](../check.md) — the size stage a large inlined SVG or a bloated
|
|
99
|
+
renderer bundle counts against.
|
|
100
|
+
- [Image slot with a locked crop](image-slot-crop.md) — the real channel for a
|
|
101
|
+
photo: an `image` slot, an `image-assign` decision, and the `<Image>`
|
|
102
|
+
primitive.
|
|
103
|
+
- [Theme and CSS](../theme-and-css.md) — where a template's fonts are declared.
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
---
|
|
2
|
+
purpose: Use a third-party npm package in a section.
|
|
3
|
+
status: living
|
|
4
|
+
related: [../islands.md, ../pack.md, ../rules/single-react.md, ../rules/no-bare-css-import.md, ../rules/no-css-import-from-render-path.md]
|
|
5
|
+
updated: 2026-07-15
|
|
6
|
+
---
|
|
7
|
+
# Use a third-party npm package in a section
|
|
8
|
+
|
|
9
|
+
## Goal
|
|
10
|
+
|
|
11
|
+
Use an npm package in a section.
|
|
12
|
+
|
|
13
|
+
## The delta
|
|
14
|
+
|
|
15
|
+
Install the package like any npm dependency:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm i swiper
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
**Only a published version.** `pack` refuses to produce a submission zip when a
|
|
22
|
+
dependency uses a local `file:`/`link:` protocol — depend on a real published
|
|
23
|
+
version, never a path on your own machine.
|
|
24
|
+
|
|
25
|
+
**Where you import it from matters.** A client library — a carousel, a map SDK,
|
|
26
|
+
anything that touches `window` or the DOM — is imported inside a `"use client"`
|
|
27
|
+
island, the same as any other browser-only code:
|
|
28
|
+
|
|
29
|
+
```tsx
|
|
30
|
+
// sections/gallery/Carousel.tsx
|
|
31
|
+
"use client";
|
|
32
|
+
|
|
33
|
+
import Swiper from "swiper";
|
|
34
|
+
|
|
35
|
+
export default function Carousel({ slides }: { slides: { id: string; url: string }[] }) {
|
|
36
|
+
// …
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
A server-side import — one that lands in the Renderer or a plain component it
|
|
41
|
+
imports, not behind `"use client"` — must stay pure and deterministic: no reads
|
|
42
|
+
of the clock, randomness, or the network at render time. A package built for a
|
|
43
|
+
browser runtime (it touches `window`, schedules a timer, generates an id) does
|
|
44
|
+
not belong there; move the usage into an island.
|
|
45
|
+
|
|
46
|
+
**A package that ships its own stylesheet** is imported by its bare specifier,
|
|
47
|
+
from the module that renders it — the Renderer's import graph (the components
|
|
48
|
+
and islands it imports, transitively) is what the CSS build bundles and
|
|
49
|
+
harvests for the section's stylesheet:
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
// sections/gallery/Carousel.tsx
|
|
53
|
+
"use client";
|
|
54
|
+
|
|
55
|
+
import Swiper from "swiper";
|
|
56
|
+
import "swiper/css"; // ← the sanctioned channel: a bare specifier reaches this section's layer
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
A **local** `.css` import from that same render path is a different, banned case
|
|
60
|
+
— see [`no-css-import-from-render-path`](../rules/no-css-import-from-render-path.md)
|
|
61
|
+
for the full distinction between the two, and
|
|
62
|
+
[`no-bare-css-import`](../rules/no-bare-css-import.md) for why a hand-written
|
|
63
|
+
stylesheet can't reach the package's CSS with its own `@import` instead.
|
|
64
|
+
|
|
65
|
+
## Commands
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
npm i <package>
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Verify
|
|
72
|
+
|
|
73
|
+
Verify with [the author loop](../quickstart.md#the-author-loop) — a green
|
|
74
|
+
`check` after the install confirms the package resolves, its CSS (if any)
|
|
75
|
+
landed in the section's layer, and the workspace still carries exactly one copy
|
|
76
|
+
of React.
|
|
77
|
+
|
|
78
|
+
## See also
|
|
79
|
+
|
|
80
|
+
- [Islands](../islands.md) — writing a `"use client"` island; a third-party
|
|
81
|
+
client library is fine inside one, never in a Renderer.
|
|
82
|
+
- [`template-kit pack`](../pack.md) — why a `file:`/`link:` dependency blocks a
|
|
83
|
+
submission.
|
|
84
|
+
- [`single-react`](../rules/single-react.md) — a package that pulls its own
|
|
85
|
+
copy of React breaks hydration; the installed tree may carry only one.
|
|
86
|
+
- [`no-css-import-from-render-path`](../rules/no-css-import-from-render-path.md) —
|
|
87
|
+
bare package specifier (legal) vs. a local `.css` import (banned) from a
|
|
88
|
+
section's render path.
|
|
89
|
+
- [`no-bare-css-import`](../rules/no-bare-css-import.md) — why a hand-written
|
|
90
|
+
stylesheet can't reach a package's CSS with its own `@import` either.
|
package/docs/rules/INDEX.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
purpose: Router for the rule corpus — every template-kit/<id> an author can hit, and the page that explains it.
|
|
3
3
|
status: living
|
|
4
|
-
related: [server-vs-client.md, ../eslint.md, ../INDEX.md]
|
|
5
|
-
updated: 2026-07-
|
|
4
|
+
related: [server-vs-client.md, ../eslint.md, ../check.md, ../INDEX.md]
|
|
5
|
+
updated: 2026-07-15
|
|
6
6
|
---
|
|
7
7
|
# Rules
|
|
8
8
|
|
|
@@ -13,8 +13,8 @@ and resolves to exactly one page here: `docs/rules/<id>.md`.
|
|
|
13
13
|
in a Renderer) and are enforced by the ESLint preset. Others are properties of the
|
|
14
14
|
**tree** (a slot declared in `schema.ts` but never marked in the JSX, a stylesheet, a
|
|
15
15
|
dependency, a byte budget) and cannot be expressed one file at a time; those are enforced
|
|
16
|
-
by `template-kit check
|
|
17
|
-
the id, which names its page either way.
|
|
16
|
+
by [`template-kit check`](../check.md). You never need to know which engine produced a
|
|
17
|
+
violation — only the id, which names its page either way.
|
|
18
18
|
|
|
19
19
|
Read [server vs client](server-vs-client.md) first if you are here for any rule that
|
|
20
20
|
mentions "server-rendered code" or an island. It is the one concept those rules assume,
|
|
@@ -31,9 +31,13 @@ and it is explained there rather than on each of them.
|
|
|
31
31
|
| [`no-raw-element`](no-raw-element.md) | eslint | No raw `<section>` or `<img>` — use `<Section>` and `<Image>`. |
|
|
32
32
|
| [`image-bare-needs-reason`](image-bare-needs-reason.md) | eslint | `<Image bare>` needs a `// bare:` comment saying why it skips the frame. |
|
|
33
33
|
| [`slot-marker-literal`](slot-marker-literal.md) | eslint | A slot-marker key must be a string literal, not an expression. |
|
|
34
|
-
| [`no-hex`](no-hex.md) | eslint | No hard-coded colours. Islands are NOT exempt. |
|
|
34
|
+
| [`no-hex`](no-hex.md) | eslint | No hard-coded colours — the one legal colour function is a `color-mix()` over theme-derived arguments. Islands are NOT exempt. |
|
|
35
35
|
| [`no-css-import-from-render-path`](no-css-import-from-render-path.md) | eslint | No relative/absolute .css import from a section's render path — a package's is the sanctioned channel. |
|
|
36
36
|
| [`bundle-incomplete`](bundle-incomplete.md) | check | A section folder is missing one of its four required files. |
|
|
37
|
+
| [`bundle-binary-asset`](bundle-binary-asset.md) | check | An unreferenced binary asset sits in a section folder. |
|
|
38
|
+
| [`schema-invalid`](schema-invalid.md) | check | A section's schema.ts or fill-spec.ts fails a contract assertion, or cannot be loaded. |
|
|
39
|
+
| [`fixtures-invalid`](fixtures-invalid.md) | check | A section's fixtures.ts does not satisfy the fixture-module shape. |
|
|
40
|
+
| [`manifest-invalid`](manifest-invalid.md) | check | A template's manifest.json is missing, unparseable, or violates the manifest contract. |
|
|
37
41
|
| [`missing-slot-marker`](missing-slot-marker.md) | check | An editable slot in schema.ts has no marker in the section's JSX. |
|
|
38
42
|
| [`slot-group-marker`](slot-group-marker.md) | check | A group with two or more members has no `<SlotGroup>` wrapper. |
|
|
39
43
|
| [`css-reason`](css-reason.md) | check | A hand-written CSS file must open with a `css-reason:` comment. |
|
|
@@ -41,6 +45,7 @@ and it is explained there rather than on each of them.
|
|
|
41
45
|
| [`unlayered-fence`](unlayered-fence.md) | check | An `/* unlayered: */` escape hatch must be balanced and give a reason. |
|
|
42
46
|
| [`sidebar-order`](sidebar-order.md) | check | Schema slot order must match the order the page renders them in. |
|
|
43
47
|
| [`typecheck`](typecheck.md) | check | The template must compile under TypeScript. |
|
|
48
|
+
| [`parse-error`](parse-error.md) | check | A file could not be parsed at all — a syntax error, not a rule violation. |
|
|
44
49
|
| [`determinism-drift`](determinism-drift.md) | check | Rendering the same fixture twice must produce byte-identical HTML. |
|
|
45
50
|
| [`render-invariant`](render-invariant.md) | check | Rendered output must carry no [object Object], bare null/undefined/NaN leaf, or src-less `<img>`. |
|
|
46
51
|
| [`lockfile-missing`](lockfile-missing.md) | check | The workspace has no package-lock.json. |
|
|
@@ -50,9 +55,13 @@ and it is explained there rather than on each of them.
|
|
|
50
55
|
| [`license-denied`](license-denied.md) | check | A production dependency's license is not on the allowlist. |
|
|
51
56
|
| [`size-renderer-bundle`](size-renderer-bundle.md) | check | A section's renderer bundle exceeds its byte budget. |
|
|
52
57
|
| [`size-section-css`](size-section-css.md) | check | A section's compiled CSS exceeds its byte budget. |
|
|
58
|
+
| [`size-island-bundle`](size-island-bundle.md) | check | A section's island (browser) bundle exceeds its byte budget. |
|
|
59
|
+
| [`size-assets`](size-assets.md) | check | A section's static assets exceed their byte budget. |
|
|
60
|
+
| [`no-templates`](no-templates.md) | check | The workspace has no template to check at all. |
|
|
53
61
|
|
|
54
62
|
## See also
|
|
55
63
|
|
|
56
64
|
- [Server vs client](server-vs-client.md) — how a file is scoped, and which rules stop at
|
|
57
65
|
the hydration boundary.
|
|
58
66
|
- [ESLint preset](../eslint.md) — setting the preset up in a template workspace.
|
|
67
|
+
- [Check](../check.md) — running `template-kit check`, its flags, output shape, and exit code.
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
---
|
|
2
|
+
purpose: The template-kit/bundle-binary-asset authoring rule — what it enforces, why, and how to fix it.
|
|
3
|
+
status: living
|
|
4
|
+
related: [INDEX.md, bundle-incomplete.md, ../primitives.md, ../assets.md]
|
|
5
|
+
updated: 2026-07-16
|
|
6
|
+
---
|
|
7
|
+
# template-kit/bundle-binary-asset
|
|
8
|
+
|
|
9
|
+
> A static asset may live in a section folder — but only if something references it. An
|
|
10
|
+
> unreferenced binary is dead weight and can never be served.
|
|
11
|
+
|
|
12
|
+
## Rule
|
|
13
|
+
|
|
14
|
+
A binary asset inside a section folder is allowed **only when the section references it** —
|
|
15
|
+
imported in `Renderer.tsx` (`import crest from "./assets/crest.svg"`) or `url()`-referenced
|
|
16
|
+
from one of the section's CSS files. A binary that nothing references is flagged. The check
|
|
17
|
+
looks two ways to find candidates:
|
|
18
|
+
|
|
19
|
+
- **By extension** — `.svg`, `.png`, `.jpg`/`.jpeg`, `.gif`, `.webp`, `.avif`, `.ico`,
|
|
20
|
+
`.woff`/`.woff2`, `.ttf`, `.otf`, `.eot`, `.mp4`, `.webm`, `.mov`, `.mp3`, `.wav`,
|
|
21
|
+
`.pdf`, `.zip`.
|
|
22
|
+
- **By content** — any other file whose first bytes contain a NUL byte.
|
|
23
|
+
|
|
24
|
+
## Reason
|
|
25
|
+
|
|
26
|
+
A referenced asset is first-class: the build content-hashes it to a stable
|
|
27
|
+
`/section-assets/` URL and the publish pipeline uploads it — identical in the `dev`
|
|
28
|
+
preview, the editor, and the published page. An *unreferenced* binary has no such URL: it
|
|
29
|
+
is never emitted or uploaded, yet it still counts toward the section's content hash (its
|
|
30
|
+
identity). So it is pure dead weight — a file that ships nowhere.
|
|
31
|
+
|
|
32
|
+
## Fix
|
|
33
|
+
|
|
34
|
+
Either reference the asset, or delete it.
|
|
35
|
+
|
|
36
|
+
- **Reference it from a component:** `import crest from "./assets/crest.svg"` and render it
|
|
37
|
+
with the `Image` primitive — `<Image src={crest} alt="…" />`. See [Static assets](../assets.md).
|
|
38
|
+
- **Reference it from CSS:** `background-image: url("./assets/crest.svg")` (or a
|
|
39
|
+
`@font-face` `src`) in the section's `styles.css`.
|
|
40
|
+
- **Delete it** if it is left over and unused.
|
|
41
|
+
|
|
42
|
+
### Before
|
|
43
|
+
|
|
44
|
+
```text
|
|
45
|
+
sections/hero/
|
|
46
|
+
Renderer.tsx
|
|
47
|
+
schema.ts
|
|
48
|
+
fill-spec.ts
|
|
49
|
+
fixtures.ts
|
|
50
|
+
assets/
|
|
51
|
+
crest.svg ← nothing imports or url()-references this file
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### After
|
|
55
|
+
|
|
56
|
+
```tsx
|
|
57
|
+
// sections/hero/Renderer.tsx
|
|
58
|
+
import { Image } from "@homepages/template-kit";
|
|
59
|
+
import crest from "./assets/crest.svg";
|
|
60
|
+
|
|
61
|
+
export default function Renderer() {
|
|
62
|
+
return <Image src={crest} alt="" />;
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
For a tiny glyph or divider, prefer inline `<svg>` markup over a file at all — it ships with
|
|
67
|
+
your JSX, needs no asset at all, and costs no extra request:
|
|
68
|
+
|
|
69
|
+
```tsx
|
|
70
|
+
export function ScrollArrow() {
|
|
71
|
+
return (
|
|
72
|
+
<svg viewBox="0 0 16 16" className="h-4 w-4" fill="none" aria-hidden="true">
|
|
73
|
+
<path d="M8 3v10M3 9l5 4 5-4" stroke="currentColor" strokeWidth="1.5" />
|
|
74
|
+
</svg>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## See also
|
|
80
|
+
|
|
81
|
+
- [Static assets](../assets.md) — where an asset may live and how to reference it.
|
|
82
|
+
- [`bundle-incomplete`](bundle-incomplete.md) — the other rule on the folder's contents.
|
|
83
|
+
- [Contract primitives](../primitives.md) — `Image`, which renders an asset URL you import.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
purpose: The template-kit/bundle-incomplete authoring rule — what it enforces, why, and how to fix it.
|
|
3
3
|
status: living
|
|
4
4
|
related: [INDEX.md, ../schema-system.md]
|
|
5
|
-
updated: 2026-07-
|
|
5
|
+
updated: 2026-07-15
|
|
6
6
|
---
|
|
7
7
|
# template-kit/bundle-incomplete
|
|
8
8
|
|
|
@@ -30,7 +30,7 @@ Two near-misses do **not** satisfy it:
|
|
|
30
30
|
searched for down the tree.
|
|
31
31
|
|
|
32
32
|
Everything else in the folder is unconstrained. `components/`, `hooks/`, `utils/`, any
|
|
33
|
-
nesting, any number of support modules, islands, stylesheets
|
|
33
|
+
nesting, any number of support modules, islands, stylesheets: the check has no
|
|
34
34
|
opinion about them, and neither does the rest of the kit.
|
|
35
35
|
|
|
36
36
|
## Reason
|
package/docs/rules/css-reason.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
purpose: The template-kit/css-reason authoring rule — what it enforces, why, and how to fix it.
|
|
3
3
|
status: living
|
|
4
4
|
related: [INDEX.md, ../theme-and-css.md]
|
|
5
|
-
updated: 2026-07-
|
|
5
|
+
updated: 2026-07-15
|
|
6
6
|
---
|
|
7
7
|
# template-kit/css-reason
|
|
8
8
|
|
|
@@ -36,7 +36,7 @@ and cannot leak into another section on the same page. Hand-written CSS gives al
|
|
|
36
36
|
up.
|
|
37
37
|
|
|
38
38
|
There is exactly one thing utilities cannot do: style **DOM that Tailwind's scanner never
|
|
39
|
-
sees**. Markup an
|
|
39
|
+
sees**. Markup an island builds at runtime has no class in your source for the scanner
|
|
40
40
|
to find, and a third-party widget's own DOM was never yours to put classes on. Those are
|
|
41
41
|
the cases hand-CSS exists for.
|
|
42
42
|
|
|
@@ -67,7 +67,7 @@ classes on the markup.
|
|
|
67
67
|
### After
|
|
68
68
|
|
|
69
69
|
```css
|
|
70
|
-
/* css-reason: styles the marker DOM the neighborhood-map
|
|
70
|
+
/* css-reason: styles the marker DOM the neighborhood-map island builds at runtime —
|
|
71
71
|
the nodes are created by the map library after hydration, so Tailwind's scanner never
|
|
72
72
|
sees a class to compile. */
|
|
73
73
|
.tr-map-marker {
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
---
|
|
2
|
+
purpose: The template-kit/fixtures-invalid authoring rule — what it enforces, why, and how to fix it.
|
|
3
|
+
status: living
|
|
4
|
+
related: [INDEX.md, ../schema-system.md, schema-invalid.md]
|
|
5
|
+
updated: 2026-07-14
|
|
6
|
+
---
|
|
7
|
+
# template-kit/fixtures-invalid
|
|
8
|
+
|
|
9
|
+
> `fixtures.ts` is the one contract file nothing else parses for you. This is that parse.
|
|
10
|
+
|
|
11
|
+
## Rule
|
|
12
|
+
|
|
13
|
+
The default export of `fixtures.ts` must satisfy the fixture-module shape:
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
{
|
|
17
|
+
base: {
|
|
18
|
+
slots: { /* every required slot, with a valid value */ },
|
|
19
|
+
options: { /* optional */ },
|
|
20
|
+
anchors: { /* optional */ },
|
|
21
|
+
variants: { /* optional */ },
|
|
22
|
+
},
|
|
23
|
+
option_matrix: [ /* optional: { label?, options } entries */ ],
|
|
24
|
+
states: { /* optional: named scenario → { label?, slots?, options?, anchors?, variants? } */ },
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`base` is required and must be an object holding a `slots` record — every other key is
|
|
29
|
+
optional. Each violation names the exact path inside the module that's wrong (`"base"`,
|
|
30
|
+
`"base.slots"`, `"states.empty-address"`, …) and what shape was expected there instead.
|
|
31
|
+
|
|
32
|
+
## Reason
|
|
33
|
+
|
|
34
|
+
`schema.ts` and `fill-spec.ts` are both Zod-parsed on the way into every other check, so a
|
|
35
|
+
malformed one is caught immediately. `fixtures.ts` used to be the exception: nothing
|
|
36
|
+
touched it until the render stage read it directly, so a wrong shape there surfaced as a
|
|
37
|
+
raw crash mid-render, blamed on whatever line happened to dereference the missing field —
|
|
38
|
+
never on the fixture that was actually wrong.
|
|
39
|
+
|
|
40
|
+
This gate exists to move that failure to where the mistake was made, with a message about
|
|
41
|
+
the fixture module instead of a stack trace from three files away.
|
|
42
|
+
|
|
43
|
+
## Fix
|
|
44
|
+
|
|
45
|
+
Read the message's path — it points at the exact key that's wrong — and bring that key's
|
|
46
|
+
shape in line with what's documented above.
|
|
47
|
+
|
|
48
|
+
### Before
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
// sections/hero/fixtures.ts
|
|
52
|
+
import type { FixtureModule } from "@homepages/template-kit";
|
|
53
|
+
|
|
54
|
+
const fixtures: FixtureModule = {
|
|
55
|
+
base: {
|
|
56
|
+
slots: { headline: "A mansard Victorian in the heart of Jamaica Plain" },
|
|
57
|
+
},
|
|
58
|
+
states: {
|
|
59
|
+
"no-photo": { slots: { hero_image: null }, label: 12 },
|
|
60
|
+
// ^ label must be a string
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export default fixtures;
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
```text
|
|
68
|
+
Section "hero": fixtures.ts — "states.no-photo.label": Expected string, received number.
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### After
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
// sections/hero/fixtures.ts
|
|
75
|
+
import type { FixtureModule } from "@homepages/template-kit";
|
|
76
|
+
|
|
77
|
+
const fixtures: FixtureModule = {
|
|
78
|
+
base: {
|
|
79
|
+
slots: { headline: "A mansard Victorian in the heart of Jamaica Plain" },
|
|
80
|
+
},
|
|
81
|
+
states: {
|
|
82
|
+
"no-photo": { slots: { hero_image: null }, label: "No hero photo" },
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export default fixtures;
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## See also
|
|
90
|
+
|
|
91
|
+
- [The schema system](../schema-system.md) — `fixtures.ts`'s place among the section's
|
|
92
|
+
four contract files.
|
|
93
|
+
- [`schema-invalid`](schema-invalid.md) — the sibling gate on `schema.ts` and
|
|
94
|
+
`fill-spec.ts`.
|
|
95
|
+
- [`bundle-incomplete`](bundle-incomplete.md) — reported instead of this id when
|
|
96
|
+
`fixtures.ts` is missing outright, rather than present and wrong.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
purpose: The template-kit/license-denied authoring rule — what it enforces, why, and how to fix it.
|
|
3
3
|
status: living
|
|
4
4
|
related: [INDEX.md, audit-severity.md]
|
|
5
|
-
updated: 2026-07-
|
|
5
|
+
updated: 2026-07-15
|
|
6
6
|
---
|
|
7
7
|
# template-kit/license-denied
|
|
8
8
|
|
|
@@ -31,8 +31,16 @@ Two cases people expect to slide through, and neither does:
|
|
|
31
31
|
- **A missing `license` field is a denial**, not a pass. An unlicensed package is not a
|
|
32
32
|
permissive one — by default, no rights are granted at all.
|
|
33
33
|
|
|
34
|
-
**Scope: the production tree only.**
|
|
35
|
-
|
|
34
|
+
**Scope: the production tree only.** "Production tree" means exactly what
|
|
35
|
+
`npm ci --omit=dev` installs. `devDependencies` are not evaluated — your bundler, your
|
|
36
|
+
test runner, and your formatter do not ship to anyone.
|
|
37
|
+
|
|
38
|
+
**An optional peer you have anyway counts.** If a production package declares an
|
|
39
|
+
optional `peerDependency` and your workspace has that package for any reason (even as a
|
|
40
|
+
`devDependency`), npm resolves the peer and keeps it in the `--omit=dev` install — so it
|
|
41
|
+
is in the production tree and its license is checked, transitively. An optional peer you
|
|
42
|
+
do **not** have is never installed and is not evaluated. Run `npm ci --omit=dev` and look
|
|
43
|
+
at the resulting `node_modules` if you want the exact set the check sees.
|
|
36
44
|
|
|
37
45
|
**There is no per-package override.** No allowlist entry you can add, no waiver comment. By
|
|
38
46
|
design.
|