@storystick/react 0.0.0-stage → 0.2.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/LICENSE +112 -0
  2. package/NOTICE +17 -0
  3. package/PUBLIC-API.md +36 -0
  4. package/README.md +226 -2
  5. package/dist/.source-hash +1 -0
  6. package/dist/file.d.ts +18 -0
  7. package/dist/file.js +44 -0
  8. package/dist/hydrate-slice.d.ts +3 -0
  9. package/dist/hydrate-slice.js +1 -0
  10. package/dist/image-component.d.ts +26 -0
  11. package/dist/image-component.js +79 -0
  12. package/dist/image-element.d.ts +6 -0
  13. package/dist/image-element.js +68 -0
  14. package/dist/image.d.ts +3 -0
  15. package/dist/image.js +3 -0
  16. package/dist/index.d.ts +1 -0
  17. package/dist/index.js +2 -0
  18. package/dist/link-adapter.d.ts +2 -0
  19. package/dist/link-adapter.js +2 -0
  20. package/dist/link-component.d.ts +53 -0
  21. package/dist/link-component.js +97 -0
  22. package/dist/link.d.ts +3 -0
  23. package/dist/link.js +3 -0
  24. package/dist/preview-generation.d.ts +21 -0
  25. package/dist/preview-generation.js +134 -0
  26. package/dist/preview-host/preview-assistant-continuation.d.ts +9 -0
  27. package/dist/preview-host/preview-assistant-continuation.js +39 -0
  28. package/dist/preview-host/preview-authoring-client.d.ts +12 -0
  29. package/dist/preview-host/preview-authoring-client.js +100 -0
  30. package/dist/preview-host/preview-editor-continuation.d.ts +13 -0
  31. package/dist/preview-host/preview-editor-continuation.js +68 -0
  32. package/dist/preview-host/preview-standalone-controls.d.ts +18 -0
  33. package/dist/preview-host/preview-standalone-controls.js +518 -0
  34. package/dist/preview-host/preview-studio-destination.d.ts +20 -0
  35. package/dist/preview-host/preview-studio-destination.js +55 -0
  36. package/dist/preview-host/use-standalone-presentation.d.ts +15 -0
  37. package/dist/preview-host/use-standalone-presentation.js +223 -0
  38. package/dist/preview-icons.d.ts +10 -0
  39. package/dist/preview-icons.js +8 -0
  40. package/dist/preview-interaction-control.d.ts +6 -0
  41. package/dist/preview-interaction-control.js +39 -0
  42. package/dist/preview-panel-state.d.ts +15 -0
  43. package/dist/preview-panel-state.js +28 -0
  44. package/dist/preview-provenance-publisher-model.d.ts +5 -0
  45. package/dist/preview-provenance-publisher-model.js +9 -0
  46. package/dist/preview-provenance-publisher.d.ts +15 -0
  47. package/dist/preview-provenance-publisher.js +68 -0
  48. package/dist/preview-provenance.d.ts +6 -0
  49. package/dist/preview-provenance.js +4 -0
  50. package/dist/preview-scope.d.ts +128 -0
  51. package/dist/preview-scope.js +161 -0
  52. package/dist/preview-slice-scope.d.ts +32 -0
  53. package/dist/preview-slice-scope.js +33 -0
  54. package/dist/preview-text-props.d.ts +18 -0
  55. package/dist/preview-text-props.js +13 -0
  56. package/dist/preview-toolbar-layout.d.ts +42 -0
  57. package/dist/preview-toolbar-layout.js +139 -0
  58. package/dist/preview-toolbar-panel.d.ts +13 -0
  59. package/dist/preview-toolbar-panel.js +6 -0
  60. package/dist/preview-toolbar-resize-handles.d.ts +9 -0
  61. package/dist/preview-toolbar-resize-handles.js +14 -0
  62. package/dist/preview-toolbar-shell.d.ts +18 -0
  63. package/dist/preview-toolbar-shell.js +138 -0
  64. package/dist/public-rich-text.d.ts +15 -0
  65. package/dist/public-rich-text.js +11 -0
  66. package/dist/public-slice-zone.d.ts +7 -0
  67. package/dist/public-slice-zone.js +6 -0
  68. package/dist/query-cache.d.ts +14 -0
  69. package/dist/query-cache.js +289 -0
  70. package/dist/query-preview-loader.d.ts +9 -0
  71. package/dist/query-preview-loader.js +28 -0
  72. package/dist/query-preview-runtime.d.ts +3 -0
  73. package/dist/query-preview-runtime.js +3 -0
  74. package/dist/rendering.d.ts +29 -0
  75. package/dist/rendering.js +17 -0
  76. package/dist/resize-edge.d.ts +8 -0
  77. package/dist/resize-edge.js +33 -0
  78. package/dist/rich-text.d.ts +150 -0
  79. package/dist/rich-text.js +618 -0
  80. package/dist/slice-render-failure.d.ts +8 -0
  81. package/dist/slice-render-failure.js +46 -0
  82. package/dist/slice-zone.d.ts +82 -0
  83. package/dist/slice-zone.js +273 -0
  84. package/dist/slice.d.ts +16 -0
  85. package/dist/slice.js +46 -0
  86. package/dist/storystick-mark.d.ts +3 -0
  87. package/dist/storystick-mark.js +5 -0
  88. package/dist/use-generated-query.d.ts +37 -0
  89. package/dist/use-generated-query.js +308 -0
  90. package/dist/use-momentary-preview-navigate-mode.d.ts +11 -0
  91. package/dist/use-momentary-preview-navigate-mode.js +158 -0
  92. package/dist/use-preview-perspective-intent.d.ts +53 -0
  93. package/dist/use-preview-perspective-intent.js +227 -0
  94. package/dist/use-preview-pointer-drag.d.ts +16 -0
  95. package/dist/use-preview-pointer-drag.js +98 -0
  96. package/dist/use-preview-toolbar-layout.d.ts +36 -0
  97. package/dist/use-preview-toolbar-layout.js +141 -0
  98. package/dist/use-render-mode-debounce.d.ts +31 -0
  99. package/dist/use-render-mode-debounce.js +143 -0
  100. package/dist/video.d.ts +21 -0
  101. package/dist/video.js +63 -0
  102. package/package.json +146 -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,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: #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;