@homepages/template-kit 0.1.0 → 0.3.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 +220 -0
- package/README.md +70 -16
- package/dist/base.css +36 -48
- package/dist/cli/check/config.js +37 -0
- package/dist/cli/check/css.js +117 -0
- package/dist/cli/check/diagnostics.js +32 -0
- package/dist/cli/check/index.js +87 -0
- package/dist/cli/check/loader.js +142 -0
- package/dist/cli/check/paths.js +15 -0
- package/dist/cli/check/relativize.js +18 -0
- package/dist/cli/check/render-invariants.js +24 -0
- package/dist/cli/check/resolve-tool.js +38 -0
- package/dist/cli/check/stages/deps.js +337 -0
- package/dist/cli/check/stages/lint.js +46 -0
- package/dist/cli/check/stages/render.js +101 -0
- package/dist/cli/check/stages/size.js +158 -0
- package/dist/cli/check/stages/tree.js +207 -0
- package/dist/cli/check/stages/typecheck.js +46 -0
- package/dist/cli/check/stages/validate/catalog-exhaustiveness.js +18 -0
- package/dist/cli/check/stages/validate/facts.js +18 -0
- package/dist/cli/check/stages/validate/formatter-contract.js +14 -0
- package/dist/cli/check/stages/validate/group-contract.js +21 -0
- package/dist/cli/check/stages/validate/index.js +66 -0
- package/dist/cli/check/stages/validate/list-scalar-contract.js +22 -0
- package/dist/cli/check/stages/validate/nav-contract.js +93 -0
- package/dist/cli/check/stages/validate/nested-slot-contract.js +12 -0
- package/dist/cli/check/stages/validate/object-list-contract.js +31 -0
- package/dist/cli/check/stages/validate/orchestrator.js +241 -0
- package/dist/cli/check/stages/validate/produced-by-contract.js +18 -0
- package/dist/cli/check/stages/validate/row-contract.js +31 -0
- package/dist/cli/check/stages/validate/select-contract.js +13 -0
- package/dist/cli/check/stages/validate/source-contract.js +21 -0
- package/dist/cli/check/stages/validate/variant-contract.js +15 -0
- package/dist/cli/check/workspace.js +86 -0
- package/dist/cli/theme/generate.js +43 -0
- package/dist/cli/theme/index.js +33 -0
- package/dist/cli/theme/load-theme.js +69 -0
- package/dist/cli.js +22 -4
- package/dist/contracts/fill-treatments.js +47 -0
- package/dist/design-system/theme.d.ts +30 -299
- package/dist/design-system/theme.js +112 -83
- 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 +84 -7
- 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/eslint.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/package.js +1 -1
- package/dist/primitives/Image.js +1 -1
- package/dist/rules/registry.js +16 -0
- package/dist/schema/fill-spec.d.ts +80 -590
- package/dist/schema/fixture-schema.d.ts +5 -81
- package/dist/schema/manifest.d.ts +39 -475
- package/dist/schema/resolve-section-ref.js +10 -0
- package/dist/schema/rows.js +10 -0
- package/dist/schema/section-nav.js +9 -0
- package/dist/schema/section-schema.d.ts +51 -437
- package/dist/schema/slot-types.d.ts +12 -16
- package/dist/styles.css +31 -88
- package/docs/INDEX.md +5 -3
- package/docs/check.md +121 -0
- package/docs/eslint.md +25 -50
- package/docs/llms.txt +26 -11
- package/docs/primitives.md +5 -0
- package/docs/rules/INDEX.md +65 -0
- package/docs/rules/audit-severity.md +91 -0
- package/docs/rules/bundle-binary-asset.md +102 -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/fixtures-invalid.md +96 -0
- package/docs/rules/image-bare-needs-reason.md +117 -0
- package/docs/rules/license-denied.md +114 -0
- package/docs/rules/lockfile-missing.md +68 -0
- package/docs/rules/lockfile-stale.md +88 -0
- package/docs/rules/manifest-invalid.md +99 -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 +163 -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/no-templates.md +87 -0
- package/docs/rules/parse-error.md +76 -0
- package/docs/rules/props-from-schema.md +119 -0
- package/docs/rules/render-invariant.md +165 -0
- package/docs/rules/schema-invalid.md +96 -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 +193 -82
- package/package.json +4 -9
- package/tsconfig.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,225 @@
|
|
|
1
1
|
# @homepages/template-kit
|
|
2
2
|
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- bdb4dec: `zod` is now the kit's only dependency of any kind, besides its React peers. ESLint,
|
|
8
|
+
`@typescript-eslint/parser` and `esbuild` are no longer declared by the kit at all — a
|
|
9
|
+
linter and a bundler are development tools, and as dependencies they landed in the
|
|
10
|
+
**production** install of every consumer, including apps that only render a published
|
|
11
|
+
template and deploy with `npm ci --omit=dev`. For a consuming service that lints, the
|
|
12
|
+
production dependency graph drops by roughly 125 packages.
|
|
13
|
+
|
|
14
|
+
**Action required if you use the ESLint preset or the `check` command** — install the
|
|
15
|
+
toolchain they run, which they now resolve from your workspace rather than shipping
|
|
16
|
+
their own copy of:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm i -D eslint @typescript-eslint/parser # for @homepages/template-kit/eslint
|
|
20
|
+
npm i -D typescript esbuild # for template-kit check (with the two above)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Nothing changes for code that only imports the kit's primitives, schema system or theme
|
|
24
|
+
tokens. The preset's rules and parser wiring are unchanged; `check`'s stages and rule ids
|
|
25
|
+
are unchanged — and it now reports a missing tool by name, with the install line, instead
|
|
26
|
+
of failing obscurely. As a side benefit, `check` runs against _your_ pinned TypeScript,
|
|
27
|
+
ESLint and esbuild.
|
|
28
|
+
|
|
29
|
+
- 49177ab: Adds the `check` command: `template-kit check [template] [--all] [--json]`. Runs a
|
|
30
|
+
template's authoring contract end to end — type errors, the lint preset, the schema and
|
|
31
|
+
manifest contracts, section-folder structure (contract files present, editor slot
|
|
32
|
+
markers, hand-CSS hygiene), a render-regression pass (determinism, broken images, and
|
|
33
|
+
other objective defects), and per-section bundle-size budgets — plus workspace-wide
|
|
34
|
+
dependency gates (lockfile health, a single copy of React, dependency licenses,
|
|
35
|
+
vulnerability severity) evaluated once per run. Every failure prints a stable rule id, a
|
|
36
|
+
file and line, and a one-line fix. Exits non-zero on any failure; pass `--json` for a
|
|
37
|
+
script-consumable report.
|
|
38
|
+
- 2a30f0b: `template-kit theme` generates your template's `theme.css` from its `theme.ts`.
|
|
39
|
+
|
|
40
|
+
`@theme` is a Tailwind directive — it only does anything when a real `.css` file carrying
|
|
41
|
+
it flows through your Tailwind entry. So the alias layer `compileThemeToCss` produces from
|
|
42
|
+
your `theme.ts` has to reach that entry as a file. Rather than call the compiler from your
|
|
43
|
+
own build script (and risk `theme.css` silently drifting from `theme.ts`), run:
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
template-kit theme # the template in the current folder
|
|
47
|
+
template-kit theme <name> # a named template
|
|
48
|
+
template-kit theme --all # every template in the workspace
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
It reads `templates/<key>/theme.ts` (which must `export const theme: TokenTheme`), validates
|
|
52
|
+
it, and writes `templates/<key>/theme.css` with a "generated — do not edit" header. Import
|
|
53
|
+
that file into your Tailwind entry after `@import "tailwindcss"` and the kit's `styles.css`:
|
|
54
|
+
|
|
55
|
+
```css
|
|
56
|
+
@import "tailwindcss";
|
|
57
|
+
@import "@homepages/template-kit/styles.css";
|
|
58
|
+
@import "./theme.css"; /* generated by `template-kit theme` */
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Re-run the command whenever you change `theme.ts`; a token you add becomes a working Tailwind
|
|
62
|
+
utility with nothing else to edit. A missing `theme.ts`, a missing `theme` export, or a theme
|
|
63
|
+
that fails validation each fail with an actionable message.
|
|
64
|
+
|
|
65
|
+
- e15de58: Your template now owns its design tokens end to end — and names them itself.
|
|
66
|
+
|
|
67
|
+
`TokenTheme` no longer imposes a vocabulary. Instead of satisfying a fixed set of
|
|
68
|
+
semantic roles (`ink`, `surfaceAlt`, `primaryStrong`, …), you declare whatever tokens
|
|
69
|
+
your design actually has, under kebab-case names you choose, and `compileThemeToCss`
|
|
70
|
+
generates the Tailwind `@theme inline` alias layer from them. Declare `lagoon` and
|
|
71
|
+
`bg-lagoon` exists. Nothing is required, and Tailwind's own defaults (`text-2xl`,
|
|
72
|
+
`rounded-lg`, `red-500`) still work underneath, so a sparse theme is a working theme.
|
|
73
|
+
|
|
74
|
+
This also closes the hole that made the old "open palette" a fiction: a brand swatch
|
|
75
|
+
only became a usable utility once it was added to the kit's shared `styles.css`, so it
|
|
76
|
+
was never really yours. There is no shared file to edit any more.
|
|
77
|
+
|
|
78
|
+
**Upgrading — three changes:**
|
|
79
|
+
|
|
80
|
+
1. **Rewrite `theme.ts`.** `colors` and `palette` merge into one open record, and keys
|
|
81
|
+
become kebab-case (`inkSoft` → `ink-soft`). `fontFaces` moves out of `fonts` to the
|
|
82
|
+
top level. Emission is verbatim: the key `ink-soft` becomes `--tr-color-ink-soft` and
|
|
83
|
+
the `text-ink-soft` utility.
|
|
84
|
+
|
|
85
|
+
One token yields exactly one utility, so two utility names mean two tokens. If you
|
|
86
|
+
relied on an alias that pointed two names at one value, declare both.
|
|
87
|
+
|
|
88
|
+
2. **Add a `document` block.** The `body` defaults can no longer be assumed — there is no
|
|
89
|
+
`ink` to guess at — so name the tokens that dress the page:
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
document: { font: "sans", color: "ink", background: "page", fontSize: "base" }
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Each reference is validated against your declared tokens: a typo fails at build
|
|
96
|
+
rather than rendering an unthemed page in silence.
|
|
97
|
+
|
|
98
|
+
3. **Import your compiled theme into your Tailwind entry.** `@theme` is a Tailwind
|
|
99
|
+
directive and is inert outside the build graph, so the theme partial must be
|
|
100
|
+
`@import`ed by your entry, after the kit's `styles.css`:
|
|
101
|
+
|
|
102
|
+
```css
|
|
103
|
+
@import "tailwindcss";
|
|
104
|
+
@import "@homepages/template-kit/styles.css";
|
|
105
|
+
@import "./theme.css"; /* compileThemeToCss output */
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
`styles.css` and `base.css` keep their export paths — no import changes. `styles.css`
|
|
109
|
+
now carries only the breakpoint ladder, the `--spacing` base unit, and the motion
|
|
110
|
+
defaults.
|
|
111
|
+
|
|
112
|
+
The `Image` primitive no longer paints its frame with `bg-surface-alt` (a token your
|
|
113
|
+
theme may not declare). The fill lives on `.tr-image-frame` in `base.css`; retint it by
|
|
114
|
+
setting `--tr-image-frame-bg`.
|
|
115
|
+
|
|
116
|
+
- 57eddee: Migrate the schema system from Zod 3 to Zod 4.
|
|
117
|
+
|
|
118
|
+
`zod` is the kit's one runtime dependency, and it now tracks Zod 4 (`"zod": "^4"`).
|
|
119
|
+
Because the types you author section schemas against are Zod types, bumping Zod a
|
|
120
|
+
major is a breaking change for authors: your workspace resolves Zod 4 in its
|
|
121
|
+
lockfile, and the schema/slot types you see in editor tooltips are now Zod 4 types.
|
|
122
|
+
|
|
123
|
+
**Action required** — if your workspace declares `zod`, move it to `^4` so a single
|
|
124
|
+
Zod copy resolves across your workspace and the kit:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
npm i zod@^4
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Nothing else changes. The authoring API is untouched — the slot-type catalog, the
|
|
131
|
+
source/transform vocabulary, and `SectionProps<typeof schema>` inference are all
|
|
132
|
+
identical, and every section still materializes to a byte-identical serialized
|
|
133
|
+
schema. This is a dependency migration, invisible to an author apart from the Zod
|
|
134
|
+
version in the lockfile.
|
|
135
|
+
|
|
136
|
+
### Patch Changes
|
|
137
|
+
|
|
138
|
+
- 6675c4c: `no-hex` no longer reports a `color-mix()` whose colour arguments are all theme-derived.
|
|
139
|
+
A mix over `var(--…)` references, `transparent`, or `currentColor` carries no palette of
|
|
140
|
+
its own — it composes a state colour (hover, active, pressed) out of the theme the
|
|
141
|
+
section was handed — so banning it contradicted the rule's own reason. A hard-coded
|
|
142
|
+
argument still reports: `color-mix(in srgb, var(--tr-color-primary) 80%, #fff)` and
|
|
143
|
+
`color-mix(…, white)` are violations, as is a `var()` fallback that bakes a colour
|
|
144
|
+
(`var(--x, #fff)`). The carve-out is `color-mix()` only — `rgb()`, `oklch()`, and the
|
|
145
|
+
rest take raw channel values and stay banned outright, so the "a section carries zero
|
|
146
|
+
palette" guarantee is unchanged.
|
|
147
|
+
- 1749cea: `no-hex` now ships as a **warning**, not an error. Centralized theme colours are
|
|
148
|
+
encouraged, not gated: a hard-coded colour still squiggles in the editor and the rule
|
|
149
|
+
still explains how to derive the colour from theme tokens, but it no longer fails
|
|
150
|
+
`template-kit check` locally or at ingest (both gate on errors only). Every other preset
|
|
151
|
+
rule stays an error — this is the one advisory rule. The `color-mix()` over theme-derived
|
|
152
|
+
arguments carve-out is unchanged; it just governs a warning now instead of an error.
|
|
153
|
+
|
|
154
|
+
## 0.2.0
|
|
155
|
+
|
|
156
|
+
### Minor Changes
|
|
157
|
+
|
|
158
|
+
- e64d4d7: `base.css` no longer ships a reset — Tailwind's preflight (from your entry's
|
|
159
|
+
`@import "tailwindcss"`) already is one, and the two were duplicating each other. It
|
|
160
|
+
now carries only the three declarations preflight does not make: font-smoothing,
|
|
161
|
+
`button { cursor: pointer }`, and `svg { max-width: 100% }`.
|
|
162
|
+
|
|
163
|
+
**`base.css` now requires a Tailwind entry on the page.** If a page loads `base.css`
|
|
164
|
+
without an `@import "tailwindcss"` entry present — a standalone preview, an embed, a
|
|
165
|
+
hand-written CSS page — it renders fully unreset (UA body margin, bulleted lists,
|
|
166
|
+
underlined links, unconstrained inline images), and nothing errors to tell you.
|
|
167
|
+
Confirm every page that loads `base.css` also loads a Tailwind entry.
|
|
168
|
+
|
|
169
|
+
`base.css` also no longer references any theme token. The `body` document defaults
|
|
170
|
+
(font, color, background, size) are now emitted by `compileThemeToCss`, alongside the
|
|
171
|
+
`:root` block that defines those tokens. **Regenerate and republish your compiled theme
|
|
172
|
+
CSS.** If you upgrade `base.css` but keep serving a previously-compiled theme
|
|
173
|
+
stylesheet, that stylesheet still only has the old `:root` block — `body` silently
|
|
174
|
+
loses `font-family`, `color`, `background`, and `font-size` page-wide, because
|
|
175
|
+
`base.css` no longer carries them as a fallback. The compiled output's content hash
|
|
176
|
+
changes.
|
|
177
|
+
|
|
178
|
+
The `--tr-space-*` scale is removed — Tailwind's own spacing scale (`p-4`, `gap-6`)
|
|
179
|
+
was never aliased to it. Any of your CSS still referencing `var(--tr-space-*)` (e.g.
|
|
180
|
+
`padding: var(--tr-space-4)`) now points at an undefined custom property, which is
|
|
181
|
+
invalid at computed-value time, so the declaration drops. The `scroll-behavior` /
|
|
182
|
+
`scroll-padding-top` policy is removed; set it in your own template stylesheet if you
|
|
183
|
+
have a sticky header.
|
|
184
|
+
|
|
185
|
+
- a24174f: Every rule-id now resolves to a page. The lint preset and `template-kit check` share one
|
|
186
|
+
id namespace (`template-kit/<id>`), and each of those ids — all 28 — now has a page at
|
|
187
|
+
`docs/rules/<id>.md` giving the rule, the reason it exists, and a before/after fix you can
|
|
188
|
+
copy.
|
|
189
|
+
|
|
190
|
+
The docs ship **inside the tarball**, so the loop closes with no network: hit
|
|
191
|
+
`template-kit/no-hex`, read `node_modules/@homepages/template-kit/docs/rules/no-hex.md`,
|
|
192
|
+
fix it. That matters most for the coding agents authoring against this kit — an agent that
|
|
193
|
+
can resolve a failure to its own reference page fixes the violation instead of guessing at
|
|
194
|
+
it.
|
|
195
|
+
|
|
196
|
+
Each ESLint rule also carries `meta.docs.url` pointing at its page, so an editor's "show
|
|
197
|
+
documentation for this rule" lands on the same file.
|
|
198
|
+
|
|
199
|
+
Start at `docs/rules/INDEX.md`. `docs/rules/server-vs-client.md` explains the one concept
|
|
200
|
+
several rules assume — that server-vs-client is a **per-file** fact, decided by the
|
|
201
|
+
`"use client"` directive prologue, not by an import graph.
|
|
202
|
+
|
|
203
|
+
### Patch Changes
|
|
204
|
+
|
|
205
|
+
- be1507d: Fixed `no-css-import-from-render-path`, which banned the one legal way to get a
|
|
206
|
+
third-party stylesheet into a section.
|
|
207
|
+
|
|
208
|
+
A **bare** `.css` specifier in the render path — `import "swiper/css"` in `Renderer.tsx`,
|
|
209
|
+
in a marked enhancer, or in any module either one imports — is how a package's CSS reaches
|
|
210
|
+
the section's cascade layer: the build bundles that import graph precisely to harvest it.
|
|
211
|
+
The rule flagged it anyway, and its `Fix:` hint sent you to `@import` the stylesheet from
|
|
212
|
+
`styles.css`, which is never legal there (hand-written CSS is wrapped in `@layer`, where a
|
|
213
|
+
browser drops an `@import` silently).
|
|
214
|
+
|
|
215
|
+
The rule now reports only a **local** stylesheet import — a relative or absolute specifier.
|
|
216
|
+
That is the real defect: the build already collects every `.css` file in the section folder
|
|
217
|
+
by filename, so importing one from code ships its rules twice and can corrupt the cascade.
|
|
218
|
+
|
|
219
|
+
If you worked around the old rule by moving a package's CSS into a stylesheet `@import`,
|
|
220
|
+
move it back into the module that needs it — see `docs/rules/no-css-import-from-render-path.md`
|
|
221
|
+
and `docs/rules/no-bare-css-import.md`, which are the two halves of the same mental model.
|
|
222
|
+
|
|
3
223
|
## 0.1.0
|
|
4
224
|
|
|
5
225
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -1,21 +1,32 @@
|
|
|
1
1
|
# @homepages/template-kit
|
|
2
2
|
|
|
3
3
|
The authoring surface for HomePages marketing-section templates: schema system,
|
|
4
|
-
contract primitives, theme
|
|
4
|
+
contract primitives, the theme system, and the `template-kit` CLI.
|
|
5
5
|
|
|
6
6
|
**Status: 0.0.x.** The **schema system** (the section authoring contract and the closed
|
|
7
|
-
vocabulary it validates against), the **contract primitives**, and the **theme
|
|
7
|
+
vocabulary it validates against), the **contract primitives**, and the **theme system**
|
|
8
8
|
are complete — a section is authorable end to end against the kit. The `template-kit`
|
|
9
|
-
CLI's commands are
|
|
9
|
+
CLI's `check` and `theme` commands are implemented — see
|
|
10
|
+
[Checking a workspace](#checking-a-workspace) and
|
|
11
|
+
[Generating a theme](#generating-a-theme) below; its other commands arrive with the
|
|
12
|
+
surfaces they drive.
|
|
10
13
|
|
|
11
14
|
## Install
|
|
12
15
|
|
|
13
16
|
```bash
|
|
14
17
|
npm install @homepages/template-kit react react-dom
|
|
18
|
+
npm install -D typescript eslint @typescript-eslint/parser esbuild # the authoring toolchain
|
|
15
19
|
```
|
|
16
20
|
|
|
17
21
|
React 19 is a **peer dependency** — the kit never bundles its own copy.
|
|
18
22
|
|
|
23
|
+
The kit has exactly **one runtime dependency** (`zod`). The authoring toolchain the
|
|
24
|
+
[`check` command](docs/check.md) and the [ESLint preset](docs/eslint.md) run on is
|
|
25
|
+
yours: both resolve TypeScript, ESLint, the parser and esbuild from your workspace, so
|
|
26
|
+
your pinned versions are the ones that judge your code. The kit does not depend on them
|
|
27
|
+
at all — which is why a linter never lands in the production install of an app that
|
|
28
|
+
merely renders a published template.
|
|
29
|
+
|
|
19
30
|
## Authoring a section
|
|
20
31
|
|
|
21
32
|
A section is four files: `schema.ts` (slots + options), `fill-spec.ts` (how AI
|
|
@@ -64,24 +75,64 @@ Full contract: **[docs/primitives.md](docs/primitives.md)**.
|
|
|
64
75
|
| `@homepages/template-kit` | Authoring API — schema system, primitives, marker contract, theme types, and the golden fixtures |
|
|
65
76
|
| `@homepages/template-kit/island-runtime` | Browser loader that hydrates a page's islands |
|
|
66
77
|
| `@homepages/template-kit/browser` | Browser helpers for published pages (site config, signed ingest) |
|
|
67
|
-
| `@homepages/template-kit/styles.css` |
|
|
68
|
-
| `@homepages/template-kit/base.css` |
|
|
78
|
+
| `@homepages/template-kit/styles.css` | The global Tailwind preamble: the breakpoint ladder, the spacing base unit, the motion defaults, kit source registration |
|
|
79
|
+
| `@homepages/template-kit/base.css` | Cascade-layer order, the three reset gaps Tailwind's preflight leaves, `.tr-section`, and the motion tokens |
|
|
69
80
|
| `@homepages/template-kit/eslint` | ESLint flat-config preset |
|
|
70
81
|
| `@homepages/template-kit/tsconfig` | tsconfig preset (`"extends"`) |
|
|
71
82
|
|
|
72
83
|
The authoring API has **no subpath imports** — one specifier for everything you
|
|
73
84
|
write a section against. The two CSS entries are the deliberate exception.
|
|
74
85
|
|
|
75
|
-
`styles.css` does **not** import Tailwind. Import it *after* Tailwind in your entry
|
|
86
|
+
`styles.css` does **not** import Tailwind. Import it *after* Tailwind in your entry, then
|
|
87
|
+
your generated theme after that:
|
|
76
88
|
|
|
77
89
|
```css
|
|
78
90
|
@import "tailwindcss";
|
|
79
91
|
@import "@homepages/template-kit/styles.css";
|
|
92
|
+
@import "./theme.css"; /* generated by `template-kit theme` from theme.ts */
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Two `@import "tailwindcss"` in one graph double-emits preflight. Your generated theme
|
|
96
|
+
belongs in this entry and nowhere else: it carries an `@theme` block, a Tailwind directive
|
|
97
|
+
that is inert outside the build graph. `base.css` is not part of that entry — it loads as
|
|
98
|
+
its own stylesheet on the page. `base.css` ships no reset of its own — it depends on the
|
|
99
|
+
preflight your entry's `@import "tailwindcss"` brings. Both must be on the page. Full
|
|
100
|
+
contract: **[docs/theme-and-css.md](docs/theme-and-css.md)**.
|
|
101
|
+
|
|
102
|
+
## Rules
|
|
103
|
+
|
|
104
|
+
The lint preset and the `template-kit check` CLI share one id namespace,
|
|
105
|
+
`template-kit/<id>`. A rule-id printed by either resolves to a page —
|
|
106
|
+
`docs/rules/<id>.md` — giving the rule, the reason it exists, and a before/after fix.
|
|
107
|
+
Start at **[docs/rules/INDEX.md](docs/rules/INDEX.md)**; the full doc set is routed
|
|
108
|
+
from **[docs/INDEX.md](docs/INDEX.md)**.
|
|
109
|
+
|
|
110
|
+
## Generating a theme
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
template-kit theme # infer the template from cwd, or the workspace's one template
|
|
114
|
+
template-kit theme --all # generate every template's theme.css
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
`theme` compiles a template's `theme.ts` into `templates/<key>/theme.css` — the file your
|
|
118
|
+
Tailwind entry imports. The output is generated: don't hand-edit it, and re-run the
|
|
119
|
+
command whenever `theme.ts` changes. Full contract:
|
|
120
|
+
**[docs/theme-and-css.md](docs/theme-and-css.md#generating-themecss)**.
|
|
121
|
+
|
|
122
|
+
## Checking a workspace
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
template-kit check # infer the template from cwd, or the workspace's one template
|
|
126
|
+
template-kit check --all # check every template
|
|
127
|
+
template-kit check --json # machine-readable report
|
|
80
128
|
```
|
|
81
129
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
130
|
+
`check` typechecks, lints, and validates a template's schema/fill-spec/fixtures/manifest
|
|
131
|
+
contracts, runs a determinism render probe, and enforces per-section byte budgets —
|
|
132
|
+
plus workspace-wide dependency gates (lockfile health, a single React, license and
|
|
133
|
+
vulnerability checks) evaluated once per run. It exits non-zero on any failure and is
|
|
134
|
+
the same gate the platform re-runs when you submit a template. Full contract:
|
|
135
|
+
**[docs/check.md](docs/check.md)**.
|
|
85
136
|
|
|
86
137
|
## Development
|
|
87
138
|
|
|
@@ -91,11 +142,13 @@ npm run check # typecheck + lint + build + island-size gate + test + publint
|
|
|
91
142
|
```
|
|
92
143
|
|
|
93
144
|
`npm run verify:consumer` is the boundary gate: it packs the kit, installs the
|
|
94
|
-
tarball into
|
|
145
|
+
tarball into scratch projects, and asserts that a service which pins the kit and
|
|
146
|
+
deploys with `npm ci --omit=dev` installs no ESLint or esbuild package at all, with
|
|
147
|
+
`dependencies` carrying nothing but `zod`; that types resolve; that a real section
|
|
95
148
|
triple typechecks against the installed package, with a misspelled property fact or
|
|
96
|
-
slot key still failing to compile; that
|
|
97
|
-
|
|
98
|
-
before every release.
|
|
149
|
+
slot key still failing to compile; that the ESLint preset lints using the consumer's
|
|
150
|
+
own parser; that Tailwind's cross-package `@source` scan reaches the shipped `dist`;
|
|
151
|
+
and that only one React ends up in the tree. Run it before every release.
|
|
99
152
|
|
|
100
153
|
The island loader (`@homepages/template-kit/island-runtime`) has its own gate,
|
|
101
154
|
`npm run size:islands`: it must stay under 3072 B gzip and never import
|
|
@@ -121,9 +174,10 @@ token anywhere, and adding one would silently disable OIDC.
|
|
|
121
174
|
|
|
122
175
|
3. **Merge the Version Packages PR.** That merge publishes to npm.
|
|
123
176
|
|
|
124
|
-
Semver contract: the platform vocabulary (slot types, sources, transforms
|
|
125
|
-
|
|
126
|
-
|
|
177
|
+
Semver contract: the platform vocabulary (slot types, sources, transforms) is
|
|
178
|
+
closed and versioned. A breaking change to it is a **major**; a new optional
|
|
179
|
+
capability is a **minor**. Design tokens are *not* part of that vocabulary —
|
|
180
|
+
your template names its own.
|
|
127
181
|
|
|
128
182
|
### Bumping the version by hand
|
|
129
183
|
|
package/dist/base.css
CHANGED
|
@@ -7,52 +7,47 @@
|
|
|
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
|
-
* -
|
|
12
|
+
* - The <Image> frame's placeholder fill (.tr-image-frame)
|
|
13
|
+
* - Universal tokens: motion
|
|
14
14
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
15
|
+
* REQUIRES A TAILWIND ENTRY ON THE PAGE. Your entry's `@import "tailwindcss"` brings
|
|
16
|
+
* Tailwind's preflight, and preflight does the reset: box-sizing, margin/padding,
|
|
17
|
+
* list-style, the button and anchor resets, display:block on replaced elements. This
|
|
18
|
+
* file only fills the three gaps preflight leaves. Ship base.css without a Tailwind
|
|
19
|
+
* entry and you get an unreset page.
|
|
20
|
+
*
|
|
21
|
+
* NOT here: anything a template owns. Colors, fonts, type, radii, shadows and layout
|
|
22
|
+
* — and the `body` document defaults that apply them — all come from your theme.ts,
|
|
23
|
+
* compiled by compileThemeToCss. This file references no theme token at all.
|
|
19
24
|
*/
|
|
20
25
|
|
|
21
|
-
/* ----- Reset
|
|
26
|
+
/* ----- Reset gaps ------------------------------------------------------- *
|
|
27
|
+
* The three declarations Tailwind's preflight does NOT make. Each is load-bearing:
|
|
28
|
+
* delete one and your page changes.
|
|
29
|
+
*
|
|
22
30
|
* Wrapped in `@layer base` so Tailwind's utilities (which compile into
|
|
23
|
-
* `@layer utilities`) win over these element defaults. Unlayered,
|
|
24
|
-
*
|
|
25
|
-
*
|
|
31
|
+
* `@layer utilities`) win over these element defaults. Unlayered, `button { cursor:
|
|
32
|
+
* pointer }` would beat a layered `.cursor-default` utility outright — unlayered
|
|
33
|
+
* always wins over layered in the cascade.
|
|
26
34
|
*/
|
|
27
35
|
|
|
28
36
|
@layer base {
|
|
29
|
-
|
|
30
|
-
html, body { margin: 0; padding: 0; }
|
|
37
|
+
/* preflight carries no font-smoothing rule at all */
|
|
31
38
|
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;
|
|
39
|
+
/* Tailwind v4 deliberately leaves buttons at the UA default `cursor: default` */
|
|
40
|
+
button { cursor: pointer; }
|
|
41
|
+
/* preflight caps max-width on img and video, but not on svg */
|
|
42
|
+
svg { max-width: 100%; }
|
|
48
43
|
}
|
|
49
44
|
|
|
50
45
|
/* ----- Primitives ------------------------------------------------------- */
|
|
51
46
|
|
|
52
|
-
/* The full-bleed section root, emitted by the <Section> primitive. Always 100%
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
47
|
+
/* The full-bleed section root, emitted by the <Section> primitive. Always 100% wide
|
|
48
|
+
* with zero padding/margin, so adjacent sections butt edge-to-edge and a page composes
|
|
49
|
+
* as a vertical stack with no framework-injected whitespace. A section brings its own
|
|
50
|
+
* background and internal padding. */
|
|
56
51
|
.tr-section {
|
|
57
52
|
display: block;
|
|
58
53
|
width: 100%;
|
|
@@ -60,24 +55,17 @@ body {
|
|
|
60
55
|
margin: 0;
|
|
61
56
|
}
|
|
62
57
|
|
|
58
|
+
/* The <Image> frame's placeholder fill, showing while an image is absent or loading.
|
|
59
|
+
* It is a plain class rather than a `bg-*` utility on the primitive because a template
|
|
60
|
+
* names its own tokens — there is no colour token this primitive may assume exists.
|
|
61
|
+
* Retint it by setting --tr-image-frame-bg in your theme's layout tokens. */
|
|
62
|
+
.tr-image-frame {
|
|
63
|
+
background-color: var(--tr-image-frame-bg, oklch(0.97 0 0));
|
|
64
|
+
}
|
|
65
|
+
|
|
63
66
|
/* ----- Universal tokens ------------------------------------------------- */
|
|
64
67
|
|
|
65
68
|
: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
69
|
/* Motion */
|
|
82
70
|
--tr-duration-fast: 150ms;
|
|
83
71
|
--tr-duration-base: 250ms;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
//#region src/cli/check/config.ts
|
|
2
|
+
const CHECK_CONFIG = {
|
|
3
|
+
size: {
|
|
4
|
+
/** Gzipped bytes of a section's renderer bundle, React excluded. */
|
|
5
|
+
rendererBundleGzip: 50 * 1024,
|
|
6
|
+
/** Gzipped bytes of a section's stylesheet, including CSS pulled in from packages. */
|
|
7
|
+
sectionCssGzip: 50 * 1024
|
|
8
|
+
},
|
|
9
|
+
audit: {
|
|
10
|
+
/** A vulnerability at or above this severity fails the check. */
|
|
11
|
+
failOn: ["high", "critical"] },
|
|
12
|
+
/**
|
|
13
|
+
* A package whose license is not on this list fails the check. A missing
|
|
14
|
+
* license field, or a bespoke "SEE LICENSE IN …", is a denial — not a pass.
|
|
15
|
+
*/
|
|
16
|
+
licenseAllowlist: [
|
|
17
|
+
"MIT",
|
|
18
|
+
"ISC",
|
|
19
|
+
"Apache-2.0",
|
|
20
|
+
"BSD-2-Clause",
|
|
21
|
+
"BSD-3-Clause",
|
|
22
|
+
"0BSD",
|
|
23
|
+
"Unlicense",
|
|
24
|
+
"CC0-1.0"
|
|
25
|
+
],
|
|
26
|
+
/**
|
|
27
|
+
* The license gate walks the *shipped* dependency graph of every third-party
|
|
28
|
+
* package a template pulls in — this is the first-party exception to that.
|
|
29
|
+
* The kit itself is `UNLICENSED` by design (it is not published for reuse
|
|
30
|
+
* outside this platform) and every workspace depends on it, so without this
|
|
31
|
+
* exemption the gate would deny the kit on a stock, correct workspace.
|
|
32
|
+
*/
|
|
33
|
+
licenseExemptions: ["@homepages/template-kit"]
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
//#endregion
|
|
37
|
+
export { CHECK_CONFIG };
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { loadEsbuild } from "./resolve-tool.js";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { readdir } from "node:fs/promises";
|
|
5
|
+
|
|
6
|
+
//#region src/cli/check/css.ts
|
|
7
|
+
/**
|
|
8
|
+
* Every `.css` file anywhere in a section folder (the folder is free-form —
|
|
9
|
+
* an author may nest CSS under components/, enhancers/, wherever), in
|
|
10
|
+
* published order: `styles.css` first (the conventional root-level home),
|
|
11
|
+
* then every other file sorted by path. `snapshots/` and `node_modules/` are
|
|
12
|
+
* skipped — generated or not authored, never hand-CSS.
|
|
13
|
+
*/
|
|
14
|
+
async function sectionCssFiles(dir) {
|
|
15
|
+
if (!existsSync(dir)) return [];
|
|
16
|
+
const found = [];
|
|
17
|
+
const walk = async (d) => {
|
|
18
|
+
for (const entry of await readdir(d, { withFileTypes: true })) {
|
|
19
|
+
const full = join(d, entry.name);
|
|
20
|
+
if (entry.isDirectory()) {
|
|
21
|
+
if (entry.name === "snapshots" || entry.name === "node_modules") continue;
|
|
22
|
+
await walk(full);
|
|
23
|
+
} else if (entry.isFile() && entry.name.endsWith(".css")) found.push(full);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
await walk(dir);
|
|
27
|
+
const own = join(dir, "styles.css");
|
|
28
|
+
const rest = found.filter((f) => f !== own).sort();
|
|
29
|
+
return (existsSync(own) ? [own] : []).concat(rest);
|
|
30
|
+
}
|
|
31
|
+
const CSS_ASSET_LOADERS = {
|
|
32
|
+
".svg": "dataurl",
|
|
33
|
+
".png": "dataurl",
|
|
34
|
+
".jpg": "dataurl",
|
|
35
|
+
".jpeg": "dataurl",
|
|
36
|
+
".gif": "dataurl",
|
|
37
|
+
".webp": "dataurl",
|
|
38
|
+
".woff": "dataurl",
|
|
39
|
+
".woff2": "dataurl",
|
|
40
|
+
".ttf": "dataurl",
|
|
41
|
+
".eot": "dataurl"
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* A section's third-party package CSS — discovered by the bundler, not
|
|
45
|
+
* declared. An author writes `import "swiper/css"` in Renderer.tsx exactly
|
|
46
|
+
* as in React; this runs a CSS-only esbuild pass rooted at Renderer.tsx and
|
|
47
|
+
* returns the emitted stylesheet (empty string if there is none).
|
|
48
|
+
*
|
|
49
|
+
* A relative CSS import in the render path is a lint error
|
|
50
|
+
* (`no-css-import-from-render-path`, already enforced by ESLint) — this pass
|
|
51
|
+
* does not re-implement that rejection, it only has to not crash on one.
|
|
52
|
+
*
|
|
53
|
+
* `platform: "neutral"` defaults `mainFields` to `[]`, which cannot resolve a
|
|
54
|
+
* package that ships only a CJS `main` (no `exports` map) — `mainFields`
|
|
55
|
+
* is set explicitly so such a package still resolves to its ESM `module`
|
|
56
|
+
* build where one exists.
|
|
57
|
+
*/
|
|
58
|
+
async function compileSectionPackageCss(workspaceRoot, sectionDir) {
|
|
59
|
+
const renderer = join(sectionDir, "Renderer.tsx");
|
|
60
|
+
if (!existsSync(renderer)) return "";
|
|
61
|
+
const { build } = await loadEsbuild(workspaceRoot);
|
|
62
|
+
const css = (await build({
|
|
63
|
+
entryPoints: [renderer],
|
|
64
|
+
bundle: true,
|
|
65
|
+
write: false,
|
|
66
|
+
minify: true,
|
|
67
|
+
outdir: join(workspaceRoot, "node_modules", ".template-kit", "check-css"),
|
|
68
|
+
format: "esm",
|
|
69
|
+
target: "es2022",
|
|
70
|
+
platform: "neutral",
|
|
71
|
+
mainFields: ["module", "main"],
|
|
72
|
+
jsx: "automatic",
|
|
73
|
+
external: [
|
|
74
|
+
"react",
|
|
75
|
+
"react-dom",
|
|
76
|
+
"react/jsx-runtime"
|
|
77
|
+
],
|
|
78
|
+
loader: { ...CSS_ASSET_LOADERS },
|
|
79
|
+
absWorkingDir: workspaceRoot,
|
|
80
|
+
logLevel: "silent"
|
|
81
|
+
})).outputFiles?.find((f) => f.path.endsWith(".css"));
|
|
82
|
+
return css ? css.text : "";
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* The 1-based line of the first top-level `@import`, or `undefined` if there
|
|
86
|
+
* is none. Comment-aware: an `@import` mentioned inside a CSS block comment
|
|
87
|
+
* is prose (e.g. a header warning authors not to write one), not a rule — a
|
|
88
|
+
* naive line-regex would false-fire on it.
|
|
89
|
+
*/
|
|
90
|
+
function findImportLine(css) {
|
|
91
|
+
const lines = css.split("\n");
|
|
92
|
+
let inComment = false;
|
|
93
|
+
for (let i = 0; i < lines.length; i++) {
|
|
94
|
+
let line = lines[i];
|
|
95
|
+
if (inComment) {
|
|
96
|
+
const close = line.indexOf("*/");
|
|
97
|
+
if (close === -1) continue;
|
|
98
|
+
line = line.slice(close + 2);
|
|
99
|
+
inComment = false;
|
|
100
|
+
}
|
|
101
|
+
for (;;) {
|
|
102
|
+
const open = line.indexOf("/*");
|
|
103
|
+
if (open === -1) break;
|
|
104
|
+
const close = line.indexOf("*/", open + 2);
|
|
105
|
+
if (close === -1) {
|
|
106
|
+
line = line.slice(0, open);
|
|
107
|
+
inComment = true;
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
line = line.slice(0, open) + line.slice(close + 2);
|
|
111
|
+
}
|
|
112
|
+
if (/^\s*@import\b/.test(line)) return i + 1;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
//#endregion
|
|
117
|
+
export { CSS_ASSET_LOADERS, compileSectionPackageCss, findImportLine, sectionCssFiles };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
//#region src/cli/check/diagnostics.ts
|
|
2
|
+
function location(d) {
|
|
3
|
+
if (!d.file) return "";
|
|
4
|
+
return d.line === void 0 ? d.file : `${d.file}:${d.line}`;
|
|
5
|
+
}
|
|
6
|
+
function formatOne(d) {
|
|
7
|
+
const where = location(d);
|
|
8
|
+
return `${where ? ` ${where}\n` : ""} [${d.ruleId}] ${d.message}\n fix: ${d.fix}\n`;
|
|
9
|
+
}
|
|
10
|
+
function formatHuman(report) {
|
|
11
|
+
const parts = [];
|
|
12
|
+
for (const [key, t] of Object.entries(report.templates)) {
|
|
13
|
+
if (t.ok) {
|
|
14
|
+
parts.push(`✓ ${key}\n`);
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
parts.push(`✗ ${key} — ${String(t.diagnostics.length)} problem(s)\n`);
|
|
18
|
+
for (const d of t.diagnostics) parts.push(formatOne(d));
|
|
19
|
+
}
|
|
20
|
+
if (!report.workspace.ok) {
|
|
21
|
+
parts.push(`✗ workspace — ${String(report.workspace.diagnostics.length)} problem(s)\n`);
|
|
22
|
+
for (const d of report.workspace.diagnostics) parts.push(formatOne(d));
|
|
23
|
+
}
|
|
24
|
+
if (report.ok) parts.push("check passed\n");
|
|
25
|
+
return parts.join("");
|
|
26
|
+
}
|
|
27
|
+
function toJson(report) {
|
|
28
|
+
return JSON.stringify(report, null, 2);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
//#endregion
|
|
32
|
+
export { formatHuman, toJson };
|