@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.
- package/CHANGELOG.md +251 -0
- package/README.md +77 -11
- package/dist/base.css +64 -0
- package/dist/browser.d.ts +26 -0
- package/dist/browser.js +55 -0
- package/dist/cli.js +1 -1
- package/dist/contracts/image-descriptor.d.ts +17 -0
- package/dist/contracts/markers.d.ts +20 -0
- package/dist/contracts/markers.js +21 -0
- package/dist/contracts/slot-catalog.d.ts +237 -0
- package/dist/contracts/slot-catalog.js +247 -0
- package/dist/contracts/transforms.d.ts +108 -0
- package/dist/contracts/transforms.js +117 -0
- package/dist/design-system/breakpoints.d.ts +5 -0
- package/dist/design-system/breakpoints.js +14 -0
- package/dist/design-system/theme.d.ts +308 -0
- package/dist/design-system/theme.js +102 -0
- package/dist/eslint/is-client-file.js +79 -0
- package/dist/eslint/rules/image-bare-needs-reason.js +43 -0
- package/dist/eslint/rules/no-client-directive-in-contract.js +31 -0
- package/dist/eslint/rules/no-client-runtime-in-server.js +103 -0
- package/dist/eslint/rules/no-css-import-from-render-path.js +29 -0
- package/dist/eslint/rules/no-hex.js +116 -0
- package/dist/eslint/rules/no-inline-style.js +27 -0
- package/dist/eslint/rules/no-nondeterminism.js +48 -0
- package/dist/eslint/rules/no-raw-element.js +37 -0
- package/dist/eslint/rules/props-from-schema.js +37 -0
- package/dist/eslint/rules/serializable-island-props.js +112 -0
- package/dist/eslint/rules/slot-marker-literal.js +38 -0
- package/dist/eslint.d.ts +4 -8
- package/dist/eslint.js +75 -11
- package/dist/fixtures/sample-property.d.ts +67 -0
- package/dist/fixtures/sample-property.js +707 -0
- package/dist/fixtures.d.ts +2 -0
- package/dist/fixtures.js +3 -0
- package/dist/index.d.ts +28 -12
- package/dist/index.js +34 -2
- package/dist/island-runtime.d.ts +30 -0
- package/dist/island-runtime.js +73 -0
- package/dist/islands/contract.d.ts +56 -0
- package/dist/islands/contract.js +71 -0
- package/dist/islands/marker.d.ts +16 -0
- package/dist/islands/marker.js +18 -0
- package/dist/package.js +5 -0
- package/dist/primitives/Image.d.ts +49 -0
- package/dist/primitives/Image.js +95 -0
- package/dist/primitives/Section.d.ts +22 -0
- package/dist/primitives/Section.js +26 -0
- package/dist/primitives/Slot.d.ts +50 -0
- package/dist/primitives/Slot.js +39 -0
- package/dist/primitives/SlotGroup.d.ts +38 -0
- package/dist/primitives/SlotGroup.js +24 -0
- package/dist/primitives/SlotItem.d.ts +43 -0
- package/dist/primitives/SlotItem.js +38 -0
- package/dist/primitives/index.d.ts +7 -0
- package/dist/primitives/picture-sources.d.ts +22 -0
- package/dist/primitives/picture-sources.js +52 -0
- package/dist/rules/registry.js +16 -0
- package/dist/schema/fill-spec.d.ts +692 -0
- package/dist/schema/fill-spec.js +98 -0
- package/dist/schema/fixture-schema.d.ts +143 -0
- package/dist/schema/fixture-schema.js +113 -0
- package/dist/schema/index.d.ts +16 -0
- package/dist/schema/manifest.d.ts +527 -0
- package/dist/schema/manifest.js +73 -0
- package/dist/schema/property-facts.d.ts +44 -0
- package/dist/schema/resolve-variants.d.ts +11 -0
- package/dist/schema/resolve-variants.js +15 -0
- package/dist/schema/runtime-values.d.ts +45 -0
- package/dist/schema/section-nav.d.ts +7 -0
- package/dist/schema/section-props.d.ts +69 -0
- package/dist/schema/section-schema.d.ts +533 -0
- package/dist/schema/section-schema.js +72 -0
- package/dist/schema/slot-types.d.ts +117 -0
- package/dist/schema/slot-types.js +181 -0
- package/dist/schema/source.d.ts +13 -0
- package/dist/schema/source.js +12 -0
- package/dist/schema/synthesize-nullable.d.ts +18 -0
- package/dist/schema/synthesize-nullable.js +47 -0
- package/dist/styles.css +131 -9
- package/docs/INDEX.md +7 -4
- package/docs/eslint.md +55 -0
- package/docs/islands.md +150 -0
- package/docs/llms.txt +33 -3
- package/docs/primitives.md +214 -0
- package/docs/rules/INDEX.md +58 -0
- package/docs/rules/audit-severity.md +91 -0
- package/docs/rules/bundle-incomplete.md +117 -0
- package/docs/rules/css-reason.md +94 -0
- package/docs/rules/determinism-drift.md +112 -0
- package/docs/rules/image-bare-needs-reason.md +117 -0
- package/docs/rules/license-denied.md +106 -0
- package/docs/rules/lockfile-missing.md +68 -0
- package/docs/rules/lockfile-stale.md +88 -0
- package/docs/rules/missing-slot-marker.md +140 -0
- package/docs/rules/no-bare-css-import.md +128 -0
- package/docs/rules/no-client-directive-in-contract.md +111 -0
- package/docs/rules/no-client-runtime-in-server.md +143 -0
- package/docs/rules/no-css-import-from-render-path.md +118 -0
- package/docs/rules/no-hex.md +97 -0
- package/docs/rules/no-inline-style.md +121 -0
- package/docs/rules/no-nondeterminism.md +100 -0
- package/docs/rules/no-raw-element.md +100 -0
- package/docs/rules/props-from-schema.md +119 -0
- package/docs/rules/render-invariant.md +165 -0
- package/docs/rules/serializable-island-props.md +146 -0
- package/docs/rules/server-vs-client.md +105 -0
- package/docs/rules/sidebar-order.md +109 -0
- package/docs/rules/single-react.md +97 -0
- package/docs/rules/size-renderer-bundle.md +109 -0
- package/docs/rules/size-section-css.md +95 -0
- package/docs/rules/slot-group-marker.md +127 -0
- package/docs/rules/slot-marker-literal.md +148 -0
- package/docs/rules/typecheck.md +103 -0
- package/docs/rules/unlayered-fence.md +113 -0
- package/docs/schema-system.md +240 -0
- package/docs/theme-and-css.md +225 -0
- package/package.json +30 -6
- package/tsconfig.base.json +16 -0
- package/tsconfig.json +2 -13
- package/dist/src-dZr5N30y.js +0 -35
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { ATTR_SLOT_ID, ATTR_SLOT_TEXT_LEAF } from "../contracts/markers.js";
|
|
2
|
+
import { Children, cloneElement, isValidElement } from "react";
|
|
3
|
+
import { jsx } from "react/jsx-runtime";
|
|
4
|
+
|
|
5
|
+
//#region src/primitives/Slot.tsx
|
|
6
|
+
/**
|
|
7
|
+
* Slot — the canonical wrapper for an editable slot's DOM boundary.
|
|
8
|
+
*
|
|
9
|
+
* Wrap each editable slot's outer DOM in `<Slot id="…">`. It emits `data-slot-id`,
|
|
10
|
+
* which is how the editor resolves a click in the preview back to the slot the user
|
|
11
|
+
* meant. A slot with no marker in the DOM cannot be selected or edited.
|
|
12
|
+
*
|
|
13
|
+
* A primitive with a natural single root for its slot type takes a `slotId` prop and
|
|
14
|
+
* emits the marker on its own root instead — `<Image slotId="hero_image">` needs no
|
|
15
|
+
* `<Slot>` wrapper. `<Slot>` is for everything else: text slots, source-bound scalars
|
|
16
|
+
* rendered as raw spans, and any slot whose markup is a hand-assembled composition.
|
|
17
|
+
*/
|
|
18
|
+
function Slot({ id, as, textLeaf, children, ...rest }) {
|
|
19
|
+
const Tag = as ?? "div";
|
|
20
|
+
const content = textLeaf ? markSingleTextLeaf(children) : children;
|
|
21
|
+
return /* @__PURE__ */ jsx(Tag, {
|
|
22
|
+
[ATTR_SLOT_ID]: id,
|
|
23
|
+
...rest,
|
|
24
|
+
children: content
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
function markSingleTextLeaf(children) {
|
|
28
|
+
if (Children.count(children) !== 1) return children;
|
|
29
|
+
const only = Children.toArray(children)[0];
|
|
30
|
+
if (!isValidElement(only)) return children;
|
|
31
|
+
const element = only;
|
|
32
|
+
return cloneElement(element, {
|
|
33
|
+
...element.props,
|
|
34
|
+
[ATTR_SLOT_TEXT_LEAF]: ""
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
//#endregion
|
|
39
|
+
export { Slot };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { ElementType, HTMLAttributes, ReactNode } from "react";
|
|
2
|
+
//#region src/primitives/SlotGroup.d.ts
|
|
3
|
+
type SlotGroupProps = HTMLAttributes<HTMLElement> & {
|
|
4
|
+
/**
|
|
5
|
+
* Must equal the `id` of a `meta.groups[]` entry in the section schema.
|
|
6
|
+
*
|
|
7
|
+
* Emitted as `data-slot-group` on the rendered container, so the editor treats
|
|
8
|
+
* the group as one selectable unit: selection chrome outlines THIS container
|
|
9
|
+
* rather than the individual members, and a click anywhere inside it —
|
|
10
|
+
* including the gaps between members — resolves to the group.
|
|
11
|
+
*/
|
|
12
|
+
id: string;
|
|
13
|
+
/**
|
|
14
|
+
* Element to render. Defaults to `"div"`. Override when the group's surrounding
|
|
15
|
+
* semantic role needs something else.
|
|
16
|
+
*/
|
|
17
|
+
as?: ElementType;
|
|
18
|
+
/**
|
|
19
|
+
* The member slots. `SlotGroup` MUST be an ANCESTOR of every member's
|
|
20
|
+
* `data-slot-id` element (it wraps their shared container) — the editor resolves
|
|
21
|
+
* a member back to its group with `closest("[data-slot-group]")`, so a marker
|
|
22
|
+
* that isn't an ancestor would never be found.
|
|
23
|
+
*/
|
|
24
|
+
children?: ReactNode;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* SlotGroup — the wrapper for a set of slots the schema groups into one editor card.
|
|
28
|
+
*
|
|
29
|
+
* Grouping is authored in two places that must agree. `meta.groups[]` declares the
|
|
30
|
+
* group (`{ id, label, members }`), which collapses its members into a single card in
|
|
31
|
+
* the editor sidebar. The renderer wraps those members' shared container in
|
|
32
|
+
* `<SlotGroup id="<the group's id>">`, which makes the canvas outline and select them
|
|
33
|
+
* as the one unit that matches that one card. Same id on both sides, or the card and
|
|
34
|
+
* the canvas disagree about what the user just clicked.
|
|
35
|
+
*/
|
|
36
|
+
declare function SlotGroup({ id, as, children, ...rest }: SlotGroupProps): ReactNode;
|
|
37
|
+
//#endregion
|
|
38
|
+
export { SlotGroup };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { ATTR_SLOT_GROUP } from "../contracts/markers.js";
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
|
|
4
|
+
//#region src/primitives/SlotGroup.tsx
|
|
5
|
+
/**
|
|
6
|
+
* SlotGroup — the wrapper for a set of slots the schema groups into one editor card.
|
|
7
|
+
*
|
|
8
|
+
* Grouping is authored in two places that must agree. `meta.groups[]` declares the
|
|
9
|
+
* group (`{ id, label, members }`), which collapses its members into a single card in
|
|
10
|
+
* the editor sidebar. The renderer wraps those members' shared container in
|
|
11
|
+
* `<SlotGroup id="<the group's id>">`, which makes the canvas outline and select them
|
|
12
|
+
* as the one unit that matches that one card. Same id on both sides, or the card and
|
|
13
|
+
* the canvas disagree about what the user just clicked.
|
|
14
|
+
*/
|
|
15
|
+
function SlotGroup({ id, as, children, ...rest }) {
|
|
16
|
+
return /* @__PURE__ */ jsx(as ?? "div", {
|
|
17
|
+
[ATTR_SLOT_GROUP]: id,
|
|
18
|
+
...rest,
|
|
19
|
+
children
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
//#endregion
|
|
24
|
+
export { SlotGroup };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { ElementType, HTMLAttributes, ReactNode } from "react";
|
|
2
|
+
//#region src/primitives/SlotItem.d.ts
|
|
3
|
+
type SlotItemProps = HTMLAttributes<HTMLElement> & {
|
|
4
|
+
/**
|
|
5
|
+
* Zero-based position of this item within its collection slot — the item's
|
|
6
|
+
* index in the array the renderer is mapping over. Emitted as
|
|
7
|
+
* `data-slot-item`.
|
|
8
|
+
*/
|
|
9
|
+
index: number;
|
|
10
|
+
/**
|
|
11
|
+
* Element to render. Defaults to `"div"`. Override when the item's surrounding
|
|
12
|
+
* semantic role needs something else (`li`, `article`, …).
|
|
13
|
+
*/
|
|
14
|
+
as?: ElementType;
|
|
15
|
+
children?: ReactNode;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* SlotItem — the per-item handle inside a collection slot (`object_list`,
|
|
19
|
+
* `image_collection`).
|
|
20
|
+
*
|
|
21
|
+
* A collection slot's `<Slot id="…">` marks the whole array; `<SlotItem index={i}>`
|
|
22
|
+
* marks one row of it. The editor needs BOTH: it resolves the slot from the ancestor
|
|
23
|
+
* `data-slot-id` and then the specific item from the nearest `data-slot-item`, which
|
|
24
|
+
* is what makes per-item selection, per-item image cropping, and per-item live
|
|
25
|
+
* surfaces work. Without it a collection is editable only as a whole.
|
|
26
|
+
*
|
|
27
|
+
* The index must be the item's position in the slot's array as rendered. The editor
|
|
28
|
+
* writes edits back by that index, so a `SlotItem` whose index doesn't match its
|
|
29
|
+
* position edits the wrong row.
|
|
30
|
+
*
|
|
31
|
+
* ```tsx
|
|
32
|
+
* <Slot id="amenities">
|
|
33
|
+
* {items.map((item, i) => (
|
|
34
|
+
* <SlotItem key={item.id} index={i} className="border-b">
|
|
35
|
+
* …
|
|
36
|
+
* </SlotItem>
|
|
37
|
+
* ))}
|
|
38
|
+
* </Slot>
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
declare function SlotItem({ index, as, children, ...rest }: SlotItemProps): ReactNode;
|
|
42
|
+
//#endregion
|
|
43
|
+
export { SlotItem };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { ATTR_SLOT_ITEM } from "../contracts/markers.js";
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
|
|
4
|
+
//#region src/primitives/SlotItem.tsx
|
|
5
|
+
/**
|
|
6
|
+
* SlotItem — the per-item handle inside a collection slot (`object_list`,
|
|
7
|
+
* `image_collection`).
|
|
8
|
+
*
|
|
9
|
+
* A collection slot's `<Slot id="…">` marks the whole array; `<SlotItem index={i}>`
|
|
10
|
+
* marks one row of it. The editor needs BOTH: it resolves the slot from the ancestor
|
|
11
|
+
* `data-slot-id` and then the specific item from the nearest `data-slot-item`, which
|
|
12
|
+
* is what makes per-item selection, per-item image cropping, and per-item live
|
|
13
|
+
* surfaces work. Without it a collection is editable only as a whole.
|
|
14
|
+
*
|
|
15
|
+
* The index must be the item's position in the slot's array as rendered. The editor
|
|
16
|
+
* writes edits back by that index, so a `SlotItem` whose index doesn't match its
|
|
17
|
+
* position edits the wrong row.
|
|
18
|
+
*
|
|
19
|
+
* ```tsx
|
|
20
|
+
* <Slot id="amenities">
|
|
21
|
+
* {items.map((item, i) => (
|
|
22
|
+
* <SlotItem key={item.id} index={i} className="border-b">
|
|
23
|
+
* …
|
|
24
|
+
* </SlotItem>
|
|
25
|
+
* ))}
|
|
26
|
+
* </Slot>
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
function SlotItem({ index, as, children, ...rest }) {
|
|
30
|
+
return /* @__PURE__ */ jsx(as ?? "div", {
|
|
31
|
+
[ATTR_SLOT_ITEM]: index,
|
|
32
|
+
...rest,
|
|
33
|
+
children
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
//#endregion
|
|
38
|
+
export { SlotItem };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Image } from "./Image.js";
|
|
2
|
+
import { PicturePlan, PictureSourceSpec } from "./picture-sources.js";
|
|
3
|
+
import { Section } from "./Section.js";
|
|
4
|
+
import { Slot } from "./Slot.js";
|
|
5
|
+
import { SlotGroup } from "./SlotGroup.js";
|
|
6
|
+
import { SlotItem } from "./SlotItem.js";
|
|
7
|
+
export { Image, type PicturePlan, type PictureSourceSpec, Section, Slot, SlotGroup, SlotItem };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
//#region src/primitives/picture-sources.d.ts
|
|
2
|
+
type PictureSourceSpec = {
|
|
3
|
+
type?: string;
|
|
4
|
+
media?: string;
|
|
5
|
+
srcSet: string;
|
|
6
|
+
sizes?: string;
|
|
7
|
+
};
|
|
8
|
+
type PicturePlan = {
|
|
9
|
+
/**
|
|
10
|
+
* Ordered <source> specs: mobile (media-gated) first, then desktop. May be empty
|
|
11
|
+
* even with a valid plan (a single-format descriptor puts everything on the <img>);
|
|
12
|
+
* `sources: []` AND no `imgSrcSet` is the "no responsive — caller degrades" signal.
|
|
13
|
+
*/
|
|
14
|
+
sources: PictureSourceSpec[];
|
|
15
|
+
/** srcset for the trailing <img> (the desktop base raster). */
|
|
16
|
+
imgSrcSet?: string;
|
|
17
|
+
imgSizes?: string;
|
|
18
|
+
width?: number;
|
|
19
|
+
height?: number;
|
|
20
|
+
};
|
|
21
|
+
//#endregion
|
|
22
|
+
export { PicturePlan, PictureSourceSpec };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
//#region src/primitives/picture-sources.ts
|
|
2
|
+
function capSrcset(srcset, maxRungWidth) {
|
|
3
|
+
const entries = srcset.split(",").map((s) => s.trim()).filter(Boolean).map((cand) => {
|
|
4
|
+
const m = cand.match(/(\d+)w\s*$/);
|
|
5
|
+
return {
|
|
6
|
+
cand,
|
|
7
|
+
w: m ? Number(m[1]) : Number.POSITIVE_INFINITY
|
|
8
|
+
};
|
|
9
|
+
}).sort((a, b) => a.w - b.w);
|
|
10
|
+
if (entries.length === 0) return srcset;
|
|
11
|
+
if (maxRungWidth == null) return entries.map((event) => event.cand).join(", ");
|
|
12
|
+
const kept = entries.filter((event) => event.w <= maxRungWidth);
|
|
13
|
+
return (kept.length > 0 ? kept : [entries[0]]).map((event) => event.cand).join(", ");
|
|
14
|
+
}
|
|
15
|
+
function buildPictureSources(args) {
|
|
16
|
+
const { responsive, responsiveMobile, sizes, maxRungWidth, mobileBreakpoint } = args;
|
|
17
|
+
if (!responsive || responsive.sources.length === 0) return { sources: [] };
|
|
18
|
+
const sources = [];
|
|
19
|
+
if (responsiveMobile && responsiveMobile.sources.length > 0 && mobileBreakpoint) {
|
|
20
|
+
const media = `(max-width: ${mobileBreakpoint - 1}px)`;
|
|
21
|
+
const m = responsiveMobile.sources;
|
|
22
|
+
const mBase = m[m.length - 1];
|
|
23
|
+
for (const s of m.slice(0, -1)) sources.push({
|
|
24
|
+
type: s.type,
|
|
25
|
+
media,
|
|
26
|
+
srcSet: capSrcset(s.srcset, maxRungWidth),
|
|
27
|
+
sizes
|
|
28
|
+
});
|
|
29
|
+
sources.push({
|
|
30
|
+
media,
|
|
31
|
+
srcSet: capSrcset(mBase.srcset, maxRungWidth),
|
|
32
|
+
sizes
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
const d = responsive.sources;
|
|
36
|
+
const dBase = d[d.length - 1];
|
|
37
|
+
for (const s of d.slice(0, -1)) sources.push({
|
|
38
|
+
type: s.type,
|
|
39
|
+
srcSet: capSrcset(s.srcset, maxRungWidth),
|
|
40
|
+
sizes
|
|
41
|
+
});
|
|
42
|
+
return {
|
|
43
|
+
sources,
|
|
44
|
+
imgSrcSet: capSrcset(dBase.srcset, maxRungWidth),
|
|
45
|
+
imgSizes: sizes,
|
|
46
|
+
width: responsive.width,
|
|
47
|
+
height: responsive.height
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
//#endregion
|
|
52
|
+
export { buildPictureSources };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//#region src/rules/registry.ts
|
|
2
|
+
/**
|
|
3
|
+
* `no-hex` → `docs/rules/no-hex.md`.
|
|
4
|
+
*
|
|
5
|
+
* Package-relative on purpose: the caller decides what to resolve it against. A
|
|
6
|
+
* printer prefixes the resolved package root so an author sees a path they can open
|
|
7
|
+
* with no network (`node_modules/@homepages/template-kit/docs/rules/no-hex.md`);
|
|
8
|
+
* ESLint's `meta.docs.url` uses it as-is. This module never touches the filesystem —
|
|
9
|
+
* the ESLint preset imports it.
|
|
10
|
+
*/
|
|
11
|
+
function docsPathForRule(id) {
|
|
12
|
+
return `docs/rules/${id}.md`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
//#endregion
|
|
16
|
+
export { docsPathForRule };
|