@homepages/template-kit 0.0.1 → 0.2.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 (121) hide show
  1. package/CHANGELOG.md +251 -0
  2. package/README.md +77 -11
  3. package/dist/base.css +64 -0
  4. package/dist/browser.d.ts +26 -0
  5. package/dist/browser.js +55 -0
  6. package/dist/cli.js +1 -1
  7. package/dist/contracts/image-descriptor.d.ts +17 -0
  8. package/dist/contracts/markers.d.ts +20 -0
  9. package/dist/contracts/markers.js +21 -0
  10. package/dist/contracts/slot-catalog.d.ts +237 -0
  11. package/dist/contracts/slot-catalog.js +247 -0
  12. package/dist/contracts/transforms.d.ts +108 -0
  13. package/dist/contracts/transforms.js +117 -0
  14. package/dist/design-system/breakpoints.d.ts +5 -0
  15. package/dist/design-system/breakpoints.js +14 -0
  16. package/dist/design-system/theme.d.ts +308 -0
  17. package/dist/design-system/theme.js +102 -0
  18. package/dist/eslint/is-client-file.js +79 -0
  19. package/dist/eslint/rules/image-bare-needs-reason.js +43 -0
  20. package/dist/eslint/rules/no-client-directive-in-contract.js +31 -0
  21. package/dist/eslint/rules/no-client-runtime-in-server.js +103 -0
  22. package/dist/eslint/rules/no-css-import-from-render-path.js +29 -0
  23. package/dist/eslint/rules/no-hex.js +116 -0
  24. package/dist/eslint/rules/no-inline-style.js +27 -0
  25. package/dist/eslint/rules/no-nondeterminism.js +48 -0
  26. package/dist/eslint/rules/no-raw-element.js +37 -0
  27. package/dist/eslint/rules/props-from-schema.js +37 -0
  28. package/dist/eslint/rules/serializable-island-props.js +112 -0
  29. package/dist/eslint/rules/slot-marker-literal.js +38 -0
  30. package/dist/eslint.d.ts +4 -8
  31. package/dist/eslint.js +75 -11
  32. package/dist/fixtures/sample-property.d.ts +67 -0
  33. package/dist/fixtures/sample-property.js +707 -0
  34. package/dist/fixtures.d.ts +2 -0
  35. package/dist/fixtures.js +3 -0
  36. package/dist/index.d.ts +28 -12
  37. package/dist/index.js +34 -2
  38. package/dist/island-runtime.d.ts +30 -0
  39. package/dist/island-runtime.js +73 -0
  40. package/dist/islands/contract.d.ts +56 -0
  41. package/dist/islands/contract.js +71 -0
  42. package/dist/islands/marker.d.ts +16 -0
  43. package/dist/islands/marker.js +18 -0
  44. package/dist/package.js +5 -0
  45. package/dist/primitives/Image.d.ts +49 -0
  46. package/dist/primitives/Image.js +95 -0
  47. package/dist/primitives/Section.d.ts +22 -0
  48. package/dist/primitives/Section.js +26 -0
  49. package/dist/primitives/Slot.d.ts +50 -0
  50. package/dist/primitives/Slot.js +39 -0
  51. package/dist/primitives/SlotGroup.d.ts +38 -0
  52. package/dist/primitives/SlotGroup.js +24 -0
  53. package/dist/primitives/SlotItem.d.ts +43 -0
  54. package/dist/primitives/SlotItem.js +38 -0
  55. package/dist/primitives/index.d.ts +7 -0
  56. package/dist/primitives/picture-sources.d.ts +22 -0
  57. package/dist/primitives/picture-sources.js +52 -0
  58. package/dist/rules/registry.js +16 -0
  59. package/dist/schema/fill-spec.d.ts +692 -0
  60. package/dist/schema/fill-spec.js +98 -0
  61. package/dist/schema/fixture-schema.d.ts +143 -0
  62. package/dist/schema/fixture-schema.js +113 -0
  63. package/dist/schema/index.d.ts +16 -0
  64. package/dist/schema/manifest.d.ts +527 -0
  65. package/dist/schema/manifest.js +73 -0
  66. package/dist/schema/property-facts.d.ts +44 -0
  67. package/dist/schema/resolve-variants.d.ts +11 -0
  68. package/dist/schema/resolve-variants.js +15 -0
  69. package/dist/schema/runtime-values.d.ts +45 -0
  70. package/dist/schema/section-nav.d.ts +7 -0
  71. package/dist/schema/section-props.d.ts +69 -0
  72. package/dist/schema/section-schema.d.ts +533 -0
  73. package/dist/schema/section-schema.js +72 -0
  74. package/dist/schema/slot-types.d.ts +117 -0
  75. package/dist/schema/slot-types.js +181 -0
  76. package/dist/schema/source.d.ts +13 -0
  77. package/dist/schema/source.js +12 -0
  78. package/dist/schema/synthesize-nullable.d.ts +18 -0
  79. package/dist/schema/synthesize-nullable.js +47 -0
  80. package/dist/styles.css +131 -9
  81. package/docs/INDEX.md +7 -4
  82. package/docs/eslint.md +55 -0
  83. package/docs/islands.md +150 -0
  84. package/docs/llms.txt +33 -3
  85. package/docs/primitives.md +214 -0
  86. package/docs/rules/INDEX.md +58 -0
  87. package/docs/rules/audit-severity.md +91 -0
  88. package/docs/rules/bundle-incomplete.md +117 -0
  89. package/docs/rules/css-reason.md +94 -0
  90. package/docs/rules/determinism-drift.md +112 -0
  91. package/docs/rules/image-bare-needs-reason.md +117 -0
  92. package/docs/rules/license-denied.md +106 -0
  93. package/docs/rules/lockfile-missing.md +68 -0
  94. package/docs/rules/lockfile-stale.md +88 -0
  95. package/docs/rules/missing-slot-marker.md +140 -0
  96. package/docs/rules/no-bare-css-import.md +128 -0
  97. package/docs/rules/no-client-directive-in-contract.md +111 -0
  98. package/docs/rules/no-client-runtime-in-server.md +143 -0
  99. package/docs/rules/no-css-import-from-render-path.md +118 -0
  100. package/docs/rules/no-hex.md +97 -0
  101. package/docs/rules/no-inline-style.md +121 -0
  102. package/docs/rules/no-nondeterminism.md +100 -0
  103. package/docs/rules/no-raw-element.md +100 -0
  104. package/docs/rules/props-from-schema.md +119 -0
  105. package/docs/rules/render-invariant.md +165 -0
  106. package/docs/rules/serializable-island-props.md +146 -0
  107. package/docs/rules/server-vs-client.md +105 -0
  108. package/docs/rules/sidebar-order.md +109 -0
  109. package/docs/rules/single-react.md +97 -0
  110. package/docs/rules/size-renderer-bundle.md +109 -0
  111. package/docs/rules/size-section-css.md +95 -0
  112. package/docs/rules/slot-group-marker.md +127 -0
  113. package/docs/rules/slot-marker-literal.md +148 -0
  114. package/docs/rules/typecheck.md +103 -0
  115. package/docs/rules/unlayered-fence.md +113 -0
  116. package/docs/schema-system.md +240 -0
  117. package/docs/theme-and-css.md +225 -0
  118. package/package.json +30 -6
  119. package/tsconfig.base.json +16 -0
  120. package/tsconfig.json +2 -13
  121. package/dist/src-dZr5N30y.js +0 -35
