@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
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,74 @@
|
|
|
1
1
|
# @homepages/template-kit
|
|
2
2
|
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- e64d4d7: `base.css` no longer ships a reset — Tailwind's preflight (from your entry's
|
|
8
|
+
`@import "tailwindcss"`) already is one, and the two were duplicating each other. It
|
|
9
|
+
now carries only the three declarations preflight does not make: font-smoothing,
|
|
10
|
+
`button { cursor: pointer }`, and `svg { max-width: 100% }`.
|
|
11
|
+
|
|
12
|
+
**`base.css` now requires a Tailwind entry on the page.** If a page loads `base.css`
|
|
13
|
+
without an `@import "tailwindcss"` entry present — a standalone preview, an embed, a
|
|
14
|
+
hand-written CSS page — it renders fully unreset (UA body margin, bulleted lists,
|
|
15
|
+
underlined links, unconstrained inline images), and nothing errors to tell you.
|
|
16
|
+
Confirm every page that loads `base.css` also loads a Tailwind entry.
|
|
17
|
+
|
|
18
|
+
`base.css` also no longer references any theme token. The `body` document defaults
|
|
19
|
+
(font, color, background, size) are now emitted by `compileThemeToCss`, alongside the
|
|
20
|
+
`:root` block that defines those tokens. **Regenerate and republish your compiled theme
|
|
21
|
+
CSS.** If you upgrade `base.css` but keep serving a previously-compiled theme
|
|
22
|
+
stylesheet, that stylesheet still only has the old `:root` block — `body` silently
|
|
23
|
+
loses `font-family`, `color`, `background`, and `font-size` page-wide, because
|
|
24
|
+
`base.css` no longer carries them as a fallback. The compiled output's content hash
|
|
25
|
+
changes.
|
|
26
|
+
|
|
27
|
+
The `--tr-space-*` scale is removed — Tailwind's own spacing scale (`p-4`, `gap-6`)
|
|
28
|
+
was never aliased to it. Any of your CSS still referencing `var(--tr-space-*)` (e.g.
|
|
29
|
+
`padding: var(--tr-space-4)`) now points at an undefined custom property, which is
|
|
30
|
+
invalid at computed-value time, so the declaration drops. The `scroll-behavior` /
|
|
31
|
+
`scroll-padding-top` policy is removed; set it in your own template stylesheet if you
|
|
32
|
+
have a sticky header.
|
|
33
|
+
|
|
34
|
+
- a24174f: Every rule-id now resolves to a page. The lint preset and `template-kit check` share one
|
|
35
|
+
id namespace (`template-kit/<id>`), and each of those ids — all 28 — now has a page at
|
|
36
|
+
`docs/rules/<id>.md` giving the rule, the reason it exists, and a before/after fix you can
|
|
37
|
+
copy.
|
|
38
|
+
|
|
39
|
+
The docs ship **inside the tarball**, so the loop closes with no network: hit
|
|
40
|
+
`template-kit/no-hex`, read `node_modules/@homepages/template-kit/docs/rules/no-hex.md`,
|
|
41
|
+
fix it. That matters most for the coding agents authoring against this kit — an agent that
|
|
42
|
+
can resolve a failure to its own reference page fixes the violation instead of guessing at
|
|
43
|
+
it.
|
|
44
|
+
|
|
45
|
+
Each ESLint rule also carries `meta.docs.url` pointing at its page, so an editor's "show
|
|
46
|
+
documentation for this rule" lands on the same file.
|
|
47
|
+
|
|
48
|
+
Start at `docs/rules/INDEX.md`. `docs/rules/server-vs-client.md` explains the one concept
|
|
49
|
+
several rules assume — that server-vs-client is a **per-file** fact, decided by the
|
|
50
|
+
`"use client"` directive prologue, not by an import graph.
|
|
51
|
+
|
|
52
|
+
### Patch Changes
|
|
53
|
+
|
|
54
|
+
- be1507d: Fixed `no-css-import-from-render-path`, which banned the one legal way to get a
|
|
55
|
+
third-party stylesheet into a section.
|
|
56
|
+
|
|
57
|
+
A **bare** `.css` specifier in the render path — `import "swiper/css"` in `Renderer.tsx`,
|
|
58
|
+
in a marked enhancer, or in any module either one imports — is how a package's CSS reaches
|
|
59
|
+
the section's cascade layer: the build bundles that import graph precisely to harvest it.
|
|
60
|
+
The rule flagged it anyway, and its `Fix:` hint sent you to `@import` the stylesheet from
|
|
61
|
+
`styles.css`, which is never legal there (hand-written CSS is wrapped in `@layer`, where a
|
|
62
|
+
browser drops an `@import` silently).
|
|
63
|
+
|
|
64
|
+
The rule now reports only a **local** stylesheet import — a relative or absolute specifier.
|
|
65
|
+
That is the real defect: the build already collects every `.css` file in the section folder
|
|
66
|
+
by filename, so importing one from code ships its rules twice and can corrupt the cascade.
|
|
67
|
+
|
|
68
|
+
If you worked around the old rule by moving a package's CSS into a stylesheet `@import`,
|
|
69
|
+
move it back into the module that needs it — see `docs/rules/no-css-import-from-render-path.md`
|
|
70
|
+
and `docs/rules/no-bare-css-import.md`, which are the two halves of the same mental model.
|
|
71
|
+
|
|
3
72
|
## 0.1.0
|
|
4
73
|
|
|
5
74
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -65,7 +65,7 @@ Full contract: **[docs/primitives.md](docs/primitives.md)**.
|
|
|
65
65
|
| `@homepages/template-kit/island-runtime` | Browser loader that hydrates a page's islands |
|
|
66
66
|
| `@homepages/template-kit/browser` | Browser helpers for published pages (site config, signed ingest) |
|
|
67
67
|
| `@homepages/template-kit/styles.css` | Tailwind v4 theme tokens, the breakpoint ladder, kit source registration |
|
|
68
|
-
| `@homepages/template-kit/base.css` |
|
|
68
|
+
| `@homepages/template-kit/base.css` | Cascade-layer order, the three reset gaps Tailwind's preflight leaves, `.tr-section`, and the motion tokens |
|
|
69
69
|
| `@homepages/template-kit/eslint` | ESLint flat-config preset |
|
|
70
70
|
| `@homepages/template-kit/tsconfig` | tsconfig preset (`"extends"`) |
|
|
71
71
|
|
|
@@ -80,9 +80,19 @@ write a section against. The two CSS entries are the deliberate exception.
|
|
|
80
80
|
```
|
|
81
81
|
|
|
82
82
|
Two `@import "tailwindcss"` in one graph double-emits preflight. `base.css` is not part
|
|
83
|
-
of that entry — it loads as its own stylesheet on the page.
|
|
83
|
+
of that entry — it loads as its own stylesheet on the page. `base.css` ships no reset
|
|
84
|
+
of its own — it depends on the preflight your entry's `@import "tailwindcss"` brings.
|
|
85
|
+
Both must be on the page. Full contract:
|
|
84
86
|
**[docs/theme-and-css.md](docs/theme-and-css.md)**.
|
|
85
87
|
|
|
88
|
+
## Rules
|
|
89
|
+
|
|
90
|
+
The lint preset and the `template-kit check` CLI share one id namespace,
|
|
91
|
+
`template-kit/<id>`. A rule-id printed by either resolves to a page —
|
|
92
|
+
`docs/rules/<id>.md` — giving the rule, the reason it exists, and a before/after fix.
|
|
93
|
+
Start at **[docs/rules/INDEX.md](docs/rules/INDEX.md)**; the full doc set is routed
|
|
94
|
+
from **[docs/INDEX.md](docs/INDEX.md)**.
|
|
95
|
+
|
|
86
96
|
## Development
|
|
87
97
|
|
|
88
98
|
```bash
|
package/dist/base.css
CHANGED
|
@@ -7,52 +7,46 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Contains:
|
|
9
9
|
* - The cascade-layer order declaration above — the one place it is stated
|
|
10
|
-
* - The
|
|
11
|
-
* - Document defaults (html/body)
|
|
10
|
+
* - The three reset gaps Tailwind's preflight leaves (below)
|
|
12
11
|
* - The section box model (.tr-section, emitted by the <Section> primitive)
|
|
13
|
-
* - Universal tokens:
|
|
12
|
+
* - Universal tokens: motion
|
|
14
13
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
14
|
+
* REQUIRES A TAILWIND ENTRY ON THE PAGE. Your entry's `@import "tailwindcss"` brings
|
|
15
|
+
* Tailwind's preflight, and preflight does the reset: box-sizing, margin/padding,
|
|
16
|
+
* list-style, the button and anchor resets, display:block on replaced elements. This
|
|
17
|
+
* file only fills the three gaps preflight leaves. Ship base.css without a Tailwind
|
|
18
|
+
* entry and you get an unreset page.
|
|
19
|
+
*
|
|
20
|
+
* NOT here: anything a template owns. Colors, fonts, type, radii, shadows and layout
|
|
21
|
+
* — and the `body` document defaults that apply them — all come from your theme.ts,
|
|
22
|
+
* compiled by compileThemeToCss. This file references no theme token at all.
|
|
19
23
|
*/
|
|
20
24
|
|
|
21
|
-
/* ----- Reset
|
|
25
|
+
/* ----- Reset gaps ------------------------------------------------------- *
|
|
26
|
+
* The three declarations Tailwind's preflight does NOT make. Each is load-bearing:
|
|
27
|
+
* delete one and your page changes.
|
|
28
|
+
*
|
|
22
29
|
* Wrapped in `@layer base` so Tailwind's utilities (which compile into
|
|
23
|
-
* `@layer utilities`) win over these element defaults. Unlayered,
|
|
24
|
-
*
|
|
25
|
-
*
|
|
30
|
+
* `@layer utilities`) win over these element defaults. Unlayered, `button { cursor:
|
|
31
|
+
* pointer }` would beat a layered `.cursor-default` utility outright — unlayered
|
|
32
|
+
* always wins over layered in the cascade.
|
|
26
33
|
*/
|
|
27
34
|
|
|
28
35
|
@layer base {
|
|
29
|
-
|
|
30
|
-
html, body { margin: 0; padding: 0; }
|
|
36
|
+
/* preflight carries no font-smoothing rule at all */
|
|
31
37
|
body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }
|
|
32
|
-
|
|
33
|
-
button {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
h1, h2, h3, h4, h5, h6, p { margin: 0; }
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/* ----- Document defaults ------------------------------------------------ */
|
|
40
|
-
|
|
41
|
-
html { scroll-behavior: smooth; scroll-padding-top: var(--tr-header-height); }
|
|
42
|
-
body {
|
|
43
|
-
font-family: var(--tr-font-sans);
|
|
44
|
-
color: var(--tr-color-ink);
|
|
45
|
-
background: var(--tr-color-background);
|
|
46
|
-
font-size: var(--tr-text-base);
|
|
47
|
-
line-height: 1.5;
|
|
38
|
+
/* Tailwind v4 deliberately leaves buttons at the UA default `cursor: default` */
|
|
39
|
+
button { cursor: pointer; }
|
|
40
|
+
/* preflight caps max-width on img and video, but not on svg */
|
|
41
|
+
svg { max-width: 100%; }
|
|
48
42
|
}
|
|
49
43
|
|
|
50
44
|
/* ----- Primitives ------------------------------------------------------- */
|
|
51
45
|
|
|
52
|
-
/* The full-bleed section root, emitted by the <Section> primitive. Always 100%
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
46
|
+
/* The full-bleed section root, emitted by the <Section> primitive. Always 100% wide
|
|
47
|
+
* with zero padding/margin, so adjacent sections butt edge-to-edge and a page composes
|
|
48
|
+
* as a vertical stack with no framework-injected whitespace. A section brings its own
|
|
49
|
+
* background and internal padding. */
|
|
56
50
|
.tr-section {
|
|
57
51
|
display: block;
|
|
58
52
|
width: 100%;
|
|
@@ -63,21 +57,6 @@ body {
|
|
|
63
57
|
/* ----- Universal tokens ------------------------------------------------- */
|
|
64
58
|
|
|
65
59
|
:root {
|
|
66
|
-
/* Spacing scale */
|
|
67
|
-
--tr-space-1: 0.25rem;
|
|
68
|
-
--tr-space-2: 0.5rem;
|
|
69
|
-
--tr-space-3: 0.75rem;
|
|
70
|
-
--tr-space-4: 1rem;
|
|
71
|
-
--tr-space-5: 1.25rem;
|
|
72
|
-
--tr-space-6: 1.5rem;
|
|
73
|
-
--tr-space-8: 2rem;
|
|
74
|
-
--tr-space-10: 2.5rem;
|
|
75
|
-
--tr-space-12: 3rem;
|
|
76
|
-
--tr-space-16: 4rem;
|
|
77
|
-
--tr-space-20: 5rem;
|
|
78
|
-
--tr-space-24: 6rem;
|
|
79
|
-
--tr-space-32: 8rem;
|
|
80
|
-
|
|
81
60
|
/* Motion */
|
|
82
61
|
--tr-duration-fast: 150ms;
|
|
83
62
|
--tr-duration-base: 250ms;
|
|
@@ -298,7 +298,8 @@ declare const TokenThemeSchema: z.ZodObject<{
|
|
|
298
298
|
type TokenTheme = z.infer<typeof TokenThemeSchema>;
|
|
299
299
|
/**
|
|
300
300
|
* Compile a `TokenTheme` into the CSS a template's bundle ships: an optional
|
|
301
|
-
* font-loading block
|
|
301
|
+
* font-loading block, a `:root { --tr-*: … }` declaration that defines the
|
|
302
|
+
* design tokens, and a `body` block that applies those tokens to the document.
|
|
302
303
|
* Deterministic (stable key order; palette keys sorted) so the same theme
|
|
303
304
|
* always content-hashes identically.
|
|
304
305
|
*/
|
|
@@ -72,9 +72,16 @@ fontFaces: z.string().optional() }),
|
|
|
72
72
|
shadows: scale(Object.keys(SHADOW_VARS)),
|
|
73
73
|
layout: scale(Object.keys(LAYOUT_VARS))
|
|
74
74
|
}).strict();
|
|
75
|
+
const BODY_DEFAULTS = `body {
|
|
76
|
+
font-family: var(--tr-font-sans);
|
|
77
|
+
color: var(--tr-color-ink);
|
|
78
|
+
background: var(--tr-color-background);
|
|
79
|
+
font-size: var(--tr-text-base);
|
|
80
|
+
}`;
|
|
75
81
|
/**
|
|
76
82
|
* Compile a `TokenTheme` into the CSS a template's bundle ships: an optional
|
|
77
|
-
* font-loading block
|
|
83
|
+
* font-loading block, a `:root { --tr-*: … }` declaration that defines the
|
|
84
|
+
* design tokens, and a `body` block that applies those tokens to the document.
|
|
78
85
|
* Deterministic (stable key order; palette keys sorted) so the same theme
|
|
79
86
|
* always content-hashes identically.
|
|
80
87
|
*/
|
|
@@ -87,8 +94,8 @@ function compileThemeToCss(theme) {
|
|
|
87
94
|
for (const k of Object.keys(RADIUS_VARS)) decls.push(` ${RADIUS_VARS[k]}: ${theme.radii[k]};`);
|
|
88
95
|
for (const k of Object.keys(SHADOW_VARS)) decls.push(` ${SHADOW_VARS[k]}: ${theme.shadows[k]};`);
|
|
89
96
|
for (const k of Object.keys(LAYOUT_VARS)) decls.push(` ${LAYOUT_VARS[k]}: ${theme.layout[k]};`);
|
|
90
|
-
const
|
|
91
|
-
return theme.fonts.fontFaces ? `${theme.fonts.fontFaces}\n\n${
|
|
97
|
+
const blocks = `${`:root {\n${decls.join("\n")}\n}`}\n\n${BODY_DEFAULTS}`;
|
|
98
|
+
return theme.fonts.fontFaces ? `${theme.fonts.fontFaces}\n\n${blocks}\n` : `${blocks}\n`;
|
|
92
99
|
}
|
|
93
100
|
|
|
94
101
|
//#endregion
|
|
@@ -1,8 +1,13 @@
|
|
|
1
|
+
import { docsPathForRule } from "../../rules/registry.js";
|
|
2
|
+
|
|
1
3
|
//#region src/eslint/rules/image-bare-needs-reason.ts
|
|
2
4
|
const imageBareNeedsReason = {
|
|
3
5
|
meta: {
|
|
4
6
|
type: "problem",
|
|
5
|
-
docs: {
|
|
7
|
+
docs: {
|
|
8
|
+
description: "<Image bare> requires a `// bare:` reason comment",
|
|
9
|
+
url: docsPathForRule("image-bare-needs-reason")
|
|
10
|
+
},
|
|
6
11
|
schema: [],
|
|
7
12
|
messages: { bareNeedsReason: "`<Image bare>` skips the framed wrapper's aspect-ratio box and fallback rect. Fix: document why this case warrants it with a `// bare: <reason>` comment immediately above the element." }
|
|
8
13
|
},
|
|
@@ -1,8 +1,13 @@
|
|
|
1
|
+
import { docsPathForRule } from "../../rules/registry.js";
|
|
2
|
+
|
|
1
3
|
//#region src/eslint/rules/no-client-directive-in-contract.ts
|
|
2
4
|
const noClientDirectiveInContract = {
|
|
3
5
|
meta: {
|
|
4
6
|
type: "problem",
|
|
5
|
-
docs: {
|
|
7
|
+
docs: {
|
|
8
|
+
description: "Ban the \"use client\" directive in a section's four contract files",
|
|
9
|
+
url: docsPathForRule("no-client-directive-in-contract")
|
|
10
|
+
},
|
|
6
11
|
schema: [],
|
|
7
12
|
messages: { clientContractFile: "A section's contract files (Renderer.tsx, schema.ts, fill-spec.ts, fixtures.ts) are server-only and must not carry \"use client\" — the platform reads them outside a browser. Fix: put the interactive part in its own \"use client\" component in the section folder and render it from the Renderer." }
|
|
8
13
|
},
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { docsPathForRule } from "../../rules/registry.js";
|
|
1
2
|
import { hasClientDirective, isClientFile, resolveImportedFile } from "../is-client-file.js";
|
|
2
3
|
|
|
3
4
|
//#region src/eslint/rules/no-client-runtime-in-server.ts
|
|
@@ -22,7 +23,10 @@ const BANNED_GLOBALS = /* @__PURE__ */ new Set([
|
|
|
22
23
|
const noClientRuntimeInServer = {
|
|
23
24
|
meta: {
|
|
24
25
|
type: "problem",
|
|
25
|
-
docs: {
|
|
26
|
+
docs: {
|
|
27
|
+
description: "Ban effects, state, handlers, network, and browser globals in server-rendered section code",
|
|
28
|
+
url: docsPathForRule("no-client-runtime-in-server")
|
|
29
|
+
},
|
|
26
30
|
schema: [],
|
|
27
31
|
messages: {
|
|
28
32
|
clientRuntime: "`{{what}}` is browser-only and is banned in server-rendered code — a Renderer must be a pure function of its props. Fix: move this into a `\"use client\"` component in the section folder and render it from the Renderer; it will be hydrated as an island.",
|
|
@@ -1,15 +1,21 @@
|
|
|
1
|
+
import { docsPathForRule } from "../../rules/registry.js";
|
|
2
|
+
|
|
1
3
|
//#region src/eslint/rules/no-css-import-from-render-path.ts
|
|
2
4
|
const noCssImportFromRenderPath = {
|
|
3
5
|
meta: {
|
|
4
6
|
type: "problem",
|
|
5
|
-
docs: {
|
|
7
|
+
docs: {
|
|
8
|
+
description: "Ban local CSS imports from a section's render path",
|
|
9
|
+
url: docsPathForRule("no-css-import-from-render-path")
|
|
10
|
+
},
|
|
6
11
|
schema: [],
|
|
7
|
-
messages: { cssFromRenderPath: "`{{source}}` is a
|
|
12
|
+
messages: { cssFromRenderPath: "`{{source}}` is a local stylesheet imported from the render path — the build already collects every `.css` file in the section folder by filename, so this ships the rules twice. Fix: delete the import; the file is picked up on its own. (A package's stylesheet belongs here, imported by its bare specifier — that is how it reaches the section's layer.)" }
|
|
8
13
|
},
|
|
9
14
|
create(context) {
|
|
10
15
|
return { ImportDeclaration(node) {
|
|
11
16
|
const source = node.source.value;
|
|
12
17
|
if (typeof source !== "string" || !source.endsWith(".css")) return;
|
|
18
|
+
if (!source.startsWith(".") && !source.startsWith("/")) return;
|
|
13
19
|
context.report({
|
|
14
20
|
node,
|
|
15
21
|
messageId: "cssFromRenderPath",
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { docsPathForRule } from "../../rules/registry.js";
|
|
2
|
+
|
|
1
3
|
//#region src/eslint/rules/no-hex.ts
|
|
2
4
|
const COLOR_FN = /(?:oklch|oklab|rgba?|hsla?|color-mix)\s*\([^)]*\)/;
|
|
3
5
|
const HEX = /#(?:[0-9a-fA-F]{8}|[0-9a-fA-F]{6}|[0-9a-fA-F]{4}|[0-9a-fA-F]{3})\b/g;
|
|
@@ -67,7 +69,10 @@ function isWithinStyleAttribute(node) {
|
|
|
67
69
|
const noHex = {
|
|
68
70
|
meta: {
|
|
69
71
|
type: "problem",
|
|
70
|
-
docs: {
|
|
72
|
+
docs: {
|
|
73
|
+
description: "Ban hard-coded colour literals in section code",
|
|
74
|
+
url: docsPathForRule("no-hex")
|
|
75
|
+
},
|
|
71
76
|
schema: [],
|
|
72
77
|
messages: { hardCodedColor: "`{{value}}` is a hard-coded colour — a section carries no palette of its own, or it breaks the first time another template reuses it. Fix: use a semantic token utility (e.g. `bg-surface-alt`, `text-ink`); if the value is genuinely brand decoration with no token, add it to the template's `theme.ts` palette and use the `--tr-color-<name>` utility it compiles to." }
|
|
73
78
|
},
|
|
@@ -1,10 +1,14 @@
|
|
|
1
|
+
import { docsPathForRule } from "../../rules/registry.js";
|
|
1
2
|
import { hasClientDirective } from "../is-client-file.js";
|
|
2
3
|
|
|
3
4
|
//#region src/eslint/rules/no-inline-style.ts
|
|
4
5
|
const noInlineStyle = {
|
|
5
6
|
meta: {
|
|
6
7
|
type: "problem",
|
|
7
|
-
docs: {
|
|
8
|
+
docs: {
|
|
9
|
+
description: "Ban the inline style prop in server-rendered section JSX",
|
|
10
|
+
url: docsPathForRule("no-inline-style")
|
|
11
|
+
},
|
|
8
12
|
schema: [],
|
|
9
13
|
messages: { inlineStyle: "The inline `style=` prop is banned in server-rendered section JSX — it bypasses the cascade and Tailwind's responsive prefixes. Fix: use Tailwind utility classes; for a value that only exists at runtime, move the markup into a `\"use client\"` island, where `style` is allowed." }
|
|
10
14
|
},
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { docsPathForRule } from "../../rules/registry.js";
|
|
1
2
|
import { hasClientDirective } from "../is-client-file.js";
|
|
2
3
|
|
|
3
4
|
//#region src/eslint/rules/no-nondeterminism.ts
|
|
@@ -10,7 +11,10 @@ const BANNED_MEMBERS = {
|
|
|
10
11
|
const noNondeterminism = {
|
|
11
12
|
meta: {
|
|
12
13
|
type: "problem",
|
|
13
|
-
docs: {
|
|
14
|
+
docs: {
|
|
15
|
+
description: "Ban clock and randomness reads in server-rendered section code",
|
|
16
|
+
url: docsPathForRule("no-nondeterminism")
|
|
17
|
+
},
|
|
14
18
|
schema: [],
|
|
15
19
|
messages: { nondeterministic: "`{{what}}` is nondeterministic and is banned in server-rendered code — the publisher and the editor must render byte-identical HTML. Fix: compute this at fill time and pass it in as a slot value, or move the code into a `\"use client\"` island, where it runs in the browser." }
|
|
16
20
|
},
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { docsPathForRule } from "../../rules/registry.js";
|
|
2
|
+
|
|
1
3
|
//#region src/eslint/rules/no-raw-element.ts
|
|
2
4
|
const FORBIDDEN = {
|
|
3
5
|
section: "<Section>",
|
|
@@ -6,7 +8,10 @@ const FORBIDDEN = {
|
|
|
6
8
|
const noRawElement = {
|
|
7
9
|
meta: {
|
|
8
10
|
type: "problem",
|
|
9
|
-
docs: {
|
|
11
|
+
docs: {
|
|
12
|
+
description: "Ban raw <section> and <img> in section JSX",
|
|
13
|
+
url: docsPathForRule("no-raw-element")
|
|
14
|
+
},
|
|
10
15
|
schema: [],
|
|
11
16
|
messages: { rawElement: "Raw `<{{tag}}>` is banned in section JSX — {{use}} carries the contract it skips. Fix: `import { {{name}} } from \"@homepages/template-kit\";` and use {{use}}." }
|
|
12
17
|
},
|
|
@@ -1,8 +1,13 @@
|
|
|
1
|
+
import { docsPathForRule } from "../../rules/registry.js";
|
|
2
|
+
|
|
1
3
|
//#region src/eslint/rules/props-from-schema.ts
|
|
2
4
|
const propsFromSchema = {
|
|
3
5
|
meta: {
|
|
4
6
|
type: "problem",
|
|
5
|
-
docs: {
|
|
7
|
+
docs: {
|
|
8
|
+
description: "A Renderer must import its Props type from ./schema",
|
|
9
|
+
url: docsPathForRule("props-from-schema")
|
|
10
|
+
},
|
|
6
11
|
schema: [],
|
|
7
12
|
messages: { handWritten: "`{{name}}` is a hand-written props type in a Renderer — it bypasses the schema→Props inference and will drift. Fix: add `export type Props = SectionProps<typeof schema>;` to schema.ts and `import type { Props } from \"./schema.js\";` here." }
|
|
8
13
|
},
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { docsPathForRule } from "../../rules/registry.js";
|
|
1
2
|
import { isClientFile, resolveImportedFile } from "../is-client-file.js";
|
|
2
3
|
|
|
3
4
|
//#region src/eslint/rules/serializable-island-props.ts
|
|
@@ -56,7 +57,10 @@ function inspect(node, path, out) {
|
|
|
56
57
|
const serializableIslandProps = {
|
|
57
58
|
meta: {
|
|
58
59
|
type: "problem",
|
|
59
|
-
docs: {
|
|
60
|
+
docs: {
|
|
61
|
+
description: "Island props must be JSON-serializable",
|
|
62
|
+
url: docsPathForRule("serializable-island-props")
|
|
63
|
+
},
|
|
60
64
|
schema: [],
|
|
61
65
|
messages: {
|
|
62
66
|
notSerializableBadType: "{{path}} is {{what}} — island props must be JSON-serializable (string, number, boolean, null, plain object, array). Fix: pass data, not behavior — an island owns its own handlers.",
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { docsPathForRule } from "../../rules/registry.js";
|
|
2
|
+
|
|
1
3
|
//#region src/eslint/rules/slot-marker-literal.ts
|
|
2
4
|
const KEY_ATTRS = /* @__PURE__ */ new Set([
|
|
3
5
|
"data-slot-id",
|
|
@@ -8,7 +10,10 @@ const KEY_COMPONENTS = /* @__PURE__ */ new Set(["Slot", "SlotGroup"]);
|
|
|
8
10
|
const slotMarkerLiteral = {
|
|
9
11
|
meta: {
|
|
10
12
|
type: "problem",
|
|
11
|
-
docs: {
|
|
13
|
+
docs: {
|
|
14
|
+
description: "Slot markers must carry a string-literal key",
|
|
15
|
+
url: docsPathForRule("slot-marker-literal")
|
|
16
|
+
},
|
|
12
17
|
schema: [],
|
|
13
18
|
messages: { markerNotLiteral: "`{{attr}}` must be a string literal, not an expression — the slot-marker completeness check reads this key statically, and cannot resolve a computed one (the slot would silently lose editor selection). Fix: write the key inline, e.g. `{{attr}}=\"headline\"`." }
|
|
14
19
|
},
|
package/dist/package.js
CHANGED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//#region src/rules/registry.ts
|
|
2
|
+
/**
|
|
3
|
+
* `no-hex` → `docs/rules/no-hex.md`.
|
|
4
|
+
*
|
|
5
|
+
* Package-relative on purpose: the caller decides what to resolve it against. A
|
|
6
|
+
* printer prefixes the resolved package root so an author sees a path they can open
|
|
7
|
+
* with no network (`node_modules/@homepages/template-kit/docs/rules/no-hex.md`);
|
|
8
|
+
* ESLint's `meta.docs.url` uses it as-is. This module never touches the filesystem —
|
|
9
|
+
* the ESLint preset imports it.
|
|
10
|
+
*/
|
|
11
|
+
function docsPathForRule(id) {
|
|
12
|
+
return `docs/rules/${id}.md`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
//#endregion
|
|
16
|
+
export { docsPathForRule };
|
package/dist/styles.css
CHANGED
|
@@ -93,7 +93,7 @@
|
|
|
93
93
|
--text-5xl: var(--tr-text-5xl);
|
|
94
94
|
--text-display: var(--tr-text-display);
|
|
95
95
|
|
|
96
|
-
/* ---------- Spacing scale
|
|
96
|
+
/* ---------- Spacing (Tailwind's own scale — no custom aliases) ---------- */
|
|
97
97
|
--spacing: 0.25rem; /* v4 base unit; Tailwind multiplies (e.g. p-4 = 1rem) */
|
|
98
98
|
|
|
99
99
|
/* ---------- Radii ---------- */
|
package/docs/INDEX.md
CHANGED
|
@@ -18,4 +18,5 @@ detail.
|
|
|
18
18
|
| `primitives.md` | The five contract primitives (`Section`, `Slot`, `SlotItem`, `SlotGroup`, `Image`) and the marker attributes they emit | writing a section's markup |
|
|
19
19
|
| `theme-and-css.md` | The two CSS entries, the one-Tailwind-import rule, `TokenTheme`, the breakpoint ladder, and the opt-in `content-visibility` recipe for long, image-heavy pages | a template's theme, its CSS entry, or a long page with a live map or full-screen overlay |
|
|
20
20
|
| `islands.md` | Interactivity: `"use client"` components, the serializable-props rule, editor options, the loader | anything interactive in a section |
|
|
21
|
-
| `eslint.md` | The `@homepages/template-kit/eslint` preset: zero-config setup
|
|
21
|
+
| `eslint.md` | The `@homepages/template-kit/eslint` preset: zero-config setup and its glob scope | wiring up lint in a template workspace |
|
|
22
|
+
| `rules/` | One page per rule-id (`template-kit/*`), across both venues — the id printed by a lint or `check` failure resolves here. `rules/INDEX.md` routes them; `rules/server-vs-client.md` is the concept the server-rendering rules assume | any lint or `check` failure |
|
package/docs/eslint.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
|
-
purpose: The authoring-lint preset shipped at `@homepages/template-kit/eslint` — setup
|
|
2
|
+
purpose: The authoring-lint preset shipped at `@homepages/template-kit/eslint` — setup and scope. Per-rule reference lives in `rules/`.
|
|
3
3
|
status: living
|
|
4
|
-
related: [INDEX.md, islands.md, theme-and-css.md, primitives.md]
|
|
4
|
+
related: [INDEX.md, rules/INDEX.md, islands.md, theme-and-css.md, primitives.md]
|
|
5
5
|
updated: 2026-07-14
|
|
6
6
|
---
|
|
7
7
|
# ESLint preset
|
|
@@ -36,55 +36,20 @@ three config entries, matched by glob rather than by directory depth:
|
|
|
36
36
|
|
|
37
37
|
| Entry | Files | Rules |
|
|
38
38
|
|---|---|---|
|
|
39
|
-
| `template-kit/authoring` | `**/*.ts`, `**/*.tsx` | the nine file-scoped rules
|
|
39
|
+
| `template-kit/authoring` | `**/*.ts`, `**/*.tsx` | the nine file-scoped rules — see [`rules/`](rules/INDEX.md) |
|
|
40
40
|
| `template-kit/contract-files` | `**/Renderer.tsx`, `**/schema.ts`, `**/fill-spec.ts`, `**/fixtures.ts` | `no-client-directive-in-contract` |
|
|
41
41
|
| `template-kit/renderer` | `**/Renderer.tsx` | `props-from-schema` |
|
|
42
42
|
|
|
43
43
|
Server-vs-client scoping is decided **per file**, by the `"use client"` directive
|
|
44
|
-
prologue —
|
|
45
|
-
|
|
46
|
-
handlers, and inline `style` are legal there and the corresponding rules exempt
|
|
47
|
-
it. A rule that does not exempt islands says so explicitly below.
|
|
44
|
+
prologue — see [server vs client](rules/server-vs-client.md) for what that means
|
|
45
|
+
and exactly which rules stop at that boundary.
|
|
48
46
|
|
|
49
47
|
## The rules
|
|
50
48
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
| Rule | What it bans |
|
|
60
|
-
|---|---|
|
|
61
|
-
| `no-nondeterminism` | Clock/randomness reads (`Date.now()`, `new Date()` with no args, `Math.random()`, `crypto.randomUUID()`/`getRandomValues()`, `performance.now()`) — the publisher and the editor must render byte-identical HTML. Exempt inside an island. |
|
|
62
|
-
| `no-client-runtime-in-server` | Effects, state hooks, event handlers, and browser globals (`fetch`, `window`, `document`, …) — a Renderer must be a pure function of its props. Exempt inside an island. |
|
|
63
|
-
| `no-inline-style` | The `style=` JSX prop — it bypasses the cascade and Tailwind's responsive prefixes. Exempt inside an island. |
|
|
64
|
-
| `no-raw-element` | Raw `<section>`/`<img>` — each has a contract-bearing primitive (`Section`/`Image`) that must be used instead. |
|
|
65
|
-
| `image-bare-needs-reason` | `<Image bare>` with no `// bare: <reason>` comment justifying the skipped frame. |
|
|
66
|
-
| `no-css-import-from-render-path` | A `.css` import anywhere in a section's render path — the SSR graph drops it silently. Import CSS from the section's own `styles.css` instead. |
|
|
67
|
-
|
|
68
|
-
### Islands
|
|
69
|
-
|
|
70
|
-
| Rule | What it checks |
|
|
71
|
-
|---|---|
|
|
72
|
-
| `serializable-island-props` | A statically-visible prop passed to an island (a literal function, `new Date()`/`Map`/`Set`/etc., a bigint literal, `NaN`/`Infinity`) that cannot cross the server→browser JSON boundary. It is the static twin of the runtime check — see [islands.md](islands.md#props-must-be-json-serializable). |
|
|
73
|
-
|
|
74
|
-
### Universal — no island exemption
|
|
75
|
-
|
|
76
|
-
| Rule | What it bans |
|
|
77
|
-
|---|---|
|
|
78
|
-
| `no-hex` | Hex color literals and CSS color functions (`oklch`/`oklab`/`rgb`/`rgba`/`hsl`/`hsla`/`color-mix`) in a color context (a Tailwind color-utility bracket, an arbitrary-property color declaration, or an inline `style` value). Token discipline is universal — see [theme-and-css.md](theme-and-css.md#color-roles). |
|
|
79
|
-
|
|
80
|
-
Deliberate asymmetry: `no-inline-style` exempts islands, `no-hex` does not — the
|
|
81
|
-
hydration boundary is not a palette boundary. A section carries no palette of its
|
|
82
|
-
own in either render path.
|
|
83
|
-
|
|
84
|
-
### Markers and schema
|
|
85
|
-
|
|
86
|
-
| Rule | What it checks |
|
|
87
|
-
|---|---|
|
|
88
|
-
| `slot-marker-literal` | A slot-key-bearing marker (`<Slot id>`, `<SlotGroup id>`, `slotId`, `data-slot-id`, `data-slot-group`) must carry a string-literal key, not an expression — the slot-marker completeness check (`template-kit check`) reads these keys statically. `SlotItem` is exempt: its prop is `index: number`, an array position, so there is no schema key to cross-check. |
|
|
89
|
-
| `props-from-schema` | A `Renderer.tsx`'s exported `Props` type must come from `SectionProps<typeof schema>`. A hand-written `Props` shadows that inference and drifts. |
|
|
90
|
-
| `no-client-directive-in-contract` | `"use client"` in a section's four contract files (`Renderer.tsx`, `schema.ts`, `fill-spec.ts`, `fixtures.ts`) — the platform reads them outside a browser; marking one an island takes the section's server-rendered HTML with it. |
|
|
49
|
+
Every rule the preset enforces — what it bans, why, and a before/after fix — is
|
|
50
|
+
documented once, per id, in [`rules/`](rules/INDEX.md). Rules scoped to a single
|
|
51
|
+
file are enforced here, by this preset; rules scoped to the **tree** — a missing
|
|
52
|
+
contract file, a slot declared in `schema.ts` but unmarked in `Renderer.tsx`, a CSS
|
|
53
|
+
budget — cannot be expressed one-file-at-a-time and belong to `template-kit check`
|
|
54
|
+
under the same `template-kit/<id>` namespace. One namespace, two venues: the id
|
|
55
|
+
printed by either failure resolves to exactly one page.
|
package/docs/llms.txt
CHANGED
|
@@ -27,7 +27,8 @@ Everything an author writes a section against is imported from the package root
|
|
|
27
27
|
and `Image` (responsive `<picture>`, empty-state box). Plus the marker-attribute
|
|
28
28
|
contract (`ATTR_SLOT_ID`, `ATTR_SLOT_ITEM`, …). Presentation is the template's own JSX.
|
|
29
29
|
- [Theme and CSS](theme-and-css.md): the two CSS entries and how they load; import
|
|
30
|
-
Tailwind exactly once; `
|
|
30
|
+
Tailwind exactly once; `base.css` ships no reset of its own — it requires the
|
|
31
|
+
preflight your Tailwind entry brings; `TokenTheme` + `compileThemeToCss`; the breakpoint ladder
|
|
31
32
|
(base 350, `bp390` `bp425` `bp512` `md` `lg` `bp1440` `bp1920`); the opt-in
|
|
32
33
|
`content-visibility` recipe for long, image-heavy pages, and why it requires a
|
|
33
34
|
full-screen overlay to be appended to `document.body` rather than rendered inside
|
|
@@ -35,6 +36,8 @@ Everything an author writes a section against is imported from the package root
|
|
|
35
36
|
- [README](../README.md): install, exports, and the release flow.
|
|
36
37
|
- [Islands](islands.md): interactivity — client components, the serializable-props contract, editor options, and the browser loader.
|
|
37
38
|
- [ESLint preset](eslint.md): `@homepages/template-kit/eslint`, the zero-config flat-config
|
|
38
|
-
preset
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
preset — setup and its three-entry glob scope. Per-rule reference lives in `rules/`.
|
|
40
|
+
- [Rules](rules/INDEX.md): every rule-id a lint or `check` failure can print, under the
|
|
41
|
+
`template-kit/<id>` namespace, one page each at `docs/rules/<id>.md` — Rule / Reason /
|
|
42
|
+
Fix / Before→After. **The loop:** see an id in a failure → read `docs/rules/<id>.md` →
|
|
43
|
+
fix. Nothing else to search for; the id is the filename.
|