@homepages/template-kit 0.2.0 → 0.4.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 +222 -0
- package/README.md +96 -17
- package/dist/asset-modules.d.ts +81 -0
- package/dist/base.css +9 -0
- package/dist/cli/check/config.js +41 -0
- package/dist/cli/check/css.js +131 -0
- package/dist/cli/check/diagnostics.js +32 -0
- package/dist/cli/check/index.js +87 -0
- package/dist/cli/check/loader.js +146 -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/section-assets.js +58 -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 +274 -0
- package/dist/cli/check/stages/tree.js +206 -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/dev/build-css.js +91 -0
- package/dist/cli/dev/catalog.js +35 -0
- package/dist/cli/dev/compose-template.js +38 -0
- package/dist/cli/dev/content-override.js +58 -0
- package/dist/cli/dev/discover.js +35 -0
- package/dist/cli/dev/fill-state.js +75 -0
- package/dist/cli/dev/index.js +32 -0
- package/dist/cli/dev/inspect.js +38 -0
- package/dist/cli/dev/island-bootstrap.js +38 -0
- package/dist/cli/dev/island-map.js +35 -0
- package/dist/cli/dev/island-transform.js +31 -0
- package/dist/cli/dev/manifest-instances.js +31 -0
- package/dist/cli/dev/render-section.js +24 -0
- package/dist/cli/dev/screenshot-target.js +24 -0
- package/dist/cli/dev/section-page.js +54 -0
- package/dist/cli/dev/server.js +480 -0
- package/dist/cli/dev/slot-schema.js +12 -0
- package/dist/cli/dev/structure-summary.js +118 -0
- package/dist/cli/dev/tailwind.js +32 -0
- package/dist/cli/dev/vite-server.js +33 -0
- package/dist/cli/dev/workspace.js +63 -0
- package/dist/cli/link/index.js +74 -0
- package/dist/cli/link/overlay.js +59 -0
- package/dist/cli/link/watch.js +25 -0
- package/dist/cli/new/emit.js +48 -0
- package/dist/cli/new/index.js +69 -0
- package/dist/cli/new/scaffold/section/_Renderer.tsx +20 -0
- package/dist/cli/new/scaffold/section/_fill-spec.ts +18 -0
- package/dist/cli/new/scaffold/section/_fixtures.ts +13 -0
- package/dist/cli/new/scaffold/section/_schema.ts +24 -0
- package/dist/cli/new/scaffold/template/_manifest.json +9 -0
- package/dist/cli/new/scaffold/template/sections/hero/ExpandableText.tsx +45 -0
- package/dist/cli/new/scaffold/template/sections/hero/Renderer.tsx +40 -0
- package/dist/cli/new/scaffold/template/sections/hero/components/Feature.tsx +14 -0
- package/dist/cli/new/scaffold/template/sections/hero/fill-spec.ts +29 -0
- package/dist/cli/new/scaffold/template/sections/hero/fixtures.ts +41 -0
- package/dist/cli/new/scaffold/template/sections/hero/schema.ts +60 -0
- package/dist/cli/new/scaffold/template/theme.css +24 -0
- package/dist/cli/new/scaffold/template/theme.ts +27 -0
- package/dist/cli/new/scaffold-assets.js +20 -0
- package/dist/cli/pack/collect.js +44 -0
- package/dist/cli/pack/guards.js +57 -0
- package/dist/cli/pack/index.js +66 -0
- package/dist/cli/pack/manifest.js +15 -0
- package/dist/cli/pack/zip.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 +100 -16
- package/dist/contracts/fill-treatments.js +47 -0
- package/dist/design-system/theme.d.ts +30 -300
- package/dist/design-system/theme.js +112 -90
- package/dist/dev-client/assets/index-DfEfrp6P.css +1 -0
- package/dist/dev-client/assets/index-bpMP0b0W.js +50 -0
- package/dist/dev-client/index.html +13 -0
- package/dist/eslint/is-client-file.d.ts +4 -0
- package/dist/eslint/rules/no-hex.js +78 -6
- package/dist/eslint.js +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -2
- package/dist/islands/detect.d.ts +16 -0
- package/dist/islands/detect.js +22 -0
- package/dist/islands/discover.d.ts +5 -0
- package/dist/islands/discover.js +19 -0
- package/dist/islands/esbuild-plugin.d.ts +13 -0
- package/dist/islands/esbuild-plugin.js +39 -0
- package/dist/islands/wrap.d.ts +11 -0
- package/dist/islands/wrap.js +48 -0
- package/dist/node_modules/@jridgewell/sourcemap-codec/dist/sourcemap-codec.js +77 -0
- package/dist/node_modules/magic-string/dist/magic-string.es.js +939 -0
- package/dist/package.js +1 -1
- package/dist/primitives/Image.js +1 -1
- 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/ssr.d.ts +31 -0
- package/dist/ssr.js +46 -0
- package/dist/styles.css +31 -88
- package/docs/INDEX.md +12 -3
- package/docs/assets.md +128 -0
- package/docs/check.md +124 -0
- package/docs/dev.md +187 -0
- package/docs/eslint.md +18 -8
- package/docs/islands.md +5 -2
- package/docs/llms.txt +59 -9
- package/docs/new.md +27 -0
- package/docs/overview.md +57 -0
- package/docs/pack.md +46 -0
- package/docs/primitives.md +10 -6
- package/docs/quickstart.md +102 -0
- package/docs/recipes/INDEX.md +27 -0
- package/docs/recipes/bind-property-fact.md +58 -0
- package/docs/recipes/collection-slot.md +100 -0
- package/docs/recipes/fill-spec-decision.md +66 -0
- package/docs/recipes/fixture-states.md +68 -0
- package/docs/recipes/image-slot-crop.md +98 -0
- package/docs/recipes/interactive-island.md +100 -0
- package/docs/recipes/organize-section-folder.md +72 -0
- package/docs/recipes/prepare-submission.md +55 -0
- package/docs/recipes/second-template.md +49 -0
- package/docs/recipes/select-slot.md +59 -0
- package/docs/recipes/static-asset.md +103 -0
- package/docs/recipes/third-party-package.md +90 -0
- package/docs/rules/INDEX.md +14 -5
- package/docs/rules/bundle-binary-asset.md +83 -0
- package/docs/rules/bundle-incomplete.md +2 -2
- package/docs/rules/css-reason.md +3 -3
- package/docs/rules/fixtures-invalid.md +96 -0
- package/docs/rules/license-denied.md +11 -3
- package/docs/rules/manifest-invalid.md +99 -0
- package/docs/rules/no-bare-css-import.md +8 -8
- package/docs/rules/no-css-import-from-render-path.md +7 -8
- package/docs/rules/no-hex.md +76 -10
- package/docs/rules/no-templates.md +87 -0
- package/docs/rules/parse-error.md +76 -0
- package/docs/rules/schema-invalid.md +96 -0
- package/docs/rules/size-assets.md +88 -0
- package/docs/rules/size-island-bundle.md +123 -0
- package/docs/rules/size-section-css.md +19 -14
- package/docs/schema-system.md +28 -28
- package/docs/theme-and-css.md +157 -92
- package/docs/vocabulary.md +98 -0
- package/package.json +14 -11
- package/tsconfig.json +1 -1
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
---
|
|
2
|
+
purpose: The template-kit/size-island-bundle authoring rule — what it enforces, why, and how to fix it.
|
|
3
|
+
status: living
|
|
4
|
+
related: [INDEX.md, size-renderer-bundle.md, size-section-css.md, server-vs-client.md]
|
|
5
|
+
updated: 2026-07-15
|
|
6
|
+
---
|
|
7
|
+
# template-kit/size-island-bundle
|
|
8
|
+
|
|
9
|
+
> Each island's browser bundle must be **≤ 50 KB gzip**. The budget is not
|
|
10
|
+
> author-overridable, and it applies per island, not per section.
|
|
11
|
+
|
|
12
|
+
## Rule
|
|
13
|
+
|
|
14
|
+
An island — a component whose file opens with the `"use client"` directive (see
|
|
15
|
+
[Server vs client](server-vs-client.md)) — ships to the browser as its own bundle,
|
|
16
|
+
separate from the section's server-rendered HTML. Each island is bundled with esbuild
|
|
17
|
+
and minified, with **React treated as external** (the page's runtime chunk supplies it
|
|
18
|
+
once, shared by every island on the page, and is not counted against any one of them).
|
|
19
|
+
The gzipped result must be **50 KB or less**.
|
|
20
|
+
|
|
21
|
+
A section can have more than one island. The budget is checked **per island**: a section
|
|
22
|
+
with three islands each at 40 KB passes; a section with one island at 60 KB fails, even
|
|
23
|
+
though the same section's other files are small.
|
|
24
|
+
|
|
25
|
+
The budget is **visible in `template-kit check`'s configuration and is not
|
|
26
|
+
author-overridable**. A template workspace cannot raise it, and there is no per-island
|
|
27
|
+
exemption.
|
|
28
|
+
|
|
29
|
+
## Reason
|
|
30
|
+
|
|
31
|
+
An island is the part of the page that has to work on a phone before the visitor does
|
|
32
|
+
anything — a carousel, a filter, a form. Every one of its bytes is downloaded, parsed,
|
|
33
|
+
and executed on the client, on top of whatever the page's shared runtime already costs.
|
|
34
|
+
Unlike the section's server-rendered markup, which the browser only has to paint, an
|
|
35
|
+
island's code has to run.
|
|
36
|
+
|
|
37
|
+
The number is set to be generous, so it does not fight ordinary authoring — a real
|
|
38
|
+
interactive widget fits comfortably inside it. You hit the budget when something
|
|
39
|
+
**runaway** gets pulled into the client bundle:
|
|
40
|
+
|
|
41
|
+
- **a whole UI or icon library imported for one component**, which drags every export
|
|
42
|
+
into the bundle so the island can use a handful of them;
|
|
43
|
+
- **a heavy dependency that belongs on the server**, ported into the island because it
|
|
44
|
+
was easiest to reach for where the code was being written, not because the browser
|
|
45
|
+
needs it.
|
|
46
|
+
|
|
47
|
+
That is the shape of essentially every violation: not an island that grew, but a single
|
|
48
|
+
import that was more expensive than it looked.
|
|
49
|
+
|
|
50
|
+
## Fix
|
|
51
|
+
|
|
52
|
+
Find the import that is not paying for itself, or narrow what ships to the browser at
|
|
53
|
+
all.
|
|
54
|
+
|
|
55
|
+
- **Import named symbols, never a namespace.** `import { ArrowRight } from "…"` is
|
|
56
|
+
tree-shakeable; `import * as Icons from "…"` is not — a namespace object has to contain
|
|
57
|
+
everything, so nothing can be dropped.
|
|
58
|
+
- **Prefer the package's narrow entry** if it offers one, instead of its default
|
|
59
|
+
"everything" bundle.
|
|
60
|
+
- **Move what doesn't need to be interactive out of the island.** If only a small part of
|
|
61
|
+
a component actually needs client-side behavior — an expand/collapse toggle inside an
|
|
62
|
+
otherwise static block — split the interactive part into its own small island and leave
|
|
63
|
+
the rest server-rendered. Server-rendered markup costs nothing against this budget.
|
|
64
|
+
- **Hand-write the residue.** If you use one function out of a large utility package, copy
|
|
65
|
+
the function instead of importing the package for it.
|
|
66
|
+
|
|
67
|
+
### Before
|
|
68
|
+
|
|
69
|
+
```tsx
|
|
70
|
+
// sections/gallery/Carousel.tsx
|
|
71
|
+
"use client";
|
|
72
|
+
|
|
73
|
+
import * as Icons from "lucide-react"; // ← the whole set: every icon, in this island's bundle
|
|
74
|
+
import { useState } from "react";
|
|
75
|
+
|
|
76
|
+
export default function Carousel({ slides }: { slides: { id: string; url: string }[] }) {
|
|
77
|
+
const [index, setIndex] = useState(0);
|
|
78
|
+
return (
|
|
79
|
+
<div>
|
|
80
|
+
<img src={slides[index]?.url} alt="" />
|
|
81
|
+
<button onClick={() => setIndex((i) => (i - 1 + slides.length) % slides.length)}>
|
|
82
|
+
<Icons.ChevronLeft aria-hidden />
|
|
83
|
+
</button>
|
|
84
|
+
<button onClick={() => setIndex((i) => (i + 1) % slides.length)}>
|
|
85
|
+
<Icons.ChevronRight aria-hidden />
|
|
86
|
+
</button>
|
|
87
|
+
</div>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### After
|
|
93
|
+
|
|
94
|
+
```tsx
|
|
95
|
+
// sections/gallery/Carousel.tsx
|
|
96
|
+
"use client";
|
|
97
|
+
|
|
98
|
+
import { ChevronLeft, ChevronRight } from "lucide-react"; // ← two icons, and the bundler can drop the rest
|
|
99
|
+
import { useState } from "react";
|
|
100
|
+
|
|
101
|
+
export default function Carousel({ slides }: { slides: { id: string; url: string }[] }) {
|
|
102
|
+
const [index, setIndex] = useState(0);
|
|
103
|
+
return (
|
|
104
|
+
<div>
|
|
105
|
+
<img src={slides[index]?.url} alt="" />
|
|
106
|
+
<button onClick={() => setIndex((i) => (i - 1 + slides.length) % slides.length)}>
|
|
107
|
+
<ChevronLeft aria-hidden />
|
|
108
|
+
</button>
|
|
109
|
+
<button onClick={() => setIndex((i) => (i + 1) % slides.length)}>
|
|
110
|
+
<ChevronRight aria-hidden />
|
|
111
|
+
</button>
|
|
112
|
+
</div>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## See also
|
|
118
|
+
|
|
119
|
+
- [`size-renderer-bundle`](size-renderer-bundle.md) — the same budget on the section's
|
|
120
|
+
server-rendered bundle.
|
|
121
|
+
- [`size-section-css`](size-section-css.md) — the same budget on the section's stylesheet.
|
|
122
|
+
- [Server vs client](server-vs-client.md) — what makes a file an island in the first
|
|
123
|
+
place.
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
purpose: The template-kit/size-section-css authoring rule — what it enforces, why, and how to fix it.
|
|
3
3
|
status: living
|
|
4
|
-
related: [INDEX.md, size-renderer-bundle.md, ../theme-and-css.md]
|
|
5
|
-
updated: 2026-07-
|
|
4
|
+
related: [INDEX.md, size-renderer-bundle.md, size-assets.md, ../theme-and-css.md]
|
|
5
|
+
updated: 2026-07-15
|
|
6
6
|
---
|
|
7
7
|
# template-kit/size-section-css
|
|
8
8
|
|
|
@@ -16,7 +16,7 @@ everything that lands in the layer:
|
|
|
16
16
|
|
|
17
17
|
- the section's hand-written CSS, and
|
|
18
18
|
- **every package stylesheet its module graph imports** — the `import "swiper/css"` in a
|
|
19
|
-
component
|
|
19
|
+
component or an island.
|
|
20
20
|
|
|
21
21
|
The budget is **visible in `template-kit check`'s configuration and is not
|
|
22
22
|
author-overridable**. A workspace cannot raise it, and there is no per-section exemption.
|
|
@@ -29,14 +29,15 @@ CSS blocks the first paint.
|
|
|
29
29
|
|
|
30
30
|
"Including package CSS" is not a technicality; it is the failure this rule exists to catch.
|
|
31
31
|
Your own stylesheet is almost never the problem — it is a few kilobytes of overrides. The
|
|
32
|
-
package's is.
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
`dist` listing looks alarming. The compiled layer is 400 KB.
|
|
32
|
+
package's is. A one-line `import "some-ui/dist.css"` can drag in thousands of rules for
|
|
33
|
+
components you never render — resets, theme variants, every widget's styles — and they all
|
|
34
|
+
land in your compiled layer. Nothing in your source looks large; the compiled layer is
|
|
35
|
+
400 KB.
|
|
37
36
|
|
|
38
37
|
So the number this check reports is frequently the first time anyone finds out what a
|
|
39
|
-
one-line CSS import actually cost.
|
|
38
|
+
one-line CSS import actually cost. (What this budget does **not** count is asset bytes: a
|
|
39
|
+
`url()`-referenced webfont or icon file resolves to a cached URL and is weighed separately —
|
|
40
|
+
see [`size-assets`](size-assets.md).)
|
|
40
41
|
|
|
41
42
|
## Fix
|
|
42
43
|
|
|
@@ -45,10 +46,11 @@ Three moves, in order of how often they work:
|
|
|
45
46
|
1. **Import the slimmest entry the package offers.** Most CSS-shipping packages have a core
|
|
46
47
|
stylesheet plus opt-in modules, *and* a fat "everything" bundle that is the one you get
|
|
47
48
|
from copying the quickstart. Take the core, plus only the modules you use.
|
|
48
|
-
2. **
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
2. **Drop the icon font.** If the package's stylesheet declares an icon font and you use
|
|
50
|
+
three of its glyphs, replace them with inline SVG — or with a family the template's theme
|
|
51
|
+
already declares in `fonts` (`--tr-font-<name>`), which costs nothing because it is
|
|
52
|
+
already loaded. (The font *file* is weighed under [`size-assets`](size-assets.md), but its
|
|
53
|
+
`@font-face` rules and every `.icon-*` class still land in this layer.)
|
|
52
54
|
3. **Drop the package and hand-write the residue.** If you use one component from a large
|
|
53
55
|
widget library, its stylesheet is paying for components you never render.
|
|
54
56
|
|
|
@@ -83,12 +85,15 @@ export default function Carousel({ slides }: { slides: { id: string; url: string
|
|
|
83
85
|
```
|
|
84
86
|
|
|
85
87
|
The arrows and dots that came from the bundle's icon font are drawn as inline SVG in the
|
|
86
|
-
component instead, so
|
|
88
|
+
component instead, so the layer no longer carries that font's `@font-face` and icon-class
|
|
89
|
+
rules (and the font file no longer counts against [`size-assets`](size-assets.md) either).
|
|
87
90
|
|
|
88
91
|
## See also
|
|
89
92
|
|
|
90
93
|
- [`size-renderer-bundle`](size-renderer-bundle.md) — the same budget on the section's
|
|
91
94
|
JavaScript.
|
|
95
|
+
- [`size-assets`](size-assets.md) — the separate budget on the static asset *files*
|
|
96
|
+
(fonts, images) a stylesheet or component references.
|
|
92
97
|
- [`no-bare-css-import`](no-bare-css-import.md) — why a package's stylesheet is imported from
|
|
93
98
|
code and never `@import`ed from CSS.
|
|
94
99
|
- [Theme and CSS](../theme-and-css.md) — the fonts and tokens the page already loads, which
|
package/docs/schema-system.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
purpose: The section authoring contract — schema.ts, fill-spec.ts, fixtures.ts, and the closed source/transform vocabulary.
|
|
3
3
|
status: living
|
|
4
|
-
related: [primitives.md, theme-and-css.md]
|
|
5
|
-
updated: 2026-07-
|
|
4
|
+
related: [primitives.md, theme-and-css.md, vocabulary.md]
|
|
5
|
+
updated: 2026-07-16
|
|
6
6
|
---
|
|
7
7
|
# The schema system
|
|
8
8
|
|
|
@@ -32,22 +32,17 @@ from. Without it, `SectionProps` cannot tell a required slot from an optional on
|
|
|
32
32
|
and your renderer props collapse to a useless widened shape.
|
|
33
33
|
|
|
34
34
|
```ts
|
|
35
|
-
import { direct, type
|
|
36
|
-
|
|
37
|
-
export const HERO_FRAME = {
|
|
38
|
-
breakpoint: 425,
|
|
39
|
-
desktop: { aspect: 5 / 3 },
|
|
40
|
-
} as const satisfies FrameConfig;
|
|
35
|
+
import { direct, type SectionProps, type SectionSchemaShape } from "@homepages/template-kit";
|
|
41
36
|
|
|
42
37
|
export const schema = {
|
|
43
38
|
meta: { displayName: "Hero" },
|
|
44
39
|
slots: {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
40
|
+
hero_image: { type: "image", required: true, produced_by: "hero_image_assign", crop: { mode: "locked", aspect: 16 / 9 } },
|
|
41
|
+
headline: { type: "text", size: "medium", required: true, source: direct("address") },
|
|
42
|
+
blurb: { type: "text", size: "long", required: true, produced_by: "blurb_text" },
|
|
48
43
|
},
|
|
49
44
|
options: {
|
|
50
|
-
align: { type: "enum", values: ["left", "center"], default: "
|
|
45
|
+
align: { type: "enum", values: ["left", "center"], default: "center" },
|
|
51
46
|
},
|
|
52
47
|
} as const satisfies SectionSchemaShape;
|
|
53
48
|
|
|
@@ -65,14 +60,20 @@ binds it to a computed transform. Both take names from a **closed vocabulary**,
|
|
|
65
60
|
a typo is a compile error at the call site — not a runtime surprise:
|
|
66
61
|
|
|
67
62
|
```ts
|
|
68
|
-
source: direct("
|
|
69
|
-
source: direct("
|
|
63
|
+
source: direct("address") // ✅
|
|
64
|
+
source: direct("addresss") // ✗ compile error: not a SOURCE_FIELDS key
|
|
70
65
|
```
|
|
71
66
|
|
|
72
|
-
The vocabulary is the kit's, not yours.
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
67
|
+
The vocabulary is the kit's, not yours. Adding to it is a kit release, not a
|
|
68
|
+
change you can make in a template. See [the closed vocabulary](vocabulary.md)
|
|
69
|
+
for the sets and when to pick each; your editor autocompletes the exact fields.
|
|
70
|
+
|
|
71
|
+
A `select` slot binds the same way as any other type — here fact-bound rather
|
|
72
|
+
than AI-produced, with `values` declaring the closed set the fact resolves to:
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
listing_status: { type: "select", required: false, source: direct("listing_intent"), values: ["for_sale", "for_rent"] },
|
|
76
|
+
```
|
|
76
77
|
|
|
77
78
|
### Grouping slots into one editor card
|
|
78
79
|
|
|
@@ -104,7 +105,6 @@ import type { Props } from "./schema.js";
|
|
|
104
105
|
|
|
105
106
|
export default function Hero({ slots, options, nav }: Props) {
|
|
106
107
|
// headline is `string` — the schema says required: true
|
|
107
|
-
// listing_status is `string | null` — required: false
|
|
108
108
|
// options.align is `"left" | "center"` — the enum's declared values
|
|
109
109
|
return (
|
|
110
110
|
<Section id={nav.selfAnchor}>
|
|
@@ -153,12 +153,12 @@ export const fillSpec = {
|
|
|
153
153
|
decisions: [
|
|
154
154
|
{ id: "hero_image_assign", type: "image-assign", produces: ["hero_image"], tag_query: "category:exterior" },
|
|
155
155
|
{
|
|
156
|
-
id: "
|
|
156
|
+
id: "blurb_text",
|
|
157
157
|
type: "text-block",
|
|
158
|
-
produces: ["
|
|
159
|
-
length_cap:
|
|
160
|
-
structure: "
|
|
161
|
-
voice_prompt: "
|
|
158
|
+
produces: ["blurb"],
|
|
159
|
+
length_cap: 400,
|
|
160
|
+
structure: "paragraph",
|
|
161
|
+
voice_prompt: "Warm, concrete paragraph a buyer would want to read.",
|
|
162
162
|
},
|
|
163
163
|
],
|
|
164
164
|
} as const satisfies FillSpecShape;
|
|
@@ -177,14 +177,14 @@ import type { FixtureModule } from "@homepages/template-kit";
|
|
|
177
177
|
const fixtures: FixtureModule = {
|
|
178
178
|
base: {
|
|
179
179
|
slots: {
|
|
180
|
-
headline: "
|
|
181
|
-
listing_status: "for_sale",
|
|
180
|
+
headline: "101 Queensway, Jamaica Plain, MA 02130",
|
|
182
181
|
hero_image: { photo_id: "p1", url: "https://example.com/a.jpg", alt: "Front elevation" },
|
|
182
|
+
blurb: "A bright corner home moments from the Emerald Necklace.",
|
|
183
183
|
},
|
|
184
|
-
options: { align: "
|
|
184
|
+
options: { align: "center" },
|
|
185
185
|
},
|
|
186
186
|
states: {
|
|
187
|
-
|
|
187
|
+
left: { label: "Left-aligned", options: { align: "left" } },
|
|
188
188
|
},
|
|
189
189
|
};
|
|
190
190
|
|
package/docs/theme-and-css.md
CHANGED
|
@@ -1,41 +1,170 @@
|
|
|
1
1
|
---
|
|
2
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, rules/INDEX.md]
|
|
5
|
-
updated: 2026-07-
|
|
4
|
+
related: [primitives.md, schema-system.md, eslint.md, rules/INDEX.md, check.md]
|
|
5
|
+
updated: 2026-07-15
|
|
6
6
|
---
|
|
7
7
|
# Theme and CSS
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
`
|
|
9
|
+
Your template owns its design system end to end — **the token names included**. You
|
|
10
|
+
declare the tokens your design actually has in a `TokenTheme`, and `template-kit theme`
|
|
11
|
+
compiles that one declaration into the Tailwind utilities that expose them. Declare a
|
|
12
|
+
colour named `lagoon` and `bg-lagoon` exists.
|
|
13
|
+
|
|
14
|
+
The kit owns only what is true of **every** template: the breakpoint ladder, the spacing
|
|
15
|
+
base unit, the motion defaults, the reset gaps Tailwind's preflight leaves, and the
|
|
16
|
+
section box model. It ships no colour, font, type, radius or shadow of its own, and there
|
|
17
|
+
is no shared file for you to edit to make a token of yours usable.
|
|
13
18
|
|
|
14
19
|
Two CSS entries ship, and they are not interchangeable.
|
|
15
20
|
|
|
16
21
|
| Entry | What it is | How it loads |
|
|
17
22
|
|---|---|---|
|
|
18
|
-
| `@homepages/template-kit/styles.css` | The Tailwind
|
|
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 |
|
|
23
|
+
| `@homepages/template-kit/styles.css` | The global Tailwind preamble: breakpoint ladder, `--spacing`, motion defaults, and `@source` registration of the kit's compiled JS | imported into **your Tailwind entry**, after Tailwind |
|
|
24
|
+
| `@homepages/template-kit/base.css` | Cascade-layer order, the three reset gaps Tailwind's preflight leaves, `.tr-section`, `.tr-image-frame`, and the motion tokens | its **own stylesheet** on the page — not part of the Tailwind entry |
|
|
20
25
|
|
|
21
|
-
##
|
|
26
|
+
## Your Tailwind entry
|
|
22
27
|
|
|
23
28
|
```css
|
|
24
29
|
/* your CSS entry */
|
|
25
30
|
@import "tailwindcss";
|
|
26
31
|
@import "@homepages/template-kit/styles.css";
|
|
32
|
+
@import "./theme.css"; /* generated by `template-kit theme` from theme.ts */
|
|
27
33
|
```
|
|
28
34
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
35
|
+
Three constraints, all of them silent when broken:
|
|
36
|
+
|
|
37
|
+
- **Import Tailwind exactly once per build graph.** `styles.css` deliberately does not
|
|
38
|
+
`@import "tailwindcss"` itself — yours does. Two double-emit Tailwind's preflight reset
|
|
39
|
+
(`box-sizing` declared twice, several KB of duplicate base layer), and nothing errors.
|
|
40
|
+
- **Your generated theme must be imported here, inside the Tailwind build graph.** It
|
|
41
|
+
contains an `@theme` block, which is a Tailwind directive: anywhere outside the graph it
|
|
42
|
+
is inert, and you get a stylesheet with no token utilities and no error explaining why.
|
|
43
|
+
- **Import it after `styles.css`**, so the kit's globals are in place before your tokens
|
|
44
|
+
land on top of them.
|
|
45
|
+
|
|
46
|
+
`styles.css` also carries an `@source` line registering the kit's own shipped `dist` for
|
|
47
|
+
Tailwind's class scan. That is how the utility classes baked into the kit's primitives
|
|
48
|
+
(`Image`'s `object-cover`, …) reach your compiled CSS; it also overrides Tailwind's default
|
|
49
|
+
exclusion of `node_modules`. You do not need to add a `@source` for the kit yourself.
|
|
50
|
+
|
|
51
|
+
## `TokenTheme` — your template's design system
|
|
52
|
+
|
|
53
|
+
A template's `theme.ts` exports a `TokenTheme`: six open records of tokens, plus the
|
|
54
|
+
document defaults that dress the page. `theme.ts` is the file you write; run
|
|
55
|
+
`template-kit theme` to compile it into the `theme.css` your Tailwind entry imports
|
|
56
|
+
(see [Generating `theme.css`](#generating-themecss) below).
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
import type { TokenTheme } from "@homepages/template-kit";
|
|
60
|
+
|
|
61
|
+
export const theme: TokenTheme = {
|
|
62
|
+
colors: { ink: "#111418", page: "#ffffff", "surface-alt": "#f4f4f5", lagoon: "#1c6e7d" },
|
|
63
|
+
fonts: { sans: '"Inter", system-ui, sans-serif', display: '"Canela", Georgia, serif' },
|
|
64
|
+
type: { base: "1rem", "2xl": "1.5rem", display: "clamp(2.5rem, 6vw, 4.5rem)" },
|
|
65
|
+
radii: { md: "6px", pill: "9999px" },
|
|
66
|
+
shadows: { card: "0 1px 2px rgb(0 0 0 / 0.06)" },
|
|
67
|
+
layout: { "container-max": "1280px", "header-height": "72px" },
|
|
68
|
+
|
|
69
|
+
// Optional font-loading block, emitted ahead of everything else.
|
|
70
|
+
fontFaces: '@import url("https://fonts.example/inter.css");',
|
|
71
|
+
|
|
72
|
+
// Which of YOUR tokens dress the document.
|
|
73
|
+
document: { font: "sans", color: "ink", background: "page", fontSize: "base" },
|
|
74
|
+
};
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Nothing is required to exist. Tailwind's own defaults survive underneath your tokens —
|
|
78
|
+
`text-2xl`, `rounded-lg`, `red-500` all still work — so a sparse theme is a working theme.
|
|
79
|
+
(The breakpoint ladder is the one exception: `sm:`, `xl:` and `2xl:` are deliberately
|
|
80
|
+
switched off. See below.)
|
|
81
|
+
|
|
82
|
+
### Namespaces and the utilities they produce
|
|
83
|
+
|
|
84
|
+
Each record maps to one Tailwind theme namespace. A token's key is emitted **verbatim**
|
|
85
|
+
into both the custom property and the utility — no camelCase-to-kebab transform, so what
|
|
86
|
+
you type is what you get.
|
|
87
|
+
|
|
88
|
+
| Record | Custom property | Tailwind utilities | Example |
|
|
89
|
+
|---|---|---|---|
|
|
90
|
+
| `colors` | `--tr-color-<name>` | `bg-`, `text-`, `border-`, `ring-`, `fill-`, `stroke-`, … | `lagoon` → `bg-lagoon` |
|
|
91
|
+
| `fonts` | `--tr-font-<name>` | `font-<name>` | `display` → `font-display` |
|
|
92
|
+
| `type` | `--tr-text-<name>` | `text-<name>` | `2xl` → `text-2xl` |
|
|
93
|
+
| `radii` | `--tr-radius-<name>` | `rounded-<name>` | `pill` → `rounded-pill` |
|
|
94
|
+
| `shadows` | `--tr-shadow-<name>` | `shadow-<name>` | `card` → `shadow-card` |
|
|
95
|
+
| `layout` | `--tr-<name>` | *(none)* | `header-height` → `var(--tr-header-height)` |
|
|
96
|
+
|
|
97
|
+
`layout` has no Tailwind utility namespace, so its tokens get a `--tr-*` value and no
|
|
98
|
+
utility — reference them with `var()` from your own CSS.
|
|
99
|
+
|
|
100
|
+
### Token names
|
|
101
|
+
|
|
102
|
+
Kebab-case: `[a-z0-9]` groups separated by single hyphens (`ink`, `ink-soft`, `bp2`).
|
|
103
|
+
`TokenNameSchema` is exported if you want to validate names yourself.
|
|
104
|
+
|
|
105
|
+
**One token, one utility.** Two utility names cannot share a token — if you want both
|
|
106
|
+
`bg-brand` and `bg-primary`, declare both.
|
|
107
|
+
|
|
108
|
+
### The `document` block
|
|
109
|
+
|
|
110
|
+
Required. It names which of your **own** tokens dress the page — the `body` font-family,
|
|
111
|
+
colour, background, and font-size. `font` and `fontSize` resolve against `fonts` and
|
|
112
|
+
`type`; `color` and `background` resolve against `colors`.
|
|
113
|
+
|
|
114
|
+
Every reference is validated against your declared tokens, in its own group: a typo, or a
|
|
115
|
+
colour name where a font belongs, fails at parse rather than rendering an unthemed page in
|
|
116
|
+
silence.
|
|
117
|
+
|
|
118
|
+
### Validation
|
|
119
|
+
|
|
120
|
+
`TokenThemeSchema` validates the object at runtime, and it is `.strict()` — an unknown
|
|
121
|
+
top-level key is an error rather than a silently-dead set of tokens.
|
|
122
|
+
|
|
123
|
+
## Generating `theme.css`
|
|
33
124
|
|
|
34
|
-
`
|
|
35
|
-
`
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
125
|
+
`template-kit theme` loads `templates/<key>/theme.ts`, validates its `theme` export
|
|
126
|
+
against `TokenThemeSchema`, and writes `templates/<key>/theme.css` — the file your
|
|
127
|
+
Tailwind entry imports.
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
template-kit theme # infer the template from cwd, or the workspace's one template
|
|
131
|
+
template-kit theme <template> # generate one named template's theme.css, by its key
|
|
132
|
+
template-kit theme --all # generate every template in the workspace
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
`theme.css` is generated — it opens with a header saying so, and it is not meant to be
|
|
136
|
+
hand-edited. Re-run `template-kit theme` after changing `theme.ts` and commit the
|
|
137
|
+
regenerated file alongside it.
|
|
138
|
+
|
|
139
|
+
A missing `theme.ts`, a `theme.ts` that doesn't export `theme`, or a `theme` that fails
|
|
140
|
+
`TokenThemeSchema` all fail the command with a message naming the file and the problem —
|
|
141
|
+
nothing is written in that case.
|
|
142
|
+
|
|
143
|
+
In order, the file it writes contains:
|
|
144
|
+
|
|
145
|
+
1. your `fontFaces` block, if you set one;
|
|
146
|
+
2. `@theme inline { --color-<name>: var(--tr-color-<name>); … }` — the alias layer, which
|
|
147
|
+
is what brings your token utilities into existence;
|
|
148
|
+
3. `:root { --tr-color-<name>: <value>; … }` — the values behind those aliases;
|
|
149
|
+
4. `body { … }` — your `document` defaults, written as `var()` indirections so a runtime
|
|
150
|
+
`:root` override still cascades to the page.
|
|
151
|
+
|
|
152
|
+
Deterministic: keys are sorted, so the same theme always content-hashes identically.
|
|
153
|
+
|
|
154
|
+
## Colour in sections
|
|
155
|
+
|
|
156
|
+
A section carries no palette of its own, in server-rendered code or inside an island — the
|
|
157
|
+
`template-kit/no-hex` [lint rule](rules/no-hex.md) flags a hard-coded hex literal in a
|
|
158
|
+
colour context, and a CSS colour function anywhere, without exempting `"use client"` files
|
|
159
|
+
the way most of the preset's other rules do. Sections spend the theme's tokens; the theme
|
|
160
|
+
is where a colour is named.
|
|
161
|
+
|
|
162
|
+
For a colour the theme does not name — a hover, active, or pressed step, or a translucent
|
|
163
|
+
tint — derive it from the tokens that *are* named, with a `color-mix()` over theme-derived
|
|
164
|
+
arguments:
|
|
165
|
+
`hover:bg-[color-mix(in_srgb,var(--tr-color-primary)_80%,var(--tr-color-surface))]`. Such a
|
|
166
|
+
mix composes a colour out of the theme rather than stating one, and is the single colour
|
|
167
|
+
function `no-hex` allows — it carves out no other.
|
|
39
168
|
|
|
40
169
|
## How a section's CSS reaches the page
|
|
41
170
|
|
|
@@ -48,8 +177,8 @@ never write an import to make either happen:
|
|
|
48
177
|
template's own hand-written stylesheet is collected the same way, into
|
|
49
178
|
`@layer template`.
|
|
50
179
|
2. **Package CSS, harvested from the render path.** The build also bundles
|
|
51
|
-
`Renderer.tsx`'s module graph — and
|
|
52
|
-
|
|
180
|
+
`Renderer.tsx`'s module graph — every component and island it imports — and pulls in
|
|
181
|
+
whatever `.css` those imports resolve to. A bare specifier
|
|
53
182
|
(`import "swiper/css";`) resolves into `node_modules` and lands in the same
|
|
54
183
|
`@layer sections`; that is the sanctioned way for a third-party package's
|
|
55
184
|
stylesheet to reach the page.
|
|
@@ -70,12 +199,10 @@ Two constraints follow directly, and both are enforced by lint:
|
|
|
70
199
|
|
|
71
200
|
A published page loads, in order:
|
|
72
201
|
|
|
73
|
-
1. **`base.css`** — layer order, the reset gaps, `.tr-section`, motion
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
3. **Your compiled Tailwind bundle** — `@import "tailwindcss"` + `styles.css` + your
|
|
78
|
-
section markup's utilities.
|
|
202
|
+
1. **`base.css`** — layer order, the reset gaps, `.tr-section`, `.tr-image-frame`, motion
|
|
203
|
+
tokens.
|
|
204
|
+
2. **Your compiled Tailwind bundle** — your entry: `@import "tailwindcss"` + the kit's
|
|
205
|
+
`styles.css` + your compiled theme + the utilities your section markup uses.
|
|
79
206
|
|
|
80
207
|
The layer order `base.css` declares is:
|
|
81
208
|
|
|
@@ -96,70 +223,9 @@ not make: `font-smoothing`, `button { cursor: pointer }` (Tailwind v4 leaves but
|
|
|
96
223
|
|
|
97
224
|
Ship `base.css` without a Tailwind entry on the page and you get an unreset page.
|
|
98
225
|
|
|
99
|
-
`base.css` references **no
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
## `TokenTheme` — your template's values
|
|
104
|
-
|
|
105
|
-
A template's `theme.ts` exports a `TokenTheme`. `compileThemeToCss` turns it into the
|
|
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.
|
|
110
|
-
|
|
111
|
-
```ts
|
|
112
|
-
import { compileThemeToCss, type TokenTheme } from "@homepages/template-kit";
|
|
113
|
-
|
|
114
|
-
export const theme: TokenTheme = {
|
|
115
|
-
colors: { ink: "#111418", background: "#ffffff", /* …every role, see below */ },
|
|
116
|
-
palette: { "brand-gold": "#c8a248" }, // optional escape hatch
|
|
117
|
-
fonts: {
|
|
118
|
-
body: '"Inter", system-ui, sans-serif',
|
|
119
|
-
heading: '"Canela", Georgia, serif',
|
|
120
|
-
numeric: '"Inter", system-ui, sans-serif',
|
|
121
|
-
label: '"Inter", system-ui, sans-serif',
|
|
122
|
-
fontFaces: '@import url("https://fonts.example/inter.css");', // optional, emitted before :root
|
|
123
|
-
},
|
|
124
|
-
type: { xs: "0.75rem", sm: "0.875rem", base: "1rem", /* lg xl 2xl 3xl 4xl 5xl display */ },
|
|
125
|
-
radii: { sm: "2px", md: "6px", lg: "12px", full: "9999px" },
|
|
126
|
-
shadows: { sm: "…", md: "…", lg: "…" },
|
|
127
|
-
layout: { containerMax: "1280px", containerPad: "1.5rem", headerHeight: "72px" },
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
const css = compileThemeToCss(theme);
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
`TokenThemeSchema` validates it at runtime; the object is `.strict()`, so an unknown key
|
|
134
|
-
is an error rather than a silently-dead token. Every scale is **complete or invalid** —
|
|
135
|
-
there are no defaults to fall back on.
|
|
136
|
-
|
|
137
|
-
### Color roles
|
|
138
|
-
|
|
139
|
-
The role keys are fixed. Each maps 1:1 to a `--tr-color-*` variable, and the Tailwind
|
|
140
|
-
utility is named after **the variable**, not the role key — so `surfaceAlt` is
|
|
141
|
-
`bg-surface-alt`, and `primaryStrong` is `bg-primary-darker`. Read the mapping off the
|
|
142
|
-
exported `COLOR_ROLE_VARS` (and `FONT_ROLE_VARS`, `TYPE_STEP_VARS`, `RADIUS_VARS`,
|
|
143
|
-
`SHADOW_VARS`, `LAYOUT_VARS` for the other scales), or let your editor autocomplete the
|
|
144
|
-
`ColorRole` union:
|
|
145
|
-
|
|
146
|
-
| Role key | CSS variable | Utility |
|
|
147
|
-
|---|---|---|
|
|
148
|
-
| `ink` `inkSoft` `inkMute` | `--tr-color-ink[-soft|-mute]` | `text-ink`, `text-ink-soft`, `text-ink-mute` |
|
|
149
|
-
| `background` `surface` `surfaceAlt` | `--tr-color-background` / `-surface` / `-surface-alt` | `bg-surface-alt`, … |
|
|
150
|
-
| `border` `borderStrong` | `--tr-color-border[-strong]` | `border-border-strong`, … |
|
|
151
|
-
| `primary` `primaryFg` `primaryStrong` | `--tr-color-primary` / `-primary-fg` / `-primary-darker` | `bg-primary`, `text-primary-fg`, `bg-primary-darker` |
|
|
152
|
-
| `accent` `accentFg` `accentStrong` | `--tr-color-accent` / `-accent-contrast` / `-accent-strong` | `bg-accent`, `text-accent-contrast`, `bg-accent-strong` |
|
|
153
|
-
| `overlay` `overlayStrong` `overlayLight` | `--tr-color-overlay[-strong|-light]` | `bg-overlay`, … |
|
|
154
|
-
| `darkBg` `darkFg` `darkFgMute` `darkBorder` | `--tr-color-footer-bg` / `-footer-fg` / `-footer-fg-mute` / `-footer-divider` | `bg-footer-bg`, `text-footer-fg`, `border-footer-divider`, … |
|
|
155
|
-
|
|
156
|
-
`palette` is the open escape hatch: each key becomes `--tr-color-<key>`. Sections normally
|
|
157
|
-
use the semantic roles — reach for a brand swatch only when no role honestly fits.
|
|
158
|
-
|
|
159
|
-
A section carries no palette of its own, in server-rendered code or inside an island —
|
|
160
|
-
the `template-kit/no-hex` [lint rule](eslint.md#universal--no-island-exemption) bans a
|
|
161
|
-
hard-coded hex literal or CSS color function in a color context and does not exempt
|
|
162
|
-
`"use client"` files, unlike most of the preset's other rules.
|
|
226
|
+
`base.css` references **no design token of yours** — nothing global is opinionated about
|
|
227
|
+
your brand. Its one tintable surface is the `Image` frame's placeholder fill; see
|
|
228
|
+
[`Image`](primitives.md#image--the-defensive-image-primitive).
|
|
163
229
|
|
|
164
230
|
## The breakpoint ladder
|
|
165
231
|
|
|
@@ -191,8 +257,7 @@ memory — enough to make it evict a live WebGL map's layer on scroll, blanking
|
|
|
191
257
|
they stop consuming that memory.
|
|
192
258
|
|
|
193
259
|
The kit does **not** apply this for you — it has a real cost (below), and only you know
|
|
194
|
-
whether your page needs it. If it does, add the rule to
|
|
195
|
-
`@import "tailwindcss"; @import "@homepages/template-kit/styles.css";` — the third
|
|
260
|
+
whether your page needs it. If it does, add the rule to your Tailwind entry — the second
|
|
196
261
|
stylesheet the page loads:
|
|
197
262
|
|
|
198
263
|
```css
|