package/CHANGELOG.md ADDED
@@ -0,0 +1,251 @@
1
+ # @homepages/template-kit
2
+
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - e64d4d7: `base.css` no longer ships a reset — Tailwind's preflight (from your entry's
8
+ `@import "tailwindcss"`) already is one, and the two were duplicating each other. It
9
+ now carries only the three declarations preflight does not make: font-smoothing,
10
+ `button { cursor: pointer }`, and `svg { max-width: 100% }`.
11
+
12
+ **`base.css` now requires a Tailwind entry on the page.** If a page loads `base.css`
13
+ without an `@import "tailwindcss"` entry present — a standalone preview, an embed, a
14
+ hand-written CSS page — it renders fully unreset (UA body margin, bulleted lists,
15
+ underlined links, unconstrained inline images), and nothing errors to tell you.
16
+ Confirm every page that loads `base.css` also loads a Tailwind entry.
17
+
18
+ `base.css` also no longer references any theme token. The `body` document defaults
19
+ (font, color, background, size) are now emitted by `compileThemeToCss`, alongside the
20
+ `:root` block that defines those tokens. **Regenerate and republish your compiled theme
21
+ CSS.** If you upgrade `base.css` but keep serving a previously-compiled theme
22
+ stylesheet, that stylesheet still only has the old `:root` block — `body` silently
23
+ loses `font-family`, `color`, `background`, and `font-size` page-wide, because
24
+ `base.css` no longer carries them as a fallback. The compiled output's content hash
25
+ changes.
26
+
27
+ The `--tr-space-*` scale is removed — Tailwind's own spacing scale (`p-4`, `gap-6`)
28
+ was never aliased to it. Any of your CSS still referencing `var(--tr-space-*)` (e.g.
29
+ `padding: var(--tr-space-4)`) now points at an undefined custom property, which is
30
+ invalid at computed-value time, so the declaration drops. The `scroll-behavior` /
31
+ `scroll-padding-top` policy is removed; set it in your own template stylesheet if you
32
+ have a sticky header.
33
+
34
+ - a24174f: Every rule-id now resolves to a page. The lint preset and `template-kit check` share one
35
+ id namespace (`template-kit/<id>`), and each of those ids — all 28 — now has a page at
36
+ `docs/rules/<id>.md` giving the rule, the reason it exists, and a before/after fix you can
37
+ copy.
38
+
39
+ The docs ship **inside the tarball**, so the loop closes with no network: hit
40
+ `template-kit/no-hex`, read `node_modules/@homepages/template-kit/docs/rules/no-hex.md`,
41
+ fix it. That matters most for the coding agents authoring against this kit — an agent that
42
+ can resolve a failure to its own reference page fixes the violation instead of guessing at
43
+ it.
44
+
45
+ Each ESLint rule also carries `meta.docs.url` pointing at its page, so an editor's "show
46
+ documentation for this rule" lands on the same file.
47
+
48
+ Start at `docs/rules/INDEX.md`. `docs/rules/server-vs-client.md` explains the one concept
49
+ several rules assume — that server-vs-client is a **per-file** fact, decided by the
50
+ `"use client"` directive prologue, not by an import graph.
51
+
52
+ ### Patch Changes
53
+
54
+ - be1507d: Fixed `no-css-import-from-render-path`, which banned the one legal way to get a
55
+ third-party stylesheet into a section.
56
+
57
+ A **bare** `.css` specifier in the render path — `import "swiper/css"` in `Renderer.tsx`,
58
+ in a marked enhancer, or in any module either one imports — is how a package's CSS reaches
59
+ the section's cascade layer: the build bundles that import graph precisely to harvest it.
60
+ The rule flagged it anyway, and its `Fix:` hint sent you to `@import` the stylesheet from
61
+ `styles.css`, which is never legal there (hand-written CSS is wrapped in `@layer`, where a
62
+ browser drops an `@import` silently).
63
+
64
+ The rule now reports only a **local** stylesheet import — a relative or absolute specifier.
65
+ That is the real defect: the build already collects every `.css` file in the section folder
66
+ by filename, so importing one from code ships its rules twice and can corrupt the cascade.
67
+
68
+ If you worked around the old rule by moving a package's CSS into a stylesheet `@import`,
69
+ move it back into the module that needs it — see `docs/rules/no-css-import-from-render-path.md`
70
+ and `docs/rules/no-bare-css-import.md`, which are the two halves of the same mental model.
71
+
72
+ ## 0.1.0
73
+
74
+ ### Minor Changes
75
+
76
+ - 46e13a0: `base.css` no longer applies `content-visibility: auto` to off-screen sections on published
77
+ pages, and no longer defines the `--tr-control-*` tokens.
78
+
79
+ `content-visibility` is now opt-in: it carries a `contain: paint` cost that clips in-section
80
+ overlays, and only a template author knows whether their page needs it. If your page is long
81
+ and image-heavy, see "Recipe: `content-visibility` on long, image-heavy pages" in
82
+ `docs/theme-and-css.md` for the rule to add to your own template stylesheet.
83
+
84
+ The `--tr-control-*` tokens were unreferenced and are removed.
85
+
86
+ - b08117b: `ImageValue` now models the shape a renderer actually receives, instead of a
87
+ CSS-framing model the runtime never implemented.
88
+
89
+ **Removed** `crop` (`{ zoom, ox, oy }`) and `focal`. Their doc comments claimed the
90
+ `<Image>` primitive applied them as a CSS transform / `object-position` — it never
91
+ did, and no renderer or primitive has ever read either field. Framing is baked into a
92
+ **pre-cropped derivative asset**: when an image has been cropped, the platform serves
93
+ that derivative as the slot's `url`, and `<Image>` is a centered `object-cover` over
94
+ whatever `url` points at. A renderer that read `crop` or `focal` was reading fields
95
+ that were always absent from the resolved value.
96
+
97
+ **Added** `crop_key` — the content-addressed key of the baked crop derivative. It was
98
+ already on the wire but undeclared here; it is the _provenance_ of `url`, not a second
99
+ thing to render.
100
+
101
+ `rect` and `ar` remain, now labelled **carried, not rendered**: they are the editor's
102
+ re-edit state (so the cropper can reopen at the last crop), not framing inputs.
103
+
104
+ Renderers read only `url`, `alt`, `mobile`, and `responsive`. This matches
105
+ `ResolvedImage`, the shape the platform emits.
106
+
107
+ - c003008: Added the **schema system** — the section authoring contract, and the first real
108
+ surface of this package.
109
+
110
+ A section is now fully declarable against the kit: `schema.ts` (slots + options,
111
+ `as const satisfies SectionSchemaShape`), `fill-spec.ts` (`FillSpecShape`),
112
+ `fixtures.ts` (`FixtureModule`), and a `Renderer.tsx` whose props are **derived**
113
+ from the schema via `SectionProps<typeof schema>` rather than hand-written — a
114
+ required text slot resolves to `string`, an optional one to `string | null`, an
115
+ enum option to the literal union of its declared values.
116
+
117
+ Slots bind to property facts through a **closed vocabulary**: `direct()` accepts
118
+ only a known fact name and `derived()` only a known transform, so a misspelling is
119
+ a compile error at the call site instead of a runtime surprise. The vocabulary
120
+ tables (`SOURCE_FIELDS`, `TRANSFORMS`, `SLOT_TYPE_DESCRIPTORS`) are exported, and
121
+ changing them is a kit release.
122
+
123
+ Also exported: fact-driven variants (`makeResolveVariants`, `TransformInput`), the
124
+ template composition manifest (`TemplateManifest`), the fixture builders
125
+ (`buildFixtureSet`), and the image/breakpoint value shapes a renderer receives.
126
+
127
+ Everything lands on the package root — `import { … } from "@homepages/template-kit"`.
128
+ There are no subpath imports for the authoring API.
129
+
130
+ Zod is now a runtime dependency (`^3.23.8`).
131
+
132
+ Docs: `docs/schema-system.md`.
133
+
134
+ - 07d5dc8: Added the **contract primitives, theme tokens, and the golden property fixtures** —
135
+ with the schema system, a section is now authorable end to end against the kit.
136
+
137
+ **Primitives.** `Section` (the full-bleed section root), `Slot` and `SlotGroup` (the
138
+ editor's slot markers), `Image` (responsive `<picture>`/srcset from the platform's
139
+ image descriptor, with a deterministic empty-state box), and a new `SlotItem`.
140
+
141
+ `SlotItem` is the per-item handle inside a collection slot (`object_list`,
142
+ `image_collection`). It emits `data-slot-item`, which is what makes per-item
143
+ selection, per-item image cropping, and per-item live surfaces work in the editor —
144
+ a collection without it is editable only as a whole. The attribute is now declared
145
+ in the marker contract (`ATTR_SLOT_ITEM`) and emitted by a primitive rather than
146
+ hand-written as a magic string.
147
+
148
+ The primitives are the _only_ components the kit ships, and that is deliberate:
149
+ each one emits something the platform breaks without. Presentation — headings,
150
+ links, layout wells, icons, lists — is your template's own JSX.
151
+
152
+ **Theme tokens.** `@homepages/template-kit/styles.css` carries the Tailwind v4
153
+ `@theme` alias layer and the breakpoint ladder; `@homepages/template-kit/base.css`
154
+ carries the cascade-layer order, the reset, and `.tr-section`. A template supplies
155
+ the values behind the aliases as a `TokenTheme` (`theme.ts`), compiled by
156
+ `compileThemeToCss` into the `:root { --tr-*: … }` block its bundle ships.
157
+
158
+ Import the token entry **after** Tailwind, and import Tailwind exactly once — a
159
+ second `@import "tailwindcss"` anywhere in the graph double-emits the preflight
160
+ reset:
161
+
162
+ ```css
163
+ @import "tailwindcss";
164
+ @import "@homepages/template-kit/styles.css";
165
+ ```
166
+
167
+ **Golden fixtures.** `sampleProperty` and friends (`sampleContacts`, `samplePois`,
168
+ `sampleAmenities`, `allPhotos`, `mkResponsive`) — a realistic 12-unit building with
169
+ long unit labels, a reserved unit and a missing price. Preview is only as honest as
170
+ the data behind it; build your `fixtures.ts` on these rather than on lorem ipsum.
171
+
172
+ **Breaking within 0.x:** the type formerly exported as `Slot` (the slot-definition
173
+ union) is now `SlotDefinition`. `Slot` is the primitive component.
174
+
175
+ - 35b0939: Added `@homepages/template-kit/eslint` — the flat-config ESLint preset for section
176
+ authoring. Eleven rules, all syntactic (no type-aware linting, so "zero extra config"
177
+ holds), catch the mistakes specific to writing a HomePages section template: reading
178
+ the clock or randomness (`no-nondeterminism`), effects/state/handlers/browser globals
179
+ outside a `"use client"` island (`no-client-runtime-in-server`), a prop crossing to an
180
+ island that can't survive JSON serialization (`serializable-island-props`), a hard-coded
181
+ colour where a theme token belongs (`no-hex`), and seven more covering the contract
182
+ files, slot markers, and raw-CSS boundaries a section must respect.
183
+
184
+ Consume it as:
185
+
186
+ ```js
187
+ import kit from "@homepages/template-kit/eslint";
188
+ export default kit;
189
+ ```
190
+
191
+ `@typescript-eslint/parser` is now a real `dependency` (not only a devDependency) —
192
+ the preset wires it directly so a consumer's `eslint.config.mjs` needs no parser
193
+ setup of its own.
194
+
195
+ - 2a32639: Islands: interactivity is now an ordinary React component marked "use client". Adds the
196
+ island contract and marker to the main entry, a new `./island-runtime` loader that
197
+ hydrates each island as its own React root, and a new `./browser` subpath with the
198
+ site-config and signed-ingest helpers for published pages.
199
+ - 96356b5: The published barrel now tree-shakes, and the golden fixtures move to their own subpath.
200
+
201
+ The build previously collapsed the primitives, the schema system, zod, the theme compiler
202
+ and the sample-property fixtures into a single chunk that `dist/index.js` only re-exported.
203
+ Because a consumer's bundler can only tree-shake at module granularity, importing one
204
+ primitive pulled in all of it: `import { Section }` cost **84,755 B** and inlined zod plus
205
+ the golden fixture dataset — agent phone numbers, listing prices, lot sizes — into every
206
+ section's published renderer.
207
+
208
+ The build is now unbundled (one output module per source module), so the import graph is
209
+ honest. `import { Section }` is **202 B**; `import { makeResolveVariants }` is **179 B**.
210
+ Neither carries zod or fixture data.
211
+
212
+ **Breaking:** `sampleProperty`, `sampleContacts`, `samplePois`, `sampleAmenities`,
213
+ `allPhotos`, `mkResponsive`, `sampleHeroSlots` and the `PhotoFixture` / `PoiFixture` /
214
+ `ContactFixture` / `AmenityItem` types are no longer exported from the root barrel. Import
215
+ them from `@homepages/template-kit/fixtures`:
216
+
217
+ ```diff
218
+ -import { sampleProperty } from "@homepages/template-kit";
219
+ +import { sampleProperty } from "@homepages/template-kit/fixtures";
220
+ ```
221
+
222
+ Dev-only sample data must not be reachable from a renderer's import graph at all: a
223
+ section's published bundle is an immutable, publicly readable artifact. Import fixtures
224
+ from `fixtures.ts`, never from `Renderer.tsx`. Everything else on the root barrel — the
225
+ primitives, the schema types, `makeResolveVariants`, `compileThemeToCss`, `KIT_VERSION`,
226
+ the marker constants, the islands contract — is unchanged.
227
+
228
+ The kit's `styles.css` also narrows its Tailwind `@source` scan to the shipped
229
+ `primitives/` modules, which are the only ones that emit markup. This drops 8 dead
230
+ utilities and 19 stray `@property` registrations that were being compiled out of the CLI's
231
+ and the ESLint preset's doc comments into consumers' stylesheets.
232
+
233
+ ### Patch Changes
234
+
235
+ - bb15161: Hardened the release pipeline that produces this package, with no change to the
236
+ authoring API. Packing or publishing now always rebuilds first, so the tarball
237
+ can never ship a stale `dist/`. The tsconfig preset shipped at
238
+ `@homepages/template-kit/tsconfig` no longer leaks this repo's own `exclude`
239
+ list or forces `noEmit` onto consumers who extend it. `CHANGELOG.md` is now
240
+ included in the published tarball, so you can read what changed in a given
241
+ version without leaving `node_modules`. The docs shipped alongside the package
242
+ no longer reference this project's private internal repo layout.
243
+
244
+ ## 0.0.1
245
+
246
+ ### Patch Changes
247
+
248
+ - c1d3127: Package skeleton: the published boundary and its hygiene gates. Ships the exports
249
+ map (root entry, ESLint flat-config preset, tsconfig preset, Tailwind v4 tokens
250
+ CSS), the `template-kit` CLI with its commands stubbed, and the changesets → npm
251
+ trusted-publishing release pipeline. No authoring surface yet.
package/README.md CHANGED
@@ -3,9 +3,10 @@
3
3
  The authoring surface for HomePages marketing-section templates: schema system,
