@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
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
---
|
|
2
|
+
purpose: The template-kit/license-denied authoring rule — what it enforces, why, and how to fix it.
|
|
3
|
+
status: living
|
|
4
|
+
related: [INDEX.md, audit-severity.md]
|
|
5
|
+
updated: 2026-07-15
|
|
6
|
+
---
|
|
7
|
+
# template-kit/license-denied
|
|
8
|
+
|
|
9
|
+
> Every package in your **production** tree must carry a license from a fixed allowlist.
|
|
10
|
+
> There is no per-package override.
|
|
11
|
+
|
|
12
|
+
## Rule
|
|
13
|
+
|
|
14
|
+
Each package in the production dependency tree must declare a license on this list:
|
|
15
|
+
|
|
16
|
+
| Allowed |
|
|
17
|
+
|---|
|
|
18
|
+
| `MIT` |
|
|
19
|
+
| `ISC` |
|
|
20
|
+
| `Apache-2.0` |
|
|
21
|
+
| `BSD-2-Clause` |
|
|
22
|
+
| `BSD-3-Clause` |
|
|
23
|
+
| `0BSD` |
|
|
24
|
+
| `Unlicense` |
|
|
25
|
+
| `CC0-1.0` |
|
|
26
|
+
|
|
27
|
+
Two cases people expect to slide through, and neither does:
|
|
28
|
+
|
|
29
|
+
- **`SEE LICENSE IN <file>` is a denial**, not a pass. The check cannot read the file, so
|
|
30
|
+
it cannot clear the terms.
|
|
31
|
+
- **A missing `license` field is a denial**, not a pass. An unlicensed package is not a
|
|
32
|
+
permissive one — by default, no rights are granted at all.
|
|
33
|
+
|
|
34
|
+
**Scope: the production tree only.** "Production tree" means exactly what
|
|
35
|
+
`npm ci --omit=dev` installs. `devDependencies` are not evaluated — your bundler, your
|
|
36
|
+
test runner, and your formatter do not ship to anyone.
|
|
37
|
+
|
|
38
|
+
**An optional peer you have anyway counts.** If a production package declares an
|
|
39
|
+
optional `peerDependency` and your workspace has that package for any reason (even as a
|
|
40
|
+
`devDependency`), npm resolves the peer and keeps it in the `--omit=dev` install — so it
|
|
41
|
+
is in the production tree and its license is checked, transitively. An optional peer you
|
|
42
|
+
do **not** have is never installed and is not evaluated. Run `npm ci --omit=dev` and look
|
|
43
|
+
at the resulting `node_modules` if you want the exact set the check sees.
|
|
44
|
+
|
|
45
|
+
**There is no per-package override.** No allowlist entry you can add, no waiver comment. By
|
|
46
|
+
design.
|
|
47
|
+
|
|
48
|
+
## Reason
|
|
49
|
+
|
|
50
|
+
Your template's production dependencies are compiled into a page we host, and serve, on
|
|
51
|
+
behalf of a paying customer. Their license terms become terms we are shipping under. A
|
|
52
|
+
license we cannot clear in advance is a license we cannot ship — so the check is a
|
|
53
|
+
gate rather than a warning, and the list is the set we have already cleared.
|
|
54
|
+
|
|
55
|
+
## Fix
|
|
56
|
+
|
|
57
|
+
**Replace the package** with an equivalent whose license is on the list. That is the only
|
|
58
|
+
fix, and for the overwhelming majority of what a section needs there is an MIT or Apache-2.0
|
|
59
|
+
alternative.
|
|
60
|
+
|
|
61
|
+
One legitimate adjacent fix: if the package is genuinely **build-time only** — it never
|
|
62
|
+
reaches the published bundle — then it belongs in `devDependencies` anyway, and
|
|
63
|
+
`devDependencies` are not evaluated. Be honest about this. If your published page imports
|
|
64
|
+
it, it is a production dependency no matter which stanza it is listed under, and moving it
|
|
65
|
+
is a bug in your `package.json` rather than a fix.
|
|
66
|
+
|
|
67
|
+
### Before
|
|
68
|
+
|
|
69
|
+
```jsonc
|
|
70
|
+
// package.json
|
|
71
|
+
{
|
|
72
|
+
"dependencies": {
|
|
73
|
+
"react": "^19.0.0",
|
|
74
|
+
"react-dom": "^19.0.0",
|
|
75
|
+
"fancy-charts": "^3.2.0" // "license": "GPL-3.0-only" → denied
|
|
76
|
+
},
|
|
77
|
+
"devDependencies": {
|
|
78
|
+
"typescript": "^5.7.2"
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### After
|
|
84
|
+
|
|
85
|
+
```jsonc
|
|
86
|
+
// package.json
|
|
87
|
+
{
|
|
88
|
+
"dependencies": {
|
|
89
|
+
"react": "^19.0.0",
|
|
90
|
+
"react-dom": "^19.0.0",
|
|
91
|
+
"lightweight-charts": "^4.2.0" // "license": "Apache-2.0" → allowed
|
|
92
|
+
},
|
|
93
|
+
"devDependencies": {
|
|
94
|
+
"typescript": "^5.7.2"
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
npm uninstall fancy-charts
|
|
101
|
+
npm install lightweight-charts
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Confirm what a package declares before you commit to it:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
npm view <package> license
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## See also
|
|
111
|
+
|
|
112
|
+
- [`audit-severity`](audit-severity.md) — the other gate on the packages you ship.
|
|
113
|
+
- [`size-renderer-bundle`](size-renderer-bundle.md) — the third: what those packages cost the
|
|
114
|
+
page in bytes.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
---
|
|
2
|
+
purpose: The template-kit/lockfile-missing authoring rule — what it enforces, why, and how to fix it.
|
|
3
|
+
status: living
|
|
4
|
+
related: [INDEX.md, lockfile-stale.md]
|
|
5
|
+
updated: 2026-07-14
|
|
6
|
+
---
|
|
7
|
+
# template-kit/lockfile-missing
|
|
8
|
+
|
|
9
|
+
> Your workspace must have a committed `package-lock.json`. Without one, your submission
|
|
10
|
+
> cannot be installed at all.
|
|
11
|
+
|
|
12
|
+
## Rule
|
|
13
|
+
|
|
14
|
+
There must be a `package-lock.json` at the **workspace root**.
|
|
15
|
+
|
|
16
|
+
This is checked once per run, not once per template. A workspace has one `package.json` and
|
|
17
|
+
one lockfile, however many templates and sections live inside it — so this violation is
|
|
18
|
+
reported once and fixed once.
|
|
19
|
+
|
|
20
|
+
A `yarn.lock` or a `pnpm-lock.yaml` does not satisfy it. The install is `npm ci`, and
|
|
21
|
+
`npm ci` reads `package-lock.json`.
|
|
22
|
+
|
|
23
|
+
## Reason
|
|
24
|
+
|
|
25
|
+
The platform installs your submission with **`npm ci`** — the reproducible install, which
|
|
26
|
+
takes the tree the lockfile pins and does not resolve anything itself. It requires a
|
|
27
|
+
lockfile, and refuses to run without one.
|
|
28
|
+
|
|
29
|
+
That is the whole point of the requirement: the tree that is built, bundled, audited, and
|
|
30
|
+
published is byte-for-byte the tree you tested against. No lockfile, no reproducible tree,
|
|
31
|
+
no build — and "works on my machine" would otherwise be the only thing standing behind a
|
|
32
|
+
page a customer is paying for.
|
|
33
|
+
|
|
34
|
+
## Fix
|
|
35
|
+
|
|
36
|
+
Run `npm install` and **commit the lockfile**. If it is being ignored, stop ignoring it —
|
|
37
|
+
that is the single most common cause of this violation, and the ignore rule is almost always
|
|
38
|
+
inherited from a template `.gitignore` that was written for an application, not a package.
|
|
39
|
+
|
|
40
|
+
### Before
|
|
41
|
+
|
|
42
|
+
```gitignore
|
|
43
|
+
# .gitignore
|
|
44
|
+
node_modules/
|
|
45
|
+
dist/
|
|
46
|
+
package-lock.json ← the lockfile never gets committed
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`npm ci` on the checked-out submission: `The 'npm ci' command can only install with an
|
|
50
|
+
existing package-lock.json`.
|
|
51
|
+
|
|
52
|
+
### After
|
|
53
|
+
|
|
54
|
+
```gitignore
|
|
55
|
+
# .gitignore
|
|
56
|
+
node_modules/
|
|
57
|
+
dist/
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
npm install # regenerates package-lock.json from package.json
|
|
62
|
+
git add package-lock.json
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## See also
|
|
66
|
+
|
|
67
|
+
- [`lockfile-stale`](lockfile-stale.md) — the lockfile exists but no longer reproduces the
|
|
68
|
+
tree `package.json` asks for.
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
---
|
|
2
|
+
purpose: The template-kit/lockfile-stale authoring rule — what it enforces, why, and how to fix it.
|
|
3
|
+
status: living
|
|
4
|
+
related: [INDEX.md, lockfile-missing.md]
|
|
5
|
+
updated: 2026-07-14
|
|
6
|
+
---
|
|
7
|
+
# template-kit/lockfile-stale
|
|
8
|
+
|
|
9
|
+
> The lockfile must satisfy `package.json`. If `npm ci` would fail, this fails first — on
|
|
10
|
+
> your machine, where you can still do something about it.
|
|
11
|
+
|
|
12
|
+
## Rule
|
|
13
|
+
|
|
14
|
+
`package-lock.json` must be consistent with `package.json`: every dependency the manifest
|
|
15
|
+
asks for is present in the lockfile, at a version its range accepts.
|
|
16
|
+
|
|
17
|
+
Like [`lockfile-missing`](lockfile-missing.md), this is a property of the **workspace**, not
|
|
18
|
+
of any one template, and is reported once.
|
|
19
|
+
|
|
20
|
+
## Reason
|
|
21
|
+
|
|
22
|
+
Your submission is installed with **`npm ci`**, which does not resolve versions — it
|
|
23
|
+
installs exactly what the lockfile pins, and **aborts** if the lockfile and the manifest
|
|
24
|
+
disagree. A lockfile that cannot reproduce the tree is not a slightly-out-of-date
|
|
25
|
+
convenience file; it is an install that does not happen.
|
|
26
|
+
|
|
27
|
+
Checking it here means you see it now. Otherwise the first thing to notice is the install
|
|
28
|
+
on our side, and the first thing you hear is that your submission was rejected.
|
|
29
|
+
|
|
30
|
+
There are two ways authors get here, and both are ordinary:
|
|
31
|
+
|
|
32
|
+
- **Hand-editing a version in `package.json`** — bumping `^11.0.0` to `^12.0.0` in the
|
|
33
|
+
editor and never running an install.
|
|
34
|
+
- **Merging a branch** and keeping only one side of the lockfile — resolving the conflict by
|
|
35
|
+
taking `package.json` from one branch and `package-lock.json` from the other. The two now
|
|
36
|
+
describe different trees.
|
|
37
|
+
|
|
38
|
+
## Fix
|
|
39
|
+
|
|
40
|
+
Run `npm install` — it resyncs the lockfile to the manifest — and commit the result. Do not
|
|
41
|
+
hand-edit `package-lock.json`; regenerate it. If you got here from a merge conflict, take
|
|
42
|
+
`package.json` first, then let `npm install` rebuild the lockfile from it.
|
|
43
|
+
|
|
44
|
+
### Before
|
|
45
|
+
|
|
46
|
+
`package.json` asks for Swiper 12; the lockfile still pins 11.
|
|
47
|
+
|
|
48
|
+
```jsonc
|
|
49
|
+
// package.json — hand-edited
|
|
50
|
+
{
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"swiper": "^12.0.0"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
```jsonc
|
|
58
|
+
// package-lock.json — never regenerated
|
|
59
|
+
{
|
|
60
|
+
"packages": {
|
|
61
|
+
"node_modules/swiper": { "version": "11.2.10" }
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`npm ci`: `Invalid: lock file's swiper@11.2.10 does not satisfy swiper@^12.0.0`.
|
|
67
|
+
|
|
68
|
+
### After
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
npm install # resolves ^12.0.0 and rewrites package-lock.json
|
|
72
|
+
git add package.json package-lock.json
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
```jsonc
|
|
76
|
+
// package-lock.json — back in sync
|
|
77
|
+
{
|
|
78
|
+
"packages": {
|
|
79
|
+
"node_modules/swiper": { "version": "12.0.4" }
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## See also
|
|
85
|
+
|
|
86
|
+
- [`lockfile-missing`](lockfile-missing.md) — there is no lockfile at all.
|
|
87
|
+
- [`single-react`](single-react.md) — the other property of the installed tree the platform
|
|
88
|
+
checks before it builds your page.
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
---
|
|
2
|
+
purpose: The template-kit/manifest-invalid authoring rule — what it enforces, why, and how to fix it.
|
|
3
|
+
status: living
|
|
4
|
+
related: [INDEX.md, ../schema-system.md, schema-invalid.md, no-templates.md]
|
|
5
|
+
updated: 2026-07-14
|
|
6
|
+
---
|
|
7
|
+
# template-kit/manifest-invalid
|
|
8
|
+
|
|
9
|
+
> `manifest.json` is the one file that says which sections make a page, and in what
|
|
10
|
+
> order. Every check on it lands under this id.
|
|
11
|
+
|
|
12
|
+
## Rule
|
|
13
|
+
|
|
14
|
+
A template folder (`templates/<key>/`) must have a `manifest.json` at its root, and that
|
|
15
|
+
file must:
|
|
16
|
+
|
|
17
|
+
- **exist** — a template folder with none at all reports this once, naming the missing
|
|
18
|
+
file;
|
|
19
|
+
- **parse as JSON, and satisfy the manifest shape** — `key`, `name`, `deliverable_type`,
|
|
20
|
+
and an ordered `sections` array of section instances (`section`, `instance_id`,
|
|
21
|
+
`options`, `locked`, `required`, `reorderable`);
|
|
22
|
+
- **agree with its own folder** — the manifest's `key` must match the template folder
|
|
23
|
+
name it lives in;
|
|
24
|
+
- **reference real sections** — every `sections[].section` must resolve to a section this
|
|
25
|
+
workspace actually has;
|
|
26
|
+
- **override only what's real and overridable** — an instance's `options` may only name an
|
|
27
|
+
option the target section declares, only when that option is not
|
|
28
|
+
`template_overridable: false`, and only with a value that actually differs from the
|
|
29
|
+
section's own default;
|
|
30
|
+
- **satisfy the whole-composition nav contract** — the cross-section rules that need the
|
|
31
|
+
full, resolved section set rather than any one section in isolation.
|
|
32
|
+
|
|
33
|
+
Every violation collects into the same run; a manifest with several wrong references
|
|
34
|
+
reports all of them together.
|
|
35
|
+
|
|
36
|
+
## Reason
|
|
37
|
+
|
|
38
|
+
A section's own four files describe that section in isolation — they say nothing about
|
|
39
|
+
which page it appears on, in what order, or which of its options a specific template has
|
|
40
|
+
chosen to lock in. `manifest.json` is where that composition is declared, and it is the
|
|
41
|
+
only place a downstream consumer (the publisher, the fill pipeline, the editor) can go to
|
|
42
|
+
learn a template's actual shape. A manifest that references a section that doesn't exist,
|
|
43
|
+
or restates a default as if it were an override, is a template whose real composition
|
|
44
|
+
nobody can trust the manifest to describe.
|
|
45
|
+
|
|
46
|
+
## Fix
|
|
47
|
+
|
|
48
|
+
Read the message: it names the template, the section instance, and what's wrong.
|
|
49
|
+
Missing entirely, add one. Otherwise, fix the field the message points at.
|
|
50
|
+
|
|
51
|
+
### Before
|
|
52
|
+
|
|
53
|
+
```text
|
|
54
|
+
templates/acme-modern/
|
|
55
|
+
manifest.json
|
|
56
|
+
sections/
|
|
57
|
+
hero/
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
```json
|
|
61
|
+
// templates/acme-modern/manifest.json
|
|
62
|
+
{
|
|
63
|
+
"key": "acme-classic",
|
|
64
|
+
"name": "Acme Classic",
|
|
65
|
+
"deliverable_type": "single_property_website",
|
|
66
|
+
"sections": [
|
|
67
|
+
{ "section": "hero", "instance_id": "hero-1", "options": {}, "locked": [], "required": true, "reorderable": false }
|
|
68
|
+
],
|
|
69
|
+
"reconcile": []
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
```text
|
|
74
|
+
Template "acme-modern": manifest key "acme-classic" does not match the template folder name "acme-modern".
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### After
|
|
78
|
+
|
|
79
|
+
```json
|
|
80
|
+
// templates/acme-modern/manifest.json
|
|
81
|
+
{
|
|
82
|
+
"key": "acme-modern",
|
|
83
|
+
"name": "Acme Modern",
|
|
84
|
+
"deliverable_type": "single_property_website",
|
|
85
|
+
"sections": [
|
|
86
|
+
{ "section": "hero", "instance_id": "hero-1", "options": {}, "locked": [], "required": true, "reorderable": false }
|
|
87
|
+
],
|
|
88
|
+
"reconcile": []
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## See also
|
|
93
|
+
|
|
94
|
+
- [The schema system](../schema-system.md#manifestjson--composing-sections-into-a-template) —
|
|
95
|
+
what a manifest declares and the type it's validated against.
|
|
96
|
+
- [`schema-invalid`](schema-invalid.md) — the same kind of cross-reference check, one
|
|
97
|
+
level down, on a single section's own contract files.
|
|
98
|
+
- [`no-templates`](no-templates.md) — reported instead when the workspace has no
|
|
99
|
+
`templates/<key>/` folder to check at all.
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
---
|
|
2
|
+
purpose: The template-kit/missing-slot-marker authoring rule — what it enforces, why, and how to fix it.
|
|
3
|
+
status: living
|
|
4
|
+
related: [INDEX.md, ../primitives.md, ../schema-system.md]
|
|
5
|
+
updated: 2026-07-14
|
|
6
|
+
---
|
|
7
|
+
# template-kit/missing-slot-marker
|
|
8
|
+
|
|
9
|
+
> Every slot declared `editable_by_user: true` must be **marked** in the section's JSX.
|
|
10
|
+
> An unmarked slot still fills and still renders — it just cannot be clicked.
|
|
11
|
+
|
|
12
|
+
## Rule
|
|
13
|
+
|
|
14
|
+
For each slot in `schema.ts` with `editable_by_user: true`, the section's JSX must carry a
|
|
15
|
+
marker whose key is that slot id, written as a **string literal**. Any of these counts:
|
|
16
|
+
|
|
17
|
+
| Marker | Example |
|
|
18
|
+
|---|---|
|
|
19
|
+
| `<Slot id>` | `<Slot id="headline" textLeaf>` |
|
|
20
|
+
| `slotId` on a primitive that emits its own marker | `<Image slotId="hero_image" …>` |
|
|
21
|
+
| a raw `data-slot-id` attribute | `<div data-slot-id="headline">` |
|
|
22
|
+
|
|
23
|
+
The scan covers **every `.tsx` in the section folder**, not only `Renderer.tsx` — a marker
|
|
24
|
+
inside a section-local component satisfies it. Test files are excluded.
|
|
25
|
+
|
|
26
|
+
Carve-outs:
|
|
27
|
+
|
|
28
|
+
- **A slot that is not `editable_by_user` needs no marker.** It cannot be selected in the
|
|
29
|
+
editor, so there is nothing to mark. (`editable_by_user` defaults to `false`.)
|
|
30
|
+
- **A computed key does not satisfy the rule.** `<Slot id={fooKey}>` or an id produced
|
|
31
|
+
inside a `.map()` is not a literal, so the check cannot resolve it — and
|
|
32
|
+
[`slot-marker-literal`](slot-marker-literal.md) reports it separately. A `.map()`-generated
|
|
33
|
+
key therefore fails both at once.
|
|
34
|
+
|
|
35
|
+
The practical corollary of the two above: when you hoist markup into a section-local
|
|
36
|
+
helper, **do not pass the slot key through it as a prop**. `<Field slotKey="headline">`
|
|
37
|
+
wrapping an inner `<Slot id={slotKey}>` erases the literal from the source entirely, and
|
|
38
|
+
the slot reads as unmarked. Keep the literal `<Slot id="…">` where the slot is rendered
|
|
39
|
+
and move only the data layer and leaf presentation out.
|
|
40
|
+
|
|
41
|
+
## Reason
|
|
42
|
+
|
|
43
|
+
The markers are the only thing connecting the pixels a user clicks to the schema key
|
|
44
|
+
behind them. The editor's click resolver walks up from the clicked node looking for a
|
|
45
|
+
`data-slot-id`; a slot that emits none is invisible to it.
|
|
46
|
+
|
|
47
|
+
Nothing throws. The slot is filled by AI, it renders its content on the published page,
|
|
48
|
+
and it looks completely correct — right up until a user clicks it in the editor and
|
|
49
|
+
selects nothing. That is the whole failure: **fillable but not editable**, discovered by a
|
|
50
|
+
customer rather than by you.
|
|
51
|
+
|
|
52
|
+
## Fix
|
|
53
|
+
|
|
54
|
+
Wrap the slot's outer DOM in `<Slot id="…">` — add `textLeaf` for a single-leaf text slot
|
|
55
|
+
so the editor can write to it on each keystroke. An image slot needs no wrapper: give
|
|
56
|
+
`<Image>` its `slotId` and it emits the marker itself.
|
|
57
|
+
|
|
58
|
+
A raw `data-slot-id` attribute is not a lesser fallback — for the marker itself, it's
|
|
59
|
+
identical to what `<Slot>` renders: `<Slot as="p" id="…">` emits exactly `<p
|
|
60
|
+
data-slot-id="…">`, nothing more. The one thing `<Slot textLeaf>` adds is
|
|
61
|
+
`data-slot-text-leaf` on the child, wired automatically, so the editor can patch text on
|
|
62
|
+
every keystroke instead of waiting for the next server render. So default to `<Slot>` for
|
|
63
|
+
the convenience — it's one prop instead of hand-wiring a second attribute — but writing
|
|
64
|
+
`data-slot-id` directly on markup you're already hand-assembling is a legitimate, idiomatic
|
|
65
|
+
fix, not a technicality; reach for it freely when you don't need `textLeaf`.
|
|
66
|
+
|
|
67
|
+
If the slot genuinely is not meant to be edited, say so in the schema rather than leaving
|
|
68
|
+
the check unsatisfied: `editable_by_user: false`.
|
|
69
|
+
|
|
70
|
+
### Before
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
// sections/hero/schema.ts
|
|
74
|
+
import type { SectionProps, SectionSchemaShape } from "@homepages/template-kit";
|
|
75
|
+
|
|
76
|
+
export const schema = {
|
|
77
|
+
meta: { displayName: "Hero" },
|
|
78
|
+
slots: {
|
|
79
|
+
headline: { type: "text", size: "medium", required: true, editable_by_user: true },
|
|
80
|
+
eyebrow: { type: "text", size: "short", required: false, editable_by_user: true },
|
|
81
|
+
},
|
|
82
|
+
} as const satisfies SectionSchemaShape;
|
|
83
|
+
|
|
84
|
+
export type Props = SectionProps<typeof schema>;
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
```tsx
|
|
88
|
+
// sections/hero/Renderer.tsx — template-kit/missing-slot-marker: eyebrow
|
|
89
|
+
import { Section, Slot } from "@homepages/template-kit";
|
|
90
|
+
|
|
91
|
+
import type { Props } from "./schema.js";
|
|
92
|
+
|
|
93
|
+
export function Renderer({ slots }: Props) {
|
|
94
|
+
return (
|
|
95
|
+
<Section>
|
|
96
|
+
<p className="text-ink-mute">{slots.eyebrow}</p>
|
|
97
|
+
<Slot id="headline" as="h1" textLeaf>
|
|
98
|
+
<span>{slots.headline}</span>
|
|
99
|
+
</Slot>
|
|
100
|
+
</Section>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### After
|
|
106
|
+
|
|
107
|
+
```tsx
|
|
108
|
+
// sections/hero/Renderer.tsx
|
|
109
|
+
import { Section, Slot } from "@homepages/template-kit";
|
|
110
|
+
|
|
111
|
+
import type { Props } from "./schema.js";
|
|
112
|
+
|
|
113
|
+
export function Renderer({ slots }: Props) {
|
|
114
|
+
return (
|
|
115
|
+
<Section>
|
|
116
|
+
<Slot id="eyebrow" textLeaf>
|
|
117
|
+
<p className="text-ink-mute">{slots.eyebrow}</p>
|
|
118
|
+
</Slot>
|
|
119
|
+
<Slot id="headline" as="h1" textLeaf>
|
|
120
|
+
<span>{slots.headline}</span>
|
|
121
|
+
</Slot>
|
|
122
|
+
</Section>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
If `eyebrow` was never meant to be editable, the other legitimate fix is in the schema —
|
|
128
|
+
and then no marker is needed:
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
eyebrow: { type: "text", size: "short", required: false, editable_by_user: false },
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## See also
|
|
135
|
+
|
|
136
|
+
- [Contract primitives](../primitives.md#the-marker-contract) — every marker attribute and
|
|
137
|
+
which primitive emits it.
|
|
138
|
+
- [`slot-marker-literal`](slot-marker-literal.md) — why the key must be a literal for this
|
|
139
|
+
check to see it at all.
|
|
140
|
+
- [`slot-group-marker`](slot-group-marker.md) — the same idea for a group of slots.
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
---
|
|
2
|
+
purpose: The template-kit/no-bare-css-import authoring rule — what it enforces, why, and how to fix it.
|
|
3
|
+
status: living
|
|
4
|
+
related: [INDEX.md, ../theme-and-css.md]
|
|
5
|
+
updated: 2026-07-14
|
|
6
|
+
---
|
|
7
|
+
# template-kit/no-bare-css-import
|
|
8
|
+
|
|
9
|
+
> No `@import` in hand-written CSS. **Not even a package's** — the browser drops it
|
|
10
|
+
> silently, and the styles simply never ship.
|
|
11
|
+
|
|
12
|
+
## Rule
|
|
13
|
+
|
|
14
|
+
A hand-written stylesheet may not contain a top-level `@import`. That means the
|
|
15
|
+
stylesheets the CSS build collects out of your section and template folders — a section's
|
|
16
|
+
`styles.css`, any other `.css` in a section folder, and a template's own hand-written
|
|
17
|
+
stylesheet.
|
|
18
|
+
|
|
19
|
+
**There is no carve-out for a package specifier.** `@import "swiper/css";` is exactly as
|
|
20
|
+
illegal here as `@import "./tokens.css";`. Both are reported, at the 1-based line of the
|
|
21
|
+
first offender.
|
|
22
|
+
|
|
23
|
+
The scan is comment-aware, so an `@import` written *about* rather than *as* code is prose
|
|
24
|
+
and passes:
|
|
25
|
+
|
|
26
|
+
```css
|
|
27
|
+
/* css-reason: … */
|
|
28
|
+
/* Never @import here — see template-kit/no-bare-css-import. */ ← fine, it's a comment
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**Your Tailwind entry is not one of these files.** The file that carries
|
|
32
|
+
`@import "tailwindcss";` and `@import "@homepages/template-kit/styles.css";` is a Tailwind
|
|
33
|
+
entry: it is compiled by Tailwind, its `@import`s are resolved at build time, and it is
|
|
34
|
+
never wrapped in a cascade layer. It is not hand-written section or template CSS, and this
|
|
35
|
+
rule does not reach it.
|
|
36
|
+
|
|
37
|
+
## Reason
|
|
38
|
+
|
|
39
|
+
Every hand-CSS file the build collects is **concatenated into a cascade layer**:
|
|
40
|
+
|
|
41
|
+
```css
|
|
42
|
+
@layer sections {
|
|
43
|
+
/* your styles.css, pasted in here */
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
That is how a section's CSS lands in the layer order (`theme, base, components, sections,
|
|
48
|
+
template, utilities`) instead of fighting Tailwind's utilities for specificity.
|
|
49
|
+
|
|
50
|
+
**`@import` is invalid inside a `@layer` block.** And CSS's error handling for an invalid
|
|
51
|
+
at-rule is not an error — it is a shrug. The browser drops the statement and parses on.
|
|
52
|
+
Nothing throws. Nothing warns. Your build succeeds, your preview looks *plausible* because
|
|
53
|
+
the package's own styling is missing in a way you have learned to read as "unstyled yet",
|
|
54
|
+
and the published page ships without the rules you imported. This is the quietest failure
|
|
55
|
+
in the whole kit: the only signal is that the styles are not there.
|
|
56
|
+
|
|
57
|
+
## Fix
|
|
58
|
+
|
|
59
|
+
Delete the `@import`.
|
|
60
|
+
|
|
61
|
+
**A package's stylesheet is reached from the code that needs it**, not from CSS. Write
|
|
62
|
+
`import "swiper/css";` in the component, island, or enhancer that renders the thing — the
|
|
63
|
+
CSS build bundles your `Renderer.tsx` and every marked enhancer, transitively, precisely to
|
|
64
|
+
harvest those imports, and it compiles what they pull in into **this section's cascade
|
|
65
|
+
layer**. Package CSS goes in first and your own hand-CSS second, so at equal specificity
|
|
66
|
+
your rules win.
|
|
67
|
+
|
|
68
|
+
Two things authors get wrong immediately after:
|
|
69
|
+
|
|
70
|
+
- **Bare specifiers only.** A CSS import that resolves outside `node_modules` is an error,
|
|
71
|
+
not a fallback. Your own stylesheets are discovered by filename — importing one from code
|
|
72
|
+
would ship it twice. See
|
|
73
|
+
[`no-css-import-from-render-path`](no-css-import-from-render-path.md).
|
|
74
|
+
- **Importing is not installing.** `import "swiper/css"` does not add `swiper` to your
|
|
75
|
+
`package.json`. Add the dependency, or the install that builds your submission has nothing
|
|
76
|
+
to resolve.
|
|
77
|
+
|
|
78
|
+
### Before
|
|
79
|
+
|
|
80
|
+
```css
|
|
81
|
+
/* sections/gallery/styles.css */
|
|
82
|
+
/* css-reason: styles the slide DOM the Swiper enhancer builds at runtime. */
|
|
83
|
+
@import "swiper/css"; /* template-kit/no-bare-css-import — silently dropped */
|
|
84
|
+
|
|
85
|
+
.tr-gallery .swiper-pagination-bullet-active {
|
|
86
|
+
background: var(--tr-color-primary);
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
The pagination override ships. Swiper's own layout CSS does not — and no error says so.
|
|
91
|
+
|
|
92
|
+
### After
|
|
93
|
+
|
|
94
|
+
The `@import` is gone from the stylesheet:
|
|
95
|
+
|
|
96
|
+
```css
|
|
97
|
+
/* sections/gallery/styles.css */
|
|
98
|
+
/* css-reason: overrides the Swiper pagination DOM the enhancer builds at runtime —
|
|
99
|
+
those nodes carry the library's own classes, which Tailwind's scanner never sees. */
|
|
100
|
+
.tr-gallery .swiper-pagination-bullet-active {
|
|
101
|
+
background: var(--tr-color-primary);
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
…and the package's CSS is imported from the module that needs it, which is what routes it
|
|
106
|
+
into this section's layer:
|
|
107
|
+
|
|
108
|
+
```tsx
|
|
109
|
+
// sections/gallery/Carousel.tsx
|
|
110
|
+
"use client";
|
|
111
|
+
|
|
112
|
+
import Swiper from "swiper";
|
|
113
|
+
import "swiper/css"; // ← the sanctioned channel: the CSS build harvests this import
|
|
114
|
+
|
|
115
|
+
export default function Carousel({ slides }: { slides: { id: string; url: string }[] }) {
|
|
116
|
+
// …
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## See also
|
|
121
|
+
|
|
122
|
+
- [`no-css-import-from-render-path`](no-css-import-from-render-path.md) — the other half of
|
|
123
|
+
this mental model. Hand-written CSS: no `@import`s at all, package or otherwise. Render
|
|
124
|
+
path: no *local* CSS imports, but a package's bare specifier is exactly right. An author
|
|
125
|
+
who reads only one of the two pages will be confused by the other.
|
|
126
|
+
- [`css-reason`](css-reason.md) — the comment every hand-written stylesheet opens with.
|
|
127
|
+
- [Theme and CSS](../theme-and-css.md) — the cascade-layer order, and the one file that
|
|
128
|
+
legitimately does `@import "tailwindcss"`.
|