@homepages/template-kit 0.1.0 → 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 +69 -0
- package/README.md +12 -2
- package/dist/base.css +27 -48
- package/dist/design-system/theme.d.ts +2 -1
- package/dist/design-system/theme.js +10 -3
- package/dist/eslint/rules/image-bare-needs-reason.js +6 -1
- package/dist/eslint/rules/no-client-directive-in-contract.js +6 -1
- package/dist/eslint/rules/no-client-runtime-in-server.js +5 -1
- package/dist/eslint/rules/no-css-import-from-render-path.js +8 -2
- package/dist/eslint/rules/no-hex.js +6 -1
- package/dist/eslint/rules/no-inline-style.js +5 -1
- package/dist/eslint/rules/no-nondeterminism.js +5 -1
- package/dist/eslint/rules/no-raw-element.js +6 -1
- package/dist/eslint/rules/props-from-schema.js +6 -1
- package/dist/eslint/rules/serializable-island-props.js +5 -1
- package/dist/eslint/rules/slot-marker-literal.js +6 -1
- package/dist/package.js +1 -1
- package/dist/rules/registry.js +16 -0
- package/dist/styles.css +1 -1
- package/docs/INDEX.md +2 -1
- package/docs/eslint.md +12 -47
- package/docs/llms.txt +7 -4
- 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/theme-and-css.md +57 -11
- package/package.json +1 -1
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
---
|
|
2
|
+
purpose: The template-kit/unlayered-fence 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/unlayered-fence
|
|
8
|
+
|
|
9
|
+
> **This is not "you forgot to layer your CSS."** Everything is layered by default — the
|
|
10
|
+
> build does it for you. This rule governs the *escape hatch* from layering: an
|
|
11
|
+
> `/* unlayered: … */` fence must give a reason and must be closed.
|
|
12
|
+
|
|
13
|
+
## Rule
|
|
14
|
+
|
|
15
|
+
Your hand-written CSS is wrapped in a cascade layer by the build. You do not write
|
|
16
|
+
`@layer` yourself, and there is nothing to forget.
|
|
17
|
+
|
|
18
|
+
The **fence** opts a run of rules *out* of that wrapping, hoisting them above every layer:
|
|
19
|
+
|
|
20
|
+
```css
|
|
21
|
+
/* unlayered: <reason> */
|
|
22
|
+
…rules that must beat every layer…
|
|
23
|
+
/* endunlayered */
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Two things are checked, both under this id:
|
|
27
|
+
|
|
28
|
+
1. **Every opener carries a non-empty reason.** A bare `/* unlayered: */` is a violation.
|
|
29
|
+
2. **Openers and closers balance.** An unterminated fence is a violation — it would swallow
|
|
30
|
+
the whole rest of the file out of the layer, which is never what anyone meant.
|
|
31
|
+
|
|
32
|
+
## Reason
|
|
33
|
+
|
|
34
|
+
Cascade layers have one rule that decides everything here: **an unlayered rule beats a
|
|
35
|
+
layered one, regardless of specificity.** Not "usually" — always. That is what layers are
|
|
36
|
+
for, and it is why the platform layers your CSS: inside the layer order (`theme, base,
|
|
37
|
+
components, sections, template, utilities`) a Tailwind utility can always override a
|
|
38
|
+
section's stylesheet, and a section's stylesheet can never leak past its layer to beat
|
|
39
|
+
someone else's utility.
|
|
40
|
+
|
|
41
|
+
So the hatch exists for exactly one situation: **a third-party library injects an unlayered
|
|
42
|
+
rule into the document at runtime** — a map SDK writing a `<style>` element when it mounts,
|
|
43
|
+
say — and that rule beats your layered override no matter how specific you make it. The
|
|
44
|
+
only thing that can beat it is a rule that is also unlayered. Nothing else qualifies. If
|
|
45
|
+
your rule merely needs to beat *your own* CSS, or a Tailwind utility, it belongs in a layer
|
|
46
|
+
and you are reaching for the wrong tool.
|
|
47
|
+
|
|
48
|
+
Which is why the reason is mandatory. A fence with no reason is a rule that outranks every
|
|
49
|
+
layer on the page for reasons the next author cannot reconstruct — they cannot tell whether
|
|
50
|
+
it is load-bearing or whether someone was fighting a specificity problem with a hammer. And
|
|
51
|
+
an unbalanced fence silently promotes every rule below it, so a stylesheet that behaved
|
|
52
|
+
yesterday starts beating the utilities that were supposed to override it.
|
|
53
|
+
|
|
54
|
+
## Fix
|
|
55
|
+
|
|
56
|
+
Close every fence with `/* endunlayered */`, and give every opener a reason that **names the
|
|
57
|
+
runtime-injected rule it has to beat**. A short phrase is enough — it only has to be
|
|
58
|
+
non-empty and specific about which rule it's beating, not a full sentence. If it does not
|
|
59
|
+
have to beat one, delete the fence: layered is the default, and the default is right.
|
|
60
|
+
|
|
61
|
+
The Before/After below both open with a `/* css-reason: … */` comment. That line has
|
|
62
|
+
nothing to do with fencing — it satisfies a separate rule, [`css-reason`](css-reason.md),
|
|
63
|
+
which every hand-written stylesheet must open with. It's shown here because real section
|
|
64
|
+
CSS carries both; this rule's own grammar starts at `/* unlayered: */`.
|
|
65
|
+
|
|
66
|
+
### Before
|
|
67
|
+
|
|
68
|
+
```css
|
|
69
|
+
/* css-reason: overrides the map SDK's injected control chrome. */
|
|
70
|
+
|
|
71
|
+
/* unlayered: */
|
|
72
|
+
.maplibregl-ctrl-group button {
|
|
73
|
+
background: var(--tr-color-surface);
|
|
74
|
+
border-radius: var(--tr-radius-sm);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.tr-map-legend {
|
|
78
|
+
padding: 0.5rem;
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Two violations at once: the reason is empty, and the fence is never closed — so
|
|
83
|
+
`.tr-map-legend` is hoisted out of the layer too, where it now outranks every utility a
|
|
84
|
+
future author might try to override it with.
|
|
85
|
+
|
|
86
|
+
### After
|
|
87
|
+
|
|
88
|
+
```css
|
|
89
|
+
/* css-reason: overrides the map SDK's control chrome — the SDK builds those nodes after
|
|
90
|
+
hydration, so Tailwind's scanner never sees a class to compile. */
|
|
91
|
+
|
|
92
|
+
/* unlayered: the map SDK injects its control styles as an unlayered <style> element at
|
|
93
|
+
runtime; a layered rule cannot beat it at any specificity. */
|
|
94
|
+
.maplibregl-ctrl-group button {
|
|
95
|
+
background: var(--tr-color-surface);
|
|
96
|
+
border-radius: var(--tr-radius-sm);
|
|
97
|
+
}
|
|
98
|
+
/* endunlayered */
|
|
99
|
+
|
|
100
|
+
/* Our own DOM — layered, like everything else. */
|
|
101
|
+
.tr-map-legend {
|
|
102
|
+
padding: 0.5rem;
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
The fence now wraps exactly the rules that need to escape, and `.tr-map-legend` is back in
|
|
107
|
+
the layer where a utility can still override it.
|
|
108
|
+
|
|
109
|
+
## See also
|
|
110
|
+
|
|
111
|
+
- [Theme and CSS](../theme-and-css.md) — the layer order, and why utilities come last.
|
|
112
|
+
- [`css-reason`](css-reason.md) — the comment every hand-written stylesheet opens with; the
|
|
113
|
+
fence reason is the same discipline, one level in.
|
package/docs/theme-and-css.md
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
---
|
|
2
|
-
purpose: The kit's two CSS entries,
|
|
2
|
+
purpose: The kit's two CSS entries, how a section's own CSS reaches the page, the TokenTheme a template supplies, and the breakpoint ladder.
|
|
3
3
|
status: living
|
|
4
|
-
related: [primitives.md, schema-system.md, eslint.md]
|
|
4
|
+
related: [primitives.md, schema-system.md, eslint.md, rules/INDEX.md]
|
|
5
5
|
updated: 2026-07-14
|
|
6
6
|
---
|
|
7
7
|
# Theme and CSS
|
|
8
8
|
|
|
9
9
|
The kit owns the **contract**: the Tailwind utility namespace (`text-ink`,
|
|
10
|
-
`bg-surface-alt`, `rounded-md`), the breakpoint ladder, the reset, and the section box
|
|
10
|
+
`bg-surface-alt`, `rounded-md`), the breakpoint ladder, the reset gaps Tailwind's preflight leaves, and the section box
|
|
11
11
|
model. Your template owns the **values** behind that namespace, declared once as a
|
|
12
12
|
`TokenTheme`.
|
|
13
13
|
|
|
@@ -16,7 +16,7 @@ Two CSS entries ship, and they are not interchangeable.
|
|
|
16
16
|
| Entry | What it is | How it loads |
|
|
17
17
|
|---|---|---|
|
|
18
18
|
| `@homepages/template-kit/styles.css` | The Tailwind v4 `@theme inline` alias layer + the breakpoint ladder + `@source` registration of the kit's compiled JS | imported into **your Tailwind entry**, after Tailwind |
|
|
19
|
-
| `@homepages/template-kit/base.css` | Cascade-layer order, the reset
|
|
19
|
+
| `@homepages/template-kit/base.css` | Cascade-layer order, the three reset gaps Tailwind's preflight leaves, `.tr-section`, and the motion tokens | its **own stylesheet** on the page — not part of the Tailwind entry |
|
|
20
20
|
|
|
21
21
|
## Import Tailwind exactly once
|
|
22
22
|
|
|
@@ -37,18 +37,47 @@ primitives (`Image`'s `bg-surface-alt`, `object-cover`, …) reach your compiled
|
|
|
37
37
|
also overrides Tailwind's default exclusion of `node_modules`. You do not need to add a
|
|
38
38
|
`@source` for the kit yourself.
|
|
39
39
|
|
|
40
|
+
## How a section's CSS reaches the page
|
|
41
|
+
|
|
42
|
+
A section's compiled CSS layer is assembled from two sources, both automatic — you
|
|
43
|
+
never write an import to make either happen:
|
|
44
|
+
|
|
45
|
+
1. **Hand-written CSS, collected by path.** Every `.css` file inside the section's
|
|
46
|
+
folder — `styles.css` and any other stylesheet nested under it — is picked up by
|
|
47
|
+
walking the folder, wherever it sits, and concatenated into `@layer sections`. A
|
|
48
|
+
template's own hand-written stylesheet is collected the same way, into
|
|
49
|
+
`@layer template`.
|
|
50
|
+
2. **Package CSS, harvested from the render path.** The build also bundles
|
|
51
|
+
`Renderer.tsx`'s module graph — and any component that carries `export const
|
|
52
|
+
enhancer` — and pulls in whatever `.css` those imports resolve to. A bare specifier
|
|
53
|
+
(`import "swiper/css";`) resolves into `node_modules` and lands in the same
|
|
54
|
+
`@layer sections`; that is the sanctioned way for a third-party package's
|
|
55
|
+
stylesheet to reach the page.
|
|
56
|
+
|
|
57
|
+
Two constraints follow directly, and both are enforced by lint:
|
|
58
|
+
|
|
59
|
+
- **Don't re-import your own stylesheet.** It is already collected by path —
|
|
60
|
+
importing it again from a `.ts`/`.tsx` file only double-emits it, and can smuggle a
|
|
61
|
+
stray `@layer` statement into a nested layer.
|
|
62
|
+
See [`no-css-import-from-render-path`](rules/no-css-import-from-render-path.md).
|
|
63
|
+
- **`@import` never works inside hand-written CSS, package or local.** An `@import`
|
|
64
|
+
landing inside `@layer sections` / `@layer template` is invalid CSS, and the
|
|
65
|
+
browser drops it silently rather than erroring — the package's styling is simply
|
|
66
|
+
missing from the published page. Reach a package's stylesheet from code instead.
|
|
67
|
+
See [`no-bare-css-import`](rules/no-bare-css-import.md).
|
|
68
|
+
|
|
40
69
|
## The page's stylesheets
|
|
41
70
|
|
|
42
71
|
A published page loads, in order:
|
|
43
72
|
|
|
44
|
-
1. **`base.css`** — layer order, reset, `.tr-section`,
|
|
45
|
-
2. **Your theme
|
|
73
|
+
1. **`base.css`** — layer order, the reset gaps, `.tr-section`, motion tokens.
|
|
74
|
+
2. **Your compiled theme CSS** — the `:root` block that defines the `--tr-*` values
|
|
75
|
+
*and* the `body` block that applies them (font-family, color, background, size),
|
|
76
|
+
both produced by `compileThemeToCss`.
|
|
46
77
|
3. **Your compiled Tailwind bundle** — `@import "tailwindcss"` + `styles.css` + your
|
|
47
78
|
section markup's utilities.
|
|
48
79
|
|
|
49
|
-
`base.css`
|
|
50
|
-
`--tr-text-base` and `--tr-header-height` for the document defaults, so the theme block
|
|
51
|
-
must be present on the page too. The layer order it declares is:
|
|
80
|
+
The layer order `base.css` declares is:
|
|
52
81
|
|
|
53
82
|
```css
|
|
54
83
|
@layer theme, base, components, sections, template, utilities;
|
|
@@ -56,11 +85,28 @@ must be present on the page too. The layer order it declares is:
|
|
|
56
85
|
|
|
57
86
|
Utilities come last, so a Tailwind utility always beats the reset's element defaults.
|
|
58
87
|
|
|
88
|
+
### base.css requires a Tailwind entry
|
|
89
|
+
|
|
90
|
+
`base.css` does **not** ship a reset. Your entry's `@import "tailwindcss"` brings
|
|
91
|
+
Tailwind's preflight, and preflight is the reset — box-sizing, margins, list-style, the
|
|
92
|
+
button and anchor resets. `base.css` carries only the three declarations preflight does
|
|
93
|
+
not make: `font-smoothing`, `button { cursor: pointer }` (Tailwind v4 leaves buttons at
|
|
94
|
+
`cursor: default`), and `svg { max-width: 100% }` (preflight caps only `img` and
|
|
95
|
+
`video`).
|
|
96
|
+
|
|
97
|
+
Ship `base.css` without a Tailwind entry on the page and you get an unreset page.
|
|
98
|
+
|
|
99
|
+
`base.css` references **no theme token**. Your fonts and colors reach the document
|
|
100
|
+
through `compileThemeToCss`, which emits both the `:root` block that defines your tokens
|
|
101
|
+
and the `body` block that applies them. Nothing global is opinionated about your brand.
|
|
102
|
+
|
|
59
103
|
## `TokenTheme` — your template's values
|
|
60
104
|
|
|
61
105
|
A template's `theme.ts` exports a `TokenTheme`. `compileThemeToCss` turns it into the
|
|
62
|
-
`:root { --tr-*: … }` block
|
|
63
|
-
|
|
106
|
+
`:root { --tr-*: … }` block that defines your tokens **and** the `body` block that
|
|
107
|
+
applies them (font-family, color, background, size) to the document — together, the
|
|
108
|
+
CSS the template's bundle ships. Deterministic (stable key order, palette keys
|
|
109
|
+
sorted), so the same theme always content-hashes identically.
|
|
64
110
|
|
|
65
111
|
```ts
|
|
66
112
|
import { compileThemeToCss, type TokenTheme } from "@homepages/template-kit";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@homepages/template-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Authoring kit for HomePages marketing-section templates: schema system, contract primitives, theme tokens, and the template-kit CLI.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|