@homepages/template-kit 0.0.1 → 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.
- package/CHANGELOG.md +182 -0
- package/README.md +67 -11
- package/dist/base.css +85 -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 +307 -0
- package/dist/design-system/theme.js +95 -0
- package/dist/eslint/is-client-file.js +79 -0
- package/dist/eslint/rules/image-bare-needs-reason.js +38 -0
- package/dist/eslint/rules/no-client-directive-in-contract.js +26 -0
- package/dist/eslint/rules/no-client-runtime-in-server.js +99 -0
- package/dist/eslint/rules/no-css-import-from-render-path.js +23 -0
- package/dist/eslint/rules/no-hex.js +111 -0
- package/dist/eslint/rules/no-inline-style.js +23 -0
- package/dist/eslint/rules/no-nondeterminism.js +44 -0
- package/dist/eslint/rules/no-raw-element.js +32 -0
- package/dist/eslint/rules/props-from-schema.js +32 -0
- package/dist/eslint/rules/serializable-island-props.js +108 -0
- package/dist/eslint/rules/slot-marker-literal.js +33 -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/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 +6 -4
- package/docs/eslint.md +90 -0
- package/docs/islands.md +150 -0
- package/docs/llms.txt +30 -3
- package/docs/primitives.md +214 -0
- package/docs/schema-system.md +240 -0
- package/docs/theme-and-css.md +179 -0
- package/package.json +30 -6
- package/tsconfig.base.json +16 -0
- package/tsconfig.json +2 -13
- package/dist/src-dZr5N30y.js +0 -35
package/dist/index.d.ts
CHANGED
|
@@ -1,14 +1,30 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
}
|
|
11
|
-
|
|
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";
|
|
12
28
|
//#region src/index.d.ts
|
|
13
29
|
/**
|
|
14
30
|
* The published version of this kit.
|
|
@@ -21,4 +37,4 @@ declare function KitSmoke({ label }: {
|
|
|
21
37
|
*/
|
|
22
38
|
declare const KIT_VERSION: string;
|
|
23
39
|
//#endregion
|
|
24
|
-
export { KIT_VERSION,
|
|
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 {
|
|
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
|
-
|
|
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 };
|
package/dist/package.js
ADDED
|
@@ -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 };
|