@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,118 @@
|
|
|
1
|
+
---
|
|
2
|
+
purpose: The template-kit/no-css-import-from-render-path authoring rule — what it enforces, why, and how to fix it.
|
|
3
|
+
status: living
|
|
4
|
+
related: [INDEX.md, ../theme-and-css.md]
|
|
5
|
+
updated: 2026-07-14
|
|
6
|
+
---
|
|
7
|
+
# template-kit/no-css-import-from-render-path
|
|
8
|
+
|
|
9
|
+
> No **local** `.css` import in a section's render path. A **package's** `.css` import
|
|
10
|
+
> belongs there — it is how its stylesheet reaches the section's cascade layer.
|
|
11
|
+
|
|
12
|
+
## Rule
|
|
13
|
+
|
|
14
|
+
An `import` whose specifier ends in **`.css`**, in any of the section's `.ts`/`.tsx`
|
|
15
|
+
files, is reported **only when the specifier is local** — relative
|
|
16
|
+
(`import "./gallery.css";`) or absolute (`import "/gallery.css";`) — including the
|
|
17
|
+
side-effect-only form with no bindings.
|
|
18
|
+
|
|
19
|
+
A **bare** specifier (`import "swiper/css";`, `import "@scope/pkg/dist/style.css";`) is
|
|
20
|
+
not reported. That is the sanctioned channel for a third-party package's stylesheet.
|
|
21
|
+
|
|
22
|
+
What it does **not** match:
|
|
23
|
+
|
|
24
|
+
- `.scss` and `.less` — a different pipeline, out of this rule's scope;
|
|
25
|
+
- `require("./x.css")`;
|
|
26
|
+
- a dynamic `import("./x.css")`.
|
|
27
|
+
|
|
28
|
+
There is no island exemption: a `"use client"` file is part of the render path too — it is
|
|
29
|
+
server-rendered inline before it is hydrated.
|
|
30
|
+
|
|
31
|
+
## Reason
|
|
32
|
+
|
|
33
|
+
The section's CSS build works by **bundling the render path itself** — `Renderer.tsx` and
|
|
34
|
+
any enhancer that carries `export const enhancer` — and harvesting the CSS its imports
|
|
35
|
+
pull in. A bare specifier resolves into `node_modules`, which is exactly what that build
|
|
36
|
+
looks for: `import "swiper/css";` in `Renderer.tsx` is not a mistake, it is the mechanism.
|
|
37
|
+
|
|
38
|
+
A **local** stylesheet is a different problem. Every `.css` file that already lives in the
|
|
39
|
+
section's folder is collected on its own, by filename — the build doesn't need an import
|
|
40
|
+
to find it. Importing it again from the render path doesn't just ship duplicate rules
|
|
41
|
+
harmlessly: it can also smuggle a stray `@layer` order statement into a nested layer and
|
|
42
|
+
corrupt the cascade for the whole page.
|
|
43
|
+
|
|
44
|
+
## Fix
|
|
45
|
+
|
|
46
|
+
Delete the import. A local stylesheet already ships — the build picks up every `.css` file
|
|
47
|
+
in the section folder on its own; importing it again only makes it double-emit.
|
|
48
|
+
|
|
49
|
+
A package's stylesheet is different: import it **by its bare specifier**, from the module
|
|
50
|
+
that needs it. The build bundles `Renderer.tsx` and every marked enhancer's import graph
|
|
51
|
+
**transitively**, so a bare specifier is equally legal in the Renderer itself, in a marked
|
|
52
|
+
enhancer, or in any component either one imports — not only at the top of the tree. That
|
|
53
|
+
import is how the build finds the stylesheet and routes it into the section's cascade
|
|
54
|
+
layer.
|
|
55
|
+
|
|
56
|
+
### Before
|
|
57
|
+
|
|
58
|
+
```tsx
|
|
59
|
+
// sections/gallery/Renderer.tsx
|
|
60
|
+
import { Section } from "@homepages/template-kit";
|
|
61
|
+
|
|
62
|
+
import "./styles.css"; // template-kit/no-css-import-from-render-path — already collected by filename
|
|
63
|
+
|
|
64
|
+
import type { Props } from "./schema.js";
|
|
65
|
+
|
|
66
|
+
export function Renderer({ slots }: Props) {
|
|
67
|
+
return (
|
|
68
|
+
<Section>
|
|
69
|
+
<div className="tr-gallery-track">{slots.headline}</div>
|
|
70
|
+
</Section>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### After
|
|
76
|
+
|
|
77
|
+
The local import is simply deleted — `styles.css` was already shipping:
|
|
78
|
+
|
|
79
|
+
```tsx
|
|
80
|
+
// sections/gallery/Renderer.tsx
|
|
81
|
+
import { Section } from "@homepages/template-kit";
|
|
82
|
+
|
|
83
|
+
import type { Props } from "./schema.js";
|
|
84
|
+
|
|
85
|
+
export function Renderer({ slots }: Props) {
|
|
86
|
+
return (
|
|
87
|
+
<Section>
|
|
88
|
+
<div className="tr-gallery-track">{slots.headline}</div>
|
|
89
|
+
</Section>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Compare a **package's** stylesheet, which is the legal counterpart — imported by its bare
|
|
95
|
+
specifier, from the same file, and left in place:
|
|
96
|
+
|
|
97
|
+
```tsx
|
|
98
|
+
// sections/gallery/Renderer.tsx
|
|
99
|
+
import { Section } from "@homepages/template-kit";
|
|
100
|
+
import "swiper/css"; // legal — a bare specifier is how package CSS reaches this section's layer
|
|
101
|
+
|
|
102
|
+
import type { Props } from "./schema.js";
|
|
103
|
+
|
|
104
|
+
export function Renderer({ slots }: Props) {
|
|
105
|
+
return (
|
|
106
|
+
<Section>
|
|
107
|
+
<div className="tr-gallery-track">{slots.headline}</div>
|
|
108
|
+
</Section>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## See also
|
|
114
|
+
|
|
115
|
+
- [`no-bare-css-import`](no-bare-css-import.md) — the other half of this mental model.
|
|
116
|
+
Render path: no *local* CSS imports. Hand-written CSS: no `@import`s at all, package or
|
|
117
|
+
otherwise. An author who reads only one of the two pages will be confused by the other.
|
|
118
|
+
- [Theme and CSS](../theme-and-css.md) — the two CSS entries and how they load.
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
---
|
|
2
|
+
purpose: The template-kit/no-hex authoring rule — what it enforces, why, and how to fix it.
|
|
3
|
+
status: living
|
|
4
|
+
related: [INDEX.md, server-vs-client.md, ../theme-and-css.md]
|
|
5
|
+
updated: 2026-07-14
|
|
6
|
+
---
|
|
7
|
+
# template-kit/no-hex
|
|
8
|
+
|
|
9
|
+
> No hard-coded colours in section code. **Islands are not exempt** — this is the one
|
|
10
|
+
> rule that holds on both sides of the hydration boundary.
|
|
11
|
+
|
|
12
|
+
## Rule
|
|
13
|
+
|
|
14
|
+
Two shapes are banned:
|
|
15
|
+
|
|
16
|
+
- **A CSS colour function**, anywhere in a string or template literal:
|
|
17
|
+
`oklch(…)`, `oklab(…)`, `rgb(…)`, `rgba(…)`, `hsl(…)`, `hsla(…)`, `color-mix(…)`.
|
|
18
|
+
No context test — nobody spells an anchor id `rgb(...)`.
|
|
19
|
+
- **A hex literal** (3, 4, 6, or 8 digits) **in a colour context**. A colour context is
|
|
20
|
+
a Tailwind arbitrary-value bracket that resolves to a colour:
|
|
21
|
+
- a colour property — `[color:#fff]`, `[background:#fff]`, `[background-color:#fff]`,
|
|
22
|
+
and the rest of `border-color`, `outline-color`, `box-shadow`, `text-shadow`,
|
|
23
|
+
`fill`, `stroke`, `caret-color`, `accent-color`, `text-decoration-color`;
|
|
24
|
+
- **any** custom property — `[--brand:#ff0000]`;
|
|
25
|
+
- a colour-utility prefix, with variants stripped — `bg-[#f00]`, `hover:bg-[#f00]`,
|
|
26
|
+
`md:dark:text-[#f00]`, `border-t-[#000]`, and the same for `ring`, `outline`,
|
|
27
|
+
`shadow`, `fill`, `stroke`, `decoration`, `divide`, `accent`, `caret`,
|
|
28
|
+
`placeholder`, `from`, `via`, `to`;
|
|
29
|
+
- or anywhere inside a JSX `style` attribute.
|
|
30
|
+
|
|
31
|
+
Everywhere else, a `#`-prefixed string is left alone: `href="#cafe"` is an anchor, and
|
|
32
|
+
`w-[#…]` is not a colour utility. `#cafe`, `#facade`, `#decade`, and `#beef` are real
|
|
33
|
+
anchor ids that happen to be spelled from hex digits.
|
|
34
|
+
|
|
35
|
+
Every hex-shaped token in a string is checked, not just the first — an out-of-context
|
|
36
|
+
token early in a `className` cannot shadow a genuine colour later in it.
|
|
37
|
+
|
|
38
|
+
## Reason
|
|
39
|
+
|
|
40
|
+
A section carries **zero palette of its own**. It is theme-agnostic, and must look
|
|
41
|
+
right under every template's theme — so a baked colour breaks the first time another
|
|
42
|
+
template reuses the section. Keeping colour in tokens is what makes a section portable
|
|
43
|
+
rather than a one-off.
|
|
44
|
+
|
|
45
|
+
This is why islands get no exemption. An island is exempt from the effect, state,
|
|
46
|
+
handler, and inline-`style` bans because those are server-render concerns and an island
|
|
47
|
+
runs in the browser. A palette is not a render-path concern: the hydration boundary is
|
|
48
|
+
not a palette boundary.
|
|
49
|
+
|
|
50
|
+
## Fix
|
|
51
|
+
|
|
52
|
+
Use a semantic token utility (`bg-surface-alt`, `text-ink`). If the value is genuinely
|
|
53
|
+
brand decoration with no token, add it to the **template's** `theme.ts` palette and use
|
|
54
|
+
the `--tr-color-<name>` utility it compiles to — which keeps the value in one place
|
|
55
|
+
instead of in one section.
|
|
56
|
+
|
|
57
|
+
### Before
|
|
58
|
+
|
|
59
|
+
```tsx
|
|
60
|
+
import { Section } from "@homepages/template-kit";
|
|
61
|
+
|
|
62
|
+
import type { Props } from "./schema.js";
|
|
63
|
+
|
|
64
|
+
export function Renderer({ slots }: Props) {
|
|
65
|
+
return (
|
|
66
|
+
<Section>
|
|
67
|
+
<div className="bg-[#f5f5f5]">
|
|
68
|
+
<h2 style={{ color: "#1a1a1a" }}>{slots.headline}</h2>
|
|
69
|
+
</div>
|
|
70
|
+
</Section>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### After
|
|
76
|
+
|
|
77
|
+
```tsx
|
|
78
|
+
import { Section } from "@homepages/template-kit";
|
|
79
|
+
|
|
80
|
+
import type { Props } from "./schema.js";
|
|
81
|
+
|
|
82
|
+
export function Renderer({ slots }: Props) {
|
|
83
|
+
return (
|
|
84
|
+
<Section>
|
|
85
|
+
<div className="bg-surface-alt">
|
|
86
|
+
<h2 className="text-ink">{slots.headline}</h2>
|
|
87
|
+
</div>
|
|
88
|
+
</Section>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## See also
|
|
94
|
+
|
|
95
|
+
- [Theme and CSS](../theme-and-css.md#color-roles) — the token system and the colour roles.
|
|
96
|
+
- [Server vs client](server-vs-client.md) — why the other bans stop at the hydration
|
|
97
|
+
boundary and this one does not.
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
---
|
|
2
|
+
purpose: The template-kit/no-inline-style authoring rule — what it enforces, why, and how to fix it.
|
|
3
|
+
status: living
|
|
4
|
+
related: [INDEX.md, server-vs-client.md, ../theme-and-css.md]
|
|
5
|
+
updated: 2026-07-14
|
|
6
|
+
---
|
|
7
|
+
# template-kit/no-inline-style
|
|
8
|
+
|
|
9
|
+
> No inline `style=` in server-rendered section JSX. Islands are exempt — but an island
|
|
10
|
+
> is **not** exempt from [`no-hex`](no-hex.md).
|
|
11
|
+
|
|
12
|
+
## Rule
|
|
13
|
+
|
|
14
|
+
Whether a file is server-rendered or an island is decided per file, by the `"use client"`
|
|
15
|
+
directive — see [server vs client](server-vs-client.md). On an island this rule installs
|
|
16
|
+
no visitors at all.
|
|
17
|
+
|
|
18
|
+
In a server-rendered file it reports **any JSX attribute named `style`, on any element**.
|
|
19
|
+
The value is never inspected: a static object, a computed one, a spread, a string — all
|
|
20
|
+
the same. There is no "but this one is fine" case in a server-rendered file.
|
|
21
|
+
|
|
22
|
+
**The island exemption does not extend to colour.** `no-hex` reads inside `style` values
|
|
23
|
+
and has no island exemption at all, so this still fails on a `"use client"` file:
|
|
24
|
+
|
|
25
|
+
```tsx
|
|
26
|
+
"use client";
|
|
27
|
+
|
|
28
|
+
export default function Badge() {
|
|
29
|
+
return <div style={{ color: "#ff0000" }} />; // template-kit/no-hex
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Exempt from *this* rule; still a hard-coded colour.
|
|
34
|
+
|
|
35
|
+
## Reason
|
|
36
|
+
|
|
37
|
+
Three concrete failures, all of them server-render concerns:
|
|
38
|
+
|
|
39
|
+
- **It bypasses the cascade.** An inline style is the highest-specificity declaration
|
|
40
|
+
short of `!important`, so a template's theme can no longer restyle the section. The
|
|
41
|
+
section stops being portable.
|
|
42
|
+
- **It skips Tailwind's responsive prefixes.** `style={{ padding: 16 }}` has no
|
|
43
|
+
breakpoint form; there is no way to say "16 on mobile, 48 on desktop" in it. Utilities
|
|
44
|
+
have exactly that (`p-4 lg:p-12`), which is why the layout ladder lives in classes.
|
|
45
|
+
- **It churns per-call identity** that the editor's hash-based diff cannot dedupe — the
|
|
46
|
+
editor re-renders the section on every keystroke, and a fresh style object each render
|
|
47
|
+
is work it cannot skip.
|
|
48
|
+
|
|
49
|
+
An island computing a transform from live pointer state has none of these problems: it
|
|
50
|
+
runs in the browser, it is not part of the cascade the theme is styling, and that is
|
|
51
|
+
precisely what an island is for. So it is exempt.
|
|
52
|
+
|
|
53
|
+
## Fix
|
|
54
|
+
|
|
55
|
+
Use Tailwind utility classes. For a value that only exists at runtime, move the markup
|
|
56
|
+
into a `"use client"` island, where `style` is allowed.
|
|
57
|
+
|
|
58
|
+
### Before
|
|
59
|
+
|
|
60
|
+
```tsx
|
|
61
|
+
import { Section } from "@homepages/template-kit";
|
|
62
|
+
|
|
63
|
+
import type { Props } from "./schema.js";
|
|
64
|
+
|
|
65
|
+
export function Renderer({ slots }: Props) {
|
|
66
|
+
return (
|
|
67
|
+
<Section>
|
|
68
|
+
<div style={{ padding: 16, display: "flex", gap: 8 }}>
|
|
69
|
+
<h2>{slots.headline}</h2>
|
|
70
|
+
</div>
|
|
71
|
+
</Section>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### After
|
|
77
|
+
|
|
78
|
+
```tsx
|
|
79
|
+
import { Section } from "@homepages/template-kit";
|
|
80
|
+
|
|
81
|
+
import type { Props } from "./schema.js";
|
|
82
|
+
|
|
83
|
+
export function Renderer({ slots }: Props) {
|
|
84
|
+
return (
|
|
85
|
+
<Section>
|
|
86
|
+
<div className="flex gap-2 p-4">
|
|
87
|
+
<h2>{slots.headline}</h2>
|
|
88
|
+
</div>
|
|
89
|
+
</Section>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
The runtime-value case — a transform driven by live state — has no utility form, and is
|
|
95
|
+
the case the island exemption exists for:
|
|
96
|
+
|
|
97
|
+
```tsx
|
|
98
|
+
// sections/hero/Parallax.tsx
|
|
99
|
+
"use client";
|
|
100
|
+
|
|
101
|
+
import { useState } from "react";
|
|
102
|
+
import type { ReactNode } from "react";
|
|
103
|
+
|
|
104
|
+
export default function Parallax({ children }: { children: ReactNode }) {
|
|
105
|
+
const [offset, setOffset] = useState(0);
|
|
106
|
+
return (
|
|
107
|
+
<div
|
|
108
|
+
onPointerMove={(e) => setOffset(e.clientX / 40)}
|
|
109
|
+
style={{ transform: `translateX(${offset}px)` }}
|
|
110
|
+
>
|
|
111
|
+
{children}
|
|
112
|
+
</div>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## See also
|
|
118
|
+
|
|
119
|
+
- [Server vs client](server-vs-client.md) — the island exemption, and the one rule that
|
|
120
|
+
does not take it.
|
|
121
|
+
- [Theme and CSS](../theme-and-css.md) — the token utilities and the breakpoint ladder.
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
---
|
|
2
|
+
purpose: The template-kit/no-nondeterminism authoring rule — what it enforces, why, and how to fix it.
|
|
3
|
+
status: living
|
|
4
|
+
related: [INDEX.md, server-vs-client.md]
|
|
5
|
+
updated: 2026-07-14
|
|
6
|
+
---
|
|
7
|
+
# template-kit/no-nondeterminism
|
|
8
|
+
|
|
9
|
+
> No clock reads and no randomness in server-rendered code. Islands are exempt.
|
|
10
|
+
|
|
11
|
+
## Rule
|
|
12
|
+
|
|
13
|
+
Whether a file is server-rendered or an island is decided per file, by the `"use client"`
|
|
14
|
+
directive — see [server vs client](server-vs-client.md). On an island this rule installs
|
|
15
|
+
no visitors at all.
|
|
16
|
+
|
|
17
|
+
In a server-rendered file, two shapes are banned:
|
|
18
|
+
|
|
19
|
+
- **A read of a nondeterministic source**: `Date.now`, `Math.random`,
|
|
20
|
+
`crypto.randomUUID`, `crypto.getRandomValues`, `performance.now`. The member expression
|
|
21
|
+
alone is enough — `Date.now` reports even without a call, because handing the function
|
|
22
|
+
itself somewhere is the same read one hop later.
|
|
23
|
+
- **`new Date()` with no arguments.** That reads the clock.
|
|
24
|
+
|
|
25
|
+
Two carve-outs:
|
|
26
|
+
|
|
27
|
+
- **`new Date(props.listed_at)` is fine.** It parses a value the section was *given*,
|
|
28
|
+
which is perfectly deterministic. Only the zero-argument form reads the clock.
|
|
29
|
+
- **Your own definitions are yours.** If the object name resolves to a real local
|
|
30
|
+
declaration — `const performance = { now: () => 1 };` — it is not the global, and the
|
|
31
|
+
rule leaves it alone.
|
|
32
|
+
|
|
33
|
+
## Reason
|
|
34
|
+
|
|
35
|
+
The publisher and the editor both run a section's `Renderer`, and they must produce
|
|
36
|
+
**byte-identical HTML** from the same props. A clock read or a random value makes the two
|
|
37
|
+
renders disagree: the section's output is hashed, so a `Date.now()` in the markup means
|
|
38
|
+
the section looks changed on every single render — a fresh version each publish, and an
|
|
39
|
+
editor canvas that never settles.
|
|
40
|
+
|
|
41
|
+
An island does not have this problem. It runs in the browser, after hydration, and its
|
|
42
|
+
output is nobody's content hash. That is why it is exempt.
|
|
43
|
+
|
|
44
|
+
## Fix
|
|
45
|
+
|
|
46
|
+
Compute the value **at fill time** and pass it in as a slot value — that is the right
|
|
47
|
+
route for anything that is content. Reach for an island only when the value is genuinely
|
|
48
|
+
live (a countdown, a "time since listed" that must tick).
|
|
49
|
+
|
|
50
|
+
### Before
|
|
51
|
+
|
|
52
|
+
```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
|
+
<Slot id="headline" as="h1" textLeaf>
|
|
61
|
+
<span>{slots.headline}</span>
|
|
62
|
+
</Slot>
|
|
63
|
+
<p>© {new Date().getFullYear()} — all rights reserved</p>
|
|
64
|
+
</Section>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### After
|
|
70
|
+
|
|
71
|
+
The year is content, so it becomes a slot the fill pipeline supplies. Declare it in
|
|
72
|
+
`schema.ts`, and the Renderer just renders what it was given:
|
|
73
|
+
|
|
74
|
+
```tsx
|
|
75
|
+
import { Section, Slot } from "@homepages/template-kit";
|
|
76
|
+
|
|
77
|
+
import type { Props } from "./schema.js";
|
|
78
|
+
|
|
79
|
+
export function Renderer({ slots }: Props) {
|
|
80
|
+
return (
|
|
81
|
+
<Section>
|
|
82
|
+
<Slot id="headline" as="h1" textLeaf>
|
|
83
|
+
<span>{slots.headline}</span>
|
|
84
|
+
</Slot>
|
|
85
|
+
<Slot id="copyright_year" textLeaf>
|
|
86
|
+
<p>© {slots.copyright_year} — all rights reserved</p>
|
|
87
|
+
</Slot>
|
|
88
|
+
</Section>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
If the value must be live in the browser rather than baked at fill time, move that
|
|
94
|
+
markup into a `"use client"` component in the section folder instead, and render it from
|
|
95
|
+
the Renderer — the rule does not run there.
|
|
96
|
+
|
|
97
|
+
## See also
|
|
98
|
+
|
|
99
|
+
- [Server vs client](server-vs-client.md) — how a file is scoped, and what an island may do.
|
|
100
|
+
- [Schema system](../schema-system.md) — declaring the slot the value arrives in.
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
---
|
|
2
|
+
purpose: The template-kit/no-raw-element authoring rule — what it enforces, why, and how to fix it.
|
|
3
|
+
status: living
|
|
4
|
+
related: [INDEX.md, ../primitives.md]
|
|
5
|
+
updated: 2026-07-14
|
|
6
|
+
---
|
|
7
|
+
# template-kit/no-raw-element
|
|
8
|
+
|
|
9
|
+
> **Exactly two** raw elements are banned: `<section>` and `<img>`. Every other tag is
|
|
10
|
+
> yours.
|
|
11
|
+
|
|
12
|
+
## Rule
|
|
13
|
+
|
|
14
|
+
Two tags, and no others:
|
|
15
|
+
|
|
16
|
+
| Raw tag | Use instead |
|
|
17
|
+
|---|---|
|
|
18
|
+
| `<section>` | `<Section>` |
|
|
19
|
+
| `<img>` | `<Image>` |
|
|
20
|
+
|
|
21
|
+
That is the entire rule. **`<a>`, `<h1>`–`<h6>`, `<div>`, `<ul>`, `<button>`, `<picture>`,
|
|
22
|
+
`<figure>` — every other element is yours to write.** Presentation is the template's own
|
|
23
|
+
JSX; the kit is not trying to own your markup. If you have concluded "the kit bans raw
|
|
24
|
+
HTML", that is exactly backwards.
|
|
25
|
+
|
|
26
|
+
There is no island exemption: the two tags are banned in a `"use client"` file too.
|
|
27
|
+
|
|
28
|
+
## Reason
|
|
29
|
+
|
|
30
|
+
Each of the two banned tags has a **contract-bearing primitive that must emit it**, and
|
|
31
|
+
writing the raw tag skips the contract.
|
|
32
|
+
|
|
33
|
+
- **`<Section>`** emits `<section class="tr-section">`, which is full-bleed by contract:
|
|
34
|
+
100% width, zero padding, zero margin, so consecutive sections butt edge-to-edge and a
|
|
35
|
+
page composes as a clean vertical stack. A raw `<section>` lacks that class, so the
|
|
36
|
+
page grows gaps the template never asked for.
|
|
37
|
+
- **`<Image>`** owns the responsive `<picture>` (per-format AVIF/WebP `srcset`, the
|
|
38
|
+
mobile crop, `sizes`), the aspect-ratio box, and the neutral fallback rect that keeps
|
|
39
|
+
the layout box when `src` is empty. A raw `<img>` with a null `src` is a collapsed box
|
|
40
|
+
or a broken-image icon on a real customer's page — and a missing image is a **supported
|
|
41
|
+
state**, not a bug: it happens whenever a listing is thin. `<Image slotId>` also emits
|
|
42
|
+
the slot marker itself, so the image stays selectable in the editor.
|
|
43
|
+
|
|
44
|
+
The two tags are banned because they are the two the platform has to be able to trust.
|
|
45
|
+
Everything else carries no contract, so nothing is gained by policing it.
|
|
46
|
+
|
|
47
|
+
## Fix
|
|
48
|
+
|
|
49
|
+
Import the primitive from the package root and use it:
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
import { Section, Image } from "@homepages/template-kit";
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Before
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
import type { Props } from "./schema.js";
|
|
59
|
+
|
|
60
|
+
export function Renderer({ slots }: Props) {
|
|
61
|
+
return (
|
|
62
|
+
<section>
|
|
63
|
+
<h2>{slots.headline}</h2>
|
|
64
|
+
<img src={slots.hero_image.url ?? ""} alt={slots.hero_image.alt} />
|
|
65
|
+
</section>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### After
|
|
71
|
+
|
|
72
|
+
```tsx
|
|
73
|
+
import { Image, Section } from "@homepages/template-kit";
|
|
74
|
+
|
|
75
|
+
import type { Props } from "./schema.js";
|
|
76
|
+
|
|
77
|
+
export function Renderer({ slots }: Props) {
|
|
78
|
+
return (
|
|
79
|
+
<Section>
|
|
80
|
+
<h2>{slots.headline}</h2>
|
|
81
|
+
<Image
|
|
82
|
+
slotId="hero_image"
|
|
83
|
+
src={slots.hero_image.url}
|
|
84
|
+
alt={slots.hero_image.alt}
|
|
85
|
+
responsive={slots.hero_image.responsive}
|
|
86
|
+
aspectRatio="5 / 3"
|
|
87
|
+
/>
|
|
88
|
+
</Section>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The `<h2>` stays exactly as it was. `Image` takes `src`/`alt` directly — a `null` or empty
|
|
94
|
+
`src` is a supported state it renders a fallback rect for, so no `?? ""` guard is needed.
|
|
95
|
+
|
|
96
|
+
## See also
|
|
97
|
+
|
|
98
|
+
- [Contract primitives](../primitives.md) — all five primitives and the markers they emit.
|
|
99
|
+
- [`image-bare-needs-reason`](image-bare-needs-reason.md) — the escape hatch that drops
|
|
100
|
+
`<Image>`'s frame, and what it costs.
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
---
|
|
2
|
+
purpose: The template-kit/props-from-schema 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/props-from-schema
|
|
8
|
+
|
|
9
|
+
> A Renderer's props type is **derived** from `schema.ts`, never hand-written in the
|
|
10
|
+
> Renderer.
|
|
11
|
+
|
|
12
|
+
## Rule
|
|
13
|
+
|
|
14
|
+
The rule runs only on `Renderer.tsx`. It reports an **exported** type alias or interface
|
|
15
|
+
whose name **ends in `Props`** — `Props`, `HeroProps`, `GalleryProps`.
|
|
16
|
+
|
|
17
|
+
Two consequences of that being the whole test:
|
|
18
|
+
|
|
19
|
+
- **It does not read the right-hand side.** Even `export type Props = SectionProps<typeof
|
|
20
|
+
schema>;` reports *if you write it in the Renderer*. The derivation belongs in
|
|
21
|
+
`schema.ts`; the Renderer **imports** it.
|
|
22
|
+
- **A non-exported local type is invisible** to the rule. An internal
|
|
23
|
+
`type Row = { id: string };` used to type a helper's parameter is your business, and is
|
|
24
|
+
not reported.
|
|
25
|
+
|
|
26
|
+
`import type { Props } from "./schema.js";` is the shape the rule is steering you to, and
|
|
27
|
+
it reports nothing.
|
|
28
|
+
|
|
29
|
+
## Reason
|
|
30
|
+
|
|
31
|
+
The schema is the section's single source of truth, and the prop type is **inferred from
|
|
32
|
+
it** — `SectionProps<typeof schema>` reads the schema literal and resolves each slot to
|
|
33
|
+
its runtime value shape: a required `text` becomes `string`, an optional one
|
|
34
|
+
`string | null`, an `image` becomes `ImageValue`, and so on.
|
|
35
|
+
|
|
36
|
+
A hand-written `Props` shadows that inference, and then **drifts silently**. Add a slot
|
|
37
|
+
to `schema.ts` and forget to render it: with the derived type, reading `slots.subhead` in
|
|
38
|
+
the Renderer is a compile error until the slot exists, and the platform's tooling and
|
|
39
|
+
your renderer agree by construction. With a hand-written `Props`, the Renderer keeps
|
|
40
|
+
compiling happily against the old shape — while the editor, the fill pipeline, and the
|
|
41
|
+
schema all now believe in a slot your markup has never heard of. Nothing fails until a
|
|
42
|
+
user is looking at a page with a missing block in it.
|
|
43
|
+
|
|
44
|
+
It also forces the editor to reason about a second definition of the same thing.
|
|
45
|
+
|
|
46
|
+
## Fix
|
|
47
|
+
|
|
48
|
+
Declare the derived type once in `schema.ts` and import it in the Renderer:
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
export type Props = SectionProps<typeof schema>;
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
import type { Props } from "./schema.js";
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Before
|
|
59
|
+
|
|
60
|
+
```tsx
|
|
61
|
+
// sections/hero/Renderer.tsx
|
|
62
|
+
import { Section, Slot } from "@homepages/template-kit";
|
|
63
|
+
|
|
64
|
+
export type Props = {
|
|
65
|
+
slots: { headline: string };
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export function Renderer({ slots }: Props) {
|
|
69
|
+
return (
|
|
70
|
+
<Section>
|
|
71
|
+
<Slot id="headline" as="h1" textLeaf>
|
|
72
|
+
<span>{slots.headline}</span>
|
|
73
|
+
</Slot>
|
|
74
|
+
</Section>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### After
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
// sections/hero/schema.ts
|
|
83
|
+
import type { SectionProps, SectionSchemaShape } from "@homepages/template-kit";
|
|
84
|
+
|
|
85
|
+
export const schema = {
|
|
86
|
+
meta: { displayName: "Hero" },
|
|
87
|
+
slots: {
|
|
88
|
+
headline: { type: "text", size: "medium", required: true, produced_by: "headline_text" },
|
|
89
|
+
},
|
|
90
|
+
} as const satisfies SectionSchemaShape;
|
|
91
|
+
|
|
92
|
+
export type Props = SectionProps<typeof schema>;
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
```tsx
|
|
96
|
+
// sections/hero/Renderer.tsx
|
|
97
|
+
import { Section, Slot } from "@homepages/template-kit";
|
|
98
|
+
|
|
99
|
+
import type { Props } from "./schema.js";
|
|
100
|
+
|
|
101
|
+
export function Renderer({ slots }: Props) {
|
|
102
|
+
return (
|
|
103
|
+
<Section>
|
|
104
|
+
<Slot id="headline" as="h1" textLeaf>
|
|
105
|
+
<span>{slots.headline}</span>
|
|
106
|
+
</Slot>
|
|
107
|
+
</Section>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
The `as const satisfies SectionSchemaShape` is load-bearing: without it the literal types
|
|
113
|
+
are widened away and `SectionProps` can no longer tell a required slot from an optional
|
|
114
|
+
one.
|
|
115
|
+
|
|
116
|
+
## See also
|
|
117
|
+
|
|
118
|
+
- [Schema system](../schema-system.md) — the schema literal, `SectionProps` inference,
|
|
119
|
+
and the four contract files.
|