@homepages/template-kit 0.3.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 +71 -0
- package/README.md +39 -4
- package/dist/asset-modules.d.ts +81 -0
- package/dist/cli/check/config.js +5 -1
- package/dist/cli/check/css.js +27 -13
- package/dist/cli/check/loader.js +4 -0
- package/dist/cli/check/section-assets.js +58 -0
- package/dist/cli/check/stages/size.js +124 -8
- package/dist/cli/check/stages/tree.js +26 -27
- 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.js +80 -14
- 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/index.d.ts +1 -0
- 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/ssr.d.ts +31 -0
- package/dist/ssr.js +46 -0
- package/docs/INDEX.md +9 -1
- package/docs/assets.md +128 -0
- package/docs/check.md +6 -3
- package/docs/dev.md +187 -0
- package/docs/islands.md +5 -2
- package/docs/llms.txt +41 -3
- package/docs/new.md +27 -0
- package/docs/overview.md +57 -0
- package/docs/pack.md +46 -0
- package/docs/primitives.md +5 -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 +4 -2
- package/docs/rules/bundle-binary-asset.md +33 -52
- package/docs/rules/bundle-incomplete.md +2 -2
- package/docs/rules/css-reason.md +3 -3
- 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/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 +2 -2
- package/docs/vocabulary.md +98 -0
- package/package.json +11 -3
- package/tsconfig.json +1 -1
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
---
|
|
2
|
+
purpose: Make part of a section interactive with a client-hydrated island.
|
|
3
|
+
status: living
|
|
4
|
+
related: [../islands.md, ../rules/server-vs-client.md, ../rules/serializable-island-props.md]
|
|
5
|
+
updated: 2026-07-15
|
|
6
|
+
---
|
|
7
|
+
# Make part of a section interactive
|
|
8
|
+
|
|
9
|
+
## Goal
|
|
10
|
+
|
|
11
|
+
Make part of a section interactive.
|
|
12
|
+
|
|
13
|
+
## The delta
|
|
14
|
+
|
|
15
|
+
Starting from the starter's `hero` section, add a `"use client"` file next to the
|
|
16
|
+
four contract files, matching the shape of the starter's own island,
|
|
17
|
+
`ExpandableText.tsx`: the directive as the file's first statement, a default-exported
|
|
18
|
+
function typed against plain props, and `useState` for the interactive part.
|
|
19
|
+
|
|
20
|
+
```tsx
|
|
21
|
+
// sections/hero/ContactReveal.tsx — a new island, same shape as ExpandableText
|
|
22
|
+
"use client";
|
|
23
|
+
|
|
24
|
+
import { type ReactNode, useState } from "react";
|
|
25
|
+
|
|
26
|
+
import type { IslandEditorOptions } from "@homepages/template-kit";
|
|
27
|
+
|
|
28
|
+
export default function ContactReveal({
|
|
29
|
+
hiddenLabel,
|
|
30
|
+
revealedValue,
|
|
31
|
+
}: {
|
|
32
|
+
hiddenLabel: string;
|
|
33
|
+
revealedValue: string;
|
|
34
|
+
}): ReactNode {
|
|
35
|
+
const [revealed, setRevealed] = useState(false);
|
|
36
|
+
return revealed ? (
|
|
37
|
+
<p className="text-base font-medium text-ink">{revealedValue}</p>
|
|
38
|
+
) : (
|
|
39
|
+
<button
|
|
40
|
+
type="button"
|
|
41
|
+
className="font-medium text-accent underline"
|
|
42
|
+
onClick={() => setRevealed(true)}
|
|
43
|
+
>
|
|
44
|
+
{hiddenLabel}
|
|
45
|
+
</button>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Optional, as on ExpandableText: hydrate live in the editor canvas too.
|
|
50
|
+
export const editor: IslandEditorOptions = {
|
|
51
|
+
live: true,
|
|
52
|
+
};
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Render it from `Renderer.tsx` like any other component:
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
// Renderer.tsx
|
|
59
|
+
import ContactReveal from "./ContactReveal";
|
|
60
|
+
// …
|
|
61
|
+
<ContactReveal hiddenLabel="Show phone number" revealedValue="(555) 010-2020" />
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
If the island renders a schema slot's own content — the way the starter's `blurb`
|
|
65
|
+
slot renders through `ExpandableText` — wrap it in `<Slot id="…">` with the matching
|
|
66
|
+
schema id, exactly as `Renderer.tsx` already does:
|
|
67
|
+
|
|
68
|
+
```tsx
|
|
69
|
+
<Slot id="blurb">
|
|
70
|
+
<ExpandableText text={slots.blurb} collapsedChars={140} moreLabel="Read more" />
|
|
71
|
+
</Slot>
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
A standalone island with no bound slot — like `ContactReveal` above — needs no
|
|
75
|
+
`<Slot>` wrapper; `<Slot>` marks an editable slot's DOM for the editor, not
|
|
76
|
+
"this is interactive."
|
|
77
|
+
|
|
78
|
+
Props crossing into an island must be JSON-serializable — strings, numbers,
|
|
79
|
+
booleans, and plain arrays/objects only, never a function, a `Date`, or a class
|
|
80
|
+
instance; the island owns its own event handlers instead of receiving one. This is
|
|
81
|
+
the only mechanism for interactivity in a section: a Renderer stays a pure function
|
|
82
|
+
of its props, and any state, effect, or event handler moves into a `"use client"`
|
|
83
|
+
island like the one above.
|
|
84
|
+
|
|
85
|
+
## Commands
|
|
86
|
+
|
|
87
|
+
None — this is a pure content edit. No CLI command beyond the author loop below.
|
|
88
|
+
|
|
89
|
+
## Verify
|
|
90
|
+
|
|
91
|
+
Verify with [the author loop](../quickstart.md#the-author-loop).
|
|
92
|
+
|
|
93
|
+
## See also
|
|
94
|
+
|
|
95
|
+
- [Islands](../islands.md) — writing one, its props contract, editor options, and
|
|
96
|
+
the DOM contract a published page hydrates.
|
|
97
|
+
- [Server vs client](../rules/server-vs-client.md) — how a file is scoped server or
|
|
98
|
+
client, and the three rules an island is exempt from.
|
|
99
|
+
- [`serializable-island-props`](../rules/serializable-island-props.md) — the rule
|
|
100
|
+
that catches a non-serializable prop before the render ever throws.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
---
|
|
2
|
+
purpose: Structure a section's files beyond the four reserved contract files.
|
|
3
|
+
status: living
|
|
4
|
+
related: [../new.md, ../islands.md, ../pack.md, ../rules/bundle-binary-asset.md]
|
|
5
|
+
updated: 2026-07-16
|
|
6
|
+
---
|
|
7
|
+
# Organize a section's files beyond the four reserved
|
|
8
|
+
|
|
9
|
+
## Goal
|
|
10
|
+
|
|
11
|
+
Structure a section beyond the four files.
|
|
12
|
+
|
|
13
|
+
## The delta
|
|
14
|
+
|
|
15
|
+
Starting from the starter's `hero` section, add a `components/` folder and pull a
|
|
16
|
+
piece of markup out into its own module — the same shape as the starter's own
|
|
17
|
+
`components/Feature.tsx`, which the starter's `Renderer.tsx` already imports and
|
|
18
|
+
renders.
|
|
19
|
+
|
|
20
|
+
```tsx
|
|
21
|
+
// sections/hero/components/Feature.tsx
|
|
22
|
+
import type { ReactNode } from "react";
|
|
23
|
+
|
|
24
|
+
export function Feature({ label, value }: { label: string; value: string }): ReactNode {
|
|
25
|
+
return (
|
|
26
|
+
<div className="rounded-lg border border-ink/10 p-4">
|
|
27
|
+
<div className="text-sm text-ink/60">{label}</div>
|
|
28
|
+
<div className="text-lg font-semibold text-ink">{value}</div>
|
|
29
|
+
</div>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
```tsx
|
|
35
|
+
// Renderer.tsx
|
|
36
|
+
import { Feature } from "./components/Feature";
|
|
37
|
+
// …
|
|
38
|
+
<Feature label="Status" value="For sale" />
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Exactly four files are reserved, and only at the section folder's root:
|
|
42
|
+
`schema.ts`, `fill-spec.ts`, `fixtures.ts`, `Renderer.tsx`. Everything else in the
|
|
43
|
+
folder is yours — `components/`, `hooks/`, `utils/`, stylesheets, any nesting, any
|
|
44
|
+
number of support modules, and `"use client"` islands. A binary asset (an image, a
|
|
45
|
+
font, an archive) may live under an `assets/` folder too, but only if something in
|
|
46
|
+
the section references it — see
|
|
47
|
+
[`bundle-binary-asset`](../rules/bundle-binary-asset.md) for the rule and what an
|
|
48
|
+
unreferenced one costs you.
|
|
49
|
+
|
|
50
|
+
Every file under `templates/<template>/` — not only the four reserved files — feeds
|
|
51
|
+
`pack`'s content hash of the template tree, stamped into `pack-manifest.json`.
|
|
52
|
+
Editing a component, a util, or even a comment inside a section folder changes that
|
|
53
|
+
hash on the next `pack`, exactly as editing `Renderer.tsx` would.
|
|
54
|
+
|
|
55
|
+
## Commands
|
|
56
|
+
|
|
57
|
+
None — this is a pure content edit. No CLI command beyond the author loop below.
|
|
58
|
+
|
|
59
|
+
## Verify
|
|
60
|
+
|
|
61
|
+
Verify with [the author loop](../quickstart.md#the-author-loop).
|
|
62
|
+
|
|
63
|
+
## See also
|
|
64
|
+
|
|
65
|
+
- [Adding templates and sections](../new.md) — `template-kit new` scaffolds a
|
|
66
|
+
starter section that already includes a `components/` folder and an island.
|
|
67
|
+
- [Islands](../islands.md) — a `"use client"` file is one more kind of file the
|
|
68
|
+
section folder is free to hold.
|
|
69
|
+
- [`template-kit pack`](../pack.md) — what produces the content hash and what it
|
|
70
|
+
refuses to pack.
|
|
71
|
+
- [`bundle-binary-asset`](../rules/bundle-binary-asset.md) — when a binary file
|
|
72
|
+
in the folder must be referenced, and what happens if it isn't.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
---
|
|
2
|
+
purpose: Package a template for submission.
|
|
3
|
+
status: living
|
|
4
|
+
related: [../pack.md, ../check.md]
|
|
5
|
+
updated: 2026-07-15
|
|
6
|
+
---
|
|
7
|
+
# Prepare a submission
|
|
8
|
+
|
|
9
|
+
## Goal
|
|
10
|
+
|
|
11
|
+
Package a template for submission.
|
|
12
|
+
|
|
13
|
+
## The delta
|
|
14
|
+
|
|
15
|
+
Get the template to a green `check`, then pack it:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
template-kit check <template>
|
|
19
|
+
template-kit pack <template>
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
`pack` will not produce a zip until `check <template>` passes — it re-runs
|
|
23
|
+
the same gate itself, and a failing `check` reports every problem in one
|
|
24
|
+
pass, not one at a time, so a manual `check` first is just a faster feedback
|
|
25
|
+
loop, not a separate requirement.
|
|
26
|
+
A passing `pack` writes `<template>.zip` at the workspace root: the
|
|
27
|
+
template's source, `pack-manifest.json` (the kit version, a content hash,
|
|
28
|
+
and the `check` flag), and the workspace's `package.json` /
|
|
29
|
+
`package-lock.json` so the build reproduces with `npm ci`.
|
|
30
|
+
|
|
31
|
+
`pack` refuses to write a zip when:
|
|
32
|
+
|
|
33
|
+
- `check <template>` is not green — fix the reported problems first.
|
|
34
|
+
- the workspace has no `package-lock.json` — run `npm install` and commit it.
|
|
35
|
+
- a dependency uses a local `file:`/`link:` protocol — depend on a published
|
|
36
|
+
version instead.
|
|
37
|
+
|
|
38
|
+
Each refusal prints the cause; fix it and run `pack` again.
|
|
39
|
+
|
|
40
|
+
## Commands
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
template-kit check <template>
|
|
44
|
+
template-kit pack <template>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Verify
|
|
48
|
+
|
|
49
|
+
Verify with [the author loop](../quickstart.md#the-author-loop) — `pack`
|
|
50
|
+
refuses to build the zip until `check` is green, so a clean loop is the gate.
|
|
51
|
+
|
|
52
|
+
## See also
|
|
53
|
+
|
|
54
|
+
- [Pack](../pack.md) — what the zip contains and the full refusal list.
|
|
55
|
+
- [Check](../check.md) — the gate `pack` runs before it will produce a zip.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
---
|
|
2
|
+
purpose: Add another template to the workspace.
|
|
3
|
+
status: living
|
|
4
|
+
related: [../new.md, ../check.md]
|
|
5
|
+
updated: 2026-07-15
|
|
6
|
+
---
|
|
7
|
+
# Add a second template
|
|
8
|
+
|
|
9
|
+
## Goal
|
|
10
|
+
|
|
11
|
+
Add another template to the workspace.
|
|
12
|
+
|
|
13
|
+
## The delta
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
template-kit new <name>
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`new` creates `templates/<name>/` as a working starter template — the same
|
|
20
|
+
starter shape a fresh workspace ships (a text slot, an image slot, a
|
|
21
|
+
fact-bound slot, an option, fixtures, a fill spec, a `components/` folder, a
|
|
22
|
+
`"use client"` island, `theme.ts`/`theme.css`, `manifest.json`) — and it
|
|
23
|
+
already passes `check` as-is. Edit its sections from there.
|
|
24
|
+
|
|
25
|
+
A template is a plain folder under `templates/`. Adding one does not
|
|
26
|
+
duplicate anything: every template in the workspace shares the same kit
|
|
27
|
+
version, the same root `package.json`/lockfile, and the same
|
|
28
|
+
`tsconfig.json`/ESLint config. There is nothing per-template to configure
|
|
29
|
+
beyond the folder itself.
|
|
30
|
+
|
|
31
|
+
`new` refuses to overwrite an existing `templates/<name>/` folder.
|
|
32
|
+
|
|
33
|
+
## Commands
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
template-kit new <name>
|
|
37
|
+
template-kit check <name>
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Verify
|
|
41
|
+
|
|
42
|
+
Verify with [the author loop](../quickstart.md#the-author-loop) — a freshly
|
|
43
|
+
created template is check-clean before you change a line.
|
|
44
|
+
|
|
45
|
+
## See also
|
|
46
|
+
|
|
47
|
+
- [Adding templates and sections](../new.md) — the full command, including
|
|
48
|
+
`new-section` for adding a section within a template.
|
|
49
|
+
- [Check](../check.md) — the gate every template (new or edited) must pass.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
---
|
|
2
|
+
purpose: Give the user a closed set of choices — an editable content slot, distinct from a behavioral enum option.
|
|
3
|
+
status: living
|
|
4
|
+
related: [../vocabulary.md, ../schema-system.md]
|
|
5
|
+
updated: 2026-07-15
|
|
6
|
+
---
|
|
7
|
+
# A select slot — a closed set of choices
|
|
8
|
+
|
|
9
|
+
## Goal
|
|
10
|
+
|
|
11
|
+
Give the user a closed set of choices — an editable content slot, distinct from
|
|
12
|
+
a behavioral `enum` option.
|
|
13
|
+
|
|
14
|
+
## The delta
|
|
15
|
+
|
|
16
|
+
A `select` slot still needs a `source` or a `produced_by`, exactly like every
|
|
17
|
+
other slot type — `values` alone only declares the closed set the editor's
|
|
18
|
+
dropdown offers. The cleanest no-AI fill for a closed set is usually a
|
|
19
|
+
`direct()` fact that already resolves to one of a closed set of strings:
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
// schema.ts — add a select slot, seeded from a closed-vocabulary fact
|
|
23
|
+
property_type: {
|
|
24
|
+
type: "select",
|
|
25
|
+
editable_by_user: true,
|
|
26
|
+
required: false,
|
|
27
|
+
values: ["single_family", "condo_townhome", "multi_family", "investment"],
|
|
28
|
+
source: direct("property_type"),
|
|
29
|
+
display_name: "Property type",
|
|
30
|
+
},
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
// Renderer.tsx — read it like any other text-shaped slot
|
|
35
|
+
<Slot id="property_type" textLeaf>
|
|
36
|
+
<span>{slots.property_type}</span>
|
|
37
|
+
</Slot>
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`slots.property_type` is `string | null` (required is `false`). The general
|
|
41
|
+
`source`/`produced_by` contract still applies to a `select` slot, but no
|
|
42
|
+
fill-spec decision type constrains its output to the closed `values` set —
|
|
43
|
+
so `source: direct(...)` (a fact that already resolves within the set) is
|
|
44
|
+
the reliable way to fill one.
|
|
45
|
+
|
|
46
|
+
## Commands
|
|
47
|
+
|
|
48
|
+
None — this is a pure content edit. No CLI command beyond the author loop below.
|
|
49
|
+
|
|
50
|
+
## Verify
|
|
51
|
+
|
|
52
|
+
Verify with [the author loop](../quickstart.md#the-author-loop).
|
|
53
|
+
|
|
54
|
+
## See also
|
|
55
|
+
|
|
56
|
+
- [The closed vocabulary](../vocabulary.md#slot-types) — the slot-type table, including
|
|
57
|
+
where `select` sits next to `text`/`number`/etc.
|
|
58
|
+
- [The schema system](../schema-system.md) — the full `schema.ts` contract, including
|
|
59
|
+
`direct()`/`derived()` binding.
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
---
|
|
2
|
+
purpose: Add a graphic — an icon, a divider, a photo, a font — to a section.
|
|
3
|
+
status: living
|
|
4
|
+
related: [../assets.md, ../check.md, ../rules/bundle-binary-asset.md, image-slot-crop.md, ../theme-and-css.md]
|
|
5
|
+
updated: 2026-07-16
|
|
6
|
+
---
|
|
7
|
+
# Add a static asset (image, icon, font)
|
|
8
|
+
|
|
9
|
+
## Goal
|
|
10
|
+
|
|
11
|
+
Add a graphic — an icon, a divider mark, a photo, a custom font — to a section.
|
|
12
|
+
|
|
13
|
+
## The delta
|
|
14
|
+
|
|
15
|
+
A binary asset — an image, a font, a video, an archive — may live in a section's own
|
|
16
|
+
`sections/<section>/assets/` folder, but only if the section references it: imported in
|
|
17
|
+
`Renderer.tsx` (`import crest from "./assets/crest.svg"`) or `url()`-referenced from one
|
|
18
|
+
of the section's CSS files. An unreferenced binary in the folder is flagged; see
|
|
19
|
+
[`bundle-binary-asset`](../rules/bundle-binary-asset.md) for the rule and why.
|
|
20
|
+
|
|
21
|
+
A file shared across sections — a logo, a brand webfont, a texture — lives at the
|
|
22
|
+
**template level** instead, referenced the same way with a `../../assets/…` path. See
|
|
23
|
+
[Static assets](../assets.md) for both paths and the full reference contract. What you
|
|
24
|
+
reach for depends on what the graphic is:
|
|
25
|
+
|
|
26
|
+
**A vector graphic — an icon, a divider, a decorative mark used only here.** Inline it as
|
|
27
|
+
SVG markup directly in the JSX that draws it. It is text, ships with the rest of your
|
|
28
|
+
component, and needs no file or import at all:
|
|
29
|
+
|
|
30
|
+
```tsx
|
|
31
|
+
// sections/hero/components/ScrollArrow.tsx
|
|
32
|
+
export function ScrollArrow() {
|
|
33
|
+
return (
|
|
34
|
+
<svg viewBox="0 0 16 16" className="h-4 w-4" fill="none" aria-hidden="true">
|
|
35
|
+
<path d="M8 3v10M3 9l5 4 5-4" stroke="currentColor" strokeWidth="1.5" />
|
|
36
|
+
</svg>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
```tsx
|
|
42
|
+
// sections/hero/Renderer.tsx
|
|
43
|
+
import { ScrollArrow } from "./components/ScrollArrow";
|
|
44
|
+
// …
|
|
45
|
+
<ScrollArrow />
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
That markup's bytes ship inside the section's renderer bundle, so they count
|
|
49
|
+
toward the same byte budget as the rest of your JSX — see the size stage in
|
|
50
|
+
[Check](../check.md).
|
|
51
|
+
|
|
52
|
+
**A raster image or watermark used only in this section.** Add it under
|
|
53
|
+
`sections/<section>/assets/` and import it into the component:
|
|
54
|
+
|
|
55
|
+
```tsx
|
|
56
|
+
// sections/hero/Renderer.tsx
|
|
57
|
+
import { Image } from "@homepages/template-kit";
|
|
58
|
+
import crest from "./assets/crest.svg";
|
|
59
|
+
|
|
60
|
+
export default function Renderer() {
|
|
61
|
+
return <Image src={crest} alt="" />;
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Or reference it from the section's CSS with `url()` instead of importing it — see
|
|
66
|
+
[Static assets](../assets.md) for both forms. Either way the file must be referenced:
|
|
67
|
+
an asset nothing imports or `url()`-references is flagged by
|
|
68
|
+
[`bundle-binary-asset`](../rules/bundle-binary-asset.md).
|
|
69
|
+
|
|
70
|
+
**A photo.** Not a section asset at all — a section never ships a photo file. A
|
|
71
|
+
photo arrives at runtime through an `image` slot: declare it in `schema.ts`,
|
|
72
|
+
produce it with an `image-assign` decision in `fill-spec.ts`, and render it
|
|
73
|
+
through the `<Image>` primitive, which reads `url`, `alt`, and `responsive` off
|
|
74
|
+
the slot's resolved value. See
|
|
75
|
+
[Image slot with a locked crop](image-slot-crop.md) for the full shape.
|
|
76
|
+
|
|
77
|
+
**A custom font shared across the template.** Declared once, for the whole
|
|
78
|
+
template, in the theme's `fonts` and optional `fontFaces` block. See
|
|
79
|
+
[Theme and CSS](../theme-and-css.md). A font only one section uses can instead
|
|
80
|
+
live in that section's `assets/` folder and be declared with a `@font-face` in
|
|
81
|
+
the section's own CSS — see [Static assets](../assets.md).
|
|
82
|
+
|
|
83
|
+
## Commands
|
|
84
|
+
|
|
85
|
+
None — inlining SVG markup and declaring a slot are content edits. No CLI
|
|
86
|
+
command beyond the author loop below.
|
|
87
|
+
|
|
88
|
+
## Verify
|
|
89
|
+
|
|
90
|
+
Verify with [the author loop](../quickstart.md#the-author-loop).
|
|
91
|
+
|
|
92
|
+
## See also
|
|
93
|
+
|
|
94
|
+
- [Static assets](../assets.md) — the full reference/import contract, template
|
|
95
|
+
level vs. section folder.
|
|
96
|
+
- [`bundle-binary-asset`](../rules/bundle-binary-asset.md) — the rule that
|
|
97
|
+
flags an unreferenced binary in a section folder, and the SVG-inlining fix.
|
|
98
|
+
- [Check](../check.md) — the size stage a large inlined SVG or a bloated
|
|
99
|
+
renderer bundle counts against.
|
|
100
|
+
- [Image slot with a locked crop](image-slot-crop.md) — the real channel for a
|
|
101
|
+
photo: an `image` slot, an `image-assign` decision, and the `<Image>`
|
|
102
|
+
primitive.
|
|
103
|
+
- [Theme and CSS](../theme-and-css.md) — where a template's fonts are declared.
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
---
|
|
2
|
+
purpose: Use a third-party npm package in a section.
|
|
3
|
+
status: living
|
|
4
|
+
related: [../islands.md, ../pack.md, ../rules/single-react.md, ../rules/no-bare-css-import.md, ../rules/no-css-import-from-render-path.md]
|
|
5
|
+
updated: 2026-07-15
|
|
6
|
+
---
|
|
7
|
+
# Use a third-party npm package in a section
|
|
8
|
+
|
|
9
|
+
## Goal
|
|
10
|
+
|
|
11
|
+
Use an npm package in a section.
|
|
12
|
+
|
|
13
|
+
## The delta
|
|
14
|
+
|
|
15
|
+
Install the package like any npm dependency:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm i swiper
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
**Only a published version.** `pack` refuses to produce a submission zip when a
|
|
22
|
+
dependency uses a local `file:`/`link:` protocol — depend on a real published
|
|
23
|
+
version, never a path on your own machine.
|
|
24
|
+
|
|
25
|
+
**Where you import it from matters.** A client library — a carousel, a map SDK,
|
|
26
|
+
anything that touches `window` or the DOM — is imported inside a `"use client"`
|
|
27
|
+
island, the same as any other browser-only code:
|
|
28
|
+
|
|
29
|
+
```tsx
|
|
30
|
+
// sections/gallery/Carousel.tsx
|
|
31
|
+
"use client";
|
|
32
|
+
|
|
33
|
+
import Swiper from "swiper";
|
|
34
|
+
|
|
35
|
+
export default function Carousel({ slides }: { slides: { id: string; url: string }[] }) {
|
|
36
|
+
// …
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
A server-side import — one that lands in the Renderer or a plain component it
|
|
41
|
+
imports, not behind `"use client"` — must stay pure and deterministic: no reads
|
|
42
|
+
of the clock, randomness, or the network at render time. A package built for a
|
|
43
|
+
browser runtime (it touches `window`, schedules a timer, generates an id) does
|
|
44
|
+
not belong there; move the usage into an island.
|
|
45
|
+
|
|
46
|
+
**A package that ships its own stylesheet** is imported by its bare specifier,
|
|
47
|
+
from the module that renders it — the Renderer's import graph (the components
|
|
48
|
+
and islands it imports, transitively) is what the CSS build bundles and
|
|
49
|
+
harvests for the section's stylesheet:
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
// sections/gallery/Carousel.tsx
|
|
53
|
+
"use client";
|
|
54
|
+
|
|
55
|
+
import Swiper from "swiper";
|
|
56
|
+
import "swiper/css"; // ← the sanctioned channel: a bare specifier reaches this section's layer
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
A **local** `.css` import from that same render path is a different, banned case
|
|
60
|
+
— see [`no-css-import-from-render-path`](../rules/no-css-import-from-render-path.md)
|
|
61
|
+
for the full distinction between the two, and
|
|
62
|
+
[`no-bare-css-import`](../rules/no-bare-css-import.md) for why a hand-written
|
|
63
|
+
stylesheet can't reach the package's CSS with its own `@import` instead.
|
|
64
|
+
|
|
65
|
+
## Commands
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
npm i <package>
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Verify
|
|
72
|
+
|
|
73
|
+
Verify with [the author loop](../quickstart.md#the-author-loop) — a green
|
|
74
|
+
`check` after the install confirms the package resolves, its CSS (if any)
|
|
75
|
+
landed in the section's layer, and the workspace still carries exactly one copy
|
|
76
|
+
of React.
|
|
77
|
+
|
|
78
|
+
## See also
|
|
79
|
+
|
|
80
|
+
- [Islands](../islands.md) — writing a `"use client"` island; a third-party
|
|
81
|
+
client library is fine inside one, never in a Renderer.
|
|
82
|
+
- [`template-kit pack`](../pack.md) — why a `file:`/`link:` dependency blocks a
|
|
83
|
+
submission.
|
|
84
|
+
- [`single-react`](../rules/single-react.md) — a package that pulls its own
|
|
85
|
+
copy of React breaks hydration; the installed tree may carry only one.
|
|
86
|
+
- [`no-css-import-from-render-path`](../rules/no-css-import-from-render-path.md) —
|
|
87
|
+
bare package specifier (legal) vs. a local `.css` import (banned) from a
|
|
88
|
+
section's render path.
|
|
89
|
+
- [`no-bare-css-import`](../rules/no-bare-css-import.md) — why a hand-written
|
|
90
|
+
stylesheet can't reach a package's CSS with its own `@import` either.
|
package/docs/rules/INDEX.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
purpose: Router for the rule corpus — every template-kit/<id> an author can hit, and the page that explains it.
|
|
3
3
|
status: living
|
|
4
4
|
related: [server-vs-client.md, ../eslint.md, ../check.md, ../INDEX.md]
|
|
5
|
-
updated: 2026-07-
|
|
5
|
+
updated: 2026-07-15
|
|
6
6
|
---
|
|
7
7
|
# Rules
|
|
8
8
|
|
|
@@ -34,7 +34,7 @@ and it is explained there rather than on each of them.
|
|
|
34
34
|
| [`no-hex`](no-hex.md) | eslint | No hard-coded colours — the one legal colour function is a `color-mix()` over theme-derived arguments. Islands are NOT exempt. |
|
|
35
35
|
| [`no-css-import-from-render-path`](no-css-import-from-render-path.md) | eslint | No relative/absolute .css import from a section's render path — a package's is the sanctioned channel. |
|
|
36
36
|
| [`bundle-incomplete`](bundle-incomplete.md) | check | A section folder is missing one of its four required files. |
|
|
37
|
-
| [`bundle-binary-asset`](bundle-binary-asset.md) | check |
|
|
37
|
+
| [`bundle-binary-asset`](bundle-binary-asset.md) | check | An unreferenced binary asset sits in a section folder. |
|
|
38
38
|
| [`schema-invalid`](schema-invalid.md) | check | A section's schema.ts or fill-spec.ts fails a contract assertion, or cannot be loaded. |
|
|
39
39
|
| [`fixtures-invalid`](fixtures-invalid.md) | check | A section's fixtures.ts does not satisfy the fixture-module shape. |
|
|
40
40
|
| [`manifest-invalid`](manifest-invalid.md) | check | A template's manifest.json is missing, unparseable, or violates the manifest contract. |
|
|
@@ -55,6 +55,8 @@ and it is explained there rather than on each of them.
|
|
|
55
55
|
| [`license-denied`](license-denied.md) | check | A production dependency's license is not on the allowlist. |
|
|
56
56
|
| [`size-renderer-bundle`](size-renderer-bundle.md) | check | A section's renderer bundle exceeds its byte budget. |
|
|
57
57
|
| [`size-section-css`](size-section-css.md) | check | A section's compiled CSS exceeds its byte budget. |
|
|
58
|
+
| [`size-island-bundle`](size-island-bundle.md) | check | A section's island (browser) bundle exceeds its byte budget. |
|
|
59
|
+
| [`size-assets`](size-assets.md) | check | A section's static assets exceed their byte budget. |
|
|
58
60
|
| [`no-templates`](no-templates.md) | check | The workspace has no template to check at all. |
|
|
59
61
|
|
|
60
62
|
## See also
|