4
4
  contract primitives, theme tokens, and the `template-kit` CLI.
5
5
 
6
- **Status: 0.0.x package skeleton.** The authoring surface is still being
7
- extracted, and the CLI commands are not implemented yet. What exists today is
8
- the published package boundary and its hygiene gates.
6
+ **Status: 0.0.x.** The **schema system** (the section authoring contract and the closed
7
+ vocabulary it validates against), the **contract primitives**, and the **theme tokens**
8
+ are complete a section is authorable end to end against the kit. The `template-kit`
9
+ CLI's commands are still being extracted.
9
10
 
10
11
  ## Install
11
12
 
@@ -15,35 +16,100 @@ npm install @homepages/template-kit react react-dom
15
16
 
16
17
  React 19 is a **peer dependency** — the kit never bundles its own copy.
17
18
 
19
+ ## Authoring a section
20
+
21
+ A section is four files: `schema.ts` (slots + options), `fill-spec.ts` (how AI
22
+ fills them), `fixtures.ts` (preview content), and `Renderer.tsx` (markup, with
23
+ props **derived** from the schema).
24
+
25
+ ```ts
26
+ import { direct, type SectionProps, type SectionSchemaShape } from "@homepages/template-kit";
27
+
28
+ export const schema = {
29
+ meta: { displayName: "Hero" },
30
+ slots: {
31
+ headline: { type: "text", required: true, produced_by: "headline_text" },
32
+ listing_status: { type: "select", required: false, source: direct("listing_intent") },
33
+ },
34
+ } as const satisfies SectionSchemaShape;
35
+
36
+ export type Props = SectionProps<typeof schema>; // slots.headline is `string`
37
+ ```
38
+
39
+ Property facts come from a closed vocabulary, so `direct("listing_intentt")` is a
40
+ compile error, not a runtime surprise. Full contract:
41
+ **[docs/schema-system.md](docs/schema-system.md)**.
42
+
43
+ Your fixtures build on the kit's golden property dataset (`sampleProperty`, `allPhotos`,
44
+ `samplePois`, …) — a realistic 12-unit building, long labels and missing values included.
45
+
46
+ ## The primitives
47
+
48
+ The kit ships **five components and nothing else**: `Section` (the full-bleed section
49
+ root), `Slot` and `SlotItem` and `SlotGroup` (the editor's marker attributes), and
50
+ `Image` (responsive `<picture>` + a deterministic empty-state box). Each earns its place
51
+ by emitting something the platform breaks without. Presentation — headings, links,
52
+ layout, icons — is your template's own JSX.
53
+
54
+ ```tsx
55
+ import { Image, Section, Slot, SlotItem } from "@homepages/template-kit";
56
+ ```
57
+
58
+ Full contract: **[docs/primitives.md](docs/primitives.md)**.
59
+
18
60
  ## Exports
19
61
 
20
62
  | Entry | Use |
21
63
  |---|---|
22
- | `@homepages/template-kit` | Authoring API (schema, primitives) |
23
- | `@homepages/template-kit/styles.css` | Tailwind v4 theme tokens + kit source registration |
64
+ | `@homepages/template-kit` | Authoring API schema system, primitives, marker contract, theme types, and the golden fixtures |
65
+ | `@homepages/template-kit/island-runtime` | Browser loader that hydrates a page's islands |
66
+ | `@homepages/template-kit/browser` | Browser helpers for published pages (site config, signed ingest) |
67
+ | `@homepages/template-kit/styles.css` | Tailwind v4 theme tokens, the breakpoint ladder, kit source registration |
68
+ | `@homepages/template-kit/base.css` | Cascade-layer order, the three reset gaps Tailwind's preflight leaves, `.tr-section`, and the motion tokens |
24
69
  | `@homepages/template-kit/eslint` | ESLint flat-config preset |
25
70
  | `@homepages/template-kit/tsconfig` | tsconfig preset (`"extends"`) |
26
71
 
27
- The CSS entry does **not** import Tailwind. Import it *after* Tailwind in your entry:
72
+ The authoring API has **no subpath imports** one specifier for everything you
73
+ write a section against. The two CSS entries are the deliberate exception.
74
+
75
+ `styles.css` does **not** import Tailwind. Import it *after* Tailwind in your entry:
28
76
 
29
77
  ```css
30
78
  @import "tailwindcss";
31
79
  @import "@homepages/template-kit/styles.css";
32
80
  ```
33
81
 
34
- Two `@import "tailwindcss"` in one graph double-emits preflight.
82
+ Two `@import "tailwindcss"` in one graph double-emits preflight. `base.css` is not part
83
+ of that entry — it loads as its own stylesheet on the page. `base.css` ships no reset
84
+ of its own — it depends on the preflight your entry's `@import "tailwindcss"` brings.
85
+ Both must be on the page. Full contract:
86
+ **[docs/theme-and-css.md](docs/theme-and-css.md)**.
87
+
88
+ ## Rules
89
+
90
+ The lint preset and the `template-kit check` CLI share one id namespace,
91
+ `template-kit/<id>`. A rule-id printed by either resolves to a page —
92
+ `docs/rules/<id>.md` — giving the rule, the reason it exists, and a before/after fix.
93
+ Start at **[docs/rules/INDEX.md](docs/rules/INDEX.md)**; the full doc set is routed
94
+ from **[docs/INDEX.md](docs/INDEX.md)**.
35
95
 
36
96
  ## Development
37
97
 
38
98
  ```bash
39
99
  npm install
40
- npm run check # typecheck + lint + test + build + publint + attw + consumer gate
100
+ npm run check # typecheck + lint + build + island-size gate + test + publint + attw + consumer gate
41
101
  ```
42
102
 
43
103
  `npm run verify:consumer` is the boundary gate: it packs the kit, installs the
44
- tarball into a scratch project, and asserts that types resolve, that Tailwind's
45
- cross-package `@source` scan reaches the shipped `dist`, and that only one React
46
- ends up in the tree. Run it before every release.
104
+ tarball into a scratch project, and asserts that types resolve; that a real section
105
+ triple typechecks against the installed package, with a misspelled property fact or
106
+ slot key still failing to compile; that Tailwind's cross-package `@source` scan
107
+ reaches the shipped `dist`; and that only one React ends up in the tree. Run it
108
+ before every release.
109
+
110
+ The island loader (`@homepages/template-kit/island-runtime`) has its own gate,
111
+ `npm run size:islands`: it must stay under 3072 B gzip and never import
112
+ `react`/`react-dom` statically, so a page with no islands loads no React.
47
113
 
48
114
  ## Releasing
49
115
 
package/dist/base.css ADDED
@@ -0,0 +1,64 @@
1
+ @layer theme, base, components, sections, template, utilities;
2
+
3
+ /* @homepages/template-kit — the base stylesheet every template's page loads.
4
+ *
5
+ * Exported as `@homepages/template-kit/base.css`. It is NOT part of the Tailwind
6
+ * entry (that is styles.css); it ships as its own stylesheet on the page.
7
+ *
8
+ * Contains:
9
+ * - The cascade-layer order declaration above — the one place it is stated
10
+ * - The three reset gaps Tailwind's preflight leaves (below)
11
+ * - The section box model (.tr-section, emitted by the <Section> primitive)
12
+ * - Universal tokens: motion
13
+ *
14
+ * REQUIRES A TAILWIND ENTRY ON THE PAGE. Your entry's `@import "tailwindcss"` brings
15
+ * Tailwind's preflight, and preflight does the reset: box-sizing, margin/padding,
16
+ * list-style, the button and anchor resets, display:block on replaced elements. This
17
+ * file only fills the three gaps preflight leaves. Ship base.css without a Tailwind
18
+ * entry and you get an unreset page.
19
+ *
20
+ * NOT here: anything a template owns. Colors, fonts, type, radii, shadows and layout
21
+ * — and the `body` document defaults that apply them — all come from your theme.ts,
22
+ * compiled by compileThemeToCss. This file references no theme token at all.
23
+ */
24
+
25
+ /* ----- Reset gaps ------------------------------------------------------- *
26
+ * The three declarations Tailwind's preflight does NOT make. Each is load-bearing:
27
+ * delete one and your page changes.
28
+ *
29
+ * Wrapped in `@layer base` so Tailwind's utilities (which compile into
30
+ * `@layer utilities`) win over these element defaults. Unlayered, `button { cursor:
31
+ * pointer }` would beat a layered `.cursor-default` utility outright — unlayered
32
+ * always wins over layered in the cascade.
33
+ */
34
+
35
+ @layer base {
36
+ /* preflight carries no font-smoothing rule at all */
37
+ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }
38
+ /* Tailwind v4 deliberately leaves buttons at the UA default `cursor: default` */
39
+ button { cursor: pointer; }
40
+ /* preflight caps max-width on img and video, but not on svg */
41
+ svg { max-width: 100%; }
42
+ }
43
+
44
+ /* ----- Primitives ------------------------------------------------------- */
45
+
46
+ /* The full-bleed section root, emitted by the <Section> primitive. Always 100% wide
47
+ * with zero padding/margin, so adjacent sections butt edge-to-edge and a page composes
48
+ * as a vertical stack with no framework-injected whitespace. A section brings its own
49
+ * background and internal padding. */
50
+ .tr-section {
51
+ display: block;
52
+ width: 100%;
53
+ padding: 0;
54
+ margin: 0;
55
+ }
56
+
57
+ /* ----- Universal tokens ------------------------------------------------- */
58
+
59
+ :root {
60
+ /* Motion */
61
+ --tr-duration-fast: 150ms;
62
+ --tr-duration-base: 250ms;
63
+ --tr-ease: cubic-bezier(0.2, 0.8, 0.2, 1);
64
+ }
@@ -0,0 +1,26 @@
1
+ //#region src/browser/site-config.d.ts
2
+ interface SiteConfig {
3
+ siteId: string;
4
+ siteSecret: string;
5
+ ingestBase: string;
6
+ }
7
+ /** Read the per-site config a published page embeds as `#tr-site-config`. Null when absent or incomplete. */
8
+ declare function readSiteConfig(): SiteConfig | null;
9
+ /**
10
+ * Decode hex to bytes. Always ArrayBuffer-backed (never SharedArrayBuffer), so the
11
+ * result satisfies the BufferSource parameter of crypto.subtle.importKey — but that
12
+ * generic stays out of this public signature: on TypeScript < 5.7, `Uint8Array` isn't
13
+ * generic at all, and writing `Uint8Array<ArrayBuffer>` here would fail every
14
+ * consumer's build below that floor (TS2315) for a detail only this module's own
15
+ * `crypto.subtle.importKey` call needs.
16
+ */
17
+ declare function hexToBytes(hex: string): Uint8Array;
18
+ declare function bytesToHex(buf: ArrayBuffer): string;
19
+ declare function hmacHex(secretHex: string, message: string): Promise<string>;
20
+ /**
21
+ * The signed header set every ingest request carries:
22
+ * `X-Signature` = hex(hmac_sha256(siteSecret, `${unixSeconds}.${body}`)).
23
+ */
24
+ declare function signedIngestHeaders(config: SiteConfig, body: string): Promise<Record<string, string>>;
25
+ //#endregion
26
+ export { SiteConfig, bytesToHex, hexToBytes, hmacHex, readSiteConfig, signedIngestHeaders };
@@ -0,0 +1,55 @@
1
+ //#region src/browser/site-config.ts
2
+ /** Read the per-site config a published page embeds as `#tr-site-config`. Null when absent or incomplete. */
3
+ function readSiteConfig() {
4
+ const el = document.getElementById("tr-site-config");
5
+ if (!el?.textContent) return null;
6
+ try {
7
+ const config = JSON.parse(el.textContent);
8
+ if (!config.siteId || !config.siteSecret || !config.ingestBase) return null;
9
+ return config;
10
+ } catch {
11
+ return null;
12
+ }
13
+ }
14
+ /**
15
+ * Decode hex to bytes. Always ArrayBuffer-backed (never SharedArrayBuffer), so the
16
+ * result satisfies the BufferSource parameter of crypto.subtle.importKey — but that
17
+ * generic stays out of this public signature: on TypeScript < 5.7, `Uint8Array` isn't
18
+ * generic at all, and writing `Uint8Array<ArrayBuffer>` here would fail every
19
+ * consumer's build below that floor (TS2315) for a detail only this module's own
20
+ * `crypto.subtle.importKey` call needs.
21
+ */
22
+ function hexToBytes(hex) {
23
+ if (hex.length % 2 !== 0) throw new Error("hexToBytes: odd-length hex string");
24
+ if (!/^[0-9a-fA-F]*$/.test(hex)) throw new Error("hexToBytes: invalid hex string");
25
+ const out = new Uint8Array(/* @__PURE__ */ new ArrayBuffer(hex.length / 2));
26
+ for (let i = 0; i < out.length; i++) out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
27
+ return out;
28
+ }
29
+ function bytesToHex(buf) {
30
+ return Array.from(new Uint8Array(buf)).map((b) => b.toString(16).padStart(2, "0")).join("");
31
+ }
32
+ async function hmacHex(secretHex, message) {
33
+ const key = await crypto.subtle.importKey("raw", hexToBytes(secretHex), {
34
+ name: "HMAC",
35
+ hash: "SHA-256"
36
+ }, false, ["sign"]);
37
+ return bytesToHex(await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(message)));
38
+ }
39
+ /**
40
+ * The signed header set every ingest request carries:
41
+ * `X-Signature` = hex(hmac_sha256(siteSecret, `${unixSeconds}.${body}`)).
42
+ */
43
+ async function signedIngestHeaders(config, body) {
44
+ const ts = Math.floor(Date.now() / 1e3).toString();
45
+ const signature = await hmacHex(config.siteSecret, `${ts}.${body}`);
46
+ return {
47
+ "Content-Type": "application/json",
48
+ "X-Site-Id": config.siteId,
49
+ "X-Timestamp": ts,
50
+ "X-Signature": signature
51
+ };
52
+ }
53
+
54
+ //#endregion
55
+ export { bytesToHex, hexToBytes, hmacHex, readSiteConfig, signedIngestHeaders };
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { t as KIT_VERSION } from "./src-dZr5N30y.js";
2
+ import { KIT_VERSION } from "./index.js";
3
3
 
