@homepages/template-kit 0.0.0 → 0.1.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 (90) hide show
  1. package/CHANGELOG.md +182 -0
  2. package/README.md +71 -14
  3. package/dist/base.css +85 -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 +307 -0
  17. package/dist/design-system/theme.js +95 -0
  18. package/dist/eslint/is-client-file.js +79 -0
  19. package/dist/eslint/rules/image-bare-needs-reason.js +38 -0
  20. package/dist/eslint/rules/no-client-directive-in-contract.js +26 -0
  21. package/dist/eslint/rules/no-client-runtime-in-server.js +99 -0
  22. package/dist/eslint/rules/no-css-import-from-render-path.js +23 -0
  23. package/dist/eslint/rules/no-hex.js +111 -0
  24. package/dist/eslint/rules/no-inline-style.js +23 -0
  25. package/dist/eslint/rules/no-nondeterminism.js +44 -0
  26. package/dist/eslint/rules/no-raw-element.js +32 -0
  27. package/dist/eslint/rules/props-from-schema.js +32 -0
  28. package/dist/eslint/rules/serializable-island-props.js +108 -0
  29. package/dist/eslint/rules/slot-marker-literal.js +33 -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 +37 -13
  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/schema/fill-spec.d.ts +692 -0
  59. package/dist/schema/fill-spec.js +98 -0
  60. package/dist/schema/fixture-schema.d.ts +143 -0
  61. package/dist/schema/fixture-schema.js +113 -0
  62. package/dist/schema/index.d.ts +16 -0
  63. package/dist/schema/manifest.d.ts +527 -0
  64. package/dist/schema/manifest.js +73 -0
  65. package/dist/schema/property-facts.d.ts +44 -0
  66. package/dist/schema/resolve-variants.d.ts +11 -0
  67. package/dist/schema/resolve-variants.js +15 -0
  68. package/dist/schema/runtime-values.d.ts +45 -0
  69. package/dist/schema/section-nav.d.ts +7 -0
  70. package/dist/schema/section-props.d.ts +69 -0
  71. package/dist/schema/section-schema.d.ts +533 -0
  72. package/dist/schema/section-schema.js +72 -0
  73. package/dist/schema/slot-types.d.ts +117 -0
  74. package/dist/schema/slot-types.js +181 -0
  75. package/dist/schema/source.d.ts +13 -0
  76. package/dist/schema/source.js +12 -0
  77. package/dist/schema/synthesize-nullable.d.ts +18 -0
  78. package/dist/schema/synthesize-nullable.js +47 -0
  79. package/dist/styles.css +131 -9
  80. package/docs/INDEX.md +6 -4
  81. package/docs/eslint.md +90 -0
  82. package/docs/islands.md +150 -0
  83. package/docs/llms.txt +30 -3
  84. package/docs/primitives.md +214 -0
  85. package/docs/schema-system.md +240 -0
  86. package/docs/theme-and-css.md +179 -0
  87. package/package.json +30 -6
  88. package/tsconfig.base.json +16 -0
  89. package/tsconfig.json +2 -13
  90. package/dist/src-CrdHXijV.js +0 -23
package/dist/index.d.ts CHANGED
@@ -1,16 +1,40 @@
1
- //#region src/kit-smoke.d.ts
1
+ import { ResponsiveImage, ResponsiveSource } from "./contracts/image-descriptor.js";
2
+ import { Coordinates, FloorplanAssignment, PropertyFacts, UnitRow } from "./schema/property-facts.js";
3
+ import { COLLECTIONS, CONTENT_SLOT_TYPES, CollectionField, CollectionIdKey, CollectionOwnership, ContentSlotType, DECISION_TYPES, DERIVED_INPUT_FIELDS, DecisionType, DerivedInputName, FORMATTER_NAMES, FormatterName, REFERRAL_FIELDS, REFERRAL_IMAGE_FIELDS, REF_SLOT_TYPES, RefSlotType, ReferralField, ReferralImageField, SLOT_TYPES, SOURCE_FIELDS, SlotType, SourceFieldContainer, SourceFieldDescriptor, SourceFieldName, SourceFieldValueType, WritebackTarget, collectionForSlot } from "./contracts/slot-catalog.js";
4
+ import { AddressParts, TRANSFORMS, TRANSFORM_NAMES, TransformDescriptor, TransformFn, TransformInput, TransformName, TransformUnit } from "./contracts/transforms.js";
5
+ import { VariantSelectorFn, VariantsMeta, makeResolveVariants } from "./schema/resolve-variants.js";
6
+ import { ContentSlot, SLOT_TYPE_DESCRIPTORS, Slot as Slot$1, SlotDef, defaultForSlotType, slotDefTypes } from "./schema/slot-types.js";
7
+ import { VERIFICATION_WIDTHS, VerificationWidth } from "./design-system/breakpoints.js";
8
+ import { FrameConfig, ImageCollectionValue, ImageValue, PoiRow } from "./schema/runtime-values.js";
9
+ import { SectionNav } from "./schema/section-nav.js";
10
+ import { OptionValue, SectionProps, SlotValue } from "./schema/section-props.js";
11
+ import { derived, direct } from "./schema/source.js";
12
+ import { OptionDef, SectionMeta, SectionMetaShape, SectionSchema, SectionSchemaResolved, SectionSchemaShape, SlotGroupDef, SlotGroupDefShape } from "./schema/section-schema.js";
13
+ import { Decision, FillSpec, FillSpecShape } from "./schema/fill-spec.js";
14
+ import { FixtureModule, FixtureModuleShape, FixtureOverride, ResolvedFixture, buildFixtureSet, buildInvariantFixtures } from "./schema/fixture-schema.js";
15
+ import { TemplateManifest, TemplateManifestShape } from "./schema/manifest.js";
16
+ import "./schema/index.js";
17
+ import { ATTR_ISLAND_ID, ATTR_ISLAND_KEY, ATTR_ISLAND_PROPS, ISLAND_ELEMENT, IslandComponent, IslandEditorOptions, IslandModule, IslandProps, IslandPropsError, ShieldMode, deserializeIslandProps, islandId, serializeIslandProps } from "./islands/contract.js";
18
+ import { IslandMarker, IslandMarkerProps } from "./islands/marker.js";
19
+ import { Image } from "./primitives/Image.js";
20
+ import { PicturePlan, PictureSourceSpec } from "./primitives/picture-sources.js";
21
+ import { Section } from "./primitives/Section.js";
22
+ import { Slot } from "./primitives/Slot.js";
23
+ import { SlotGroup } from "./primitives/SlotGroup.js";
24
+ import { SlotItem } from "./primitives/SlotItem.js";
25
+ import "./primitives/index.js";
26
+ import { ATTR_SECTION_INSTANCE_ID, ATTR_SLOT_GROUP, ATTR_SLOT_ID, ATTR_SLOT_ITEM, ATTR_SLOT_TEXT_LEAF, ATTR_TR_ENHANCE } from "./contracts/markers.js";
27
+ import { COLOR_ROLE_VARS, ColorRole, FONT_ROLE_VARS, LAYOUT_VARS, RADIUS_VARS, SHADOW_VARS, TYPE_STEP_VARS, TokenTheme, TokenThemeSchema, compileThemeToCss } from "./design-system/theme.js";
28
+ //#region src/index.d.ts
2
29
  /**
3
- * Boundary probe, not authoring surface. Its only job is to make the package
4
- * boundary testable: React stays a peer, types resolve, and Tailwind's
5
- * cross-package @source scan reaches this file's compiled output.
6
- * Removed when the real contract primitives land.
30
+ * The published version of this kit.
31
+ *
32
+ * Read from package.json and inlined at build time, so it cannot drift: changesets
33
+ * owns package.json's version, and a hardcoded constant here would have to be
34
+ * hand-edited on every release — on a branch changesets force-pushes, which would
35
+ * silently discard the edit. `dist/cli.js --version` is asserted against
36
+ * package.json to prove the inlining actually happened.
7
37
  */
