@homepages/template-kit 0.3.0 → 0.4.1
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 +84 -0
- package/README.md +39 -4
- package/dist/asset-modules.d.ts +81 -0
- package/dist/cli/check/config.js +5 -1
- package/dist/cli/check/css.js +27 -13
- package/dist/cli/check/loader.js +4 -0
- package/dist/cli/check/section-assets.js +58 -0
- package/dist/cli/check/stages/size.js +124 -8
- package/dist/cli/check/stages/tree.js +26 -27
- package/dist/cli/dev/build-css.js +91 -0
- package/dist/cli/dev/catalog.js +35 -0
- package/dist/cli/dev/compose-template.js +38 -0
- package/dist/cli/dev/content-override.js +58 -0
- package/dist/cli/dev/discover.js +35 -0
- package/dist/cli/dev/fill-state.js +75 -0
- package/dist/cli/dev/index.js +32 -0
- package/dist/cli/dev/inspect.js +38 -0
- package/dist/cli/dev/island-bootstrap.js +38 -0
- package/dist/cli/dev/island-map.js +35 -0
- package/dist/cli/dev/island-transform.js +31 -0
- package/dist/cli/dev/manifest-instances.js +31 -0
- package/dist/cli/dev/render-section.js +24 -0
- package/dist/cli/dev/screenshot-target.js +24 -0
- package/dist/cli/dev/section-page.js +54 -0
- package/dist/cli/dev/server.js +480 -0
- package/dist/cli/dev/slot-schema.js +12 -0
- package/dist/cli/dev/structure-summary.js +118 -0
- package/dist/cli/dev/tailwind.js +32 -0
- package/dist/cli/dev/vite-server.js +33 -0
- package/dist/cli/dev/workspace.js +63 -0
- package/dist/cli/link/index.js +74 -0
- package/dist/cli/link/overlay.js +59 -0
- package/dist/cli/link/watch.js +25 -0
- package/dist/cli/new/emit.js +48 -0
- package/dist/cli/new/index.js +69 -0
- package/dist/cli/new/scaffold/section/_Renderer.tsx +20 -0
- package/dist/cli/new/scaffold/section/_fill-spec.ts +18 -0
- package/dist/cli/new/scaffold/section/_fixtures.ts +13 -0
- package/dist/cli/new/scaffold/section/_schema.ts +24 -0
- package/dist/cli/new/scaffold/template/_manifest.json +9 -0
- package/dist/cli/new/scaffold/template/sections/hero/ExpandableText.tsx +45 -0
- package/dist/cli/new/scaffold/template/sections/hero/Renderer.tsx +40 -0
- package/dist/cli/new/scaffold/template/sections/hero/components/Feature.tsx +14 -0
- package/dist/cli/new/scaffold/template/sections/hero/fill-spec.ts +29 -0
- package/dist/cli/new/scaffold/template/sections/hero/fixtures.ts +41 -0
- package/dist/cli/new/scaffold/template/sections/hero/schema.ts +60 -0
- package/dist/cli/new/scaffold/template/theme.css +24 -0
- package/dist/cli/new/scaffold/template/theme.ts +27 -0
- package/dist/cli/new/scaffold-assets.js +20 -0
- package/dist/cli/pack/collect.js +44 -0
- package/dist/cli/pack/guards.js +57 -0
- package/dist/cli/pack/index.js +66 -0
- package/dist/cli/pack/manifest.js +15 -0
- package/dist/cli/pack/zip.js +86 -0
- package/dist/cli.js +80 -14
- package/dist/dev-client/assets/index-DfEfrp6P.css +1 -0
- package/dist/dev-client/assets/index-bpMP0b0W.js +50 -0
- package/dist/dev-client/index.html +13 -0
- package/dist/eslint/is-client-file.d.ts +4 -0
- package/dist/index.d.ts +1 -0
- package/dist/islands/detect.d.ts +16 -0
- package/dist/islands/detect.js +22 -0
- package/dist/islands/discover.d.ts +5 -0
- package/dist/islands/discover.js +19 -0
- package/dist/islands/esbuild-plugin.d.ts +13 -0
- package/dist/islands/esbuild-plugin.js +39 -0
- package/dist/islands/wrap.d.ts +12 -0
- package/dist/islands/wrap.js +48 -0
- package/dist/package.js +1 -1
- package/dist/ssr.d.ts +31 -0
- package/dist/ssr.js +46 -0
- package/docs/INDEX.md +9 -1
- package/docs/assets.md +128 -0
- package/docs/check.md +6 -3
- package/docs/dev.md +187 -0
- package/docs/islands.md +5 -2
- package/docs/llms.txt +41 -3
- package/docs/new.md +27 -0
- package/docs/overview.md +57 -0
- package/docs/pack.md +46 -0
- package/docs/primitives.md +5 -6
- package/docs/quickstart.md +102 -0
- package/docs/recipes/INDEX.md +27 -0
- package/docs/recipes/bind-property-fact.md +58 -0
- package/docs/recipes/collection-slot.md +100 -0
- package/docs/recipes/fill-spec-decision.md +66 -0
- package/docs/recipes/fixture-states.md +68 -0
- package/docs/recipes/image-slot-crop.md +98 -0
- package/docs/recipes/interactive-island.md +100 -0
- package/docs/recipes/organize-section-folder.md +72 -0
- package/docs/recipes/prepare-submission.md +55 -0
- package/docs/recipes/second-template.md +49 -0
- package/docs/recipes/select-slot.md +59 -0
- package/docs/recipes/static-asset.md +103 -0
- package/docs/recipes/third-party-package.md +90 -0
- package/docs/rules/INDEX.md +4 -2
- package/docs/rules/bundle-binary-asset.md +33 -52
- package/docs/rules/bundle-incomplete.md +2 -2
- package/docs/rules/css-reason.md +3 -3
- package/docs/rules/no-bare-css-import.md +8 -8
- package/docs/rules/no-css-import-from-render-path.md +7 -8
- package/docs/rules/size-assets.md +88 -0
- package/docs/rules/size-island-bundle.md +123 -0
- package/docs/rules/size-section-css.md +19 -14
- package/docs/schema-system.md +28 -28
- package/docs/theme-and-css.md +2 -2
- package/docs/vocabulary.md +98 -0
- package/package.json +12 -3
- package/tsconfig.json +1 -1
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
---
|
|
2
|
+
purpose: The template-kit/size-island-bundle authoring rule — what it enforces, why, and how to fix it.
|
|
3
|
+
status: living
|
|
4
|
+
related: [INDEX.md, size-renderer-bundle.md, size-section-css.md, server-vs-client.md]
|
|
5
|
+
updated: 2026-07-15
|
|
6
|
+
---
|
|
7
|
+
# template-kit/size-island-bundle
|
|
8
|
+
|
|
9
|
+
> Each island's browser bundle must be **≤ 50 KB gzip**. The budget is not
|
|
10
|
+
> author-overridable, and it applies per island, not per section.
|
|
11
|
+
|
|
12
|
+
## Rule
|
|
13
|
+
|
|
14
|
+
An island — a component whose file opens with the `"use client"` directive (see
|
|
15
|
+
[Server vs client](server-vs-client.md)) — ships to the browser as its own bundle,
|
|
16
|
+
separate from the section's server-rendered HTML. Each island is bundled with esbuild
|
|
17
|
+
and minified, with **React treated as external** (the page's runtime chunk supplies it
|
|
18
|
+
once, shared by every island on the page, and is not counted against any one of them).
|
|
19
|
+
The gzipped result must be **50 KB or less**.
|
|
20
|
+
|
|
21
|
+
A section can have more than one island. The budget is checked **per island**: a section
|
|
22
|
+
with three islands each at 40 KB passes; a section with one island at 60 KB fails, even
|
|
23
|
+
though the same section's other files are small.
|
|
24
|
+
|
|
25
|
+
The budget is **visible in `template-kit check`'s configuration and is not
|
|
26
|
+
author-overridable**. A template workspace cannot raise it, and there is no per-island
|
|
27
|
+
exemption.
|
|
28
|
+
|
|
29
|
+
## Reason
|
|
30
|
+
|
|
31
|
+
An island is the part of the page that has to work on a phone before the visitor does
|
|
32
|
+
anything — a carousel, a filter, a form. Every one of its bytes is downloaded, parsed,
|
|
33
|
+
and executed on the client, on top of whatever the page's shared runtime already costs.
|
|
34
|
+
Unlike the section's server-rendered markup, which the browser only has to paint, an
|
|
35
|
+
island's code has to run.
|
|
36
|
+
|
|
37
|
+
The number is set to be generous, so it does not fight ordinary authoring — a real
|
|
38
|
+
interactive widget fits comfortably inside it. You hit the budget when something
|
|
39
|
+
**runaway** gets pulled into the client bundle:
|
|
40
|
+
|
|
41
|
+
- **a whole UI or icon library imported for one component**, which drags every export
|
|
42
|
+
into the bundle so the island can use a handful of them;
|
|
43
|
+
- **a heavy dependency that belongs on the server**, ported into the island because it
|
|
44
|
+
was easiest to reach for where the code was being written, not because the browser
|
|
45
|
+
needs it.
|
|
46
|
+
|
|
47
|
+
That is the shape of essentially every violation: not an island that grew, but a single
|
|
48
|
+
import that was more expensive than it looked.
|
|
49
|
+
|
|
50
|
+
## Fix
|
|
51
|
+
|
|
52
|
+
Find the import that is not paying for itself, or narrow what ships to the browser at
|
|
53
|
+
all.
|
|
54
|
+
|
|
55
|
+
- **Import named symbols, never a namespace.** `import { ArrowRight } from "…"` is
|
|
56
|
+
tree-shakeable; `import * as Icons from "…"` is not — a namespace object has to contain
|
|
57
|
+
everything, so nothing can be dropped.
|
|
58
|
+
- **Prefer the package's narrow entry** if it offers one, instead of its default
|
|
59
|
+
"everything" bundle.
|
|
60
|
+
- **Move what doesn't need to be interactive out of the island.** If only a small part of
|
|
61
|
+
a component actually needs client-side behavior — an expand/collapse toggle inside an
|
|
62
|
+
otherwise static block — split the interactive part into its own small island and leave
|
|
63
|
+
the rest server-rendered. Server-rendered markup costs nothing against this budget.
|
|
64
|
+
- **Hand-write the residue.** If you use one function out of a large utility package, copy
|
|
65
|
+
the function instead of importing the package for it.
|
|
66
|
+
|
|
67
|
+
### Before
|
|
68
|
+
|
|
69
|
+
```tsx
|
|
70
|
+
// sections/gallery/Carousel.tsx
|
|
71
|
+
"use client";
|
|
72
|
+
|
|
73
|
+
import * as Icons from "lucide-react"; // ← the whole set: every icon, in this island's bundle
|
|
74
|
+
import { useState } from "react";
|
|
75
|
+
|
|
76
|
+
export default function Carousel({ slides }: { slides: { id: string; url: string }[] }) {
|
|
77
|
+
const [index, setIndex] = useState(0);
|
|
78
|
+
return (
|
|
79
|
+
<div>
|
|
80
|
+
<img src={slides[index]?.url} alt="" />
|
|
81
|
+
<button onClick={() => setIndex((i) => (i - 1 + slides.length) % slides.length)}>
|
|
82
|
+
<Icons.ChevronLeft aria-hidden />
|
|
83
|
+
</button>
|
|
84
|
+
<button onClick={() => setIndex((i) => (i + 1) % slides.length)}>
|
|
85
|
+
<Icons.ChevronRight aria-hidden />
|
|
86
|
+
</button>
|
|
87
|
+
</div>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### After
|
|
93
|
+
|
|
94
|
+
```tsx
|
|
95
|
+
// sections/gallery/Carousel.tsx
|
|
96
|
+
"use client";
|
|
97
|
+
|
|
98
|
+
import { ChevronLeft, ChevronRight } from "lucide-react"; // ← two icons, and the bundler can drop the rest
|
|
99
|
+
import { useState } from "react";
|
|
100
|
+
|
|
101
|
+
export default function Carousel({ slides }: { slides: { id: string; url: string }[] }) {
|
|
102
|
+
const [index, setIndex] = useState(0);
|
|
103
|
+
return (
|
|
104
|
+
<div>
|
|
105
|
+
<img src={slides[index]?.url} alt="" />
|
|
106
|
+
<button onClick={() => setIndex((i) => (i - 1 + slides.length) % slides.length)}>
|
|
107
|
+
<ChevronLeft aria-hidden />
|
|
108
|
+
</button>
|
|
109
|
+
<button onClick={() => setIndex((i) => (i + 1) % slides.length)}>
|
|
110
|
+
<ChevronRight aria-hidden />
|
|
111
|
+
</button>
|
|
112
|
+
</div>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## See also
|
|
118
|
+
|
|
119
|
+
- [`size-renderer-bundle`](size-renderer-bundle.md) — the same budget on the section's
|
|
120
|
+
server-rendered bundle.
|
|
121
|
+
- [`size-section-css`](size-section-css.md) — the same budget on the section's stylesheet.
|
|
122
|
+
- [Server vs client](server-vs-client.md) — what makes a file an island in the first
|
|
123
|
+
place.
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
purpose: The template-kit/size-section-css authoring rule — what it enforces, why, and how to fix it.
|
|
3
3
|
status: living
|
|
4
|
-
related: [INDEX.md, size-renderer-bundle.md, ../theme-and-css.md]
|
|
5
|
-
updated: 2026-07-
|
|
4
|
+
related: [INDEX.md, size-renderer-bundle.md, size-assets.md, ../theme-and-css.md]
|
|
5
|
+
updated: 2026-07-15
|
|
6
6
|
---
|
|
7
7
|
# template-kit/size-section-css
|
|
8
8
|
|
|
@@ -16,7 +16,7 @@ everything that lands in the layer:
|
|
|
16
16
|
|
|
17
17
|
- the section's hand-written CSS, and
|
|
18
18
|
- **every package stylesheet its module graph imports** — the `import "swiper/css"` in a
|
|
19
|
-
component
|
|
19
|
+
component or an island.
|
|
20
20
|
|
|
21
21
|
The budget is **visible in `template-kit check`'s configuration and is not
|
|
22
22
|
author-overridable**. A workspace cannot raise it, and there is no per-section exemption.
|
|
@@ -29,14 +29,15 @@ CSS blocks the first paint.
|
|
|
29
29
|
|
|
30
30
|
"Including package CSS" is not a technicality; it is the failure this rule exists to catch.
|
|
31
31
|
Your own stylesheet is almost never the problem — it is a few kilobytes of overrides. The
|
|
32
|
-
package's is.
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
`dist` listing looks alarming. The compiled layer is 400 KB.
|
|
32
|
+
package's is. A one-line `import "some-ui/dist.css"` can drag in thousands of rules for
|
|
33
|
+
components you never render — resets, theme variants, every widget's styles — and they all
|
|
34
|
+
land in your compiled layer. Nothing in your source looks large; the compiled layer is
|
|
35
|
+
400 KB.
|
|
37
36
|
|
|
38
37
|
So the number this check reports is frequently the first time anyone finds out what a
|
|
39
|
-
one-line CSS import actually cost.
|
|
38
|
+
one-line CSS import actually cost. (What this budget does **not** count is asset bytes: a
|
|
39
|
+
`url()`-referenced webfont or icon file resolves to a cached URL and is weighed separately —
|
|
40
|
+
see [`size-assets`](size-assets.md).)
|
|
40
41
|
|
|
41
42
|
## Fix
|
|
42
43
|
|
|
@@ -45,10 +46,11 @@ Three moves, in order of how often they work:
|
|
|
45
46
|
1. **Import the slimmest entry the package offers.** Most CSS-shipping packages have a core
|
|
46
47
|
stylesheet plus opt-in modules, *and* a fat "everything" bundle that is the one you get
|
|
47
48
|
from copying the quickstart. Take the core, plus only the modules you use.
|
|
48
|
-
2. **
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
2. **Drop the icon font.** If the package's stylesheet declares an icon font and you use
|
|
50
|
+
three of its glyphs, replace them with inline SVG — or with a family the template's theme
|
|
51
|
+
already declares in `fonts` (`--tr-font-<name>`), which costs nothing because it is
|
|
52
|
+
already loaded. (The font *file* is weighed under [`size-assets`](size-assets.md), but its
|
|
53
|
+
`@font-face` rules and every `.icon-*` class still land in this layer.)
|
|
52
54
|
3. **Drop the package and hand-write the residue.** If you use one component from a large
|
|
53
55
|
widget library, its stylesheet is paying for components you never render.
|
|
54
56
|
|
|
@@ -83,12 +85,15 @@ export default function Carousel({ slides }: { slides: { id: string; url: string
|
|
|
83
85
|
```
|
|
84
86
|
|
|
85
87
|
The arrows and dots that came from the bundle's icon font are drawn as inline SVG in the
|
|
86
|
-
component instead, so
|
|
88
|
+
component instead, so the layer no longer carries that font's `@font-face` and icon-class
|
|
89
|
+
rules (and the font file no longer counts against [`size-assets`](size-assets.md) either).
|
|
87
90
|
|
|
88
91
|
## See also
|
|
89
92
|
|
|
90
93
|
- [`size-renderer-bundle`](size-renderer-bundle.md) — the same budget on the section's
|
|
91
94
|
JavaScript.
|
|
95
|
+
- [`size-assets`](size-assets.md) — the separate budget on the static asset *files*
|
|
96
|
+
(fonts, images) a stylesheet or component references.
|
|
92
97
|
- [`no-bare-css-import`](no-bare-css-import.md) — why a package's stylesheet is imported from
|
|
93
98
|
code and never `@import`ed from CSS.
|
|
94
99
|
- [Theme and CSS](../theme-and-css.md) — the fonts and tokens the page already loads, which
|
package/docs/schema-system.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
purpose: The section authoring contract — schema.ts, fill-spec.ts, fixtures.ts, and the closed source/transform vocabulary.
|
|
3
3
|
status: living
|
|
4
|
-
related: [primitives.md, theme-and-css.md]
|
|
5
|
-
updated: 2026-07-
|
|
4
|
+
related: [primitives.md, theme-and-css.md, vocabulary.md]
|
|
5
|
+
updated: 2026-07-16
|
|
6
6
|
---
|
|
7
7
|
# The schema system
|
|
8
8
|
|
|
@@ -32,22 +32,17 @@ from. Without it, `SectionProps` cannot tell a required slot from an optional on
|
|
|
32
32
|
and your renderer props collapse to a useless widened shape.
|
|
33
33
|
|
|
34
34
|
```ts
|
|
35
|
-
import { direct, type
|
|
36
|
-
|
|
37
|
-
export const HERO_FRAME = {
|
|
38
|
-
breakpoint: 425,
|
|
39
|
-
desktop: { aspect: 5 / 3 },
|
|
40
|
-
} as const satisfies FrameConfig;
|
|
35
|
+
import { direct, type SectionProps, type SectionSchemaShape } from "@homepages/template-kit";
|
|
41
36
|
|
|
42
37
|
export const schema = {
|
|
43
38
|
meta: { displayName: "Hero" },
|
|
44
39
|
slots: {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
40
|
+
hero_image: { type: "image", required: true, produced_by: "hero_image_assign", crop: { mode: "locked", aspect: 16 / 9 } },
|
|
41
|
+
headline: { type: "text", size: "medium", required: true, source: direct("address") },
|
|
42
|
+
blurb: { type: "text", size: "long", required: true, produced_by: "blurb_text" },
|
|
48
43
|
},
|
|
49
44
|
options: {
|
|
50
|
-
align: { type: "enum", values: ["left", "center"], default: "
|
|
45
|
+
align: { type: "enum", values: ["left", "center"], default: "center" },
|
|
51
46
|
},
|
|
52
47
|
} as const satisfies SectionSchemaShape;
|
|
53
48
|
|
|
@@ -65,14 +60,20 @@ binds it to a computed transform. Both take names from a **closed vocabulary**,
|
|
|
65
60
|
a typo is a compile error at the call site — not a runtime surprise:
|
|
66
61
|
|
|
67
62
|
```ts
|
|
68
|
-
source: direct("
|
|
69
|
-
source: direct("
|
|
63
|
+
source: direct("address") // ✅
|
|
64
|
+
source: direct("addresss") // ✗ compile error: not a SOURCE_FIELDS key
|
|
70
65
|
```
|
|
71
66
|
|
|
72
|
-
The vocabulary is the kit's, not yours.
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
67
|
+
The vocabulary is the kit's, not yours. Adding to it is a kit release, not a
|
|
68
|
+
change you can make in a template. See [the closed vocabulary](vocabulary.md)
|
|
69
|
+
for the sets and when to pick each; your editor autocompletes the exact fields.
|
|
70
|
+
|
|
71
|
+
A `select` slot binds the same way as any other type — here fact-bound rather
|
|
72
|
+
than AI-produced, with `values` declaring the closed set the fact resolves to:
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
listing_status: { type: "select", required: false, source: direct("listing_intent"), values: ["for_sale", "for_rent"] },
|
|
76
|
+
```
|
|
76
77
|
|
|
77
78
|
### Grouping slots into one editor card
|
|
78
79
|
|
|
@@ -104,7 +105,6 @@ import type { Props } from "./schema.js";
|
|
|
104
105
|
|
|
105
106
|
export default function Hero({ slots, options, nav }: Props) {
|
|
106
107
|
// headline is `string` — the schema says required: true
|
|
107
|
-
// listing_status is `string | null` — required: false
|
|
108
108
|
// options.align is `"left" | "center"` — the enum's declared values
|
|
109
109
|
return (
|
|
110
110
|
<Section id={nav.selfAnchor}>
|
|
@@ -153,12 +153,12 @@ export const fillSpec = {
|
|
|
153
153
|
decisions: [
|
|
154
154
|
{ id: "hero_image_assign", type: "image-assign", produces: ["hero_image"], tag_query: "category:exterior" },
|
|
155
155
|
{
|
|
156
|
-
id: "
|
|
156
|
+
id: "blurb_text",
|
|
157
157
|
type: "text-block",
|
|
158
|
-
produces: ["
|
|
159
|
-
length_cap:
|
|
160
|
-
structure: "
|
|
161
|
-
voice_prompt: "
|
|
158
|
+
produces: ["blurb"],
|
|
159
|
+
length_cap: 400,
|
|
160
|
+
structure: "paragraph",
|
|
161
|
+
voice_prompt: "Warm, concrete paragraph a buyer would want to read.",
|
|
162
162
|
},
|
|
163
163
|
],
|
|
164
164
|
} as const satisfies FillSpecShape;
|
|
@@ -177,14 +177,14 @@ import type { FixtureModule } from "@homepages/template-kit";
|
|
|
177
177
|
const fixtures: FixtureModule = {
|
|
178
178
|
base: {
|
|
179
179
|
slots: {
|
|
180
|
-
headline: "
|
|
181
|
-
listing_status: "for_sale",
|
|
180
|
+
headline: "101 Queensway, Jamaica Plain, MA 02130",
|
|
182
181
|
hero_image: { photo_id: "p1", url: "https://example.com/a.jpg", alt: "Front elevation" },
|
|
182
|
+
blurb: "A bright corner home moments from the Emerald Necklace.",
|
|
183
183
|
},
|
|
184
|
-
options: { align: "
|
|
184
|
+
options: { align: "center" },
|
|
185
185
|
},
|
|
186
186
|
states: {
|
|
187
|
-
|
|
187
|
+
left: { label: "Left-aligned", options: { align: "left" } },
|
|
188
188
|
},
|
|
189
189
|
};
|
|
190
190
|
|
package/docs/theme-and-css.md
CHANGED
|
@@ -177,8 +177,8 @@ never write an import to make either happen:
|
|
|
177
177
|
template's own hand-written stylesheet is collected the same way, into
|
|
178
178
|
`@layer template`.
|
|
179
179
|
2. **Package CSS, harvested from the render path.** The build also bundles
|
|
180
|
-
`Renderer.tsx`'s module graph — and
|
|
181
|
-
|
|
180
|
+
`Renderer.tsx`'s module graph — every component and island it imports — and pulls in
|
|
181
|
+
whatever `.css` those imports resolve to. A bare specifier
|
|
182
182
|
(`import "swiper/css";`) resolves into `node_modules` and lands in the same
|
|
183
183
|
`@layer sections`; that is the sanctioned way for a third-party package's
|
|
184
184
|
stylesheet to reach the page.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
---
|
|
2
|
+
purpose: The closed authoring vocabulary — slot types, direct() sources, derived() transforms, with selection semantics.
|
|
3
|
+
status: living
|
|
4
|
+
related: [schema-system.md]
|
|
5
|
+
updated: 2026-07-15
|
|
6
|
+
---
|
|
7
|
+
# The closed authoring vocabulary
|
|
8
|
+
|
|
9
|
+
Three closed sets. A slot's `type` picks its runtime value shape; a slot's
|
|
10
|
+
`source` (`direct()` or `derived()`) picks which fact or computation fills it.
|
|
11
|
+
This page is a selection reference — *which one to pick* — not a restatement
|
|
12
|
+
of their field shapes.
|
|
13
|
+
|
|
14
|
+
## Slot types
|
|
15
|
+
|
|
16
|
+
| type | runtime value shape | pick it when |
|
|
17
|
+
|---|---|---|
|
|
18
|
+
| `text` | `string` (`string \| null` if optional) | one editable line/paragraph (`size` short\|medium\|long) |
|
|
19
|
+
| `number` | `number` (`number \| null` if optional) | a numeric fact with formatting |
|
|
20
|
+
| `select` | `string` (`string \| null` if optional) | a closed set of choices the user picks from |
|
|
21
|
+
| `image` | `ImageValue` (`ImageValue \| null` if optional) | one framed photo (add `crop`/`frame`) |
|
|
22
|
+
| `image_collection` | `ImageCollectionValue` | a gallery of framed photos |
|
|
23
|
+
| `list` | `string[]` for a `text` element, `unknown[]` otherwise | a flat list of scalar items |
|
|
24
|
+
| `poi_list` | `PoiRow[]` | neighborhood points of interest |
|
|
25
|
+
| `url` | `string` (`string \| null` if optional) | a link the user edits in a URL field |
|
|
26
|
+
| `object_list` | `Record<string, unknown>[]` | repeating, independently-editable multi-field items |
|
|
27
|
+
|
|
28
|
+
The full per-type field shapes live in your editor's autocomplete and
|
|
29
|
+
[schema-system.md](schema-system.md) — this page is only *which to pick*.
|
|
30
|
+
|
|
31
|
+
## `direct()` sources
|
|
32
|
+
|
|
33
|
+
The property facts a slot binds verbatim. A bad name is a compile error at
|
|
34
|
+
the `direct(...)` call site.
|
|
35
|
+
|
|
36
|
+
**Property facts** (resolve once per property):
|
|
37
|
+
- `address` — the full formatted address
|
|
38
|
+
- `listing_intent` — for-sale vs. for-rent state
|
|
39
|
+
- `status` — the listing status, free text as shown on the listing (e.g. "For Sale")
|
|
40
|
+
- `property_type` — the property type (`single_family` \| `condo_townhome` \| `multi_family` \| `investment`)
|
|
41
|
+
- `year_built` — the year the property was built
|
|
42
|
+
- `lot_size` — the lot size, pre-formatted text
|
|
43
|
+
|
|
44
|
+
**Per-unit facts** (resolve against the current/single unit row):
|
|
45
|
+
- `unit_label` — the unit's display label (e.g. "Unit 3B")
|
|
46
|
+
- `beds` — bedroom count for this unit
|
|
47
|
+
- `baths` — bathroom count for this unit
|
|
48
|
+
- `sqft` — square footage for this unit
|
|
49
|
+
- `price` — this unit's price, pre-formatted text
|
|
50
|
+
- `notes` — free-text notes on this unit
|
|
51
|
+
|
|
52
|
+
**List sources** (bind the whole array verbatim — pick these for `object_list`
|
|
53
|
+
slots that repeat over every row):
|
|
54
|
+
- `units` — every unit row on the property
|
|
55
|
+
- `contacts` — every contact attached to the section
|
|
56
|
+
|
|
57
|
+
**Per-contact fields** (resolve against the attached contact):
|
|
58
|
+
- `name` — the contact's display name
|
|
59
|
+
- `title` — the contact's job title/role
|
|
60
|
+
- `bio` — the contact's bio copy
|
|
61
|
+
- `email` — the contact's email address
|
|
62
|
+
- `phone` — the contact's phone number
|
|
63
|
+
- `website` — the contact's website URL
|
|
64
|
+
- `instagram` — the contact's Instagram handle/URL
|
|
65
|
+
- `linkedin` — the contact's LinkedIn handle/URL
|
|
66
|
+
- `x` — the contact's X (Twitter) handle/URL
|
|
67
|
+
- `facebook` — the contact's Facebook handle/URL
|
|
68
|
+
|
|
69
|
+
**Contact images** (bind only to `image` slots):
|
|
70
|
+
- `headshot` — the contact's headshot photo
|
|
71
|
+
- `logo_dark` — the contact's logo, dark-background variant
|
|
72
|
+
- `logo_light` — the contact's logo, light-background variant
|
|
73
|
+
|
|
74
|
+
## `derived()` transforms
|
|
75
|
+
|
|
76
|
+
Computed values, grouped by family. A bad name is a compile error at the
|
|
77
|
+
`derived(...)` call site.
|
|
78
|
+
|
|
79
|
+
| transform | what it computes |
|
|
80
|
+
|---|---|
|
|
81
|
+
| `units.bedsPrimary` | beds of the primary (lowest-position) unit |
|
|
82
|
+
| `units.bathsPrimary` | baths of the primary unit |
|
|
83
|
+
| `units.sqftPrimary` | sqft of the primary unit |
|
|
84
|
+
| `units.pricePrimary` | price of the primary unit, pre-formatted text |
|
|
85
|
+
| `units.bedsRange` | formatted beds range across all units (e.g. "2–4"), collapsing to one value when uniform |
|
|
86
|
+
| `units.priceStart` | the lowest numeric unit price; falls back to the first non-numeric price (e.g. "Sold") when none is numeric |
|
|
87
|
+
| `units.bedsMin` | the minimum beds across all units, formatted |
|
|
88
|
+
| `units.bedsMax` | the maximum beds across all units, formatted |
|
|
89
|
+
| `units.bathsMin` | the minimum baths across all units, formatted |
|
|
90
|
+
| `units.bathsMax` | the maximum baths across all units, formatted |
|
|
91
|
+
| `units.sqftMax` | the maximum sqft across all units, formatted |
|
|
92
|
+
| `address.line1` | the property address's street line |
|
|
93
|
+
| `address.line2` | the property address's city/state/zip line |
|
|
94
|
+
| `contact_address.line1` | a contact's address street line |
|
|
95
|
+
| `contact_address.line2` | a contact's address city/state/zip line |
|
|
96
|
+
| `clock.currentYear` | the current year (e.g. for a copyright line) |
|
|
97
|
+
| `coordinate.lat` | latitude, split from the property's geocode |
|
|
98
|
+
| `coordinate.lng` | longitude, split from the property's geocode |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@homepages/template-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Authoring kit for HomePages marketing-section templates: schema system, contract primitives, theme tokens, and the template-kit CLI.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
|
@@ -41,6 +41,10 @@
|
|
|
41
41
|
"types": "./dist/browser.d.ts",
|
|
42
42
|
"default": "./dist/browser.js"
|
|
43
43
|
},
|
|
44
|
+
"./ssr": {
|
|
45
|
+
"types": "./dist/ssr.d.ts",
|
|
46
|
+
"default": "./dist/ssr.js"
|
|
47
|
+
},
|
|
44
48
|
"./eslint": {
|
|
45
49
|
"types": "./dist/eslint.d.ts",
|
|
46
50
|
"default": "./dist/eslint.js"
|
|
@@ -55,7 +59,7 @@
|
|
|
55
59
|
],
|
|
56
60
|
"scripts": {
|
|
57
61
|
"prepack": "npm run build",
|
|
58
|
-
"build": "tsdown",
|
|
62
|
+
"build": "tsdown && node scripts/build-dev-client.mjs && node scripts/build-cli-assets.mjs",
|
|
59
63
|
"typecheck": "tsc --noEmit",
|
|
60
64
|
"lint": "eslint .",
|
|
61
65
|
"pretest": "node test/link-fixture-workspace.mjs",
|
|
@@ -75,12 +79,15 @@
|
|
|
75
79
|
"@arethetypeswrong/cli": "^0.18.5",
|
|
76
80
|
"@changesets/cli": "^2.29.8",
|
|
77
81
|
"@eslint/js": "^9.39.4",
|
|
82
|
+
"@playwright/test": "^1.60.0",
|
|
78
83
|
"@tailwindcss/cli": "^4.3.0",
|
|
79
84
|
"@types/jsdom": "^21.1.7",
|
|
80
85
|
"@types/node": "^20.17.10",
|
|
81
86
|
"@types/react": "^19.0.0",
|
|
82
87
|
"@types/react-dom": "^19.0.0",
|
|
83
88
|
"@typescript-eslint/parser": "^8.60.1",
|
|
89
|
+
"@vitejs/plugin-react": "^4.3.4",
|
|
90
|
+
"chokidar": "^4.0.1",
|
|
84
91
|
"esbuild": "^0.28.1",
|
|
85
92
|
"eslint": "^9.39.4",
|
|
86
93
|
"jsdom": "^26.1.0",
|
|
@@ -91,9 +98,11 @@
|
|
|
91
98
|
"tsdown": "0.22.7",
|
|
92
99
|
"tsx": "^4.19.2",
|
|
93
100
|
"typescript": "^5.7.2",
|
|
94
|
-
"typescript-eslint": "^8.60.1"
|
|
101
|
+
"typescript-eslint": "^8.60.1",
|
|
102
|
+
"vite": "^6.0.5"
|
|
95
103
|
},
|
|
96
104
|
"dependencies": {
|
|
105
|
+
"magic-string": "^0.30.21",
|
|
97
106
|
"zod": "^4.4.3"
|
|
98
107
|
}
|
|
99
108
|
}
|
package/tsconfig.json
CHANGED