@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,88 @@
|
|
|
1
|
+
---
|
|
2
|
+
purpose: The template-kit/lockfile-stale authoring rule — what it enforces, why, and how to fix it.
|
|
3
|
+
status: living
|
|
4
|
+
related: [INDEX.md, lockfile-missing.md]
|
|
5
|
+
updated: 2026-07-14
|
|
6
|
+
---
|
|
7
|
+
# template-kit/lockfile-stale
|
|
8
|
+
|
|
9
|
+
> The lockfile must satisfy `package.json`. If `npm ci` would fail, this fails first — on
|
|
10
|
+
> your machine, where you can still do something about it.
|
|
11
|
+
|
|
12
|
+
## Rule
|
|
13
|
+
|
|
14
|
+
`package-lock.json` must be consistent with `package.json`: every dependency the manifest
|
|
15
|
+
asks for is present in the lockfile, at a version its range accepts.
|
|
16
|
+
|
|
17
|
+
Like [`lockfile-missing`](lockfile-missing.md), this is a property of the **workspace**, not
|
|
18
|
+
of any one template, and is reported once.
|
|
19
|
+
|
|
20
|
+
## Reason
|
|
21
|
+
|
|
22
|
+
Your submission is installed with **`npm ci`**, which does not resolve versions — it
|
|
23
|
+
installs exactly what the lockfile pins, and **aborts** if the lockfile and the manifest
|
|
24
|
+
disagree. A lockfile that cannot reproduce the tree is not a slightly-out-of-date
|
|
25
|
+
convenience file; it is an install that does not happen.
|
|
26
|
+
|
|
27
|
+
Checking it here means you see it now. Otherwise the first thing to notice is the install
|
|
28
|
+
on our side, and the first thing you hear is that your submission was rejected.
|
|
29
|
+
|
|
30
|
+
There are two ways authors get here, and both are ordinary:
|
|
31
|
+
|
|
32
|
+
- **Hand-editing a version in `package.json`** — bumping `^11.0.0` to `^12.0.0` in the
|
|
33
|
+
editor and never running an install.
|
|
34
|
+
- **Merging a branch** and keeping only one side of the lockfile — resolving the conflict by
|
|
35
|
+
taking `package.json` from one branch and `package-lock.json` from the other. The two now
|
|
36
|
+
describe different trees.
|
|
37
|
+
|
|
38
|
+
## Fix
|
|
39
|
+
|
|
40
|
+
Run `npm install` — it resyncs the lockfile to the manifest — and commit the result. Do not
|
|
41
|
+
hand-edit `package-lock.json`; regenerate it. If you got here from a merge conflict, take
|
|
42
|
+
`package.json` first, then let `npm install` rebuild the lockfile from it.
|
|
43
|
+
|
|
44
|
+
### Before
|
|
45
|
+
|
|
46
|
+
`package.json` asks for Swiper 12; the lockfile still pins 11.
|
|
47
|
+
|
|
48
|
+
```jsonc
|
|
49
|
+
// package.json — hand-edited
|
|
50
|
+
{
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"swiper": "^12.0.0"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
```jsonc
|
|
58
|
+
// package-lock.json — never regenerated
|
|
59
|
+
{
|
|
60
|
+
"packages": {
|
|
61
|
+
"node_modules/swiper": { "version": "11.2.10" }
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`npm ci`: `Invalid: lock file's swiper@11.2.10 does not satisfy swiper@^12.0.0`.
|
|
67
|
+
|
|
68
|
+
### After
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
npm install # resolves ^12.0.0 and rewrites package-lock.json
|
|
72
|
+
git add package.json package-lock.json
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
```jsonc
|
|
76
|
+
// package-lock.json — back in sync
|
|
77
|
+
{
|
|
78
|
+
"packages": {
|
|
79
|
+
"node_modules/swiper": { "version": "12.0.4" }
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## See also
|
|
85
|
+
|
|
86
|
+
- [`lockfile-missing`](lockfile-missing.md) — there is no lockfile at all.
|
|
87
|
+
- [`single-react`](single-react.md) — the other property of the installed tree the platform
|
|
88
|
+
checks before it builds your page.
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
---
|
|
2
|
+
purpose: The template-kit/missing-slot-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/missing-slot-marker
|
|
8
|
+
|
|
9
|
+
> Every slot declared `editable_by_user: true` must be **marked** in the section's JSX.
|
|
10
|
+
> An unmarked slot still fills and still renders — it just cannot be clicked.
|
|
11
|
+
|
|
12
|
+
## Rule
|
|
13
|
+
|
|
14
|
+
For each slot in `schema.ts` with `editable_by_user: true`, the section's JSX must carry a
|
|
15
|
+
marker whose key is that slot id, written as a **string literal**. Any of these counts:
|
|
16
|
+
|
|
17
|
+
| Marker | Example |
|
|
18
|
+
|---|---|
|
|
19
|
+
| `<Slot id>` | `<Slot id="headline" textLeaf>` |
|
|
20
|
+
| `slotId` on a primitive that emits its own marker | `<Image slotId="hero_image" …>` |
|
|
21
|
+
| a raw `data-slot-id` attribute | `<div data-slot-id="headline">` |
|
|
22
|
+
|
|
23
|
+
The scan covers **every `.tsx` in the section folder**, not only `Renderer.tsx` — a marker
|
|
24
|
+
inside a section-local component satisfies it. Test files are excluded.
|
|
25
|
+
|
|
26
|
+
Carve-outs:
|
|
27
|
+
|
|
28
|
+
- **A slot that is not `editable_by_user` needs no marker.** It cannot be selected in the
|
|
29
|
+
editor, so there is nothing to mark. (`editable_by_user` defaults to `false`.)
|
|
30
|
+
- **A computed key does not satisfy the rule.** `<Slot id={fooKey}>` or an id produced
|
|
31
|
+
inside a `.map()` is not a literal, so the check cannot resolve it — and
|
|
32
|
+
[`slot-marker-literal`](slot-marker-literal.md) reports it separately. A `.map()`-generated
|
|
33
|
+
key therefore fails both at once.
|
|
34
|
+
|
|
35
|
+
The practical corollary of the two above: when you hoist markup into a section-local
|
|
36
|
+
helper, **do not pass the slot key through it as a prop**. `<Field slotKey="headline">`
|
|
37
|
+
wrapping an inner `<Slot id={slotKey}>` erases the literal from the source entirely, and
|
|
38
|
+
the slot reads as unmarked. Keep the literal `<Slot id="…">` where the slot is rendered
|
|
39
|
+
and move only the data layer and leaf presentation out.
|
|
40
|
+
|
|
41
|
+
## Reason
|
|
42
|
+
|
|
43
|
+
The markers are the only thing connecting the pixels a user clicks to the schema key
|
|
44
|
+
behind them. The editor's click resolver walks up from the clicked node looking for a
|
|
45
|
+
`data-slot-id`; a slot that emits none is invisible to it.
|
|
46
|
+
|
|
47
|
+
Nothing throws. The slot is filled by AI, it renders its content on the published page,
|
|
48
|
+
and it looks completely correct — right up until a user clicks it in the editor and
|
|
49
|
+
selects nothing. That is the whole failure: **fillable but not editable**, discovered by a
|
|
50
|
+
customer rather than by you.
|
|
51
|
+
|
|
52
|
+
## Fix
|
|
53
|
+
|
|
54
|
+
Wrap the slot's outer DOM in `<Slot id="…">` — add `textLeaf` for a single-leaf text slot
|
|
55
|
+
so the editor can write to it on each keystroke. An image slot needs no wrapper: give
|
|
56
|
+
`<Image>` its `slotId` and it emits the marker itself.
|
|
57
|
+
|
|
58
|
+
A raw `data-slot-id` attribute is not a lesser fallback — for the marker itself, it's
|
|
59
|
+
identical to what `<Slot>` renders: `<Slot as="p" id="…">` emits exactly `<p
|
|
60
|
+
data-slot-id="…">`, nothing more. The one thing `<Slot textLeaf>` adds is
|
|
61
|
+
`data-slot-text-leaf` on the child, wired automatically, so the editor can patch text on
|
|
62
|
+
every keystroke instead of waiting for the next server render. So default to `<Slot>` for
|
|
63
|
+
the convenience — it's one prop instead of hand-wiring a second attribute — but writing
|
|
64
|
+
`data-slot-id` directly on markup you're already hand-assembling is a legitimate, idiomatic
|
|
65
|
+
fix, not a technicality; reach for it freely when you don't need `textLeaf`.
|
|
66
|
+
|
|
67
|
+
If the slot genuinely is not meant to be edited, say so in the schema rather than leaving
|
|
68
|
+
the check unsatisfied: `editable_by_user: false`.
|
|
69
|
+
|
|
70
|
+
### Before
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
// sections/hero/schema.ts
|
|
74
|
+
import type { SectionProps, SectionSchemaShape } from "@homepages/template-kit";
|
|
75
|
+
|
|
76
|
+
export const schema = {
|
|
77
|
+
meta: { displayName: "Hero" },
|
|
78
|
+
slots: {
|
|
79
|
+
headline: { type: "text", size: "medium", required: true, editable_by_user: true },
|
|
80
|
+
eyebrow: { type: "text", size: "short", required: false, editable_by_user: true },
|
|
81
|
+
},
|
|
82
|
+
} as const satisfies SectionSchemaShape;
|
|
83
|
+
|
|
84
|
+
export type Props = SectionProps<typeof schema>;
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
```tsx
|
|
88
|
+
// sections/hero/Renderer.tsx — template-kit/missing-slot-marker: eyebrow
|
|
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
|
+
<p className="text-ink-mute">{slots.eyebrow}</p>
|
|
97
|
+
<Slot id="headline" as="h1" textLeaf>
|
|
98
|
+
<span>{slots.headline}</span>
|
|
99
|
+
</Slot>
|
|
100
|
+
</Section>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### After
|
|
106
|
+
|
|
107
|
+
```tsx
|
|
108
|
+
// sections/hero/Renderer.tsx
|
|
109
|
+
import { Section, Slot } from "@homepages/template-kit";
|
|
110
|
+
|
|
111
|
+
import type { Props } from "./schema.js";
|
|
112
|
+
|
|
113
|
+
export function Renderer({ slots }: Props) {
|
|
114
|
+
return (
|
|
115
|
+
<Section>
|
|
116
|
+
<Slot id="eyebrow" textLeaf>
|
|
117
|
+
<p className="text-ink-mute">{slots.eyebrow}</p>
|
|
118
|
+
</Slot>
|
|
119
|
+
<Slot id="headline" as="h1" textLeaf>
|
|
120
|
+
<span>{slots.headline}</span>
|
|
121
|
+
</Slot>
|
|
122
|
+
</Section>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
If `eyebrow` was never meant to be editable, the other legitimate fix is in the schema —
|
|
128
|
+
and then no marker is needed:
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
eyebrow: { type: "text", size: "short", required: false, editable_by_user: false },
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## See also
|
|
135
|
+
|
|
136
|
+
- [Contract primitives](../primitives.md#the-marker-contract) — every marker attribute and
|
|
137
|
+
which primitive emits it.
|
|
138
|
+
- [`slot-marker-literal`](slot-marker-literal.md) — why the key must be a literal for this
|
|
139
|
+
check to see it at all.
|
|
140
|
+
- [`slot-group-marker`](slot-group-marker.md) — the same idea for a group of slots.
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
---
|
|
2
|
+
purpose: The template-kit/no-bare-css-import 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-bare-css-import
|
|
8
|
+
|
|
9
|
+
> No `@import` in hand-written CSS. **Not even a package's** — the browser drops it
|
|
10
|
+
> silently, and the styles simply never ship.
|
|
11
|
+
|
|
12
|
+
## Rule
|
|
13
|
+
|
|
14
|
+
A hand-written stylesheet may not contain a top-level `@import`. That means the
|
|
15
|
+
stylesheets the CSS build collects out of your section and template folders — a section's
|
|
16
|
+
`styles.css`, any other `.css` in a section folder, and a template's own hand-written
|
|
17
|
+
stylesheet.
|
|
18
|
+
|
|
19
|
+
**There is no carve-out for a package specifier.** `@import "swiper/css";` is exactly as
|
|
20
|
+
illegal here as `@import "./tokens.css";`. Both are reported, at the 1-based line of the
|
|
21
|
+
first offender.
|
|
22
|
+
|
|
23
|
+
The scan is comment-aware, so an `@import` written *about* rather than *as* code is prose
|
|
24
|
+
and passes:
|
|
25
|
+
|
|
26
|
+
```css
|
|
27
|
+
/* css-reason: … */
|
|
28
|
+
/* Never @import here — see template-kit/no-bare-css-import. */ ← fine, it's a comment
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**Your Tailwind entry is not one of these files.** The file that carries
|
|
32
|
+
`@import "tailwindcss";` and `@import "@homepages/template-kit/styles.css";` is a Tailwind
|
|
33
|
+
entry: it is compiled by Tailwind, its `@import`s are resolved at build time, and it is
|
|
34
|
+
never wrapped in a cascade layer. It is not hand-written section or template CSS, and this
|
|
35
|
+
rule does not reach it.
|
|
36
|
+
|
|
37
|
+
## Reason
|
|
38
|
+
|
|
39
|
+
Every hand-CSS file the build collects is **concatenated into a cascade layer**:
|
|
40
|
+
|
|
41
|
+
```css
|
|
42
|
+
@layer sections {
|
|
43
|
+
/* your styles.css, pasted in here */
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
That is how a section's CSS lands in the layer order (`theme, base, components, sections,
|
|
48
|
+
template, utilities`) instead of fighting Tailwind's utilities for specificity.
|
|
49
|
+
|
|
50
|
+
**`@import` is invalid inside a `@layer` block.** And CSS's error handling for an invalid
|
|
51
|
+
at-rule is not an error — it is a shrug. The browser drops the statement and parses on.
|
|
52
|
+
Nothing throws. Nothing warns. Your build succeeds, your preview looks *plausible* because
|
|
53
|
+
the package's own styling is missing in a way you have learned to read as "unstyled yet",
|
|
54
|
+
and the published page ships without the rules you imported. This is the quietest failure
|
|
55
|
+
in the whole kit: the only signal is that the styles are not there.
|
|
56
|
+
|
|
57
|
+
## Fix
|
|
58
|
+
|
|
59
|
+
Delete the `@import`.
|
|
60
|
+
|
|
61
|
+
**A package's stylesheet is reached from the code that needs it**, not from CSS. Write
|
|
62
|
+
`import "swiper/css";` in the component, island, or enhancer that renders the thing — the
|
|
63
|
+
CSS build bundles your `Renderer.tsx` and every marked enhancer, transitively, precisely to
|
|
64
|
+
harvest those imports, and it compiles what they pull in into **this section's cascade
|
|
65
|
+
layer**. Package CSS goes in first and your own hand-CSS second, so at equal specificity
|
|
66
|
+
your rules win.
|
|
67
|
+
|
|
68
|
+
Two things authors get wrong immediately after:
|
|
69
|
+
|
|
70
|
+
- **Bare specifiers only.** A CSS import that resolves outside `node_modules` is an error,
|
|
71
|
+
not a fallback. Your own stylesheets are discovered by filename — importing one from code
|
|
72
|
+
would ship it twice. See
|
|
73
|
+
[`no-css-import-from-render-path`](no-css-import-from-render-path.md).
|
|
74
|
+
- **Importing is not installing.** `import "swiper/css"` does not add `swiper` to your
|
|
75
|
+
`package.json`. Add the dependency, or the install that builds your submission has nothing
|
|
76
|
+
to resolve.
|
|
77
|
+
|
|
78
|
+
### Before
|
|
79
|
+
|
|
80
|
+
```css
|
|
81
|
+
/* sections/gallery/styles.css */
|
|
82
|
+
/* css-reason: styles the slide DOM the Swiper enhancer builds at runtime. */
|
|
83
|
+
@import "swiper/css"; /* template-kit/no-bare-css-import — silently dropped */
|
|
84
|
+
|
|
85
|
+
.tr-gallery .swiper-pagination-bullet-active {
|
|
86
|
+
background: var(--tr-color-primary);
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
The pagination override ships. Swiper's own layout CSS does not — and no error says so.
|
|
91
|
+
|
|
92
|
+
### After
|
|
93
|
+
|
|
94
|
+
The `@import` is gone from the stylesheet:
|
|
95
|
+
|
|
96
|
+
```css
|
|
97
|
+
/* sections/gallery/styles.css */
|
|
98
|
+
/* css-reason: overrides the Swiper pagination DOM the enhancer builds at runtime —
|
|
99
|
+
those nodes carry the library's own classes, which Tailwind's scanner never sees. */
|
|
100
|
+
.tr-gallery .swiper-pagination-bullet-active {
|
|
101
|
+
background: var(--tr-color-primary);
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
…and the package's CSS is imported from the module that needs it, which is what routes it
|
|
106
|
+
into this section's layer:
|
|
107
|
+
|
|
108
|
+
```tsx
|
|
109
|
+
// sections/gallery/Carousel.tsx
|
|
110
|
+
"use client";
|
|
111
|
+
|
|
112
|
+
import Swiper from "swiper";
|
|
113
|
+
import "swiper/css"; // ← the sanctioned channel: the CSS build harvests this import
|
|
114
|
+
|
|
115
|
+
export default function Carousel({ slides }: { slides: { id: string; url: string }[] }) {
|
|
116
|
+
// …
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## See also
|
|
121
|
+
|
|
122
|
+
- [`no-css-import-from-render-path`](no-css-import-from-render-path.md) — the other half of
|
|
123
|
+
this mental model. Hand-written CSS: no `@import`s at all, package or otherwise. Render
|
|
124
|
+
path: no *local* CSS imports, but a package's bare specifier is exactly right. An author
|
|
125
|
+
who reads only one of the two pages will be confused by the other.
|
|
126
|
+
- [`css-reason`](css-reason.md) — the comment every hand-written stylesheet opens with.
|
|
127
|
+
- [Theme and CSS](../theme-and-css.md) — the cascade-layer order, and the one file that
|
|
128
|
+
legitimately does `@import "tailwindcss"`.
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
---
|
|
2
|
+
purpose: The template-kit/no-client-directive-in-contract authoring rule — what it enforces, why, and how to fix it.
|
|
3
|
+
status: living
|
|
4
|
+
related: [INDEX.md, server-vs-client.md, ../schema-system.md]
|
|
5
|
+
updated: 2026-07-14
|
|
6
|
+
---
|
|
7
|
+
# template-kit/no-client-directive-in-contract
|
|
8
|
+
|
|
9
|
+
> A section's four contract files may never carry `"use client"`. This is the one rule
|
|
10
|
+
> scoped by **filename**, not by the directive.
|
|
11
|
+
|
|
12
|
+
## Rule
|
|
13
|
+
|
|
14
|
+
The rule runs only on a section's four contract files — `Renderer.tsx`, `schema.ts`,
|
|
15
|
+
`fill-spec.ts`, `fixtures.ts` — matched by glob. On one of those files, a `"use client"`
|
|
16
|
+
string in the **directive prologue** is reported.
|
|
17
|
+
|
|
18
|
+
The prologue is the run of leading string-literal statements (see
|
|
19
|
+
[server vs client](server-vs-client.md)). Three consequences worth knowing:
|
|
20
|
+
|
|
21
|
+
- **`"use strict"; "use client";` reports.** A second directive is still in the prologue.
|
|
22
|
+
- **`const x = 1;` above `"use client"` does not report** — real code ended the prologue,
|
|
23
|
+
so that string is not a directive. This is a trap, not a loophole: the file is also
|
|
24
|
+
**not an island**, and every server-rendering rule still applies to it. It is a dead
|
|
25
|
+
string.
|
|
26
|
+
- **`foo("use client")` is not a directive** — it is a call argument, and is ignored.
|
|
27
|
+
|
|
28
|
+
Any other file in the section folder may carry the directive freely. That is exactly
|
|
29
|
+
where interactivity belongs.
|
|
30
|
+
|
|
31
|
+
## Reason
|
|
32
|
+
|
|
33
|
+
The four contract files are the platform's interface to a section. They are read by the
|
|
34
|
+
publisher, the editor, and the fill pipeline — **none of which is a browser**.
|
|
35
|
+
`schema.ts` is loaded to learn what slots exist; `fill-spec.ts` to learn how AI fills
|
|
36
|
+
them; `fixtures.ts` to render a preview; `Renderer.tsx` to produce the section's HTML.
|
|
37
|
+
|
|
38
|
+
Marking one `"use client"` would make the whole section an island — and take the
|
|
39
|
+
section's server-rendered HTML with it. The section would ship as an empty shell that
|
|
40
|
+
only appears once JavaScript has run, which is precisely what a published property page
|
|
41
|
+
must never do.
|
|
42
|
+
|
|
43
|
+
## Fix
|
|
44
|
+
|
|
45
|
+
Put the interactive part in its **own** `"use client"` component in the section folder,
|
|
46
|
+
and render it from the Renderer. The contract file stays server-only; the island next to
|
|
47
|
+
it does the browser work.
|
|
48
|
+
|
|
49
|
+
### Before
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
// sections/gallery/Renderer.tsx
|
|
53
|
+
"use client";
|
|
54
|
+
|
|
55
|
+
import { useState } from "react";
|
|
56
|
+
|
|
57
|
+
import { Section } from "@homepages/template-kit";
|
|
58
|
+
|
|
59
|
+
import type { Props } from "./schema.js";
|
|
60
|
+
|
|
61
|
+
export function Renderer({ slots }: Props) {
|
|
62
|
+
const [open, setOpen] = useState(false);
|
|
63
|
+
return (
|
|
64
|
+
<Section>
|
|
65
|
+
<button onClick={() => setOpen(true)}>Open gallery</button>
|
|
66
|
+
{open && <p>{slots.headline}</p>}
|
|
67
|
+
</Section>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### After
|
|
73
|
+
|
|
74
|
+
```tsx
|
|
75
|
+
// sections/gallery/Lightbox.tsx
|
|
76
|
+
"use client";
|
|
77
|
+
|
|
78
|
+
import { useState } from "react";
|
|
79
|
+
|
|
80
|
+
export default function Lightbox({ headline }: { headline: string }) {
|
|
81
|
+
const [open, setOpen] = useState(false);
|
|
82
|
+
return (
|
|
83
|
+
<div>
|
|
84
|
+
<button onClick={() => setOpen(true)}>Open gallery</button>
|
|
85
|
+
{open && <p>{headline}</p>}
|
|
86
|
+
</div>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
```tsx
|
|
92
|
+
// sections/gallery/Renderer.tsx — no directive; server-rendered again
|
|
93
|
+
import { Section } from "@homepages/template-kit";
|
|
94
|
+
|
|
95
|
+
import Lightbox from "./Lightbox.js";
|
|
96
|
+
import type { Props } from "./schema.js";
|
|
97
|
+
|
|
98
|
+
export function Renderer({ slots }: Props) {
|
|
99
|
+
return (
|
|
100
|
+
<Section>
|
|
101
|
+
<Lightbox headline={slots.headline} />
|
|
102
|
+
</Section>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## See also
|
|
108
|
+
|
|
109
|
+
- [Server vs client](server-vs-client.md) — the directive, the prologue, and what an
|
|
110
|
+
island may do.
|
|
111
|
+
- [Schema system](../schema-system.md) — what each of the four contract files is for.
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
---
|
|
2
|
+
purpose: The template-kit/no-client-runtime-in-server authoring rule — what it enforces, why, and how to fix it.
|
|
3
|
+
status: living
|
|
4
|
+
related: [INDEX.md, server-vs-client.md, ../islands.md, serializable-island-props.md]
|
|
5
|
+
updated: 2026-07-14
|
|
6
|
+
---
|
|
7
|
+
# template-kit/no-client-runtime-in-server
|
|
8
|
+
|
|
9
|
+
> No effects, state, event handlers, network, or browser globals in server-rendered code.
|
|
10
|
+
> Interactivity is not banned — it is **relocated** into an island. Islands are exempt.
|
|
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 three things:
|
|
19
|
+
|
|
20
|
+
- **React hooks that only mean something in the browser** — `useEffect`,
|
|
21
|
+
`useLayoutEffect`, `useState`, `useReducer`, `useRef`, `useSyncExternalStore`,
|
|
22
|
+
`useImperativeHandle` — called bare (`useState(…)`) or namespace-qualified
|
|
23
|
+
(`React.useEffect(…)`).
|
|
24
|
+
- **Browser globals read as a value** — `fetch`, `XMLHttpRequest`, `window`, `document`,
|
|
25
|
+
`localStorage`, `sessionStorage`, `navigator`. "Read as a value" is exact: the SSR-guard
|
|
26
|
+
idiom `typeof window !== "undefined"` **is** reported, while `cfg.window` (a property)
|
|
27
|
+
and `{ window: 1 }` (an object key) are not.
|
|
28
|
+
- **Event-handler props** — any JSX prop matching `on[A-Z]…` whose value is an expression:
|
|
29
|
+
`onClick={…}`, `onChange={…}`, `onPointerMove={…}`.
|
|
30
|
+
|
|
31
|
+
Carve-outs:
|
|
32
|
+
|
|
33
|
+
- **Your own definitions are yours.** A name that resolves to a real local declaration —
|
|
34
|
+
`function fetch() {}`, your own `useRef` helper — is not the global or React's hook, and
|
|
35
|
+
is left alone.
|
|
36
|
+
- **`onSurface={1}` and `onIcon={<Icon />}` are not handlers.** A prop whose value is
|
|
37
|
+
plainly a literal or a JSX element is not reported.
|
|
38
|
+
- **A handler on an island is not reported here.** If the element's component resolves to
|
|
39
|
+
a real `"use client"` file on disk, this rule stays quiet:
|
|
40
|
+
[`serializable-island-props`](serializable-island-props.md) already speaks for that
|
|
41
|
+
prop, and "move it into a client component" would be nonsense advice for something that
|
|
42
|
+
already is one.
|
|
43
|
+
|
|
44
|
+
## Reason
|
|
45
|
+
|
|
46
|
+
A server-rendered file is run once, to a string of HTML, in a process that is not a
|
|
47
|
+
browser. There is no `window` to read and no DOM to mutate; an effect never fires, and
|
|
48
|
+
`useState` has no second render to give you. An event handler is worse than useless: it
|
|
49
|
+
is a function, and a function cannot be serialized into HTML, so **it is silently dropped
|
|
50
|
+
from the published page**. The button ships. The click does nothing. Nothing errors.
|
|
51
|
+
|
|
52
|
+
So a Renderer must be a **pure function of its props**. Interactivity is not something the
|
|
53
|
+
kit forbids — it is something the kit gives a specific home: a `"use client"` island,
|
|
54
|
+
which is hydrated in the browser and is exempt from every one of these bans.
|
|
55
|
+
|
|
56
|
+
## Fix
|
|
57
|
+
|
|
58
|
+
Move the interactive markup into a `"use client"` component in the section folder, and
|
|
59
|
+
render it from the Renderer with plain data props. The Renderer keeps the static markup
|
|
60
|
+
and passes the island what it needs to do its job; the island owns the state and the
|
|
61
|
+
handlers.
|
|
62
|
+
|
|
63
|
+
### Before
|
|
64
|
+
|
|
65
|
+
```tsx
|
|
66
|
+
// sections/floorplan/Renderer.tsx
|
|
67
|
+
import { useState } from "react";
|
|
68
|
+
|
|
69
|
+
import { Section } from "@homepages/template-kit";
|
|
70
|
+
|
|
71
|
+
import type { Props } from "./schema.js";
|
|
72
|
+
|
|
73
|
+
export function Renderer({ slots }: Props) {
|
|
74
|
+
const [beds, setBeds] = useState<number | null>(null);
|
|
75
|
+
const shown = beds === null ? slots.units : slots.units.filter((u) => u.beds === beds);
|
|
76
|
+
return (
|
|
77
|
+
<Section>
|
|
78
|
+
<button onClick={() => setBeds(2)}>2 beds</button>
|
|
79
|
+
<ul>
|
|
80
|
+
{shown.map((u) => (
|
|
81
|
+
<li key={u.id}>{u.label}</li>
|
|
82
|
+
))}
|
|
83
|
+
</ul>
|
|
84
|
+
</Section>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### After
|
|
90
|
+
|
|
91
|
+
The state and the handler move into an island. Its props are plain data — see
|
|
92
|
+
[`serializable-island-props`](serializable-island-props.md).
|
|
93
|
+
|
|
94
|
+
```tsx
|
|
95
|
+
// sections/floorplan/UnitFilter.tsx
|
|
96
|
+
"use client";
|
|
97
|
+
|
|
98
|
+
import { useState } from "react";
|
|
99
|
+
|
|
100
|
+
type Unit = { id: string; label: string; beds: number };
|
|
101
|
+
|
|
102
|
+
export default function UnitFilter({ units }: { units: Unit[] }) {
|
|
103
|
+
const [beds, setBeds] = useState<number | null>(null);
|
|
104
|
+
const shown = beds === null ? units : units.filter((u) => u.beds === beds);
|
|
105
|
+
return (
|
|
106
|
+
<div>
|
|
107
|
+
<button onClick={() => setBeds(2)}>2 beds</button>
|
|
108
|
+
<ul>
|
|
109
|
+
{shown.map((u) => (
|
|
110
|
+
<li key={u.id}>{u.label}</li>
|
|
111
|
+
))}
|
|
112
|
+
</ul>
|
|
113
|
+
</div>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
```tsx
|
|
119
|
+
// sections/floorplan/Renderer.tsx
|
|
120
|
+
import { Section } from "@homepages/template-kit";
|
|
121
|
+
|
|
122
|
+
import UnitFilter from "./UnitFilter.js";
|
|
123
|
+
import type { Props } from "./schema.js";
|
|
124
|
+
|
|
125
|
+
export function Renderer({ slots }: Props) {
|
|
126
|
+
return (
|
|
127
|
+
<Section>
|
|
128
|
+
<UnitFilter units={slots.units} />
|
|
129
|
+
</Section>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
The import carries the **emitted** extension (`./UnitFilter.js`) while the file on disk is
|
|
135
|
+
`UnitFilter.tsx` — that is TypeScript's ESM convention, and it is what the kit's rules
|
|
136
|
+
resolve against.
|
|
137
|
+
|
|
138
|
+
## See also
|
|
139
|
+
|
|
140
|
+
- [Server vs client](server-vs-client.md) — how a file is scoped, and what an island may do.
|
|
141
|
+
- [Islands](../islands.md) — writing one, its props contract, and its editor options.
|
|
142
|
+
- [`serializable-island-props`](serializable-island-props.md) — the rule that instead
|
|
143
|
+
reports a function-valued prop when the target *is* an island.
|