@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.
Files changed (111) hide show
  1. package/CHANGELOG.md +220 -0
  2. package/README.md +70 -16
  3. package/dist/base.css +36 -48
  4. package/dist/cli/check/config.js +37 -0
  5. package/dist/cli/check/css.js +117 -0
  6. package/dist/cli/check/diagnostics.js +32 -0
  7. package/dist/cli/check/index.js +87 -0
  8. package/dist/cli/check/loader.js +142 -0
  9. package/dist/cli/check/paths.js +15 -0
  10. package/dist/cli/check/relativize.js +18 -0
  11. package/dist/cli/check/render-invariants.js +24 -0
  12. package/dist/cli/check/resolve-tool.js +38 -0
  13. package/dist/cli/check/stages/deps.js +337 -0
  14. package/dist/cli/check/stages/lint.js +46 -0
  15. package/dist/cli/check/stages/render.js +101 -0
  16. package/dist/cli/check/stages/size.js +158 -0
  17. package/dist/cli/check/stages/tree.js +207 -0
  18. package/dist/cli/check/stages/typecheck.js +46 -0
  19. package/dist/cli/check/stages/validate/catalog-exhaustiveness.js +18 -0
  20. package/dist/cli/check/stages/validate/facts.js +18 -0
  21. package/dist/cli/check/stages/validate/formatter-contract.js +14 -0
  22. package/dist/cli/check/stages/validate/group-contract.js +21 -0
  23. package/dist/cli/check/stages/validate/index.js +66 -0
  24. package/dist/cli/check/stages/validate/list-scalar-contract.js +22 -0
  25. package/dist/cli/check/stages/validate/nav-contract.js +93 -0
  26. package/dist/cli/check/stages/validate/nested-slot-contract.js +12 -0
  27. package/dist/cli/check/stages/validate/object-list-contract.js +31 -0
  28. package/dist/cli/check/stages/validate/orchestrator.js +241 -0
  29. package/dist/cli/check/stages/validate/produced-by-contract.js +18 -0
  30. package/dist/cli/check/stages/validate/row-contract.js +31 -0
  31. package/dist/cli/check/stages/validate/select-contract.js +13 -0
  32. package/dist/cli/check/stages/validate/source-contract.js +21 -0
  33. package/dist/cli/check/stages/validate/variant-contract.js +15 -0
  34. package/dist/cli/check/workspace.js +86 -0
  35. package/dist/cli/theme/generate.js +43 -0
  36. package/dist/cli/theme/index.js +33 -0
  37. package/dist/cli/theme/load-theme.js +69 -0
  38. package/dist/cli.js +22 -4
  39. package/dist/contracts/fill-treatments.js +47 -0
  40. package/dist/design-system/theme.d.ts +30 -299
  41. package/dist/design-system/theme.js +112 -83
  42. package/dist/eslint/rules/image-bare-needs-reason.js +6 -1
  43. package/dist/eslint/rules/no-client-directive-in-contract.js +6 -1
  44. package/dist/eslint/rules/no-client-runtime-in-server.js +5 -1
  45. package/dist/eslint/rules/no-css-import-from-render-path.js +8 -2
  46. package/dist/eslint/rules/no-hex.js +84 -7
  47. package/dist/eslint/rules/no-inline-style.js +5 -1
  48. package/dist/eslint/rules/no-nondeterminism.js +5 -1
  49. package/dist/eslint/rules/no-raw-element.js +6 -1
  50. package/dist/eslint/rules/props-from-schema.js +6 -1
  51. package/dist/eslint/rules/serializable-island-props.js +5 -1
  52. package/dist/eslint/rules/slot-marker-literal.js +6 -1
  53. package/dist/eslint.js +1 -1
  54. package/dist/index.d.ts +2 -2
  55. package/dist/index.js +2 -2
  56. package/dist/package.js +1 -1
  57. package/dist/primitives/Image.js +1 -1
  58. package/dist/rules/registry.js +16 -0
  59. package/dist/schema/fill-spec.d.ts +80 -590
  60. package/dist/schema/fixture-schema.d.ts +5 -81
  61. package/dist/schema/manifest.d.ts +39 -475
  62. package/dist/schema/resolve-section-ref.js +10 -0
  63. package/dist/schema/rows.js +10 -0
  64. package/dist/schema/section-nav.js +9 -0
  65. package/dist/schema/section-schema.d.ts +51 -437
  66. package/dist/schema/slot-types.d.ts +12 -16
  67. package/dist/styles.css +31 -88
  68. package/docs/INDEX.md +5 -3
  69. package/docs/check.md +121 -0
  70. package/docs/eslint.md +25 -50
  71. package/docs/llms.txt +26 -11
  72. package/docs/primitives.md +5 -0
  73. package/docs/rules/INDEX.md +65 -0
  74. package/docs/rules/audit-severity.md +91 -0
  75. package/docs/rules/bundle-binary-asset.md +102 -0
  76. package/docs/rules/bundle-incomplete.md +117 -0
  77. package/docs/rules/css-reason.md +94 -0
  78. package/docs/rules/determinism-drift.md +112 -0
  79. package/docs/rules/fixtures-invalid.md +96 -0
  80. package/docs/rules/image-bare-needs-reason.md +117 -0
  81. package/docs/rules/license-denied.md +114 -0
  82. package/docs/rules/lockfile-missing.md +68 -0
  83. package/docs/rules/lockfile-stale.md +88 -0
  84. package/docs/rules/manifest-invalid.md +99 -0
  85. package/docs/rules/missing-slot-marker.md +140 -0
  86. package/docs/rules/no-bare-css-import.md +128 -0
  87. package/docs/rules/no-client-directive-in-contract.md +111 -0
  88. package/docs/rules/no-client-runtime-in-server.md +143 -0
  89. package/docs/rules/no-css-import-from-render-path.md +118 -0
  90. package/docs/rules/no-hex.md +163 -0
  91. package/docs/rules/no-inline-style.md +121 -0
  92. package/docs/rules/no-nondeterminism.md +100 -0
  93. package/docs/rules/no-raw-element.md +100 -0
  94. package/docs/rules/no-templates.md +87 -0
  95. package/docs/rules/parse-error.md +76 -0
  96. package/docs/rules/props-from-schema.md +119 -0
  97. package/docs/rules/render-invariant.md +165 -0
  98. package/docs/rules/schema-invalid.md +96 -0
  99. package/docs/rules/serializable-island-props.md +146 -0
  100. package/docs/rules/server-vs-client.md +105 -0
  101. package/docs/rules/sidebar-order.md +109 -0
  102. package/docs/rules/single-react.md +97 -0
  103. package/docs/rules/size-renderer-bundle.md +109 -0
  104. package/docs/rules/size-section-css.md +95 -0
  105. package/docs/rules/slot-group-marker.md +127 -0
  106. package/docs/rules/slot-marker-literal.md +148 -0
  107. package/docs/rules/typecheck.md +103 -0
  108. package/docs/rules/unlayered-fence.md +113 -0
  109. package/docs/theme-and-css.md +193 -82
  110. package/package.json +4 -9
  111. 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"`.