8
- declare function KitSmoke({ label }: {
9
- label: string;
10
- }): import("react").JSX.Element;
11
- //#endregion
12
- //#region src/index.d.ts
13
- /** The published version of this kit. Kept in sync with package.json by src/index.test.tsx. */
14
- declare const KIT_VERSION = "0.0.0";
38
+ declare const KIT_VERSION: string;
15
39
  //#endregion
16
- export { KIT_VERSION, KitSmoke };
40
+ export { ATTR_ISLAND_ID, ATTR_ISLAND_KEY, ATTR_ISLAND_PROPS, ATTR_SECTION_INSTANCE_ID, ATTR_SLOT_GROUP, ATTR_SLOT_ID, ATTR_SLOT_ITEM, ATTR_SLOT_TEXT_LEAF, ATTR_TR_ENHANCE, type AddressParts, COLLECTIONS, COLOR_ROLE_VARS, CONTENT_SLOT_TYPES, type CollectionField, type CollectionIdKey, type CollectionOwnership, type ColorRole, ContentSlot, type ContentSlotType, type Coordinates, DECISION_TYPES, DERIVED_INPUT_FIELDS, Decision, type DecisionType, type DerivedInputName, FONT_ROLE_VARS, FORMATTER_NAMES, FillSpec, type FillSpecShape, type FixtureModule, FixtureModuleShape, type FixtureOverride, type FloorplanAssignment, type FormatterName, type FrameConfig, ISLAND_ELEMENT, Image, type ImageCollectionValue, type ImageValue, IslandComponent, IslandEditorOptions, IslandMarker, type IslandMarkerProps, IslandModule, IslandProps, IslandPropsError, KIT_VERSION, LAYOUT_VARS, OptionDef, type OptionValue, type PicturePlan, type PictureSourceSpec, type PoiRow, type PropertyFacts, RADIUS_VARS, REFERRAL_FIELDS, REFERRAL_IMAGE_FIELDS, REF_SLOT_TYPES, type RefSlotType, type ReferralField, type ReferralImageField, type ResolvedFixture, type ResponsiveImage, type ResponsiveSource, SHADOW_VARS, SLOT_TYPES, SLOT_TYPE_DESCRIPTORS, SOURCE_FIELDS, Section, SectionMeta, type SectionMetaShape, type SectionNav, type SectionProps, SectionSchema, type SectionSchemaResolved, type SectionSchemaShape, ShieldMode, Slot, SlotDef, type Slot$1 as SlotDefinition, SlotGroup, SlotGroupDef, type SlotGroupDefShape, SlotItem, type SlotType, type SlotValue, type SourceFieldContainer, type SourceFieldDescriptor, type SourceFieldName, type SourceFieldValueType, TRANSFORMS, TRANSFORM_NAMES, TYPE_STEP_VARS, TemplateManifest, type TemplateManifestShape, type TokenTheme, TokenThemeSchema, type TransformDescriptor, type TransformFn, type TransformInput, type TransformName, type TransformUnit, type UnitRow, VERIFICATION_WIDTHS, type VariantSelectorFn, type VariantsMeta, type VerificationWidth, type WritebackTarget, buildFixtureSet, buildInvariantFixtures, collectionForSlot, compileThemeToCss, defaultForSlotType, derived, deserializeIslandProps, direct, islandId, makeResolveVariants, serializeIslandProps, slotDefTypes };
package/dist/index.js CHANGED
@@ -1,3 +1,35 @@
1
- import { n as KitSmoke, t as KIT_VERSION } from "./src-CrdHXijV.js";
1
+ import { version } from "./package.js";
2
+ import { COLLECTIONS, CONTENT_SLOT_TYPES, DECISION_TYPES, DERIVED_INPUT_FIELDS, FORMATTER_NAMES, REFERRAL_FIELDS, REFERRAL_IMAGE_FIELDS, REF_SLOT_TYPES, SLOT_TYPES, SOURCE_FIELDS, collectionForSlot } from "./contracts/slot-catalog.js";
3
+ import { ContentSlot, SLOT_TYPE_DESCRIPTORS, SlotDef, defaultForSlotType, slotDefTypes } from "./schema/slot-types.js";
4
+ import { derived, direct } from "./schema/source.js";
5
+ import { OptionDef, SectionMeta, SectionSchema, SlotGroupDef } from "./schema/section-schema.js";
6
+ import { makeResolveVariants } from "./schema/resolve-variants.js";
7
+ import { Decision, FillSpec } from "./schema/fill-spec.js";
8
+ import { FixtureModuleShape, buildFixtureSet, buildInvariantFixtures } from "./schema/fixture-schema.js";
9
+ import { TemplateManifest } from "./schema/manifest.js";
10
+ import { TRANSFORMS, TRANSFORM_NAMES } from "./contracts/transforms.js";
11
+ import { VERIFICATION_WIDTHS } from "./design-system/breakpoints.js";
12
+ import { ATTR_ISLAND_ID, ATTR_ISLAND_KEY, ATTR_ISLAND_PROPS, ISLAND_ELEMENT, IslandPropsError, deserializeIslandProps, islandId, serializeIslandProps } from "./islands/contract.js";
13
+ import { IslandMarker } from "./islands/marker.js";
14
+ import { ATTR_SECTION_INSTANCE_ID, ATTR_SLOT_GROUP, ATTR_SLOT_ID, ATTR_SLOT_ITEM, ATTR_SLOT_TEXT_LEAF, ATTR_TR_ENHANCE } from "./contracts/markers.js";
15
+ import { Image } from "./primitives/Image.js";
16
+ import { Section } from "./primitives/Section.js";
17
+ import { Slot } from "./primitives/Slot.js";
18
+ import { SlotGroup } from "./primitives/SlotGroup.js";
19
+ import { SlotItem } from "./primitives/SlotItem.js";
20
+ import { COLOR_ROLE_VARS, FONT_ROLE_VARS, LAYOUT_VARS, RADIUS_VARS, SHADOW_VARS, TYPE_STEP_VARS, TokenThemeSchema, compileThemeToCss } from "./design-system/theme.js";
2
21
 
