@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,223 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { previewDraftHref } from '@storystick/preview/identity';
|
|
3
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
4
|
+
import { previewRuntime } from '@storystick/preview/runtime/store';
|
|
5
|
+
import { createPreviewRequestId } from '@storystick/preview/runtime/request-id';
|
|
6
|
+
import { getNavigateHandler, hasNavigateHandler, } from '@storystick/preview/nav-handler';
|
|
7
|
+
import { usePreviewPerspectiveIntent, isAcceptedPreviewPerspectiveIntent, } from '@storystick/react/internal/use-preview-perspective-intent';
|
|
8
|
+
import { beginPreviewTransition, advancePreviewTransitionClock, confirmPreviewTransition, } from '@storystick/preview/runtime/preview-transition-state';
|
|
9
|
+
import { previewCurrentPrimaryPage, readPrimaryPagesSnapshot, subscribePrimaryPages, } from './preview-studio-destination.js';
|
|
10
|
+
/** Acceptance releases the editor; render proof releases the page overlays.
|
|
11
|
+
* No authoring credentials cross this port; the framework uses its Preview cookie. */
|
|
12
|
+
export function useStandalonePresentation(basePath) {
|
|
13
|
+
const pending = useRef(null);
|
|
14
|
+
const generation = useRef(0);
|
|
15
|
+
const completedGeneration = useRef(0);
|
|
16
|
+
const [transition, setTransition] = useState({
|
|
17
|
+
kind: 'idle',
|
|
18
|
+
});
|
|
19
|
+
const clearPending = useCallback(() => {
|
|
20
|
+
const flight = pending.current;
|
|
21
|
+
if (!flight)
|
|
22
|
+
return;
|
|
23
|
+
clearTimeout(flight.timer);
|
|
24
|
+
pending.current = null;
|
|
25
|
+
}, []);
|
|
26
|
+
const perspective = usePreviewPerspectiveIntent({
|
|
27
|
+
getSessionId: () => previewRuntime.getState().sessionId,
|
|
28
|
+
getReadContext: () => previewRuntime.getState().session?.readContext ?? null,
|
|
29
|
+
beginTransition: () => ({ generation: generation.current }),
|
|
30
|
+
registerCommand: () => undefined,
|
|
31
|
+
clearTransition: () => undefined,
|
|
32
|
+
dispatch: async (scope) => {
|
|
33
|
+
const context = previewRuntime.getState().session?.readContext;
|
|
34
|
+
const flight = pending.current;
|
|
35
|
+
if (!context || !hasNavigateHandler())
|
|
36
|
+
return { commandId: null };
|
|
37
|
+
const commandId = flight?.commandId ?? createPreviewRequestId();
|
|
38
|
+
await getNavigateHandler()(flight
|
|
39
|
+
? previewDraftHref(flight.request.pathname, flight.request.draftDocumentId)
|
|
40
|
+
: `${window.location.pathname}${window.location.search}`, {
|
|
41
|
+
scope,
|
|
42
|
+
commandId,
|
|
43
|
+
refresh: true,
|
|
44
|
+
history: 'push',
|
|
45
|
+
contextCommand: {
|
|
46
|
+
expectedContextId: context.contextId,
|
|
47
|
+
expectedRevision: context.revision,
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
return { commandId };
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
const observe = useCallback(() => {
|
|
54
|
+
const lifecycle = previewRuntime.getLifecycleSnapshot();
|
|
55
|
+
if (lifecycle)
|
|
56
|
+
perspective.observeLifecycle(lifecycle);
|
|
57
|
+
const flight = pending.current;
|
|
58
|
+
if (!flight?.accepted)
|
|
59
|
+
return;
|
|
60
|
+
const state = previewRuntime.getState();
|
|
61
|
+
const context = state.session?.readContext;
|
|
62
|
+
if (!context ||
|
|
63
|
+
context.contextId !== flight.accepted.contextId ||
|
|
64
|
+
context.revision !== flight.accepted.revision) {
|
|
65
|
+
clearPending();
|
|
66
|
+
setTransition({ kind: 'idle' });
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const committed = flight.committed;
|
|
70
|
+
const receipt = committed?.presentation;
|
|
71
|
+
if (receipt && committed.commandId === flight.commandId &&
|
|
72
|
+
receipt.readContext.contextId === context.contextId &&
|
|
73
|
+
receipt.readContext.revision === context.revision &&
|
|
74
|
+
receipt.pathname === flight.request.pathname &&
|
|
75
|
+
receipt.pathname === (window.location.pathname.slice(basePath.length) || '/')) {
|
|
76
|
+
// The framework committed a fresh server request in the accepted
|
|
77
|
+
// context. This proof does not depend on editable fields being present.
|
|
78
|
+
clearPending();
|
|
79
|
+
completedGeneration.current = flight.generation;
|
|
80
|
+
setTransition({ kind: 'idle' });
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const page = previewCurrentPrimaryPage(readPrimaryPagesSnapshot(), state.sessionId, context, window.location.pathname, basePath);
|
|
84
|
+
if (!page ||
|
|
85
|
+
state.renderedReadContext?.contextId !== context.contextId ||
|
|
86
|
+
state.renderedReadContext.revision !== context.revision)
|
|
87
|
+
return;
|
|
88
|
+
const confirmed = confirmPreviewTransition(flight.transition, {
|
|
89
|
+
kind: 'presentation',
|
|
90
|
+
pageDocId: page.documentId,
|
|
91
|
+
locale: page.locale,
|
|
92
|
+
pathname: window.location.pathname.slice(basePath.length) || '/',
|
|
93
|
+
context,
|
|
94
|
+
acceptedContext: flight.accepted,
|
|
95
|
+
});
|
|
96
|
+
if (confirmed.kind !== 'idle')
|
|
97
|
+
return;
|
|
98
|
+
clearTimeout(flight.timer);
|
|
99
|
+
pending.current = null;
|
|
100
|
+
completedGeneration.current = generation.current;
|
|
101
|
+
setTransition(confirmed);
|
|
102
|
+
}, [basePath, perspective.observeLifecycle, clearPending]);
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
const onCommit = (event) => {
|
|
105
|
+
const committed = event.detail;
|
|
106
|
+
if (pending.current && committed?.commandId === pending.current.commandId) {
|
|
107
|
+
pending.current.committed = committed;
|
|
108
|
+
observe();
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
const stopRuntime = previewRuntime.subscribe(observe);
|
|
112
|
+
const stopPages = subscribePrimaryPages(observe);
|
|
113
|
+
window.addEventListener('storystick.preview.location-committed', onCommit);
|
|
114
|
+
return () => {
|
|
115
|
+
stopRuntime();
|
|
116
|
+
stopPages();
|
|
117
|
+
window.removeEventListener('storystick.preview.location-committed', onCommit);
|
|
118
|
+
clearPending();
|
|
119
|
+
};
|
|
120
|
+
}, [observe, clearPending]);
|
|
121
|
+
const present = useCallback(async (request) => {
|
|
122
|
+
if (!hasNavigateHandler())
|
|
123
|
+
throw new Error('PREVIEW_NAVIGATION_UNAVAILABLE');
|
|
124
|
+
// The local port has no iframe envelope; validate before invoking the same adapter.
|
|
125
|
+
if (!request.pathname.startsWith('/') ||
|
|
126
|
+
request.pathname.startsWith('//') ||
|
|
127
|
+
/[\\\u0000-\u0020]/.test(request.pathname)) {
|
|
128
|
+
throw new Error('PREVIEW_DESTINATION_INVALID');
|
|
129
|
+
}
|
|
130
|
+
clearPending();
|
|
131
|
+
perspective.cancelPending();
|
|
132
|
+
const next = beginPreviewTransition({
|
|
133
|
+
generation: ++generation.current,
|
|
134
|
+
reason: 'preview-intent',
|
|
135
|
+
now: Date.now(),
|
|
136
|
+
target: {
|
|
137
|
+
sessionId: previewRuntime.getState().sessionId,
|
|
138
|
+
editorDocumentId: null,
|
|
139
|
+
expectedPreviewDocumentId: request.documentId,
|
|
140
|
+
expectedLocale: request.locale,
|
|
141
|
+
expectedPathname: request.pathname,
|
|
142
|
+
expectedPerspective: request.perspective,
|
|
143
|
+
expectedSearch: '',
|
|
144
|
+
expectedHash: '',
|
|
145
|
+
hostStatus: 'valid',
|
|
146
|
+
sourceOfTruth: 'system',
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
setTransition(next);
|
|
150
|
+
const flight = {
|
|
151
|
+
request,
|
|
152
|
+
transition: next,
|
|
153
|
+
accepted: null,
|
|
154
|
+
commandId: createPreviewRequestId(),
|
|
155
|
+
committed: null,
|
|
156
|
+
generation: generation.current,
|
|
157
|
+
timer: setTimeout(() => {
|
|
158
|
+
if (pending.current !== flight)
|
|
159
|
+
return;
|
|
160
|
+
// Keep observing: a slow page may still provide valid proof later.
|
|
161
|
+
setTransition(advancePreviewTransitionClock(next, Date.now()));
|
|
162
|
+
}, 12_000),
|
|
163
|
+
};
|
|
164
|
+
pending.current = flight;
|
|
165
|
+
try {
|
|
166
|
+
const accepted = await perspective.changePerspective(request.perspective);
|
|
167
|
+
if (pending.current !== flight)
|
|
168
|
+
throw new Error('PREVIEW_PRESENTATION_SUPERSEDED');
|
|
169
|
+
if (!isAcceptedPreviewPerspectiveIntent(accepted))
|
|
170
|
+
throw new Error(`PREVIEW_CONTEXT_${accepted.status}`);
|
|
171
|
+
flight.accepted = accepted.context;
|
|
172
|
+
if (accepted.status === 'local-noop') {
|
|
173
|
+
await getNavigateHandler()(previewDraftHref(request.pathname, request.draftDocumentId), {
|
|
174
|
+
scope: accepted.context.perspective,
|
|
175
|
+
refresh: true,
|
|
176
|
+
history: 'push',
|
|
177
|
+
commandId: flight.commandId,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
if (pending.current !== flight && completedGeneration.current !== flight.generation)
|
|
181
|
+
throw new Error('PREVIEW_PRESENTATION_SUPERSEDED');
|
|
182
|
+
// Publish acceptedContext even when no render evidence has arrived.
|
|
183
|
+
if (pending.current === flight)
|
|
184
|
+
setTransition(current => ({ ...current }));
|
|
185
|
+
observe();
|
|
186
|
+
if (pending.current !== flight && completedGeneration.current !== flight.generation)
|
|
187
|
+
throw new Error('PREVIEW_PRESENTATION_SUPERSEDED');
|
|
188
|
+
return accepted.context;
|
|
189
|
+
}
|
|
190
|
+
catch (cause) {
|
|
191
|
+
if (pending.current === flight) {
|
|
192
|
+
clearPending();
|
|
193
|
+
setTransition(flight.accepted
|
|
194
|
+
? advancePreviewTransitionClock(next, Number.POSITIVE_INFINITY)
|
|
195
|
+
: { kind: 'idle' });
|
|
196
|
+
}
|
|
197
|
+
throw cause;
|
|
198
|
+
}
|
|
199
|
+
}, [
|
|
200
|
+
observe,
|
|
201
|
+
perspective.changePerspective,
|
|
202
|
+
perspective.cancelPending,
|
|
203
|
+
clearPending,
|
|
204
|
+
]);
|
|
205
|
+
return {
|
|
206
|
+
present,
|
|
207
|
+
/** The project menu also exists on routes without an editable document. */
|
|
208
|
+
async changePerspective(next) {
|
|
209
|
+
clearPending();
|
|
210
|
+
perspective.cancelPending();
|
|
211
|
+
setTransition({ kind: 'idle' });
|
|
212
|
+
const accepted = await perspective.changePerspective(next);
|
|
213
|
+
if (!isAcceptedPreviewPerspectiveIntent(accepted))
|
|
214
|
+
throw new Error(`PREVIEW_CONTEXT_${accepted.status}`);
|
|
215
|
+
return accepted.context;
|
|
216
|
+
},
|
|
217
|
+
transition,
|
|
218
|
+
completedGeneration: completedGeneration.current,
|
|
219
|
+
acceptedContext: pending.current?.accepted ?? null,
|
|
220
|
+
acceptedPresentation: pending.current?.accepted ? pending.current.request : null,
|
|
221
|
+
isPending: transition.kind !== 'idle',
|
|
222
|
+
};
|
|
223
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { SVGProps } from 'react';
|
|
2
|
+
type IconProps = SVGProps<SVGSVGElement> & {
|
|
3
|
+
size?: number | string;
|
|
4
|
+
absoluteStrokeWidth?: boolean;
|
|
5
|
+
};
|
|
6
|
+
export declare function X(props: IconProps): import("react").JSX.Element;
|
|
7
|
+
export declare function ArrowUpRight(props: IconProps): import("react").JSX.Element;
|
|
8
|
+
export declare function MessageCircle(props: IconProps): import("react").JSX.Element;
|
|
9
|
+
export declare function SquarePen(props: IconProps): import("react").JSX.Element;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
function Icon({ size = 24, strokeWidth = 2, absoluteStrokeWidth, children, ...props }) {
|
|
3
|
+
return _jsx("svg", { xmlns: "http://www.w3.org/2000/svg", width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: absoluteStrokeWidth ? Number(strokeWidth) * 24 / Number(size) : strokeWidth, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", ...props, children: children });
|
|
4
|
+
}
|
|
5
|
+
export function X(props) { return _jsxs(Icon, { ...props, children: [_jsx("path", { d: "M18 6 6 18" }), _jsx("path", { d: "m6 6 12 12" })] }); }
|
|
6
|
+
export function ArrowUpRight(props) { return _jsxs(Icon, { ...props, children: [_jsx("path", { d: "M7 7h10v10" }), _jsx("path", { d: "M7 17 17 7" })] }); }
|
|
7
|
+
export function MessageCircle(props) { return _jsx(Icon, { ...props, children: _jsx("path", { d: "M7.9 20A9 9 0 1 0 4 16.1L2 22Z" }) }); }
|
|
8
|
+
export function SquarePen(props) { return _jsxs(Icon, { ...props, children: [_jsx("path", { d: "M12 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" }), _jsx("path", { d: "M18.375 2.625a1 1 0 0 1 3 3l-9.013 9.014a2 2 0 0 1-.853.505l-2.873.84a.5.5 0 0 1-.62-.62l.84-2.873a2 2 0 0 1 .506-.852z" })] }); }
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type PreviewInteractionMode = "edit" | "browse";
|
|
2
|
+
/** Lightweight common control: available before loading the authoring runtime. */
|
|
3
|
+
export declare function PreviewInteractionControl({ interactionMode, onChange, }: {
|
|
4
|
+
interactionMode: PreviewInteractionMode;
|
|
5
|
+
onChange(mode: PreviewInteractionMode): void;
|
|
6
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Radio } from "@base-ui/react/radio";
|
|
4
|
+
import { RadioGroup } from "@base-ui/react/radio-group";
|
|
5
|
+
/** Lightweight common control: available before loading the authoring runtime. */
|
|
6
|
+
export function PreviewInteractionControl({ interactionMode, onChange, }) {
|
|
7
|
+
return (_jsx(RadioGroup, { value: interactionMode, onValueChange: onChange, "aria-label": "Preview interaction mode", "data-slot": "preview-frame-interaction-mode", style: {
|
|
8
|
+
display: "inline-flex",
|
|
9
|
+
alignItems: "center",
|
|
10
|
+
padding: 4,
|
|
11
|
+
gap: 2,
|
|
12
|
+
borderRadius: 999,
|
|
13
|
+
background: "var(--color-surface-card, #fff)",
|
|
14
|
+
color: "var(--color-text-primary, #17191b)",
|
|
15
|
+
boxShadow: "var(--ss-shadow-control, 0 0 0 1px rgb(0 0 0 / 8%), 0 2px 4px rgb(0 0 0 / 5%))",
|
|
16
|
+
}, children: ["edit", "browse"].map((mode) => (_jsx(Radio.Root, { value: mode, nativeButton: true, render: _jsx("button", { type: "button" }), "data-slot": `preview-frame-interaction-${mode}`, "aria-label": mode === "edit"
|
|
17
|
+
? "Inspect: select content to edit"
|
|
18
|
+
: "Navigate: use site links", title: mode === "edit"
|
|
19
|
+
? "Select content to edit"
|
|
20
|
+
: "Use site links. Hold Option/Alt to navigate temporarily.", style: {
|
|
21
|
+
appearance: "none",
|
|
22
|
+
display: "inline-flex",
|
|
23
|
+
alignItems: "center",
|
|
24
|
+
justifyContent: "center",
|
|
25
|
+
height: 28,
|
|
26
|
+
padding: "0 8px",
|
|
27
|
+
border: 0,
|
|
28
|
+
borderRadius: 999,
|
|
29
|
+
cursor: "pointer",
|
|
30
|
+
background: interactionMode === mode
|
|
31
|
+
? "var(--color-surface-field, #f0f0f1)"
|
|
32
|
+
: "transparent",
|
|
33
|
+
color: "inherit",
|
|
34
|
+
fontFamily: "inherit",
|
|
35
|
+
fontSize: 13,
|
|
36
|
+
fontWeight: 500,
|
|
37
|
+
lineHeight: "17px",
|
|
38
|
+
}, children: mode === "edit" ? (_jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: (1.25 * 24) / 14, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [_jsx("path", { d: "M2 10h6V4" }), _jsx("path", { d: "m2 4 6 6" }), _jsx("path", { d: "M21 10V7a2 2 0 0 0-2-2h-7" }), _jsx("path", { d: "M3 14v2a2 2 0 0 0 2 2h3" }), _jsx("rect", { x: "12", y: "14", width: "10", height: "7", rx: "1" })] })) : ("Navigate") }, mode))) }));
|
|
39
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
/** One page composition for launch, activation and terminal Preview feedback.
|
|
3
|
+
* Actions and progress belong to the caller's existing lifecycle. */
|
|
4
|
+
export declare function PreviewPageState({ title, description, role, indicator, actions, children }: {
|
|
5
|
+
title: string;
|
|
6
|
+
description: ReactNode;
|
|
7
|
+
role: 'status' | 'alert';
|
|
8
|
+
indicator?: ReactNode;
|
|
9
|
+
actions?: ReactNode;
|
|
10
|
+
children?: ReactNode;
|
|
11
|
+
}): import("react").JSX.Element;
|
|
12
|
+
export declare function PreviewPageProgress(): import("react").JSX.Element;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { PREVIEW_PAGE_STYLES } from '@storystick/preview/page-presentation';
|
|
3
|
+
import { StorystickMark } from './storystick-mark.js';
|
|
4
|
+
/** One page composition for launch, activation and terminal Preview feedback.
|
|
5
|
+
* Actions and progress belong to the caller's existing lifecycle. */
|
|
6
|
+
export function PreviewPageState({ title, description, role, indicator, actions, children }) {
|
|
7
|
+
return _jsxs(_Fragment, { children: [_jsx("style", { children: PREVIEW_PAGE_STYLES }), _jsx("main", { "data-ss-preview-page": "", children: _jsxs("div", { "data-ss-preview-page-content": "", children: [_jsxs("div", { "data-ss-preview-page-brand": "", children: [_jsx(StorystickMark, {}), _jsx("span", { children: "Storystick" })] }), _jsxs("div", { role: role, children: [_jsxs("div", { "data-ss-preview-page-heading": "", children: [indicator, _jsx("h1", { "data-ss-preview-page-title": "", children: title })] }), _jsx("p", { "data-ss-preview-page-description": "", children: description })] }), actions ? _jsx("div", { "data-ss-preview-page-actions": "", children: actions }) : null, children] }) })] });
|
|
8
|
+
}
|
|
9
|
+
export function PreviewPageProgress() {
|
|
10
|
+
return _jsx("span", { "data-ss-preview-page-progress": "", "aria-hidden": "true" });
|
|
11
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ComponentProps, ReactNode } from 'react';
|
|
2
|
+
type MessageProps = {
|
|
3
|
+
title: string;
|
|
4
|
+
description: ReactNode;
|
|
5
|
+
actions?: ReactNode;
|
|
6
|
+
children?: ReactNode;
|
|
7
|
+
role?: 'alert' | 'status';
|
|
8
|
+
};
|
|
9
|
+
/** Also works before the authoring runtime loads, or after its providers fail.
|
|
10
|
+
* Keep these styles self-contained: the host and runtime own separate bundles. */
|
|
11
|
+
export declare function PreviewPanelMessage({ title, description, actions, children, role }: MessageProps): import("react").JSX.Element;
|
|
12
|
+
/** A blocking state fills the panel; recoverable notices use Message directly. */
|
|
13
|
+
export declare function PreviewPanelState(props: MessageProps): import("react").JSX.Element;
|
|
14
|
+
export declare function PreviewPanelAction(props: ComponentProps<'button'>): import("react").JSX.Element;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
/** Also works before the authoring runtime loads, or after its providers fail.
|
|
3
|
+
* Keep these styles self-contained: the host and runtime own separate bundles. */
|
|
4
|
+
export function PreviewPanelMessage({ title, description, actions, children, role = 'alert' }) {
|
|
5
|
+
return _jsxs(_Fragment, { children: [_jsx("style", { children: MESSAGE_STYLES }), _jsxs("div", { "data-ss-panel-message": "", role: role, children: [_jsxs("div", { "data-ss-panel-message-copy": "", children: [_jsx("h2", { children: title }), _jsx("p", { children: description })] }), actions ? _jsx("div", { "data-ss-panel-message-actions": "", children: actions }) : null, children] })] });
|
|
6
|
+
}
|
|
7
|
+
/** A blocking state fills the panel; recoverable notices use Message directly. */
|
|
8
|
+
export function PreviewPanelState(props) {
|
|
9
|
+
return _jsx("div", { "data-ss-panel-state": "", children: _jsx(PreviewPanelMessage, { ...props }) });
|
|
10
|
+
}
|
|
11
|
+
export function PreviewPanelAction(props) {
|
|
12
|
+
return _jsx("button", { type: "button", "data-ss-panel-action": "", ...props });
|
|
13
|
+
}
|
|
14
|
+
const MESSAGE_STYLES = `
|
|
15
|
+
[data-ss-panel-state] { box-sizing: border-box; display: flex; flex-direction: column; width: 100%; height: 100%; min-height: 0; min-width: 0; overflow: auto; padding: 24px; background: var(--ss-surface-page, #fff); }
|
|
16
|
+
[data-ss-panel-state] > [data-ss-panel-message] { margin: auto; padding: 0; }
|
|
17
|
+
[data-ss-panel-message] { box-sizing: border-box; flex: none; width: 100%; max-width: 360px; margin-inline: auto; padding: 16px; color: #52525b; font: 400 13px/1.5 "Storystick Inter", "Inter Variable", ui-sans-serif, system-ui, sans-serif; text-align: center; overflow-wrap: anywhere; }
|
|
18
|
+
[data-ss-panel-message-copy] h2 { margin: 0; color: #18181b; font-size: 15px; font-weight: 500; line-height: 1.4; text-wrap: balance; }
|
|
19
|
+
[data-ss-panel-message-copy] p { margin: 8px 0 0; text-wrap: pretty; }
|
|
20
|
+
[data-ss-panel-message-actions] { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: 8px; margin-top: 16px; }
|
|
21
|
+
[data-ss-panel-action] { display: inline-flex; align-items: center; justify-content: center; min-height: 32px; padding: 6px 14px; border: 1px solid transparent; border-radius: 999px; background: #18181b; color: #fff; font: inherit; line-height: 20px; cursor: pointer; text-decoration: none; }
|
|
22
|
+
[data-ss-panel-action]:hover { background: #303034; }
|
|
23
|
+
[data-ss-panel-action]:focus-visible, [data-ss-panel-message] summary:focus-visible { outline: 2px solid #3b82f6; outline-offset: 3px; }
|
|
24
|
+
[data-ss-panel-action]:disabled { opacity: .5; cursor: default; }
|
|
25
|
+
[data-ss-panel-message] details { width: 100%; margin-top: 16px; }
|
|
26
|
+
[data-ss-panel-message] summary { cursor: pointer; border-radius: 4px; }
|
|
27
|
+
[data-ss-panel-message] pre { max-height: 240px; overflow: auto; margin: 12px 0 0; padding: 12px; border-radius: 12px; background: #f4f4f5; color: #52525b; font: 12px/1.5 ui-monospace, monospace; text-align: left; white-space: pre; }
|
|
28
|
+
`;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare const PREVIEW_PROVENANCE_PUBLISHER_CONSUMED_EVENT: "storystick.preview.provenance-publisher-consumed";
|
|
2
|
+
export declare const PREVIEW_PROVENANCE_PUBLISHER_CONSUMED_REGISTRY: "__ssPreviewProvenanceConsumedPublishersV2";
|
|
3
|
+
export declare function serializePreviewProvenancePublisher(value: unknown): string;
|
|
4
|
+
export declare function previewProvenancePublisherConsumptionKey(frameId: string, renderNonce: string): string;
|
|
5
|
+
export declare const PREVIEW_PROVENANCE_OCCURRENCE_EVENT: "storystick.preview.provenance-occurrence";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export const PREVIEW_PROVENANCE_PUBLISHER_CONSUMED_EVENT = 'storystick.preview.provenance-publisher-consumed';
|
|
2
|
+
export const PREVIEW_PROVENANCE_PUBLISHER_CONSUMED_REGISTRY = '__ssPreviewProvenanceConsumedPublishersV2';
|
|
3
|
+
export function serializePreviewProvenancePublisher(value) {
|
|
4
|
+
return JSON.stringify(value).replace(/</g, '\\u003c');
|
|
5
|
+
}
|
|
6
|
+
export function previewProvenancePublisherConsumptionKey(frameId, renderNonce) {
|
|
7
|
+
return JSON.stringify([frameId, renderNonce]);
|
|
8
|
+
}
|
|
9
|
+
export const PREVIEW_PROVENANCE_OCCURRENCE_EVENT = 'storystick.preview.provenance-occurrence';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type ReactElement } from 'react';
|
|
2
|
+
import type { PreviewProvenancePublicSidecar } from './preview-provenance.js';
|
|
3
|
+
export interface PreviewProvenanceSidecarProps {
|
|
4
|
+
readonly frameId: string;
|
|
5
|
+
readonly sessionId?: string | null;
|
|
6
|
+
readonly sidecar: PreviewProvenancePublicSidecar | null | undefined;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Publishes one server-rendered frame early enough for the deferred preview
|
|
10
|
+
* runtime to consume it, then lets React retire the inert capsule immediately
|
|
11
|
+
* after hydration. The runtime deliberately leaves React-owned capsules in
|
|
12
|
+
* place so it never mutates the tree React is about to hydrate.
|
|
13
|
+
*/
|
|
14
|
+
export declare function PreviewProvenanceSidecar({ frameId, sessionId, sidecar, }: PreviewProvenanceSidecarProps): ReactElement | null;
|
|
15
|
+
export declare const PreviewProvenancePublisher: typeof PreviewProvenanceSidecar;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { createElement, useLayoutEffect, useState } from 'react';
|
|
3
|
+
import { PREVIEW_PROVENANCE_PUBLISHER_CONSUMED_EVENT, PREVIEW_PROVENANCE_OCCURRENCE_EVENT, PREVIEW_PROVENANCE_PUBLISHER_CONSUMED_REGISTRY, previewProvenancePublisherConsumptionKey, serializePreviewProvenancePublisher, } from './preview-provenance-publisher-model.js';
|
|
4
|
+
/**
|
|
5
|
+
* Publishes one server-rendered frame early enough for the deferred preview
|
|
6
|
+
* runtime to consume it, then lets React retire the inert capsule immediately
|
|
7
|
+
* after hydration. The runtime deliberately leaves React-owned capsules in
|
|
8
|
+
* place so it never mutates the tree React is about to hydrate.
|
|
9
|
+
*/
|
|
10
|
+
export function PreviewProvenanceSidecar({ frameId, sessionId, sidecar, }) {
|
|
11
|
+
const [awaitingConsumption, setAwaitingConsumption] = useState(true);
|
|
12
|
+
const renderNonce = sidecar?.renderNonce;
|
|
13
|
+
const publisherKey = typeof renderNonce === 'string'
|
|
14
|
+
? previewProvenancePublisherConsumptionKey(frameId, renderNonce)
|
|
15
|
+
: null;
|
|
16
|
+
useLayoutEffect(() => {
|
|
17
|
+
if (publisherKey === null)
|
|
18
|
+
return;
|
|
19
|
+
// Consumption belongs to one render epoch. A new RSC occurrence must
|
|
20
|
+
// republish even when this component instance survived the refresh.
|
|
21
|
+
setAwaitingConsumption(true);
|
|
22
|
+
window.dispatchEvent(new CustomEvent(PREVIEW_PROVENANCE_OCCURRENCE_EVENT, {
|
|
23
|
+
detail: { operation: 'mount', frameId, renderNonce },
|
|
24
|
+
}));
|
|
25
|
+
const finishIfConsumed = (event) => {
|
|
26
|
+
const detail = event instanceof CustomEvent
|
|
27
|
+
? event.detail
|
|
28
|
+
: null;
|
|
29
|
+
if (detail !== null && detail.publisherKey !== publisherKey)
|
|
30
|
+
return;
|
|
31
|
+
const registry = window[PREVIEW_PROVENANCE_PUBLISHER_CONSUMED_REGISTRY];
|
|
32
|
+
if (registry?.[publisherKey] === true)
|
|
33
|
+
setAwaitingConsumption(false);
|
|
34
|
+
};
|
|
35
|
+
window.addEventListener(PREVIEW_PROVENANCE_PUBLISHER_CONSUMED_EVENT, finishIfConsumed);
|
|
36
|
+
finishIfConsumed();
|
|
37
|
+
return () => {
|
|
38
|
+
window.dispatchEvent(new CustomEvent(PREVIEW_PROVENANCE_OCCURRENCE_EVENT, {
|
|
39
|
+
detail: { operation: 'unmount', frameId, renderNonce },
|
|
40
|
+
}));
|
|
41
|
+
window.removeEventListener(PREVIEW_PROVENANCE_PUBLISHER_CONSUMED_EVENT, finishIfConsumed);
|
|
42
|
+
};
|
|
43
|
+
}, [publisherKey, frameId, renderNonce]);
|
|
44
|
+
if (!awaitingConsumption ||
|
|
45
|
+
!sidecar ||
|
|
46
|
+
(!sessionId && !sidecar.dependencySet?.readContext) ||
|
|
47
|
+
typeof sidecar.renderNonce !== 'string' ||
|
|
48
|
+
sidecar.renderNonce.length === 0 ||
|
|
49
|
+
typeof sidecar.emissionRevision !== 'number' ||
|
|
50
|
+
!Number.isFinite(sidecar.emissionRevision) ||
|
|
51
|
+
sidecar.emissionRevision <= 0)
|
|
52
|
+
return null;
|
|
53
|
+
return createElement('template', {
|
|
54
|
+
'data-ss-provenance-publisher': frameId,
|
|
55
|
+
'data-ss-provenance-react-owned': 'true',
|
|
56
|
+
'data-preview-message-session-id': sessionId,
|
|
57
|
+
suppressHydrationWarning: true,
|
|
58
|
+
dangerouslySetInnerHTML: {
|
|
59
|
+
__html: serializePreviewProvenancePublisher({
|
|
60
|
+
type: 'storystick.preview.provenance-publisher',
|
|
61
|
+
version: 2,
|
|
62
|
+
sessionId,
|
|
63
|
+
frame: { frameId, sidecar },
|
|
64
|
+
}),
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
export const PreviewProvenancePublisher = PreviewProvenanceSidecar;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { PreviewProvenancePublisher, PreviewProvenanceSidecar, } from './preview-provenance-publisher.js';
|
|
2
|
+
export type { PreviewProvenanceSidecarProps } from './preview-provenance-publisher.js';
|
|
3
|
+
export { PREVIEW_PROVENANCE_PUBLISHER_CONSUMED_EVENT, PREVIEW_PROVENANCE_PUBLISHER_CONSUMED_REGISTRY, previewProvenancePublisherConsumptionKey, serializePreviewProvenancePublisher, } from './preview-provenance-publisher-model.js';
|
|
4
|
+
export { isPreviewSliceDirectInvocationCandidate, readPreviewSliceScopeAdapter, registerPreviewSliceScopeAdapter, } from './preview-slice-scope.js';
|
|
5
|
+
export type { PreviewSliceScopeAdapter, PreviewSliceScopeDescriptor, PreviewSliceScopeInvocation, } from './preview-slice-scope.js';
|
|
6
|
+
export * from '@storystick/preview/provenance/render-context';
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { PreviewProvenancePublisher, PreviewProvenanceSidecar, } from './preview-provenance-publisher.js';
|
|
2
|
+
export { PREVIEW_PROVENANCE_PUBLISHER_CONSUMED_EVENT, PREVIEW_PROVENANCE_PUBLISHER_CONSUMED_REGISTRY, previewProvenancePublisherConsumptionKey, serializePreviewProvenancePublisher, } from './preview-provenance-publisher-model.js';
|
|
3
|
+
export { isPreviewSliceDirectInvocationCandidate, readPreviewSliceScopeAdapter, registerPreviewSliceScopeAdapter, } from './preview-slice-scope.js';
|
|
4
|
+
export * from '@storystick/preview/provenance/render-context';
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* preview-scope — the SliceZone ↔ `Slice.*` prod-strip contract (ADR-129
|
|
3
|
+
* D3 / D4-amended).
|
|
4
|
+
*
|
|
5
|
+
* `Slice.section` emits the `data-ss-slice` binding attribute ONLY in
|
|
6
|
+
* preview. The runtime preview-mode signal cannot be a React context —
|
|
7
|
+
* a React Server Component cannot read context, so a context-gated
|
|
8
|
+
* `Slice.*` would be forced `'use client'` and stop being RSC-safe
|
|
9
|
+
* (empirically confirmed, ADR-129 vertical-slice de-risk, check C).
|
|
10
|
+
*
|
|
11
|
+
* Instead the signal travels on the one object `Slice.section` already
|
|
12
|
+
* receives: the `slice` prop. `<SliceZone>` is the single component
|
|
13
|
+
* that knows preview-vs-production (the `publishedAt`/`snapshotId`
|
|
14
|
+
* discriminator — see `slice-zone.tsx`). In preview it STAMPS each
|
|
15
|
+
* slice object it passes to a slice component with that slice's
|
|
16
|
+
* serialized per-slice model (ADR-129 D4/D9). `Slice.section` reads the
|
|
17
|
+
* stamp and emits it back as the `data-ss-model` attribute — on the
|
|
18
|
+
* slice's own root element, alongside `data-ss-slice`. Outside preview
|
|
19
|
+
* the slice is passed through unstamped and neither attribute is
|
|
20
|
+
* emitted — a true prod-strip, RSC-safe, zero hooks.
|
|
21
|
+
*
|
|
22
|
+
* D9 amendment (cross-model /review R5) — the per-slice model is a
|
|
23
|
+
* `data-ss-model` ATTRIBUTE, never a `<script>` node. A `<script>`
|
|
24
|
+
* sibling is `display:none` but still an element, so it shifts
|
|
25
|
+
* `:nth-child` / `first:` / `odd:` / `even:` for the customer's slices
|
|
26
|
+
* (preview ≠ production). An attribute on the slice's own element
|
|
27
|
+
* shifts nothing — the same rule that drove D4 from a marker-node to
|
|
28
|
+
* an attribute. With the island node gone, ADR-129 injects zero nodes.
|
|
29
|
+
*
|
|
30
|
+
* ── RSC AMENDMENT (2026-05-27) ──────────────────────────────────────
|
|
31
|
+
* The stamp was originally a module-local `Symbol` key. Next.js 15's
|
|
32
|
+
* RSC serializer rejects Symbol-keyed properties on objects passed
|
|
33
|
+
* from a Server Component to a Client Component with the error
|
|
34
|
+
* "Only plain objects can be passed to Client Components from
|
|
35
|
+
* Server Components. Objects with symbol properties like
|
|
36
|
+
* `storystick.previewScope` are not supported."
|
|
37
|
+
* Customer slice components are routinely `'use client'` (interactive
|
|
38
|
+
* forms, hooks, …); even when the slice component is RSC, its
|
|
39
|
+
* descendants are usually client components, so the stamped slice
|
|
40
|
+
* always ends up crossing the RSC→Client boundary in real apps. The
|
|
41
|
+
* Symbol was a non-starter on Next 15.
|
|
42
|
+
*
|
|
43
|
+
* The stamp is now a string-keyed property under a fresh, namespaced
|
|
44
|
+
* Storystick reserved key: `_ssPreview`. The earlier `_model` slot
|
|
45
|
+
* was rejected by codex R1 — `_model` is the ADR-053 model-linking
|
|
46
|
+
* sidecar key that `hydrateSlice` reads as a string model id, so
|
|
47
|
+
* reusing it would (a) overwrite the model link in preview and
|
|
48
|
+
* (b) false-positive `isPreviewScoped()` on every production-linked
|
|
49
|
+
* slice. The `_ss*` prefix mirrors the existing `data-ss-*` DOM
|
|
50
|
+
* attribute convention used by `Slice.*` (`data-ss-slice`,
|
|
51
|
+
* `data-ss-bind`, `data-ss-model`) — one namespace, no ambiguity.
|
|
52
|
+
*
|
|
53
|
+
* Codex R2 then raised a P2: a string key alone is forgeable — a
|
|
54
|
+
* customer could (in theory) define a slice field literally named
|
|
55
|
+
* `_ssPreview` of type `string`, and SliceZone would mistake it for
|
|
56
|
+
* a preview stamp. The mitigation is structural: the stamp value is
|
|
57
|
+
* not a bare string but a tagged object `{ v: 'ss-preview-1', model:
|
|
58
|
+
* <JSON> }`. `isPreviewScoped()` checks the tag, not just the key —
|
|
59
|
+
* a customer field with the same string-typed name is rejected by
|
|
60
|
+
* shape.
|
|
61
|
+
*
|
|
62
|
+
* Codex R3 P2 (accepted as documented tradeoff): a customer COULD
|
|
63
|
+
* still define a field literally named `_ssPreview` with an object
|
|
64
|
+
* value that exactly matches the tagged shape `{ v: 'ss-preview-1',
|
|
65
|
+
* model: <string> }`. Two reasons we accept this:
|
|
66
|
+
*
|
|
67
|
+
* 1. The `_<x>` prefix is the existing Storystick reserved
|
|
68
|
+
* namespace — `_type`, `_key`, `_variation`, `_model` all live
|
|
69
|
+
* there. Adding `_ssPreview` extends the established convention.
|
|
70
|
+
* The storyspec format documents leading-underscore field names
|
|
71
|
+
* as SDK-reserved; the schema generator doesn't emit them.
|
|
72
|
+
* 2. The Symbol-keyed alternative was the ONLY structurally
|
|
73
|
+
* unforgeable choice, and it is incompatible with Next 15's RSC
|
|
74
|
+
* serializer (the bug this fix addresses). There is no third
|
|
75
|
+
* design that satisfies BOTH "RSC-serializable" AND
|
|
76
|
+
* "structurally unforgeable from any customer field".
|
|
77
|
+
*
|
|
78
|
+
* If a customer in v∞ produces a field shaped exactly like the stamp,
|
|
79
|
+
* the failure mode is overwrite-in-preview only (production renders
|
|
80
|
+
* never call `stampPreviewScope`). Detection would be loud (the
|
|
81
|
+
* customer's component receives a model JSON instead of its data),
|
|
82
|
+
* and the schema reader is the right place to forbid `_<x>` field
|
|
83
|
+
* names — a follow-up scoped to the schema parser, not this SDK seam.
|
|
84
|
+
*
|
|
85
|
+
* `slice-zone.tsx::SLICE_META_KEYS` gains `_ssPreview` so
|
|
86
|
+
* `extractSliceFields` filters it out of the serialized model
|
|
87
|
+
* payload (no recursive self-embedding of the stamp into its own
|
|
88
|
+
* model). Customer iteration (`Object.keys`, `JSON.stringify` of the
|
|
89
|
+
* slice) sees one extra meta key under the same `_*` convention
|
|
90
|
+
* customers already filter when walking slice fields; in production
|
|
91
|
+
* the property is never stamped, so production renders are
|
|
92
|
+
* byte-identical to a slice with no preview machinery — the
|
|
93
|
+
* reference-stable fast path is preserved.
|
|
94
|
+
*
|
|
95
|
+
* Why this is solid:
|
|
96
|
+
* - Plain object property ⇒ RSC-safe by construction.
|
|
97
|
+
* - Fresh namespaced key + tagged value shape ⇒ unforgeable from a
|
|
98
|
+
* customer field of any type the schema generator can emit.
|
|
99
|
+
* - Filtered by `extractSliceFields` via `SLICE_META_KEYS` ⇒ doesn't
|
|
100
|
+
* recurse into the serialized model payload.
|
|
101
|
+
* - Production stays unstamped ⇒ no leak in prod, no reference churn.
|
|
102
|
+
* - The wire contract with `@storystick/preview` is the DOM
|
|
103
|
+
* `data-ss-model` attribute — that has not changed.
|
|
104
|
+
*/
|
|
105
|
+
/**
|
|
106
|
+
* Return a shallow copy of `slice` carrying the preview-scope stamp —
|
|
107
|
+
* the serialized per-slice model string `<SliceZone>` built for it.
|
|
108
|
+
* A copy (never a mutation) so `hydrateSlice`'s no-mutation invariant
|
|
109
|
+
* holds and the customer's query-cache entry is untouched. The copy
|
|
110
|
+
* costs one object identity per slice per render — accepted: it is
|
|
111
|
+
* stamped ONLY in preview, where slices already re-render on every
|
|
112
|
+
* live-content revision, so `React.memo` stability is not in play.
|
|
113
|
+
* Production renders never call this — there `hydrateSlice`'s
|
|
114
|
+
* reference-stable fast path is preserved intact.
|
|
115
|
+
*/
|
|
116
|
+
export declare function stampPreviewScope<T extends object>(slice: T, modelJson: string): T;
|
|
117
|
+
/**
|
|
118
|
+
* The serialized per-slice model `<SliceZone>` stamped on this slice,
|
|
119
|
+
* or `undefined` when the slice is not preview-scoped (production, a
|
|
120
|
+
* bare slice, a test stub, or a customer field that happens to share
|
|
121
|
+
* the `_ssPreview` name but doesn't match the stamp shape).
|
|
122
|
+
*/
|
|
123
|
+
export declare function getPreviewModel(slice: unknown): string | undefined;
|
|
124
|
+
/**
|
|
125
|
+
* Was this slice object stamped by `<SliceZone>` as preview-scoped?
|
|
126
|
+
* Equivalent to "has a per-slice model" — the two are co-stamped.
|
|
127
|
+
*/
|
|
128
|
+
export declare function isPreviewScoped(slice: unknown): boolean;
|