@homepages/template-kit 0.0.1 → 0.2.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 +251 -0
- package/README.md +77 -11
- package/dist/base.css +64 -0
- package/dist/browser.d.ts +26 -0
- package/dist/browser.js +55 -0
- package/dist/cli.js +1 -1
- package/dist/contracts/image-descriptor.d.ts +17 -0
- package/dist/contracts/markers.d.ts +20 -0
- package/dist/contracts/markers.js +21 -0
- package/dist/contracts/slot-catalog.d.ts +237 -0
- package/dist/contracts/slot-catalog.js +247 -0
- package/dist/contracts/transforms.d.ts +108 -0
- package/dist/contracts/transforms.js +117 -0
- package/dist/design-system/breakpoints.d.ts +5 -0
- package/dist/design-system/breakpoints.js +14 -0
- package/dist/design-system/theme.d.ts +308 -0
- package/dist/design-system/theme.js +102 -0
- package/dist/eslint/is-client-file.js +79 -0
- package/dist/eslint/rules/image-bare-needs-reason.js +43 -0
- package/dist/eslint/rules/no-client-directive-in-contract.js +31 -0
- package/dist/eslint/rules/no-client-runtime-in-server.js +103 -0
- package/dist/eslint/rules/no-css-import-from-render-path.js +29 -0
- package/dist/eslint/rules/no-hex.js +116 -0
- package/dist/eslint/rules/no-inline-style.js +27 -0
- package/dist/eslint/rules/no-nondeterminism.js +48 -0
- package/dist/eslint/rules/no-raw-element.js +37 -0
- package/dist/eslint/rules/props-from-schema.js +37 -0
- package/dist/eslint/rules/serializable-island-props.js +112 -0
- package/dist/eslint/rules/slot-marker-literal.js +38 -0
- package/dist/eslint.d.ts +4 -8
- package/dist/eslint.js +75 -11
- package/dist/fixtures/sample-property.d.ts +67 -0
- package/dist/fixtures/sample-property.js +707 -0
- package/dist/fixtures.d.ts +2 -0
- package/dist/fixtures.js +3 -0
- package/dist/index.d.ts +28 -12
- package/dist/index.js +34 -2
- package/dist/island-runtime.d.ts +30 -0
- package/dist/island-runtime.js +73 -0
- package/dist/islands/contract.d.ts +56 -0
- package/dist/islands/contract.js +71 -0
- package/dist/islands/marker.d.ts +16 -0
- package/dist/islands/marker.js +18 -0
- package/dist/package.js +5 -0
- package/dist/primitives/Image.d.ts +49 -0
- package/dist/primitives/Image.js +95 -0
- package/dist/primitives/Section.d.ts +22 -0
- package/dist/primitives/Section.js +26 -0
- package/dist/primitives/Slot.d.ts +50 -0
- package/dist/primitives/Slot.js +39 -0
- package/dist/primitives/SlotGroup.d.ts +38 -0
- package/dist/primitives/SlotGroup.js +24 -0
- package/dist/primitives/SlotItem.d.ts +43 -0
- package/dist/primitives/SlotItem.js +38 -0
- package/dist/primitives/index.d.ts +7 -0
- package/dist/primitives/picture-sources.d.ts +22 -0
- package/dist/primitives/picture-sources.js +52 -0
- package/dist/rules/registry.js +16 -0
- package/dist/schema/fill-spec.d.ts +692 -0
- package/dist/schema/fill-spec.js +98 -0
- package/dist/schema/fixture-schema.d.ts +143 -0
- package/dist/schema/fixture-schema.js +113 -0
- package/dist/schema/index.d.ts +16 -0
- package/dist/schema/manifest.d.ts +527 -0
- package/dist/schema/manifest.js +73 -0
- package/dist/schema/property-facts.d.ts +44 -0
- package/dist/schema/resolve-variants.d.ts +11 -0
- package/dist/schema/resolve-variants.js +15 -0
- package/dist/schema/runtime-values.d.ts +45 -0
- package/dist/schema/section-nav.d.ts +7 -0
- package/dist/schema/section-props.d.ts +69 -0
- package/dist/schema/section-schema.d.ts +533 -0
- package/dist/schema/section-schema.js +72 -0
- package/dist/schema/slot-types.d.ts +117 -0
- package/dist/schema/slot-types.js +181 -0
- package/dist/schema/source.d.ts +13 -0
- package/dist/schema/source.js +12 -0
- package/dist/schema/synthesize-nullable.d.ts +18 -0
- package/dist/schema/synthesize-nullable.js +47 -0
- package/dist/styles.css +131 -9
- package/docs/INDEX.md +7 -4
- package/docs/eslint.md +55 -0
- package/docs/islands.md +150 -0
- package/docs/llms.txt +33 -3
- package/docs/primitives.md +214 -0
- package/docs/rules/INDEX.md +58 -0
- package/docs/rules/audit-severity.md +91 -0
- package/docs/rules/bundle-incomplete.md +117 -0
- package/docs/rules/css-reason.md +94 -0
- package/docs/rules/determinism-drift.md +112 -0
- package/docs/rules/image-bare-needs-reason.md +117 -0
- package/docs/rules/license-denied.md +106 -0
- package/docs/rules/lockfile-missing.md +68 -0
- package/docs/rules/lockfile-stale.md +88 -0
- package/docs/rules/missing-slot-marker.md +140 -0
- package/docs/rules/no-bare-css-import.md +128 -0
- package/docs/rules/no-client-directive-in-contract.md +111 -0
- package/docs/rules/no-client-runtime-in-server.md +143 -0
- package/docs/rules/no-css-import-from-render-path.md +118 -0
- package/docs/rules/no-hex.md +97 -0
- package/docs/rules/no-inline-style.md +121 -0
- package/docs/rules/no-nondeterminism.md +100 -0
- package/docs/rules/no-raw-element.md +100 -0
- package/docs/rules/props-from-schema.md +119 -0
- package/docs/rules/render-invariant.md +165 -0
- package/docs/rules/serializable-island-props.md +146 -0
- package/docs/rules/server-vs-client.md +105 -0
- package/docs/rules/sidebar-order.md +109 -0
- package/docs/rules/single-react.md +97 -0
- package/docs/rules/size-renderer-bundle.md +109 -0
- package/docs/rules/size-section-css.md +95 -0
- package/docs/rules/slot-group-marker.md +127 -0
- package/docs/rules/slot-marker-literal.md +148 -0
- package/docs/rules/typecheck.md +103 -0
- package/docs/rules/unlayered-fence.md +113 -0
- package/docs/schema-system.md +240 -0
- package/docs/theme-and-css.md +225 -0
- package/package.json +30 -6
- package/tsconfig.base.json +16 -0
- package/tsconfig.json +2 -13
- package/dist/src-dZr5N30y.js +0 -35
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
---
|
|
2
|
+
purpose: The template-kit/size-renderer-bundle authoring rule — what it enforces, why, and how to fix it.
|
|
3
|
+
status: living
|
|
4
|
+
related: [INDEX.md, size-section-css.md]
|
|
5
|
+
updated: 2026-07-14
|
|
6
|
+
---
|
|
7
|
+
# template-kit/size-renderer-bundle
|
|
8
|
+
|
|
9
|
+
> A section's renderer bundle must be **≤ 50 KB gzip**. The budget is not
|
|
10
|
+
> author-overridable.
|
|
11
|
+
|
|
12
|
+
## Rule
|
|
13
|
+
|
|
14
|
+
Each section's renderer is bundled with esbuild and minified, with **React treated as
|
|
15
|
+
external** (it is a peer, shared by the whole page, and is not counted against any one
|
|
16
|
+
section). The gzipped result must be **50 KB or less**.
|
|
17
|
+
|
|
18
|
+
The budget is **visible in `template-kit check`'s configuration and is not
|
|
19
|
+
author-overridable**. A template workspace cannot raise it, and there is no per-section
|
|
20
|
+
exemption. If you go looking for the flag, this is the answer: there isn't one.
|
|
21
|
+
|
|
22
|
+
## Reason
|
|
23
|
+
|
|
24
|
+
The output is a real page for a real buyer, standing in a driveway on a phone. Bytes are the
|
|
25
|
+
one cost the section cannot delegate.
|
|
26
|
+
|
|
27
|
+
The number is set to be generous — roughly **twice** the largest section shipping today — so
|
|
28
|
+
it does not fight ordinary authoring. You will not hit it by writing a rich section. You hit
|
|
29
|
+
it when something **runaway** gets pulled in:
|
|
30
|
+
|
|
31
|
+
- **a barrel import of an icon set**, which drags every icon into the bundle so you can use
|
|
32
|
+
three;
|
|
33
|
+
- **a package that brings a font, a locale table, or a polyfill along with it**, none of
|
|
34
|
+
which you asked for.
|
|
35
|
+
|
|
36
|
+
That is the shape of essentially every violation: not a section that grew, but a single
|
|
37
|
+
import that was more expensive than it looked.
|
|
38
|
+
|
|
39
|
+
## Fix
|
|
40
|
+
|
|
41
|
+
Find the import that is not paying for itself.
|
|
42
|
+
|
|
43
|
+
- **Import named symbols, never a namespace.** `import { ArrowRight } from "…"` is
|
|
44
|
+
tree-shakeable; `import * as Icons from "…"` is not — a namespace object has to contain
|
|
45
|
+
everything, so nothing can be dropped.
|
|
46
|
+
- **Prefer the package's narrow entry** if it offers one.
|
|
47
|
+
- **Hand-write the residue.** If you use one function out of a large utility package, copy
|
|
48
|
+
the function.
|
|
49
|
+
|
|
50
|
+
**Do not try to hoist code out of the section to dodge the budget.** Each template owns its
|
|
51
|
+
own presentation code; there is no shared library above a section to hoist it into, and the
|
|
52
|
+
bytes would still land on the same page.
|
|
53
|
+
|
|
54
|
+
### Before
|
|
55
|
+
|
|
56
|
+
```tsx
|
|
57
|
+
// sections/amenities/Renderer.tsx
|
|
58
|
+
import { Section, Slot, SlotItem } from "@homepages/template-kit";
|
|
59
|
+
import * as Icons from "lucide-react"; // ← the whole set: every icon, in the bundle
|
|
60
|
+
|
|
61
|
+
import type { Props } from "./schema.js";
|
|
62
|
+
|
|
63
|
+
export function Renderer({ slots }: Props) {
|
|
64
|
+
return (
|
|
65
|
+
<Section>
|
|
66
|
+
<Slot id="amenities">
|
|
67
|
+
{slots.amenities.map((item, i) => (
|
|
68
|
+
<SlotItem key={typeof item.id === "string" ? item.id : i} index={i} as="li">
|
|
69
|
+
<Icons.Check aria-hidden />
|
|
70
|
+
<h3>{typeof item.label === "string" ? item.label : null}</h3>
|
|
71
|
+
</SlotItem>
|
|
72
|
+
))}
|
|
73
|
+
</Slot>
|
|
74
|
+
</Section>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### After
|
|
80
|
+
|
|
81
|
+
```tsx
|
|
82
|
+
// sections/amenities/Renderer.tsx
|
|
83
|
+
import { Section, Slot, SlotItem } from "@homepages/template-kit";
|
|
84
|
+
import { Check } from "lucide-react"; // ← one icon, and the bundler can drop the rest
|
|
85
|
+
|
|
86
|
+
import type { Props } from "./schema.js";
|
|
87
|
+
|
|
88
|
+
export function Renderer({ slots }: Props) {
|
|
89
|
+
return (
|
|
90
|
+
<Section>
|
|
91
|
+
<Slot id="amenities">
|
|
92
|
+
{slots.amenities.map((item, i) => (
|
|
93
|
+
<SlotItem key={typeof item.id === "string" ? item.id : i} index={i} as="li">
|
|
94
|
+
<Check aria-hidden />
|
|
95
|
+
<h3>{typeof item.label === "string" ? item.label : null}</h3>
|
|
96
|
+
</SlotItem>
|
|
97
|
+
))}
|
|
98
|
+
</Slot>
|
|
99
|
+
</Section>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## See also
|
|
105
|
+
|
|
106
|
+
- [`size-section-css`](size-section-css.md) — the same budget on the other half of what the
|
|
107
|
+
section ships.
|
|
108
|
+
- [`license-denied`](license-denied.md) — the other reason to look hard at a dependency
|
|
109
|
+
before you take it.
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
---
|
|
2
|
+
purpose: The template-kit/size-section-css authoring rule — what it enforces, why, and how to fix it.
|
|
3
|
+
status: living
|
|
4
|
+
related: [INDEX.md, size-renderer-bundle.md, ../theme-and-css.md]
|
|
5
|
+
updated: 2026-07-14
|
|
6
|
+
---
|
|
7
|
+
# template-kit/size-section-css
|
|
8
|
+
|
|
9
|
+
> A section's compiled CSS must be **≤ 50 KB gzip** — **including the package CSS its module
|
|
10
|
+
> graph imports.** That inclusion is the whole point of the rule.
|
|
11
|
+
|
|
12
|
+
## Rule
|
|
13
|
+
|
|
14
|
+
A section's compiled cascade layer must gzip to **50 KB or less**. What is measured is
|
|
15
|
+
everything that lands in the layer:
|
|
16
|
+
|
|
17
|
+
- the section's hand-written CSS, and
|
|
18
|
+
- **every package stylesheet its module graph imports** — the `import "swiper/css"` in a
|
|
19
|
+
component, an island, or a marked enhancer.
|
|
20
|
+
|
|
21
|
+
The budget is **visible in `template-kit check`'s configuration and is not
|
|
22
|
+
author-overridable**. A workspace cannot raise it, and there is no per-section exemption.
|
|
23
|
+
|
|
24
|
+
## Reason
|
|
25
|
+
|
|
26
|
+
Same page, same phone, same buyer as
|
|
27
|
+
[`size-renderer-bundle`](size-renderer-bundle.md) — and here the bytes are worse, because
|
|
28
|
+
CSS blocks the first paint.
|
|
29
|
+
|
|
30
|
+
"Including package CSS" is not a technicality; it is the failure this rule exists to catch.
|
|
31
|
+
Your own stylesheet is almost never the problem — it is a few kilobytes of overrides. The
|
|
32
|
+
package's is. And the reason it is invisible to you is the mechanism: a package stylesheet's
|
|
33
|
+
`url()` references — its icon sprites, its webfont — are **inlined as `data:` URIs** into the
|
|
34
|
+
compiled stylesheet. A 300 KB font file arrives in your published CSS without you ever
|
|
35
|
+
having seen a 300 KB file. Nothing in your source looks large. Nothing in the package's
|
|
36
|
+
`dist` listing looks alarming. The compiled layer is 400 KB.
|
|
37
|
+
|
|
38
|
+
So the number this check reports is frequently the first time anyone finds out what a
|
|
39
|
+
one-line CSS import actually cost.
|
|
40
|
+
|
|
41
|
+
## Fix
|
|
42
|
+
|
|
43
|
+
Three moves, in order of how often they work:
|
|
44
|
+
|
|
45
|
+
1. **Import the slimmest entry the package offers.** Most CSS-shipping packages have a core
|
|
46
|
+
stylesheet plus opt-in modules, *and* a fat "everything" bundle that is the one you get
|
|
47
|
+
from copying the quickstart. Take the core, plus only the modules you use.
|
|
48
|
+
2. **Cut the webfont.** If the package's stylesheet drags in a font for its icons, and you
|
|
49
|
+
are using three of those icons, replace them with inline SVG — or with a family the page's
|
|
50
|
+
theme already declares (`--tr-font-sans`, `--tr-font-serif`), which costs nothing
|
|
51
|
+
because it is already loaded.
|
|
52
|
+
3. **Drop the package and hand-write the residue.** If you use one component from a large
|
|
53
|
+
widget library, its stylesheet is paying for components you never render.
|
|
54
|
+
|
|
55
|
+
### Before
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
// sections/gallery/Carousel.tsx
|
|
59
|
+
"use client";
|
|
60
|
+
|
|
61
|
+
import Swiper from "swiper";
|
|
62
|
+
import "swiper/swiper-bundle.css"; // ← every module Swiper ships, plus its icon font
|
|
63
|
+
|
|
64
|
+
export default function Carousel({ slides }: { slides: { id: string; url: string }[] }) {
|
|
65
|
+
// …uses navigation arrows and pagination dots, and nothing else
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### After
|
|
70
|
+
|
|
71
|
+
```tsx
|
|
72
|
+
// sections/gallery/Carousel.tsx
|
|
73
|
+
"use client";
|
|
74
|
+
|
|
75
|
+
import Swiper from "swiper";
|
|
76
|
+
import "swiper/css"; // ← the core layout only
|
|
77
|
+
import "swiper/css/navigation"; // ← the two modules this carousel actually uses
|
|
78
|
+
import "swiper/css/pagination";
|
|
79
|
+
|
|
80
|
+
export default function Carousel({ slides }: { slides: { id: string; url: string }[] }) {
|
|
81
|
+
// …
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The arrows and dots that came from the bundle's icon font are drawn as inline SVG in the
|
|
86
|
+
component instead, so nothing pulls a webfont into the layer.
|
|
87
|
+
|
|
88
|
+
## See also
|
|
89
|
+
|
|
90
|
+
- [`size-renderer-bundle`](size-renderer-bundle.md) — the same budget on the section's
|
|
91
|
+
JavaScript.
|
|
92
|
+
- [`no-bare-css-import`](no-bare-css-import.md) — why a package's stylesheet is imported from
|
|
93
|
+
code and never `@import`ed from CSS.
|
|
94
|
+
- [Theme and CSS](../theme-and-css.md) — the fonts and tokens the page already loads, which
|
|
95
|
+
cost you nothing to reuse.
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
---
|
|
2
|
+
purpose: The template-kit/slot-group-marker authoring rule — what it enforces, why, and how to fix it.
|
|
3
|
+
status: living
|
|
4
|
+
related: [INDEX.md, ../primitives.md, ../schema-system.md]
|
|
5
|
+
updated: 2026-07-14
|
|
6
|
+
---
|
|
7
|
+
# template-kit/slot-group-marker
|
|
8
|
+
|
|
9
|
+
> A group declared in `meta.groups` with **two or more members** must be wrapped in
|
|
10
|
+
> `<SlotGroup id="…">` in the JSX. Grouping is authored in two places, and they must agree.
|
|
11
|
+
|
|
12
|
+
## Rule
|
|
13
|
+
|
|
14
|
+
For every entry in `schema.ts`'s `meta.groups` whose flattened member list has **≥ 2
|
|
15
|
+
members**, the section's JSX must contain `<SlotGroup id="<that group's id>">` wrapping a
|
|
16
|
+
container that is a **shared ancestor** of every member's marker.
|
|
17
|
+
|
|
18
|
+
"Flattened" is literal: `members: ["agent_name", ["agent_email", "agent_phone"]]` is three
|
|
19
|
+
members, not two — a `[a, b]` tuple is a 2-up row, not a single member.
|
|
20
|
+
|
|
21
|
+
Membership is read from the typed `meta.groups` block. The check never guesses a group by
|
|
22
|
+
scanning your markup for things that look related.
|
|
23
|
+
|
|
24
|
+
Carve-outs:
|
|
25
|
+
|
|
26
|
+
- **A 1-member group needs no wrapper.** One slot, one card — the group adds no grouping
|
|
27
|
+
the marker isn't already doing.
|
|
28
|
+
- **A single slot rendered more than once is not a group.** An address shown in both a
|
|
29
|
+
desktop and a mobile layout is one slot in two places; it needs no `meta.groups` entry
|
|
30
|
+
and no `<SlotGroup>`.
|
|
31
|
+
|
|
32
|
+
## Reason
|
|
33
|
+
|
|
34
|
+
`meta.groups` makes the editor collapse several slots into **one accordion card** in the
|
|
35
|
+
sidebar. The `<SlotGroup>` wrapper is the other half of that promise: the canvas outlines
|
|
36
|
+
and selects the group as **one unit**, because its hover/selection chrome targets the
|
|
37
|
+
`data-slot-group` container.
|
|
38
|
+
|
|
39
|
+
Declare the group and omit the wrapper and the two halves disagree. The sidebar shows one
|
|
40
|
+
card, and the canvas — with no group container to find — falls back to outlining the
|
|
41
|
+
members one at a time. The user sees a single "Agent" card that highlights three separate
|
|
42
|
+
boxes on the page.
|
|
43
|
+
|
|
44
|
+
`SlotGroup` must be an **ancestor** of every member's `data-slot-id` element, because the
|
|
45
|
+
editor resolves the group with `closest("[data-slot-group]")` from the clicked slot. A
|
|
46
|
+
marker that sits beside the members rather than around them is never found.
|
|
47
|
+
|
|
48
|
+
## Fix
|
|
49
|
+
|
|
50
|
+
Wrap the members' shared container in `<SlotGroup id="…">`, using the **same id** as the
|
|
51
|
+
`meta.groups` entry. If the slots do not actually edit as a unit, the other honest fix is
|
|
52
|
+
to delete the group from `meta.groups` — then each slot is its own card and no wrapper is
|
|
53
|
+
wanted.
|
|
54
|
+
|
|
55
|
+
### Before
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
// sections/contact/schema.ts
|
|
59
|
+
import type { SectionProps, SectionSchemaShape } from "@homepages/template-kit";
|
|
60
|
+
|
|
61
|
+
export const schema = {
|
|
62
|
+
meta: {
|
|
63
|
+
displayName: "Contact",
|
|
64
|
+
groups: [
|
|
65
|
+
{ id: "agent", label: "Agent", members: ["agent_name", ["agent_email", "agent_phone"]] },
|
|
66
|
+
],
|
|
67
|
+
},
|
|
68
|
+
slots: {
|
|
69
|
+
agent_name: { type: "text", size: "short", required: true, editable_by_user: true },
|
|
70
|
+
agent_email: { type: "text", size: "short", required: false, editable_by_user: true },
|
|
71
|
+
agent_phone: { type: "text", size: "short", required: false, editable_by_user: true },
|
|
72
|
+
},
|
|
73
|
+
} as const satisfies SectionSchemaShape;
|
|
74
|
+
|
|
75
|
+
export type Props = SectionProps<typeof schema>;
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
```tsx
|
|
79
|
+
// sections/contact/Renderer.tsx — template-kit/slot-group-marker: agent
|
|
80
|
+
import { Section, Slot } from "@homepages/template-kit";
|
|
81
|
+
|
|
82
|
+
import type { Props } from "./schema.js";
|
|
83
|
+
|
|
84
|
+
export function Renderer({ slots }: Props) {
|
|
85
|
+
return (
|
|
86
|
+
<Section>
|
|
87
|
+
<div className="flex flex-col gap-2">
|
|
88
|
+
<Slot id="agent_name" textLeaf><h3>{slots.agent_name}</h3></Slot>
|
|
89
|
+
<Slot id="agent_email" textLeaf><span>{slots.agent_email}</span></Slot>
|
|
90
|
+
<Slot id="agent_phone" textLeaf><span>{slots.agent_phone}</span></Slot>
|
|
91
|
+
</div>
|
|
92
|
+
</Section>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### After
|
|
98
|
+
|
|
99
|
+
The container was already there — it just needed to be the group:
|
|
100
|
+
|
|
101
|
+
```tsx
|
|
102
|
+
// sections/contact/Renderer.tsx
|
|
103
|
+
import { Section, Slot, SlotGroup } from "@homepages/template-kit";
|
|
104
|
+
|
|
105
|
+
import type { Props } from "./schema.js";
|
|
106
|
+
|
|
107
|
+
export function Renderer({ slots }: Props) {
|
|
108
|
+
return (
|
|
109
|
+
<Section>
|
|
110
|
+
<SlotGroup id="agent" className="flex flex-col gap-2">
|
|
111
|
+
<Slot id="agent_name" textLeaf><h3>{slots.agent_name}</h3></Slot>
|
|
112
|
+
<Slot id="agent_email" textLeaf><span>{slots.agent_email}</span></Slot>
|
|
113
|
+
<Slot id="agent_phone" textLeaf><span>{slots.agent_phone}</span></Slot>
|
|
114
|
+
</SlotGroup>
|
|
115
|
+
</Section>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## See also
|
|
121
|
+
|
|
122
|
+
- [Contract primitives: `SlotGroup`](../primitives.md#slotgroup--slots-edited-as-one-unit) —
|
|
123
|
+
the ancestor requirement and what the marker does.
|
|
124
|
+
- [The schema system](../schema-system.md#grouping-slots-into-one-editor-card) — grouping is
|
|
125
|
+
declared in `meta.groups`; there is no per-slot `group` field.
|
|
126
|
+
- [`missing-slot-marker`](missing-slot-marker.md) — the per-slot marker each member still
|
|
127
|
+
needs.
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
---
|
|
2
|
+
purpose: The template-kit/slot-marker-literal authoring rule — what it enforces, why, and how to fix it.
|
|
3
|
+
status: living
|
|
4
|
+
related: [INDEX.md, ../primitives.md, ../schema-system.md]
|
|
5
|
+
updated: 2026-07-14
|
|
6
|
+
---
|
|
7
|
+
# template-kit/slot-marker-literal
|
|
8
|
+
|
|
9
|
+
> A slot key must be written as a string literal wherever it is marked. `SlotItem` is
|
|
10
|
+
> exempt.
|
|
11
|
+
|
|
12
|
+
## Rule
|
|
13
|
+
|
|
14
|
+
These four places carry a slot key, and each must be a **string literal**:
|
|
15
|
+
|
|
16
|
+
| Where | Example |
|
|
17
|
+
|---|---|
|
|
18
|
+
| `<Slot id>` | `<Slot id="headline">` |
|
|
19
|
+
| `<SlotGroup id>` | `<SlotGroup id="agent">` |
|
|
20
|
+
| `slotId` on any element | `<Image slotId="hero_image" …>` |
|
|
21
|
+
| `data-slot-id` / `data-slot-group` on any element | `<div data-slot-id="headline">` |
|
|
22
|
+
|
|
23
|
+
Anything that is not a plain string literal reports: an expression (`<Slot id={key}>`), a
|
|
24
|
+
template literal (`` <Slot id={`slot_${n}`}> ``), and a valueless attribute
|
|
25
|
+
(`<div data-slot-id>`).
|
|
26
|
+
|
|
27
|
+
**`SlotItem` is deliberately exempt.** Its prop is `index: number` — an array position,
|
|
28
|
+
written `index={i}` inside a `.map()`, not a schema key. There is nothing to cross-check
|
|
29
|
+
it against, so an expression there is correct and expected.
|
|
30
|
+
|
|
31
|
+
## Reason
|
|
32
|
+
|
|
33
|
+
This is not style. A separate, cross-file completeness check verifies that **every slot
|
|
34
|
+
declared in `schema.ts` has a marker in the section's JSX** — and it reads these keys
|
|
35
|
+
**statically**, from the source text, without running your Renderer.
|
|
36
|
+
|
|
37
|
+
A key written as an expression is a key it cannot resolve. So the check cannot see the
|
|
38
|
+
slot as marked, and — worse — the marker that actually reaches the browser is whatever
|
|
39
|
+
your expression happened to evaluate to. The slot **silently loses editor selection**: it
|
|
40
|
+
still fills, it still renders, and a user clicking on it in the editor selects nothing.
|
|
41
|
+
Nothing throws. The failure is invisible until someone tries to edit that block.
|
|
42
|
+
|
|
43
|
+
This rule exists **so that** [`missing-slot-marker`](missing-slot-marker.md) can work. A
|
|
44
|
+
`.map()`-generated slot key defeats both at once: the completeness check reports the slot
|
|
45
|
+
as unmarked, and no lint tells you why.
|
|
46
|
+
|
|
47
|
+
## Fix
|
|
48
|
+
|
|
49
|
+
This shows up in two different shapes, and the fix depends on which one you're in.
|
|
50
|
+
|
|
51
|
+
**A key that is static but not literal** — held in a `const`, or assembled with a
|
|
52
|
+
template literal — isn't generating anything: there is exactly one possible value, so
|
|
53
|
+
unwrap it back into a plain inline string literal.
|
|
54
|
+
|
|
55
|
+
**A key generated inside a `.map()` over an array** is a genuine collection: declare it
|
|
56
|
+
as one slot in `schema.ts`, mark the array **once** with `<Slot>`, and mark each row with
|
|
57
|
+
`<SlotItem index={i}>`.
|
|
58
|
+
|
|
59
|
+
### Before
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
62
|
+
import { Section, Slot } from "@homepages/template-kit";
|
|
63
|
+
|
|
64
|
+
import type { Props } from "./schema.js";
|
|
65
|
+
|
|
66
|
+
const FIELD = "headline";
|
|
67
|
+
|
|
68
|
+
export function Renderer({ slots }: Props) {
|
|
69
|
+
return (
|
|
70
|
+
<Section>
|
|
71
|
+
<Slot id={FIELD} as="h1" textLeaf>
|
|
72
|
+
<span>{slots.headline}</span>
|
|
73
|
+
</Slot>
|
|
74
|
+
<Slot id={`sub${"title"}`} textLeaf>
|
|
75
|
+
<p>{slots.subtitle}</p>
|
|
76
|
+
</Slot>
|
|
77
|
+
</Section>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Neither `FIELD` nor the template literal is doing any work — both spell out their whole
|
|
83
|
+
value at the call site. That's the common case: a constant or a template literal standing
|
|
84
|
+
in for a string that was always going to be one string.
|
|
85
|
+
|
|
86
|
+
### After
|
|
87
|
+
|
|
88
|
+
```tsx
|
|
89
|
+
import { Section, Slot } from "@homepages/template-kit";
|
|
90
|
+
|
|
91
|
+
import type { Props } from "./schema.js";
|
|
92
|
+
|
|
93
|
+
export function Renderer({ slots }: Props) {
|
|
94
|
+
return (
|
|
95
|
+
<Section>
|
|
96
|
+
<Slot id="headline" as="h1" textLeaf>
|
|
97
|
+
<span>{slots.headline}</span>
|
|
98
|
+
</Slot>
|
|
99
|
+
<Slot id="subtitle" textLeaf>
|
|
100
|
+
<p>{slots.subtitle}</p>
|
|
101
|
+
</Slot>
|
|
102
|
+
</Section>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
The other shape is different: a key actually produced by iterating an array is modelling
|
|
108
|
+
a real collection, and the fix is structural, not a matter of unwrapping.
|
|
109
|
+
|
|
110
|
+
```tsx
|
|
111
|
+
// Before — `.map()` produces a different key each iteration; none of them is a literal.
|
|
112
|
+
const FIELDS = ["headline", "subhead"] as const;
|
|
113
|
+
|
|
114
|
+
export function Renderer({ slots }: Props) {
|
|
115
|
+
return (
|
|
116
|
+
<Section>
|
|
117
|
+
{FIELDS.map((key) => (
|
|
118
|
+
<Slot key={key} id={key} textLeaf>
|
|
119
|
+
<p>{slots[key]}</p>
|
|
120
|
+
</Slot>
|
|
121
|
+
))}
|
|
122
|
+
</Section>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
A genuine collection keeps its loop — the literal key is on the `<Slot>`, and the loop
|
|
128
|
+
variable stays on `SlotItem`'s `index`, which is exempt:
|
|
129
|
+
|
|
130
|
+
```tsx
|
|
131
|
+
<Slot id="amenities">
|
|
132
|
+
{slots.amenities.map((item, i) => (
|
|
133
|
+
<SlotItem key={item.id} index={i} as="li">
|
|
134
|
+
<h3>{item.label}</h3>
|
|
135
|
+
</SlotItem>
|
|
136
|
+
))}
|
|
137
|
+
</Slot>
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Don't reach for this structural rewrite for the first case above — a named constant or a
|
|
141
|
+
template literal is not a collection, and modelling it as one is solving a problem you
|
|
142
|
+
don't have.
|
|
143
|
+
|
|
144
|
+
## See also
|
|
145
|
+
|
|
146
|
+
- [Contract primitives](../primitives.md#the-marker-contract) — the marker attributes and
|
|
147
|
+
which primitive emits each.
|
|
148
|
+
- [Schema system](../schema-system.md) — where slot keys are declared.
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
---
|
|
2
|
+
purpose: The template-kit/typecheck authoring rule — what it enforces, why, and how to fix it.
|
|
3
|
+
status: living
|
|
4
|
+
related: [INDEX.md, ../schema-system.md]
|
|
5
|
+
updated: 2026-07-14
|
|
6
|
+
---
|
|
7
|
+
# template-kit/typecheck
|
|
8
|
+
|
|
9
|
+
> The template must compile. Every type error is one violation.
|
|
10
|
+
|
|
11
|
+
## Rule
|
|
12
|
+
|
|
13
|
+
The template's TypeScript must compile cleanly. The check resolves `typescript` from **your
|
|
14
|
+
workspace's** `node_modules` — the compiler that runs is the one you installed, at the
|
|
15
|
+
version you pinned, against your own `tsconfig.json`.
|
|
16
|
+
|
|
17
|
+
If `typescript` is not installed, that is itself the diagnostic (with the fix), not a crash.
|
|
18
|
+
|
|
19
|
+
## Reason
|
|
20
|
+
|
|
21
|
+
The schema system is the section contract, and it is enforced **through the type system**.
|
|
22
|
+
`SectionProps<typeof schema>` is not a convenience — it is the mechanism that keeps
|
|
23
|
+
`schema.ts` and `Renderer.tsx` in agreement: a slot you add to the schema and forget to
|
|
24
|
+
render, a slot you render under the wrong name, an optional slot you treat as always
|
|
25
|
+
present, a slot whose type you assume wrong. Each of those is a compile error rather than a
|
|
26
|
+
surprise on a stranger's published page.
|
|
27
|
+
|
|
28
|
+
A template that does not typecheck has opted out of the only thing checking that the two
|
|
29
|
+
files still describe the same section.
|
|
30
|
+
|
|
31
|
+
## Fix
|
|
32
|
+
|
|
33
|
+
Fix the type error. If the compiler itself is missing, install it and point your
|
|
34
|
+
`tsconfig.json` at the kit's base config, which sets the module resolution and JSX settings
|
|
35
|
+
the kit's types expect:
|
|
36
|
+
|
|
37
|
+
```jsonc
|
|
38
|
+
// tsconfig.json
|
|
39
|
+
{ "extends": "@homepages/template-kit/tsconfig", "include": ["sections", "theme.ts"] }
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npm install --save-dev typescript
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Before
|
|
47
|
+
|
|
48
|
+
`year_built` is declared `required: false`, so `SectionProps` resolves it to
|
|
49
|
+
`string | null` — and the Renderer treats it as a `string`:
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
// sections/facts/Renderer.tsx
|
|
53
|
+
import { Section, Slot } from "@homepages/template-kit";
|
|
54
|
+
|
|
55
|
+
import type { Props } from "./schema.js";
|
|
56
|
+
|
|
57
|
+
export function Renderer({ slots }: Props) {
|
|
58
|
+
return (
|
|
59
|
+
<Section>
|
|
60
|
+
{/* error TS18047: 'slots.year_built' is possibly 'null'. */}
|
|
61
|
+
<Slot id="year_built" textLeaf>
|
|
62
|
+
<p>Built {slots.year_built.slice(0, 4)}</p>
|
|
63
|
+
</Slot>
|
|
64
|
+
</Section>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
That is not a pedantic complaint. `required: false` means a thin listing really does arrive
|
|
70
|
+
with no year, and this line really does throw on it.
|
|
71
|
+
|
|
72
|
+
### After
|
|
73
|
+
|
|
74
|
+
```tsx
|
|
75
|
+
// sections/facts/Renderer.tsx
|
|
76
|
+
import { Section, Slot } from "@homepages/template-kit";
|
|
77
|
+
|
|
78
|
+
import type { Props } from "./schema.js";
|
|
79
|
+
|
|
80
|
+
export function Renderer({ slots }: Props) {
|
|
81
|
+
return (
|
|
82
|
+
<Section>
|
|
83
|
+
{slots.year_built ? (
|
|
84
|
+
<Slot id="year_built" textLeaf>
|
|
85
|
+
<p>Built {slots.year_built.slice(0, 4)}</p>
|
|
86
|
+
</Slot>
|
|
87
|
+
) : null}
|
|
88
|
+
</Section>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
If the slot should never be absent, the other correct fix is in the schema — declare it
|
|
94
|
+
`required: true`, and the type narrows to `string` everywhere.
|
|
95
|
+
|
|
96
|
+
## See also
|
|
97
|
+
|
|
98
|
+
- [The schema system](../schema-system.md#renderertsx--props-derived-never-hand-written) —
|
|
99
|
+
how `SectionProps` resolves each slot to its runtime type.
|
|
100
|
+
- [`props-from-schema`](props-from-schema.md) — the rule that stops a hand-written `Props`
|
|
101
|
+
from disabling this inference in the first place.
|
|
102
|
+
- [`render-invariant`](render-invariant.md) — the same class of bug caught at render time,
|
|
103
|
+
for the cases the types cannot see.
|