3
- export { KIT_VERSION, KitSmoke };
22
+ //#region src/index.ts
23
+ /**
24
+ * The published version of this kit.
25
+ *
26
+ * Read from package.json and inlined at build time, so it cannot drift: changesets
27
+ * owns package.json's version, and a hardcoded constant here would have to be
28
+ * hand-edited on every release — on a branch changesets force-pushes, which would
29
+ * silently discard the edit. `dist/cli.js --version` is asserted against
30
+ * package.json to prove the inlining actually happened.
31
+ */
32
+ const KIT_VERSION = version;
33
+
34
+ //#endregion
35
+ export { ATTR_ISLAND_ID, ATTR_ISLAND_KEY, ATTR_ISLAND_PROPS, ATTR_SECTION_INSTANCE_ID, ATTR_SLOT_GROUP, ATTR_SLOT_ID, ATTR_SLOT_ITEM, ATTR_SLOT_TEXT_LEAF, ATTR_TR_ENHANCE, COLLECTIONS, COLOR_ROLE_VARS, CONTENT_SLOT_TYPES, ContentSlot, DECISION_TYPES, DERIVED_INPUT_FIELDS, Decision, FONT_ROLE_VARS, FORMATTER_NAMES, FillSpec, FixtureModuleShape, ISLAND_ELEMENT, Image, IslandMarker, IslandPropsError, KIT_VERSION, LAYOUT_VARS, OptionDef, RADIUS_VARS, REFERRAL_FIELDS, REFERRAL_IMAGE_FIELDS, REF_SLOT_TYPES, SHADOW_VARS, SLOT_TYPES, SLOT_TYPE_DESCRIPTORS, SOURCE_FIELDS, Section, SectionMeta, SectionSchema, Slot, SlotDef, SlotGroup, SlotGroupDef, SlotItem, TRANSFORMS, TRANSFORM_NAMES, TYPE_STEP_VARS, TemplateManifest, TokenThemeSchema, VERIFICATION_WIDTHS, buildFixtureSet, buildInvariantFixtures, collectionForSlot, compileThemeToCss, defaultForSlotType, derived, deserializeIslandProps, direct, islandId, makeResolveVariants, serializeIslandProps, slotDefTypes };
@@ -0,0 +1,30 @@
1
+ import { ATTR_ISLAND_ID, ATTR_ISLAND_KEY, ATTR_ISLAND_PROPS, ISLAND_ELEMENT, IslandComponent, IslandEditorOptions, IslandModule, IslandProps, IslandPropsError, ShieldMode, deserializeIslandProps, islandId, serializeIslandProps } from "./islands/contract.js";
2
+ //#region src/islands/runtime.d.ts
3
+ /** What the loader knows about one island once its module has loaded. */
4
+ interface IslandContext {
5
+ id: string;
6
+ key: string;
7
+ element: HTMLElement;
8
+ editor?: IslandEditorOptions;
9
+ }
10
+ type IslandHandle = IslandContext;
11
+ interface HydrateIslandsOptions {
12
+ /** Where to look for markers. Default: the whole document. */
13
+ root?: ParentNode;
14
+ /** Resolve an island key to its module. The host owns this map (the build generates it). */
15
+ load: (islandKey: string) => Promise<IslandModule>;
16
+ /** Decide whether to hydrate, once the module (and so its editor options) is known. */
17
+ shouldHydrate?: (ctx: IslandContext) => boolean;
18
+ /** Merge `editor.props` over the server-rendered props (editor canvas only). */
19
+ applyEditorProps?: boolean;
20
+ /** A failed island is reported here; its neighbors still hydrate. */
21
+ onError?: (error: unknown, ctx: IslandContext) => void;
22
+ }
23
+ declare function hydrateIslands(options: HydrateIslandsOptions): Promise<IslandHandle[]>;
24
+ /**
25
+ * Unmount every island hydrated under `root`. Synchronous, so the editor can call it
26
+ * immediately before replacing a section's innerHTML.
27
+ */
28
+ declare function unmountIslands(root?: ParentNode): void;
29
+ //#endregion
30
+ export { ATTR_ISLAND_ID, ATTR_ISLAND_KEY, ATTR_ISLAND_PROPS, HydrateIslandsOptions, ISLAND_ELEMENT, IslandComponent, IslandContext, IslandEditorOptions, IslandHandle, IslandModule, IslandProps, IslandPropsError, ShieldMode, deserializeIslandProps, hydrateIslands, islandId, serializeIslandProps, unmountIslands };
@@ -0,0 +1,73 @@
1
+ import { ATTR_ISLAND_ID, ATTR_ISLAND_KEY, ATTR_ISLAND_PROPS, ISLAND_ELEMENT, IslandPropsError, deserializeIslandProps, islandId, serializeIslandProps } from "./islands/contract.js";
2
+
3
+ //#region src/islands/runtime.ts
4
+ const roots = /* @__PURE__ */ new WeakMap();
5
+ async function hydrateIslands(options) {
6
+ const root = options.root ?? document;
7
+ const markers = Array.from(root.querySelectorAll(`[${ATTR_ISLAND_KEY}]`)).filter((element) => !roots.has(element));
8
+ if (markers.length === 0) return [];
9
+ const payloads = /* @__PURE__ */ new Map();
10
+ const duplicateIds = /* @__PURE__ */ new Set();
11
+ for (const script of root.querySelectorAll(`script[${ATTR_ISLAND_PROPS}]`)) {
12
+ const id = script.getAttribute(ATTR_ISLAND_PROPS);
13
+ if (id === null) continue;
14
+ if (payloads.has(id)) duplicateIds.add(id);
15
+ payloads.set(id, script.textContent ?? "");
16
+ }
17
+ const [{ createElement }, { hydrateRoot }] = await Promise.all([import("react"), import("react-dom/client")]);
18
+ const handles = [];
19
+ for (const element of markers) {
20
+ const key = element.getAttribute("data-tr-island") ?? "";
21
+ const id = element.getAttribute("data-tr-island-id") ?? "";
22
+ const ctx = {
23
+ id,
24
+ key,
25
+ element
26
+ };
27
+ try {
28
+ if (duplicateIds.has(id)) throw new Error(`island "${id}" (${key}) shares its id with another ${ATTR_ISLAND_PROPS} payload — refusing to hydrate either instance rather than guess whose props belong to which marker`);
29
+ const payload = payloads.get(id);
30
+ if (payload === void 0) throw new Error(`island "${id}" (${key}) has no ${ATTR_ISLAND_PROPS} payload`);
31
+ const props = deserializeIslandProps(payload);
32
+ const module = await options.load(key);
33
+ ctx.editor = module.editor;
34
+ if (options.shouldHydrate && !options.shouldHydrate(ctx)) continue;
35
+ if (roots.has(element)) continue;
36
+ if (!element.isConnected) continue;
37
+ const reactRoot = hydrateRoot(element, createElement(module.default, props), {
38
+ onUncaughtError: (renderError) => {
39
+ options.onError?.(renderError, ctx);
40
+ },
41
+ onCaughtError: (renderError) => {
42
+ options.onError?.(renderError, ctx);
43
+ }
44
+ });
45
+ roots.set(element, reactRoot);
46
+ const editorProps = options.applyEditorProps ? module.editor?.props : void 0;
47
+ if (editorProps) reactRoot.render(createElement(module.default, {
48
+ ...props,
49
+ ...editorProps
50
+ }));
51
+ handles.push(ctx);
52
+ } catch (error) {
53
+ options.onError?.(error, ctx);
54
+ }
55
+ }
56
+ return handles;
57
+ }
58
+ /**
59
+ * Unmount every island hydrated under `root`. Synchronous, so the editor can call it
60
+ * immediately before replacing a section's innerHTML.
61
+ */
62
+ function unmountIslands(root = document) {
63
+ const self = root instanceof Element && root.matches(`[${"data-tr-island"}]`) ? [root] : [];
64
+ for (const element of [...self, ...root.querySelectorAll(`[${ATTR_ISLAND_KEY}]`)]) {
65
+ const reactRoot = roots.get(element);
66
+ if (!reactRoot) continue;
67
+ roots.delete(element);
68
+ reactRoot.unmount();
69
+ }
70
+ }
71
+
72
+ //#endregion
73
+ export { ATTR_ISLAND_ID, ATTR_ISLAND_KEY, ATTR_ISLAND_PROPS, ISLAND_ELEMENT, IslandPropsError, deserializeIslandProps, hydrateIslands, islandId, serializeIslandProps, unmountIslands };
@@ -0,0 +1,56 @@
1
+ import { ComponentType } from "react";
2
+ //#region src/islands/contract.d.ts
3
+ /** The custom element wrapping an island's server-rendered output. Layout-invisible (`display:contents`). */
4
+ declare const ISLAND_ELEMENT = "tr-island";
5
+ /** Island key on the marker: `<section-key>:<ComponentName>`, e.g. `spw-essential/floorplan:UnitFilter`. */
6
+ declare const ATTR_ISLAND_KEY = "data-tr-island";
7
+ /** Island id on the marker: unique in the document, stable across re-renders of the same island. */
8
+ declare const ATTR_ISLAND_ID = "data-tr-island-id";
9
+ /** On the JSON `<script>` sibling; its value is the island id of the marker the props belong to. */
10
+ declare const ATTR_ISLAND_PROPS = "data-tr-island-props";
11
+ /** Props handed from a server-rendered Renderer to a client component. Strictly JSON-serializable. */
12
+ type IslandProps = Record<string, unknown>;
13
+ /** How the editor sizes the transparent selection shield over a live island. */
14
+ type ShieldMode = "fill-self" | "fill-parent";
15
+ /** Optional `export const editor` on a client component: how it behaves inside the editor canvas. */
16
+ interface IslandEditorOptions {
17
+ /** Hydrate this island in the editor canvas (behind the editor's selection gate). */
18
+ live?: boolean;
19
+ /** Shield placement. Default "fill-parent". */
20
+ shieldMode?: ShieldMode;
21
+ /** Which element(s) inside the island the shield covers. Omit → the whole island. Runs in the browser. */
22
+ liveSurface?: (root: HTMLElement) => HTMLElement[];
23
+ /** Editor-only props, merged over the server-rendered props (e.g. an accordion's single-open canvas mirror). */
24
+ props?: IslandProps;
25
+ }
26
+ /**
27
+ * A client component's own prop type is the author's business — island props are
28
+ * validated at runtime (serializeIslandProps), not through this alias, and a
29
+ * narrower type here would reject every real component through parameter
30
+ * contravariance.
31
+ */
32
+ type IslandComponent = ComponentType<any>;
33
+ /** The shape a `"use client"` module must have: a default-exported component, optionally `editor`. */
34
+ interface IslandModule {
35
+ default: IslandComponent;
36
+ editor?: IslandEditorOptions;
37
+ }
38
+ /** A prop that cannot cross the server→browser boundary, or a payload that cannot be read back. */
39
+ declare class IslandPropsError extends Error {
40
+ name: string;
41
+ }
42
+ /**
43
+ * Compose an island id. `scope` is the emitter's per-section-instance identity —
44
+ * without it, two instances of one section on a page collide.
45
+ */
46
+ declare function islandId(scope: string, islandKey: string, ordinal: number): string;
47
+ /**
48
+ * Validate + encode island props for the `<script type="application/json">` payload.
49
+ * Throws IslandPropsError naming the offending path. Every `<` is emitted as its
50
+ * unicode escape so a string value containing a closing script tag cannot break out.
51
+ */
52
+ declare function serializeIslandProps(props: IslandProps): string;
53
+ /** Read a props payload back. Throws IslandPropsError on anything that is not a JSON object. */
54
+ declare function deserializeIslandProps(payload: string): IslandProps;
55
+ //#endregion
56
+ export { ATTR_ISLAND_ID, ATTR_ISLAND_KEY, ATTR_ISLAND_PROPS, ISLAND_ELEMENT, IslandComponent, IslandEditorOptions, IslandModule, IslandProps, IslandPropsError, ShieldMode, deserializeIslandProps, islandId, serializeIslandProps };
@@ -0,0 +1,71 @@
1
+ //#region src/islands/contract.ts
2
+ /** The custom element wrapping an island's server-rendered output. Layout-invisible (`display:contents`). */
3
+ const ISLAND_ELEMENT = "tr-island";
4
+ /** Island key on the marker: `<section-key>:<ComponentName>`, e.g. `spw-essential/floorplan:UnitFilter`. */
5
+ const ATTR_ISLAND_KEY = "data-tr-island";
6
+ /** Island id on the marker: unique in the document, stable across re-renders of the same island. */
7
+ const ATTR_ISLAND_ID = "data-tr-island-id";
8
+ /** On the JSON `<script>` sibling; its value is the island id of the marker the props belong to. */
9
+ const ATTR_ISLAND_PROPS = "data-tr-island-props";
10
+ /** A prop that cannot cross the server→browser boundary, or a payload that cannot be read back. */
11
+ var IslandPropsError = class extends Error {
12
+ name = "IslandPropsError";
13
+ };
14
+ /**
15
+ * Compose an island id. `scope` is the emitter's per-section-instance identity —
16
+ * without it, two instances of one section on a page collide.
17
+ */
18
+ function islandId(scope, islandKey, ordinal) {
19
+ return `${scope}~${islandKey}#${ordinal}`;
20
+ }
21
+ const JSON_TYPES = /* @__PURE__ */ new Set([
22
+ "string",
23
+ "number",
24
+ "boolean",
25
+ "undefined"
26
+ ]);
27
+ function assertSerializable(value, path) {
28
+ if (value === null) return;
29
+ const type = typeof value;
30
+ if (JSON_TYPES.has(type)) {
31
+ if (type === "number" && !Number.isFinite(value)) throw new IslandPropsError(`${path} is ${String(value)} — island props must be JSON-serializable, and JSON has no NaN or Infinity`);
32
+ return;
33
+ }
34
+ if (type !== "object") throw new IslandPropsError(`${path} is a ${type} — island props must be JSON-serializable (string, number, boolean, null, plain object, array)`);
35
+ if (Array.isArray(value)) {
36
+ value.forEach((entry, i) => {
37
+ assertSerializable(entry, `${path}[${i}]`);
38
+ });
39
+ return;
40
+ }
41
+ const proto = Object.getPrototypeOf(value);
42
+ if (proto !== Object.prototype && proto !== null) {
43
+ const ctor = value.constructor?.name ?? "class instance";
44
+ throw new IslandPropsError(`${path} is a ${ctor} — island props must be JSON-serializable (plain objects and arrays only)`);
45
+ }
46
+ for (const [key, entry] of Object.entries(value)) assertSerializable(entry, `${path}.${key}`);
47
+ }
48
+ /**
49
+ * Validate + encode island props for the `<script type="application/json">` payload.
50
+ * Throws IslandPropsError naming the offending path. Every `<` is emitted as its
51
+ * unicode escape so a string value containing a closing script tag cannot break out.
52
+ */
53
+ function serializeIslandProps(props) {
54
+ if (props === null || typeof props !== "object" || Array.isArray(props)) throw new IslandPropsError("props must be a plain object, not an array or a bare value");
55
+ assertSerializable(props, "props");
56
+ return JSON.stringify(props).replace(/</g, "\\u003c");
57
+ }
58
+ /** Read a props payload back. Throws IslandPropsError on anything that is not a JSON object. */
59
+ function deserializeIslandProps(payload) {
60
+ let parsed;
61
+ try {
62
+ parsed = JSON.parse(payload);
63
+ } catch (cause) {
64
+ throw new IslandPropsError(`island props payload is not valid JSON: ${String(cause)}`);
65
+ }
66
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) throw new IslandPropsError("island props payload must be a JSON object");
67
+ return parsed;
68
+ }
69
+
70
+ //#endregion
71
+ export { ATTR_ISLAND_ID, ATTR_ISLAND_KEY, ATTR_ISLAND_PROPS, ISLAND_ELEMENT, IslandPropsError, deserializeIslandProps, islandId, serializeIslandProps };
@@ -0,0 +1,16 @@
1
+ import { IslandProps } from "./contract.js";
2
+ import { ReactNode } from "react";
3
+ //#region src/islands/marker.d.ts
4
+ interface IslandMarkerProps {
5
+ /** `<section-key>:<ComponentName>` — the key the browser loader resolves the module by. */
6
+ islandKey: string;
7
+ /** Unique in the document, stable across re-renders of this island. Compose with `islandId()`. */
8
+ islandId: string;
9
+ /** Strictly JSON-serializable. A bad prop throws IslandPropsError here, at render time. */
10
+ props: IslandProps;
11
+ /** The client component, server-rendered. */
12
+ children: ReactNode;
13
+ }
14
+ declare function IslandMarker({ islandKey, islandId, props, children }: IslandMarkerProps): ReactNode;
15
+ //#endregion
16
+ export { IslandMarker, IslandMarkerProps };
@@ -0,0 +1,18 @@
1
+ import { ATTR_ISLAND_ID, ATTR_ISLAND_KEY, ATTR_ISLAND_PROPS, ISLAND_ELEMENT, serializeIslandProps } from "./contract.js";
2
+ import { Fragment, createElement } from "react";
3
+
4
+ //#region src/islands/marker.tsx
5
+ function IslandMarker({ islandKey, islandId, props, children }) {
6
+ return createElement(Fragment, null, createElement(ISLAND_ELEMENT, {
7
+ style: { display: "contents" },
8
+ [ATTR_ISLAND_KEY]: islandKey,
9
+ [ATTR_ISLAND_ID]: islandId
10
+ }, children), createElement("script", {
11
+ type: "application/json",
12
+ [ATTR_ISLAND_PROPS]: islandId,
13
+ dangerouslySetInnerHTML: { __html: serializeIslandProps(props) }
14
+ }));
15
+ }
16
+
17
+ //#endregion
18
+ export { IslandMarker };
@@ -0,0 +1,5 @@
1
+ //#region package.json
2
+ var version = "0.1.0";
3
+
4
+ //#endregion
5
+ export { version };
@@ -0,0 +1,49 @@
1
+ import { ResponsiveImage } from "../contracts/image-descriptor.js";
2
+ import { ImgHTMLAttributes, ReactNode } from "react";
3
+ //#region src/primitives/Image.d.ts
4
+ type Props = Omit<ImgHTMLAttributes<HTMLImageElement>, "src" | "alt"> & {
5
+ /** Image URL. Null/empty in framed mode renders a fallback rect; in bare mode an empty <img>. */
6
+ src: string | null | undefined;
7
+ /** Required alt text. Empty string for decorative images. */
8
+ alt: string;
9
+ /** CSS aspect-ratio reserved on the framed wrapper; survives missing `src`. */
10
+ aspectRatio?: string;
11
+ /** Slot binding — emits `data-slot-id` on the wrapper for the editor click resolver. */
12
+ slotId?: string;
13
+ /** Escape hatch — a bare <img>, no wrapper. Requires a `// bare:` reason comment at the call site. */
14
+ bare?: boolean;
15
+ /** Legacy single-URL mobile crop (no responsive descriptor). Composes with `mobileBreakpoint`. */
16
+ srcMobile?: string | null;
17
+ /** Desktop↔mobile switch width (px); the `<picture>` mobile source uses `max-width: breakpoint − 1`. */
18
+ mobileBreakpoint?: number;
19
+ /** Desktop responsive descriptor (ImageValue.responsive). Absent ⇒ today's single <img src>. */
20
+ responsive?: ResponsiveImage | null;
21
+ /** Mobile-crop descriptor (ImageValue.mobile.responsive); composes with `mobileBreakpoint`. */
22
+ responsiveMobile?: ResponsiveImage | null;
23
+ /** Slot `sizes` (e.g. "(min-width:1024px) 1200px, 100vw"); applied to every srcset candidate. */
24
+ sizes?: string;
25
+ /** Per-section srcset ceiling (px). Rungs wider than this are dropped (≥1 always kept). */
26
+ maxRungWidth?: number;
27
+ };
28
+ /**
29
+ * Image — the defensive image primitive. Sections never write a raw `<img>`.
30
+ *
31
+ * **Framed mode** (default): a wrapper div carries the `data-slot-id` marker, holds
32
+ * the optional aspect-ratio box, renders a neutral fallback rect when `src` is empty,
33
+ * and absolutely-positions the inner `<img>` with `object-cover`. Cropping is handled
34
+ * upstream — the platform delivers a pre-cropped asset — so the image is simply
35
+ * centered here.
36
+ *
37
+ * **Bare mode** (`bare`): a single `<img>`, no wrapper. For chrome images whose parent
38
+ * already establishes the layout box (a logo with `object-contain`; a slide inside its
39
+ * own aspect-ratio parent). An escape hatch — it forfeits the empty-state box.
40
+ *
41
+ * **Responsive:** given a `responsive` descriptor, the `<img>` is wrapped in `<picture>`
42
+ * with per-format AVIF/WebP `<source srcset>` — plus a media-gated mobile crop when
43
+ * `responsiveMobile` is set — and carries `sizes` and intrinsic `width`/`height`. With
44
+ * no descriptor it degrades to a plain single `<img src>`. All `<picture>` planning
45
+ * lives in `buildPictureSources`.
46
+ */
47
+ declare function Image({ src, alt, aspectRatio, slotId, bare, srcMobile, mobileBreakpoint, responsive, responsiveMobile, sizes, maxRungWidth, className, loading, style, ...rest }: Props): string | number | bigint | boolean | Iterable<ReactNode> | Promise<string | number | bigint | boolean | import("react").ReactPortal | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | import("react").JSX.Element | null | undefined;
48
+ //#endregion
49
+ export { Image };
@@ -0,0 +1,95 @@
1
+ import { ATTR_SLOT_ID } from "../contracts/markers.js";
2
+ import { buildPictureSources } from "./picture-sources.js";
3
+ import { jsx, jsxs } from "react/jsx-runtime";
4
+
5
+ //#region src/primitives/Image.tsx
6
+ /**
7
+ * Image — the defensive image primitive. Sections never write a raw `<img>`.
8
+ *
9
+ * **Framed mode** (default): a wrapper div carries the `data-slot-id` marker, holds
10
+ * the optional aspect-ratio box, renders a neutral fallback rect when `src` is empty,
11
+ * and absolutely-positions the inner `<img>` with `object-cover`. Cropping is handled
12
+ * upstream — the platform delivers a pre-cropped asset — so the image is simply
13
+ * centered here.
14
+ *
15
+ * **Bare mode** (`bare`): a single `<img>`, no wrapper. For chrome images whose parent
16
+ * already establishes the layout box (a logo with `object-contain`; a slide inside its
17
+ * own aspect-ratio parent). An escape hatch — it forfeits the empty-state box.
18
+ *
19
+ * **Responsive:** given a `responsive` descriptor, the `<img>` is wrapped in `<picture>`
20
+ * with per-format AVIF/WebP `<source srcset>` — plus a media-gated mobile crop when
21
+ * `responsiveMobile` is set — and carries `sizes` and intrinsic `width`/`height`. With
22
+ * no descriptor it degrades to a plain single `<img src>`. All `<picture>` planning
23
+ * lives in `buildPictureSources`.
24
+ */
25
+ function Image({ src, alt, aspectRatio, slotId, bare, srcMobile, mobileBreakpoint, responsive, responsiveMobile, sizes, maxRungWidth, className, loading, style, ...rest }) {
26
+ const plan = responsive ? buildPictureSources({
27
+ responsive,
28
+ responsiveMobile,
29
+ sizes,
30
+ maxRungWidth,
31
+ mobileBreakpoint
32
+ }) : null;
33
+ const hasResponsive = Boolean(plan && (plan.imgSrcSet || plan.sources.length > 0));
34
+ const usePicture = Boolean(src && srcMobile && mobileBreakpoint) && !hasResponsive;
35
+ const mobileMedia = mobileBreakpoint ? `(max-width: ${mobileBreakpoint - 1}px)` : void 0;
36
+ const responsiveImgAttrs = hasResponsive ? {
37
+ srcSet: plan.imgSrcSet,
38
+ sizes: plan.imgSizes,
39
+ width: plan.width,
40
+ height: plan.height
41
+ } : {};
42
+ function wrap(imgEl) {
43
+ if (hasResponsive && plan.sources.length > 0) return /* @__PURE__ */ jsxs("picture", { children: [plan.sources.map((s, i) => /* @__PURE__ */ jsx("source", {
44
+ type: s.type,
45
+ media: s.media,
46
+ srcSet: s.srcSet,
47
+ sizes: s.sizes
48
+ }, i)), imgEl] });
49
+ if (usePicture) return /* @__PURE__ */ jsxs("picture", { children: [/* @__PURE__ */ jsx("source", {
50
+ media: mobileMedia,
51
+ srcSet: srcMobile ?? void 0
52
+ }), imgEl] });
53
+ return imgEl;
54
+ }
55
+ if (bare) {
56
+ const cls = ["w-full", className].filter(Boolean).join(" ");
57
+ return wrap(/* @__PURE__ */ jsx("img", {
58
+ src: src ?? "",
59
+ alt,
60
+ loading: loading ?? "lazy",
61
+ className: cls,
62
+ style,
63
+ ...rest,
64
+ ...responsiveImgAttrs
65
+ }));
66
+ }
67
+ const containerCls = ["tr-image-frame relative block w-full overflow-hidden bg-surface-alt", className].filter(Boolean).join(" ");
68
+ const containerStyle = aspectRatio ? { aspectRatio } : void 0;
69
+ const slotAttrs = slotId ? { [ATTR_SLOT_ID]: slotId } : {};
70
+ if (!src) return /* @__PURE__ */ jsx("div", {
71
+ className: containerCls,
72
+ style: containerStyle,
73
+ role: "img",
74
+ "aria-label": alt || void 0,
75
+ ...slotAttrs
76
+ });
77
+ const img = /* @__PURE__ */ jsx("img", {
78
+ src,
79
+ alt,
80
+ loading: loading ?? "lazy",
81
+ decoding: "async",
82
+ className: "absolute inset-0 h-full w-full object-cover",
83
+ ...rest,
84
+ ...responsiveImgAttrs
85
+ });
86
+ return /* @__PURE__ */ jsx("div", {
87
+ className: containerCls,
88
+ style: containerStyle,
89
+ ...slotAttrs,
90
+ children: wrap(img)
91
+ });
92
+ }
93
+
94
+ //#endregion
95
+ export { Image };
@@ -0,0 +1,22 @@
1
+ import { HTMLAttributes, ReactNode } from "react";
2
+ //#region src/primitives/Section.d.ts
3
+ type Props = HTMLAttributes<HTMLElement> & {
4
+ as?: "section" | "div" | "article" | "header" | "footer" | "aside";
5
+ children: ReactNode;
6
+ };
7
+ /**
8
+ * The section root. Every section renderer roots at exactly one `<Section>`.
9
+ *
10
+ * Full-bleed by contract: 100% width with zero padding and margin (`.tr-section`,
11
+ * from the kit's base stylesheet), so consecutive sections butt edge-to-edge and a
12
+ * page composes as a vertical stack with no framework-injected whitespace. Vertical
13
+ * separation is the section's own job — a background, padding on a child element —
14
+ * never padding on this root, which would re-introduce gaps between sections and
15
+ * shrink the section's full-bleed background.
16
+ *
17
+ * When content needs a centered, max-width well, write it as JSX on a child:
18
+ * `w-full max-w-[var(--tr-container-max)] mx-auto px-[var(--tr-container-pad)]`.
19
+ */
20
+ declare function Section({ as, children, className, ...rest }: Props): import("react").JSX.Element;
21
+ //#endregion
22
+ export { Section };
@@ -0,0 +1,26 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+
3
+ //#region src/primitives/Section.tsx
4
+ /**
5
+ * The section root. Every section renderer roots at exactly one `<Section>`.
6
+ *
7
+ * Full-bleed by contract: 100% width with zero padding and margin (`.tr-section`,
8
+ * from the kit's base stylesheet), so consecutive sections butt edge-to-edge and a
9
+ * page composes as a vertical stack with no framework-injected whitespace. Vertical
10
+ * separation is the section's own job — a background, padding on a child element —
11
+ * never padding on this root, which would re-introduce gaps between sections and
12
+ * shrink the section's full-bleed background.
13
+ *
14
+ * When content needs a centered, max-width well, write it as JSX on a child:
15
+ * `w-full max-w-[var(--tr-container-max)] mx-auto px-[var(--tr-container-pad)]`.
16
+ */
17
+ function Section({ as = "section", children, className, ...rest }) {
18
+ return /* @__PURE__ */ jsx(as, {
19
+ className: ["tr-section", className].filter(Boolean).join(" "),
20
+ ...rest,
21
+ children
22
+ });
23
+ }
24
+
25
+ //#endregion
26
+ export { Section };
@@ -0,0 +1,50 @@
1
+ import { ElementType, HTMLAttributes, ReactNode } from "react";
2
+ //#region src/primitives/Slot.d.ts
3
+ type SlotProps = HTMLAttributes<HTMLElement> & {
4
+ /**
5
+ * Slot id from the section schema. Emitted as `data-slot-id` on the
6
+ * rendered element so the editor's click resolver can target the slot.
7
+ */
8
+ id: string;
9
+ /**
10
+ * Element to render. Defaults to `"div"`. Override when the slot's
11
+ * surrounding semantic role needs something else (`address`, `span`,
12
+ * `nav`, …).
13
+ */
14
+ as?: ElementType;
15
+ /**
16
+ * For a `text` slot rendered as a single leaf: applies `data-slot-text-leaf`
17
+ * to the immediate child element via `cloneElement`, so the editor's
18
+ * optimistic patcher can write `textContent` on every keystroke. Without it
19
+ * the canvas only updates once the server re-renders, and typing feels laggy.
20
+ *
21
+ * A slot with several text-bearing elements (a multi-line address, one leaf
22
+ * per line) marks its leaves manually inside this wrapper — `textLeaf` only
23
+ * handles the common single-child case.
24
+ *
25
+ * When `textLeaf` is true but the child isn't a single element (raw text, a
26
+ * fragment, several children), this prop is a no-op and the slot still emits
27
+ * its outer `data-slot-id`.
28
+ */
29
+ textLeaf?: boolean;
30
+ /**
31
+ * Optional. The wrapper still emits its `data-slot-id` when empty — which is
32
+ * what makes the empty-shell fallback (fill produced nothing) still selectable.
33
+ */
34
+ children?: ReactNode;
35
+ };
36
+ /**
37
+ * Slot — the canonical wrapper for an editable slot's DOM boundary.
38
+ *
39
+ * Wrap each editable slot's outer DOM in `<Slot id="…">`. It emits `data-slot-id`,
40
+ * which is how the editor resolves a click in the preview back to the slot the user
41
+ * meant. A slot with no marker in the DOM cannot be selected or edited.
42
+ *
43
+ * A primitive with a natural single root for its slot type takes a `slotId` prop and
44
+ * emits the marker on its own root instead — `<Image slotId="hero_image">` needs no
45
+ * `<Slot>` wrapper. `<Slot>` is for everything else: text slots, source-bound scalars
46
+ * rendered as raw spans, and any slot whose markup is a hand-assembled composition.
47
+ */
48
+ declare function Slot({ id, as, textLeaf, children, ...rest }: SlotProps): ReactNode;
49
+ //#endregion
50
+ export { Slot };