4
4
  //#region src/cli/index.ts
5
5
  /**
@@ -0,0 +1,17 @@
1
+ //#region src/contracts/image-descriptor.d.ts
2
+ interface ResponsiveSource {
3
+ /** Image MIME type: 'image/avif' | 'image/webp' | 'image/jpeg' | 'image/png'. */
4
+ type: string;
5
+ /** "<presigned-url> 320w, … 2048w" — rungs ascending by ACTUAL width. */
6
+ srcset: string;
7
+ }
8
+ interface ResponsiveImage {
9
+ /** Per-format candidate sets, ordered preferred→fallback: avif, webp, jpeg, png. */
10
+ sources: ResponsiveSource[];
11
+ /** Ceiling-rung (MAX edge) actual width — drives <img> width/aspect/sizes. */
12
+ width: number;
13
+ /** Ceiling-rung actual height. */
14
+ height: number;
15
+ }
16
+ //#endregion
17
+ export { ResponsiveImage, ResponsiveSource };
@@ -0,0 +1,20 @@
1
+ //#region src/contracts/markers.d.ts
2
+ /** Wrapper marker the editor resolves a slot from (`Slot`, primitive `slotId`). */
3
+ declare const ATTR_SLOT_ID = "data-slot-id";
4
+ /** Marks the immediate text-leaf child of a `textLeaf` slot. */
5
+ declare const ATTR_SLOT_TEXT_LEAF = "data-slot-text-leaf";
6
+ /** Wraps a set of slots edited/regenerated as one unit (`SlotGroup`). */
7
+ declare const ATTR_SLOT_GROUP = "data-slot-group";
8
+ /**
9
+ * Zero-based index of one item inside a collection slot (`SlotItem`) — the
10
+ * per-item handle for `object_list` / `image_collection` slots. The editor
11
+ * resolves per-item selection, live enhancer surfaces, and per-item image
12
+ * crops against it, scoped to the enclosing `ATTR_SLOT_ID` element.
13
+ */
14
+ declare const ATTR_SLOT_ITEM = "data-slot-item";
15
+ /** Per-section-instance identity on the section root. */
16
+ declare const ATTR_SECTION_INSTANCE_ID = "data-section-instance-id";
17
+ /** Opt-in hook telling an enhancer to attach to this element. */
18
+ declare const ATTR_TR_ENHANCE = "data-tr-enhance";
19
+ //#endregion
20
+ export { ATTR_SECTION_INSTANCE_ID, ATTR_SLOT_GROUP, ATTR_SLOT_ID, ATTR_SLOT_ITEM, ATTR_SLOT_TEXT_LEAF, ATTR_TR_ENHANCE };
@@ -0,0 +1,21 @@
1
+ //#region src/contracts/markers.ts
2
+ /** Wrapper marker the editor resolves a slot from (`Slot`, primitive `slotId`). */
3
+ const ATTR_SLOT_ID = "data-slot-id";
4
+ /** Marks the immediate text-leaf child of a `textLeaf` slot. */
5
+ const ATTR_SLOT_TEXT_LEAF = "data-slot-text-leaf";
6
+ /** Wraps a set of slots edited/regenerated as one unit (`SlotGroup`). */
7
+ const ATTR_SLOT_GROUP = "data-slot-group";
8
+ /**
9
+ * Zero-based index of one item inside a collection slot (`SlotItem`) — the
10
+ * per-item handle for `object_list` / `image_collection` slots. The editor
11
+ * resolves per-item selection, live enhancer surfaces, and per-item image
12
+ * crops against it, scoped to the enclosing `ATTR_SLOT_ID` element.
13
+ */
14
+ const ATTR_SLOT_ITEM = "data-slot-item";
15
+ /** Per-section-instance identity on the section root. */
16
+ const ATTR_SECTION_INSTANCE_ID = "data-section-instance-id";
17
+ /** Opt-in hook telling an enhancer to attach to this element. */
18
+ const ATTR_TR_ENHANCE = "data-tr-enhance";
19
+
20
+ //#endregion
21
+ export { ATTR_SECTION_INSTANCE_ID, ATTR_SLOT_GROUP, ATTR_SLOT_ID, ATTR_SLOT_ITEM, ATTR_SLOT_TEXT_LEAF, ATTR_TR_ENHANCE };