@storystick/react 0.0.0-stage → 0.2.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +112 -0
- package/NOTICE +17 -0
- package/PUBLIC-API.md +38 -0
- package/README.md +226 -2
- package/dist/.source-hash +1 -0
- package/dist/authoring-skeleton.d.ts +5 -0
- package/dist/authoring-skeleton.js +29 -0
- package/dist/file.d.ts +18 -0
- package/dist/file.js +44 -0
- package/dist/hydrate-slice.d.ts +3 -0
- package/dist/hydrate-slice.js +1 -0
- package/dist/image-component.d.ts +26 -0
- package/dist/image-component.js +79 -0
- package/dist/image-element.d.ts +6 -0
- package/dist/image-element.js +68 -0
- package/dist/image.d.ts +3 -0
- package/dist/image.js +3 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/link-adapter.d.ts +2 -0
- package/dist/link-adapter.js +2 -0
- package/dist/link-component.d.ts +53 -0
- package/dist/link-component.js +97 -0
- package/dist/link.d.ts +3 -0
- package/dist/link.js +3 -0
- package/dist/preview-generation.d.ts +21 -0
- package/dist/preview-generation.js +134 -0
- package/dist/preview-host/preview-assistant-continuation.d.ts +9 -0
- package/dist/preview-host/preview-assistant-continuation.js +39 -0
- package/dist/preview-host/preview-authoring-client.d.ts +12 -0
- package/dist/preview-host/preview-authoring-client.js +108 -0
- package/dist/preview-host/preview-editor-continuation.d.ts +13 -0
- package/dist/preview-host/preview-editor-continuation.js +68 -0
- package/dist/preview-host/preview-standalone-controls.d.ts +18 -0
- package/dist/preview-host/preview-standalone-controls.js +528 -0
- package/dist/preview-host/preview-studio-destination.d.ts +20 -0
- package/dist/preview-host/preview-studio-destination.js +55 -0
- package/dist/preview-host/use-standalone-presentation.d.ts +15 -0
- package/dist/preview-host/use-standalone-presentation.js +223 -0
- package/dist/preview-icons.d.ts +10 -0
- package/dist/preview-icons.js +8 -0
- package/dist/preview-interaction-control.d.ts +6 -0
- package/dist/preview-interaction-control.js +39 -0
- package/dist/preview-page-state.d.ts +12 -0
- package/dist/preview-page-state.js +11 -0
- package/dist/preview-panel-state.d.ts +15 -0
- package/dist/preview-panel-state.js +28 -0
- package/dist/preview-provenance-publisher-model.d.ts +5 -0
- package/dist/preview-provenance-publisher-model.js +9 -0
- package/dist/preview-provenance-publisher.d.ts +15 -0
- package/dist/preview-provenance-publisher.js +68 -0
- package/dist/preview-provenance.d.ts +6 -0
- package/dist/preview-provenance.js +4 -0
- package/dist/preview-scope.d.ts +128 -0
- package/dist/preview-scope.js +161 -0
- package/dist/preview-slice-scope.d.ts +32 -0
- package/dist/preview-slice-scope.js +33 -0
- package/dist/preview-text-props.d.ts +18 -0
- package/dist/preview-text-props.js +13 -0
- package/dist/preview-toolbar-layout.d.ts +42 -0
- package/dist/preview-toolbar-layout.js +139 -0
- package/dist/preview-toolbar-panel.d.ts +14 -0
- package/dist/preview-toolbar-panel.js +6 -0
- package/dist/preview-toolbar-resize-handles.d.ts +9 -0
- package/dist/preview-toolbar-resize-handles.js +14 -0
- package/dist/preview-toolbar-shell.d.ts +19 -0
- package/dist/preview-toolbar-shell.js +144 -0
- package/dist/public-rich-text.d.ts +15 -0
- package/dist/public-rich-text.js +11 -0
- package/dist/public-slice-zone.d.ts +7 -0
- package/dist/public-slice-zone.js +6 -0
- package/dist/query-cache.d.ts +14 -0
- package/dist/query-cache.js +289 -0
- package/dist/query-preview-loader.d.ts +9 -0
- package/dist/query-preview-loader.js +28 -0
- package/dist/query-preview-runtime.d.ts +3 -0
- package/dist/query-preview-runtime.js +3 -0
- package/dist/rendering.d.ts +29 -0
- package/dist/rendering.js +17 -0
- package/dist/resize-edge.d.ts +8 -0
- package/dist/resize-edge.js +33 -0
- package/dist/rich-text.d.ts +150 -0
- package/dist/rich-text.js +618 -0
- package/dist/slice-render-failure.d.ts +8 -0
- package/dist/slice-render-failure.js +46 -0
- package/dist/slice-zone.d.ts +82 -0
- package/dist/slice-zone.js +273 -0
- package/dist/slice.d.ts +16 -0
- package/dist/slice.js +46 -0
- package/dist/storystick-mark.d.ts +3 -0
- package/dist/storystick-mark.js +6 -0
- package/dist/use-generated-query.d.ts +37 -0
- package/dist/use-generated-query.js +308 -0
- package/dist/use-momentary-preview-navigate-mode.d.ts +11 -0
- package/dist/use-momentary-preview-navigate-mode.js +158 -0
- package/dist/use-preview-perspective-intent.d.ts +53 -0
- package/dist/use-preview-perspective-intent.js +227 -0
- package/dist/use-preview-pointer-drag.d.ts +16 -0
- package/dist/use-preview-pointer-drag.js +98 -0
- package/dist/use-preview-toolbar-layout.d.ts +36 -0
- package/dist/use-preview-toolbar-layout.js +141 -0
- package/dist/use-render-mode-debounce.d.ts +31 -0
- package/dist/use-render-mode-debounce.js +143 -0
- package/dist/video.d.ts +21 -0
- package/dist/video.js +63 -0
- package/package.json +154 -3
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { type ContentSource } from '@storystick/client/internal';
|
|
2
|
+
/** Dispatch autonomous SDK slices; context is opaque application data. */
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import type { ReactElement, ReactNode } from 'react';
|
|
5
|
+
import type { SectionAnchorData } from '@storystick/types';
|
|
6
|
+
import { type SliceContractRegistry, type SliceFallback, type SliceRenderFailureHandler } from './slice-render-failure.js';
|
|
7
|
+
export type { SliceContract, SliceContractRegistry, SliceFallback, SliceRenderFailure, SliceRenderFailureHandler, } from './slice-render-failure.js';
|
|
8
|
+
import { type SliceAnchor } from '@storystick/preview/provenance/slice-model';
|
|
9
|
+
import { type SliceProvenance } from './preview-provenance.js';
|
|
10
|
+
/**
|
|
11
|
+
* Data shape for a single slice.
|
|
12
|
+
* Every slice must have _type (discriminant) and _key (React key).
|
|
13
|
+
*/
|
|
14
|
+
export interface SliceData {
|
|
15
|
+
_type: string;
|
|
16
|
+
_key: string;
|
|
17
|
+
_variation?: string;
|
|
18
|
+
/** Authored deep-link target rendered by `Slice.*` as a native `id`. */
|
|
19
|
+
_anchor?: SectionAnchorData;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Props received by every slice component.
|
|
23
|
+
* Uniform contract — no exceptions.
|
|
24
|
+
*/
|
|
25
|
+
export interface SliceComponentProps<T extends SliceData = SliceData, TContext = unknown, TZones extends Record<string, ReactNode> = Record<string, ReactNode>> {
|
|
26
|
+
/** The slice data (all fields + _type, _key, _variation) */
|
|
27
|
+
slice: T;
|
|
28
|
+
/** Optional context passed through from SliceZone (e.g., document data) */
|
|
29
|
+
context?: TContext;
|
|
30
|
+
/**
|
|
31
|
+
* Renderable zones for zone container slices. Populated by SliceZone
|
|
32
|
+
* from the slice’s local render metadata — absent for non-zone-container slices.
|
|
33
|
+
* Applications can specialize `TZones` for a container registry; this base
|
|
34
|
+
* type stays permissive so the runtime contract remains unopinionated.
|
|
35
|
+
* Prefer rendering with <ZoneSlot zone={zones.main} />.
|
|
36
|
+
*/
|
|
37
|
+
zones?: TZones;
|
|
38
|
+
/** Field binding attributes for this slice, grouped as plain serializable data. */
|
|
39
|
+
bindings?: SliceProvenance;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Shape of the `_zoneMappings` metadata attached to document responses by
|
|
43
|
+
* the backend (ADR-051 §9). Consumed here to resolve zone content before
|
|
44
|
+
* dispatching to slice components.
|
|
45
|
+
*/
|
|
46
|
+
export type ZoneMappings = Record<string, Record<string, string>>;
|
|
47
|
+
export type SliceComponentRegistry<TSliceMap extends object = Record<string, SliceData>, TContext = unknown, TZones extends Record<string, ReactNode> = Record<string, ReactNode>> = {
|
|
48
|
+
readonly [TType in keyof TSliceMap]: React.ComponentType<SliceComponentProps<Extract<TSliceMap[TType], SliceData>, TContext, TZones>>;
|
|
49
|
+
};
|
|
50
|
+
type SliceFromMap<TSliceMap extends object> = Extract<TSliceMap[keyof TSliceMap], SliceData>;
|
|
51
|
+
export interface SliceZoneProps<TSliceMap extends object = Record<string, SliceData>, TContext = unknown, TZones extends Record<string, ReactNode> = Record<string, ReactNode>> {
|
|
52
|
+
/** Array of slice data objects from a sliceZone field */
|
|
53
|
+
slices: readonly SliceFromMap<TSliceMap>[] | undefined | null;
|
|
54
|
+
/** Registry mapping slice _type to React components */
|
|
55
|
+
components: SliceComponentRegistry<TSliceMap, TContext, TZones>;
|
|
56
|
+
/** Optional context passed to every slice component */
|
|
57
|
+
context?: TContext;
|
|
58
|
+
/** Customer-owned output for malformed or unregistered slice entries. */
|
|
59
|
+
fallback?: SliceFallback;
|
|
60
|
+
/** Runtime variation IDs generated from the same resolved Story schema. */
|
|
61
|
+
contracts?: SliceContractRegistry;
|
|
62
|
+
/** Local observability hook for every slice render failure. */
|
|
63
|
+
onRenderFailure?: SliceRenderFailureHandler;
|
|
64
|
+
/** Explicit owner for raw content; ordinary SDK values already carry it. */
|
|
65
|
+
source?: ContentSource;
|
|
66
|
+
/** Optional owning document field; usually inferred by the public SDK seam */
|
|
67
|
+
sliceField?: string;
|
|
68
|
+
/** Internal preview ancestry for recursive zone-container SliceZones. */
|
|
69
|
+
parentAnchors?: ReadonlyArray<SliceAnchor>;
|
|
70
|
+
/** Internal numeric visual path of the parent slice. */
|
|
71
|
+
parentPath?: string;
|
|
72
|
+
}
|
|
73
|
+
export type ZoneSlotKind = 'richtext' | 'sliceZone' | 'empty';
|
|
74
|
+
export interface ZoneSlotDescriptor {
|
|
75
|
+
readonly kind: ZoneSlotKind;
|
|
76
|
+
readonly fieldPath: string;
|
|
77
|
+
}
|
|
78
|
+
export interface ZoneSlotProps {
|
|
79
|
+
readonly zone?: ReactNode;
|
|
80
|
+
}
|
|
81
|
+
export declare function ZoneSlot({ zone, }: ZoneSlotProps): ReactElement | null;
|
|
82
|
+
export declare function SliceZone<TSliceMap extends object = Record<string, SliceData>, TContext = unknown, TZones extends Record<string, ReactNode> = Record<string, ReactNode>>({ slices: inputSlices, components, context, source, fallback, contracts, onRenderFailure, sliceField, parentAnchors, parentPath, }: SliceZoneProps<TSliceMap, TContext, TZones>): React.JSX.Element | null;
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { readContentOrigin, prepareContentSource } from '@storystick/client/internal';
|
|
3
|
+
/** Dispatch autonomous SDK slices; context is opaque application data. */
|
|
4
|
+
import React from 'react';
|
|
5
|
+
import { RichText } from './rich-text.js';
|
|
6
|
+
import { hydrateSliceWithDiagnostics } from './hydrate-slice.js';
|
|
7
|
+
import { projectHydratedSlice, renderSliceFallback, reportSliceFailure, validateSliceVariation, } from './slice-render-failure.js';
|
|
8
|
+
import { stampPreviewScope } from './preview-scope.js';
|
|
9
|
+
import { extractSliceFields, getPreviewDocumentId, isPublishedSnapshotContext, makeSliceRenderId, serializeSliceModel, } from '@storystick/preview/provenance/slice-model';
|
|
10
|
+
import { buildSliceProvenance, readPreviewProvenanceRouteFrame, readPreviewProvenanceRouteRender, } from './preview-provenance.js';
|
|
11
|
+
import { isPreviewSliceDirectInvocationCandidate, readPreviewSliceScopeAdapter, } from './preview-slice-scope.js';
|
|
12
|
+
export function ZoneSlot({ zone, }) {
|
|
13
|
+
if (zone === undefined || zone === null || zone === false)
|
|
14
|
+
return null;
|
|
15
|
+
return _jsx(_Fragment, { children: zone });
|
|
16
|
+
}
|
|
17
|
+
function ZoneSlotCarrier({ children, }) {
|
|
18
|
+
return children === undefined || children === null ? null : _jsx(_Fragment, { children: children });
|
|
19
|
+
}
|
|
20
|
+
export function SliceZone({ slices: inputSlices, components, context, source, fallback, contracts, onRenderFailure, sliceField, parentAnchors = [], parentPath, }) {
|
|
21
|
+
const slices = prepareContentSource(inputSlices, source);
|
|
22
|
+
if (!slices)
|
|
23
|
+
return null;
|
|
24
|
+
const runtimeComponents = components;
|
|
25
|
+
// Explicit raw sources use the same resolver without borrowing app context.
|
|
26
|
+
const zoneMappings = getZoneMappings(source?.document);
|
|
27
|
+
const documentFields = zoneMappings ? getDocumentFields(source?.document) : null;
|
|
28
|
+
const routeFrame = readPreviewProvenanceRouteRender(source?.document)?.frame ??
|
|
29
|
+
readPreviewProvenanceRouteFrame(source?.document) ??
|
|
30
|
+
null;
|
|
31
|
+
const scopeAdapter = readPreviewSliceScopeAdapter();
|
|
32
|
+
const renderIds = new Map();
|
|
33
|
+
const rendered = slices.map((rawSlice, index) => {
|
|
34
|
+
if (!isRecord(rawSlice)) {
|
|
35
|
+
const failure = {
|
|
36
|
+
reason: 'invalid-slice',
|
|
37
|
+
value: rawSlice,
|
|
38
|
+
index,
|
|
39
|
+
};
|
|
40
|
+
reportSliceFailure(failure, onRenderFailure);
|
|
41
|
+
return renderSliceFallback(fallback, failure, `invalid-slice-${index}`, process.env.NODE_ENV !== 'production');
|
|
42
|
+
}
|
|
43
|
+
if (typeof rawSlice._type !== 'string' || rawSlice._type.length === 0) {
|
|
44
|
+
const failure = {
|
|
45
|
+
reason: 'missing-type',
|
|
46
|
+
slice: rawSlice,
|
|
47
|
+
index,
|
|
48
|
+
};
|
|
49
|
+
reportSliceFailure(failure, onRenderFailure);
|
|
50
|
+
return renderSliceFallback(fallback, failure, `missing-type-${index}`, process.env.NODE_ENV !== 'production');
|
|
51
|
+
}
|
|
52
|
+
const slice = rawSlice;
|
|
53
|
+
const content = readContentOrigin(slice);
|
|
54
|
+
const scopeDocId = content?.documentId ?? getPreviewDocumentId(source?.document);
|
|
55
|
+
const previewing = content ? content.preview === true : scopeDocId !== null && !isPublishedSnapshotContext(source?.document);
|
|
56
|
+
const scopeKey = content?.path.replace(/\[\d+\]$/, '');
|
|
57
|
+
const renderKey = JSON.stringify([scopeDocId, scopeKey]);
|
|
58
|
+
let renderId = '';
|
|
59
|
+
if (previewing && scopeDocId) {
|
|
60
|
+
renderId = renderIds.get(renderKey) ?? makeSliceRenderId({ scopeDocId, slices, scopeKey });
|
|
61
|
+
renderIds.set(renderKey, renderId);
|
|
62
|
+
}
|
|
63
|
+
const sourceIndex = content?.index ?? index;
|
|
64
|
+
const effectiveParentAnchors = content?.parentAnchors ?? parentAnchors;
|
|
65
|
+
const variationFailure = validateSliceVariation(slice, contracts, index);
|
|
66
|
+
if (variationFailure) {
|
|
67
|
+
reportSliceFailure(variationFailure, onRenderFailure);
|
|
68
|
+
return renderSliceFallback(fallback, variationFailure, slice._key || `invalid-variation-${index}`, previewing || process.env.NODE_ENV !== 'production');
|
|
69
|
+
}
|
|
70
|
+
const Component = runtimeComponents[slice._type];
|
|
71
|
+
if (!Component) {
|
|
72
|
+
const failure = {
|
|
73
|
+
reason: 'unregistered-type',
|
|
74
|
+
slice,
|
|
75
|
+
index,
|
|
76
|
+
registeredTypes: Object.keys(runtimeComponents).sort(),
|
|
77
|
+
};
|
|
78
|
+
reportSliceFailure(failure, onRenderFailure);
|
|
79
|
+
return renderSliceFallback(fallback, failure, slice._key || `unregistered-slice-${index}`, previewing || process.env.NODE_ENV !== 'production');
|
|
80
|
+
}
|
|
81
|
+
const zoneMapping = zoneMappings?.[slice._type];
|
|
82
|
+
const effectiveSliceField = content?.zoneField ?? sliceField ?? source?.path ?? 'sections';
|
|
83
|
+
const slicePath = content?.path ?? (parentPath
|
|
84
|
+
? `${parentPath}.${effectiveSliceField}[${index}]`
|
|
85
|
+
: `${effectiveSliceField}[${index}]`);
|
|
86
|
+
const currentAnchorPrefix = parentPath
|
|
87
|
+
? `${parentPath}.${effectiveSliceField}`
|
|
88
|
+
: effectiveSliceField;
|
|
89
|
+
const currentAnchor = typeof slice._key === 'string' && slice._key.length > 0
|
|
90
|
+
? { pathPrefix: currentAnchorPrefix, key: slice._key }
|
|
91
|
+
: null;
|
|
92
|
+
const zones = content?.zones
|
|
93
|
+
? Object.fromEntries(Object.entries(content.zones).map(([name, zone]) => [name,
|
|
94
|
+
renderZoneValue(zone.path, zone.value, runtimeComponents, context, fallback, contracts, onRenderFailure, [], '')]))
|
|
95
|
+
: zoneMapping
|
|
96
|
+
? resolveZones(zoneMapping, documentFields, runtimeComponents, context, fallback, contracts, onRenderFailure, currentAnchor === null ? parentAnchors : [...parentAnchors, currentAnchor], slicePath, source?.document)
|
|
97
|
+
: undefined;
|
|
98
|
+
// Field bindings (ADR-052): fill bound fields that have no stored
|
|
99
|
+
// value on the instance. Reference-stable when nothing to hydrate —
|
|
100
|
+
// memoization and React.memo stay sharp. Safe when the explicit source has
|
|
101
|
+
// no `_fieldBindings` sidecar (e.g., older backends).
|
|
102
|
+
const hydration = content ? { slice, failures: content.failures ?? [] } : hydrateSliceWithDiagnostics(slice, source?.document, {
|
|
103
|
+
sourceRefPath: `${slicePath}._model`,
|
|
104
|
+
});
|
|
105
|
+
if (hydration.failures.length > 0) {
|
|
106
|
+
const failures = hydration.failures.map((failure) => failure.reason === 'missing-model'
|
|
107
|
+
? { ...failure, slice, index }
|
|
108
|
+
: { reason: 'invalid-bindings', slice, index });
|
|
109
|
+
failures.forEach((failure) => reportSliceFailure(failure, onRenderFailure));
|
|
110
|
+
return renderSliceFallback(fallback, failures[0], slice._key || `slice-hydration-${index}`, previewing || process.env.NODE_ENV !== 'production');
|
|
111
|
+
}
|
|
112
|
+
const hydrated = projectHydratedSlice(hydration.slice, contracts);
|
|
113
|
+
const hydratedFields = extractSliceFields(hydrated);
|
|
114
|
+
// ADR-129 D4/D9 — NO injected node. In preview, stamp the slice
|
|
115
|
+
// with its serialized per-slice model; `<Slice.section slice>` reads
|
|
116
|
+
// the stamp and emits `data-ss-slice` (the `_key`) + `data-ss-model`
|
|
117
|
+
// (this model) — both ATTRIBUTES on the slice's own root element.
|
|
118
|
+
// The model is built from the HYDRATED slice (post-binding values)
|
|
119
|
+
// so inference + the coverage model see the same field values the
|
|
120
|
+
// component renders. Only when previewing + the slice has a usable
|
|
121
|
+
// `_key`. React keying lives on the component element itself.
|
|
122
|
+
let propSlice = hydrated;
|
|
123
|
+
if (previewing &&
|
|
124
|
+
scopeDocId !== null &&
|
|
125
|
+
typeof slice._key === 'string' &&
|
|
126
|
+
slice._key.length > 0) {
|
|
127
|
+
const modelJson = serializeSliceModel({
|
|
128
|
+
v: 129,
|
|
129
|
+
renderId,
|
|
130
|
+
doc: scopeDocId,
|
|
131
|
+
i: sourceIndex,
|
|
132
|
+
type: slice._type,
|
|
133
|
+
fields: hydratedFields,
|
|
134
|
+
zoneField: effectiveSliceField,
|
|
135
|
+
zoneFieldSource: content || sliceField !== undefined ? 'explicit' : 'default',
|
|
136
|
+
parentAnchors: effectiveParentAnchors,
|
|
137
|
+
...(content ? { source: { path: content.path, anchors: [...content.parentAnchors, { pathPrefix: content.path.replace(/\[\d+\]$/, ''), key: slice._key }] } } : {}),
|
|
138
|
+
});
|
|
139
|
+
// A model that fails to serialize ⇒ the slice is left unstamped:
|
|
140
|
+
// no `data-ss-slice`/`data-ss-model`, no overlay (honest absence),
|
|
141
|
+
// every sibling slice unaffected.
|
|
142
|
+
if (modelJson !== null) {
|
|
143
|
+
propSlice = stampPreviewScope(hydrated, modelJson);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const bindings = buildSliceProvenance(content ?? source?.document, slice);
|
|
147
|
+
const componentProps = {
|
|
148
|
+
slice: propSlice,
|
|
149
|
+
context,
|
|
150
|
+
zones,
|
|
151
|
+
bindings,
|
|
152
|
+
};
|
|
153
|
+
const key = slice._key || `slice-${index}`;
|
|
154
|
+
if (scopeAdapter !== undefined &&
|
|
155
|
+
scopeDocId !== null &&
|
|
156
|
+
isPreviewSliceDirectInvocationCandidate(Component)) {
|
|
157
|
+
const scoped = scopeAdapter({
|
|
158
|
+
component: Component,
|
|
159
|
+
props: componentProps,
|
|
160
|
+
scope: {
|
|
161
|
+
routeFrame: routeFrame?.frameId === 'route' ? routeFrame : null,
|
|
162
|
+
documentId: scopeDocId,
|
|
163
|
+
sliceField: effectiveSliceField,
|
|
164
|
+
slicePath,
|
|
165
|
+
sliceKey: slice._key,
|
|
166
|
+
sliceType: slice._type,
|
|
167
|
+
sliceIndex: index,
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
return React.createElement(React.Fragment, { key }, scoped);
|
|
171
|
+
}
|
|
172
|
+
return React.createElement(Component, { key, ...componentProps });
|
|
173
|
+
});
|
|
174
|
+
// ADR-129 D9 — no `<script>` island node: every slice carries its own
|
|
175
|
+
// model on its `data-ss-model` attribute. The render is byte-identical
|
|
176
|
+
// to a SliceZone with no preview seam outside preview, and injects
|
|
177
|
+
// zero nodes inside it.
|
|
178
|
+
return _jsx(_Fragment, { children: rendered });
|
|
179
|
+
}
|
|
180
|
+
// ─────────────────────────────────────────────
|
|
181
|
+
// Zone container helpers
|
|
182
|
+
// ─────────────────────────────────────────────
|
|
183
|
+
/**
|
|
184
|
+
* Read zone mappings from an explicit raw document source. Returns
|
|
185
|
+
* `null` when that source carries no mappings, which lets
|
|
186
|
+
* SliceZone fall back to the original (zoneless) dispatch.
|
|
187
|
+
*/
|
|
188
|
+
function getZoneMappings(source) {
|
|
189
|
+
if (!source || typeof source !== 'object')
|
|
190
|
+
return null;
|
|
191
|
+
const c = source;
|
|
192
|
+
const m = c._zoneMappings;
|
|
193
|
+
if (!m || typeof m !== 'object')
|
|
194
|
+
return null;
|
|
195
|
+
return m;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Document responses wrap content under `fields`. Zone refs resolve against
|
|
199
|
+
* that object. Returning `null` when the shape isn't recognised keeps the
|
|
200
|
+
* caller's null-checks narrow.
|
|
201
|
+
*/
|
|
202
|
+
function getDocumentFields(source) {
|
|
203
|
+
if (!source || typeof source !== 'object')
|
|
204
|
+
return null;
|
|
205
|
+
const c = source;
|
|
206
|
+
if (c.fields && typeof c.fields === 'object') {
|
|
207
|
+
return c.fields;
|
|
208
|
+
}
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Build the `zones` prop for a zone container slice by reading each mapped
|
|
213
|
+
* field from the document and rendering it with the appropriate component.
|
|
214
|
+
*
|
|
215
|
+
* Field-type detection is shape-based: a string is treated as richtext (a
|
|
216
|
+
* ProseMirror JSON payload), an array as a sliceZone. This mirrors the
|
|
217
|
+
* resolver's push-time validation (ADR-051 check 9 — zone refs must point
|
|
218
|
+
* to richtext or sliceZone fields), so we never get here with a field of a
|
|
219
|
+
* different type. `null`/`undefined` zones become explicit empty zones.
|
|
220
|
+
*/
|
|
221
|
+
function resolveZones(mapping, fields, components, context, fallback, contracts, onRenderFailure, parentAnchors, parentPath, sourceDocument) {
|
|
222
|
+
const zones = {};
|
|
223
|
+
for (const [zoneName, fieldPath] of Object.entries(mapping)) {
|
|
224
|
+
const value = fields?.[fieldPath];
|
|
225
|
+
zones[zoneName] = renderZoneValue(fieldPath, value, components, context, fallback, contracts, onRenderFailure, parentAnchors, parentPath, sourceDocument);
|
|
226
|
+
}
|
|
227
|
+
return zones;
|
|
228
|
+
}
|
|
229
|
+
function renderZoneValue(fieldPath, value, components, context, fallback, contracts, onRenderFailure, parentAnchors, parentPath, sourceDocument) {
|
|
230
|
+
if (value === null || value === undefined) {
|
|
231
|
+
return createZoneCarrier({ kind: 'empty', fieldPath }, null);
|
|
232
|
+
}
|
|
233
|
+
if (typeof value === 'string' || readContentOrigin(value)?.kind === 'richtext') {
|
|
234
|
+
// Richtext zone — the stored value is a ProseMirror JSON string. <RichText>
|
|
235
|
+
// parses and walks it using the same components registry so embeds inside
|
|
236
|
+
// the zone keep dispatching to the consumer's slice components.
|
|
237
|
+
return createZoneCarrier({ kind: 'richtext', fieldPath }, React.createElement(RichText, {
|
|
238
|
+
content: value,
|
|
239
|
+
components: { embeds: components },
|
|
240
|
+
context,
|
|
241
|
+
...(sourceDocument ? { source: { document: sourceDocument, path: fieldPath } } : {}),
|
|
242
|
+
sliceFallback: fallback,
|
|
243
|
+
contracts,
|
|
244
|
+
onRenderFailure,
|
|
245
|
+
sliceField: fieldPath,
|
|
246
|
+
parentAnchors,
|
|
247
|
+
parentPath,
|
|
248
|
+
}));
|
|
249
|
+
}
|
|
250
|
+
if (Array.isArray(value)) {
|
|
251
|
+
// SliceZone zone — recurse with the same registry and context so nested
|
|
252
|
+
// zone containers (should a future version allow them) would also resolve.
|
|
253
|
+
return createZoneCarrier({ kind: 'sliceZone', fieldPath }, React.createElement((SliceZone), {
|
|
254
|
+
slices: value,
|
|
255
|
+
components,
|
|
256
|
+
context,
|
|
257
|
+
...(sourceDocument ? { source: { document: sourceDocument, path: fieldPath } } : {}),
|
|
258
|
+
fallback,
|
|
259
|
+
contracts,
|
|
260
|
+
onRenderFailure,
|
|
261
|
+
sliceField: fieldPath,
|
|
262
|
+
parentAnchors,
|
|
263
|
+
parentPath,
|
|
264
|
+
}));
|
|
265
|
+
}
|
|
266
|
+
return createZoneCarrier({ kind: 'empty', fieldPath }, null);
|
|
267
|
+
}
|
|
268
|
+
function createZoneCarrier(descriptor, children) {
|
|
269
|
+
return React.createElement(ZoneSlotCarrier, { descriptor }, children);
|
|
270
|
+
}
|
|
271
|
+
function isRecord(value) {
|
|
272
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
273
|
+
}
|
package/dist/slice.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type ComponentType } from 'react';
|
|
2
|
+
import type { SliceData } from './slice-zone.js';
|
|
3
|
+
/** The slice received by a SliceZone or rich-text embed component. */
|
|
4
|
+
export interface SliceRootProps {
|
|
5
|
+
slice: SliceData;
|
|
6
|
+
}
|
|
7
|
+
/** Native elements that declare a slice's visual root, without a wrapper. */
|
|
8
|
+
export type SliceElements = {
|
|
9
|
+
readonly [Tag in keyof React.JSX.IntrinsicElements]: ComponentType<React.JSX.IntrinsicElements[Tag] & SliceRootProps>;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Render a slice's own native root: `<Slice.section slice={slice}>`.
|
|
13
|
+
* SliceZone supplies the serializable Preview identity. Without it, this
|
|
14
|
+
* renders ordinary HTML. Fields use automatic provenance or `bindings`.
|
|
15
|
+
*/
|
|
16
|
+
export declare const Slice: SliceElements;
|
package/dist/slice.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { createElement } from 'react';
|
|
2
|
+
import { isSectionAnchorSlug } from '@storystick/types';
|
|
3
|
+
import { getPreviewModel } from './preview-scope.js';
|
|
4
|
+
function createSliceRoot(tag) {
|
|
5
|
+
function SliceRoot({ slice, children, ...props }) {
|
|
6
|
+
const anchor = slice
|
|
7
|
+
?._anchor;
|
|
8
|
+
if (props.id === undefined &&
|
|
9
|
+
typeof anchor?.slug === 'string' &&
|
|
10
|
+
isSectionAnchorSlug(anchor.slug)) {
|
|
11
|
+
props.id = anchor.slug;
|
|
12
|
+
}
|
|
13
|
+
const model = getPreviewModel(slice);
|
|
14
|
+
const key = slice?._key;
|
|
15
|
+
if (model !== undefined && typeof key === 'string' && key.length > 0) {
|
|
16
|
+
props['data-ss-slice'] = key;
|
|
17
|
+
props['data-ss-model'] = model;
|
|
18
|
+
props.suppressHydrationWarning ??= true;
|
|
19
|
+
}
|
|
20
|
+
return createElement(tag, props, children);
|
|
21
|
+
}
|
|
22
|
+
SliceRoot.displayName = `Slice.${tag}`;
|
|
23
|
+
return SliceRoot;
|
|
24
|
+
}
|
|
25
|
+
const inspectionKeys = new Set(['then', 'toJSON', 'constructor', 'toString', 'valueOf', 'inspect']);
|
|
26
|
+
const elements = new Map();
|
|
27
|
+
/**
|
|
28
|
+
* Render a slice's own native root: `<Slice.section slice={slice}>`.
|
|
29
|
+
* SliceZone supplies the serializable Preview identity. Without it, this
|
|
30
|
+
* renders ordinary HTML. Fields use automatic provenance or `bindings`.
|
|
31
|
+
*/
|
|
32
|
+
export const Slice = new Proxy(Object.create(null), {
|
|
33
|
+
get(_target, tag) {
|
|
34
|
+
// Ignore Promise assimilation and React/JS inspection probes. SVG tags
|
|
35
|
+
// can contain capitals (e.g. foreignObject); native tags start lowercase.
|
|
36
|
+
if (typeof tag !== 'string' || inspectionKeys.has(tag) || !/^[a-z][a-zA-Z0-9]*$/.test(tag)) {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
let element = elements.get(tag);
|
|
40
|
+
if (!element) {
|
|
41
|
+
element = createSliceRoot(tag);
|
|
42
|
+
elements.set(tag, element);
|
|
43
|
+
}
|
|
44
|
+
return element;
|
|
45
|
+
},
|
|
46
|
+
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { STORYSTICK_MARK_RECTS, STORYSTICK_MARK_VIEW_BOX } from '@storystick/preview/page-presentation';
|
|
3
|
+
/** Canonical brand geometry, shared by Studio and the lightweight preview host. */
|
|
4
|
+
export function StorystickMark(props) {
|
|
5
|
+
return (_jsx("svg", { ...props, "data-slot": "storystick-mark", viewBox: STORYSTICK_MARK_VIEW_BOX, fill: "currentColor", focusable: "false", "aria-hidden": "true", children: STORYSTICK_MARK_RECTS.map(rect => _jsx("rect", { ...rect }, rect.x)) }));
|
|
6
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { StorystickError } from '@storystick/client';
|
|
2
|
+
import type { CompiledQueryV2, QueryPageInfo, QueryResult } from '@storystick/types/query-contract';
|
|
3
|
+
export interface UseQueryOptions {
|
|
4
|
+
readonly enabled?: boolean;
|
|
5
|
+
}
|
|
6
|
+
interface ExecutableQuery<TData, TPageInfo extends QueryPageInfo | null> {
|
|
7
|
+
compile(): {
|
|
8
|
+
readonly canonical: string;
|
|
9
|
+
readonly query: CompiledQueryV2;
|
|
10
|
+
};
|
|
11
|
+
execute(options?: {
|
|
12
|
+
readonly signal?: AbortSignal;
|
|
13
|
+
}): Promise<QueryResult<TData, TPageInfo>>;
|
|
14
|
+
}
|
|
15
|
+
interface UseQueryCommon<TData, TPageInfo extends QueryPageInfo | null> {
|
|
16
|
+
readonly data: TData | undefined;
|
|
17
|
+
readonly pageInfo: TPageInfo | undefined;
|
|
18
|
+
readonly error: StorystickError | null;
|
|
19
|
+
readonly isFetching: boolean;
|
|
20
|
+
refetch(): Promise<TData>;
|
|
21
|
+
}
|
|
22
|
+
export type UseQueryResult<TData, TPageInfo extends QueryPageInfo | null> = (UseQueryCommon<TData, TPageInfo> & {
|
|
23
|
+
readonly status: 'pending';
|
|
24
|
+
readonly data: undefined;
|
|
25
|
+
readonly pageInfo: undefined;
|
|
26
|
+
readonly error: null;
|
|
27
|
+
}) | (UseQueryCommon<TData, TPageInfo> & {
|
|
28
|
+
readonly status: 'success';
|
|
29
|
+
readonly data: TData;
|
|
30
|
+
readonly pageInfo: TPageInfo;
|
|
31
|
+
readonly error: null;
|
|
32
|
+
}) | (UseQueryCommon<TData, TPageInfo> & {
|
|
33
|
+
readonly status: 'error';
|
|
34
|
+
readonly error: StorystickError;
|
|
35
|
+
});
|
|
36
|
+
export declare function useQuery<TData, TPageInfo extends QueryPageInfo | null>(executableQuery: ExecutableQuery<TData, TPageInfo>, options?: UseQueryOptions): UseQueryResult<TData, TPageInfo>;
|
|
37
|
+
export {};
|