@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.
Files changed (106) hide show
  1. package/LICENSE +112 -0
  2. package/NOTICE +17 -0
  3. package/PUBLIC-API.md +38 -0
  4. package/README.md +226 -2
  5. package/dist/.source-hash +1 -0
  6. package/dist/authoring-skeleton.d.ts +5 -0
  7. package/dist/authoring-skeleton.js +29 -0
  8. package/dist/file.d.ts +18 -0
  9. package/dist/file.js +44 -0
  10. package/dist/hydrate-slice.d.ts +3 -0
  11. package/dist/hydrate-slice.js +1 -0
  12. package/dist/image-component.d.ts +26 -0
  13. package/dist/image-component.js +79 -0
  14. package/dist/image-element.d.ts +6 -0
  15. package/dist/image-element.js +68 -0
  16. package/dist/image.d.ts +3 -0
  17. package/dist/image.js +3 -0
  18. package/dist/index.d.ts +1 -0
  19. package/dist/index.js +2 -0
  20. package/dist/link-adapter.d.ts +2 -0
  21. package/dist/link-adapter.js +2 -0
  22. package/dist/link-component.d.ts +53 -0
  23. package/dist/link-component.js +97 -0
  24. package/dist/link.d.ts +3 -0
  25. package/dist/link.js +3 -0
  26. package/dist/preview-generation.d.ts +21 -0
  27. package/dist/preview-generation.js +134 -0
  28. package/dist/preview-host/preview-assistant-continuation.d.ts +9 -0
  29. package/dist/preview-host/preview-assistant-continuation.js +39 -0
  30. package/dist/preview-host/preview-authoring-client.d.ts +12 -0
  31. package/dist/preview-host/preview-authoring-client.js +108 -0
  32. package/dist/preview-host/preview-editor-continuation.d.ts +13 -0
  33. package/dist/preview-host/preview-editor-continuation.js +68 -0
  34. package/dist/preview-host/preview-standalone-controls.d.ts +18 -0
  35. package/dist/preview-host/preview-standalone-controls.js +528 -0
  36. package/dist/preview-host/preview-studio-destination.d.ts +20 -0
  37. package/dist/preview-host/preview-studio-destination.js +55 -0
  38. package/dist/preview-host/use-standalone-presentation.d.ts +15 -0
  39. package/dist/preview-host/use-standalone-presentation.js +223 -0
  40. package/dist/preview-icons.d.ts +10 -0
  41. package/dist/preview-icons.js +8 -0
  42. package/dist/preview-interaction-control.d.ts +6 -0
  43. package/dist/preview-interaction-control.js +39 -0
  44. package/dist/preview-page-state.d.ts +12 -0
  45. package/dist/preview-page-state.js +11 -0
  46. package/dist/preview-panel-state.d.ts +15 -0
  47. package/dist/preview-panel-state.js +28 -0
  48. package/dist/preview-provenance-publisher-model.d.ts +5 -0
  49. package/dist/preview-provenance-publisher-model.js +9 -0
  50. package/dist/preview-provenance-publisher.d.ts +15 -0
  51. package/dist/preview-provenance-publisher.js +68 -0
  52. package/dist/preview-provenance.d.ts +6 -0
  53. package/dist/preview-provenance.js +4 -0
  54. package/dist/preview-scope.d.ts +128 -0
  55. package/dist/preview-scope.js +161 -0
  56. package/dist/preview-slice-scope.d.ts +32 -0
  57. package/dist/preview-slice-scope.js +33 -0
  58. package/dist/preview-text-props.d.ts +18 -0
  59. package/dist/preview-text-props.js +13 -0
  60. package/dist/preview-toolbar-layout.d.ts +42 -0
  61. package/dist/preview-toolbar-layout.js +139 -0
  62. package/dist/preview-toolbar-panel.d.ts +14 -0
  63. package/dist/preview-toolbar-panel.js +6 -0
  64. package/dist/preview-toolbar-resize-handles.d.ts +9 -0
  65. package/dist/preview-toolbar-resize-handles.js +14 -0
  66. package/dist/preview-toolbar-shell.d.ts +19 -0
  67. package/dist/preview-toolbar-shell.js +144 -0
  68. package/dist/public-rich-text.d.ts +15 -0
  69. package/dist/public-rich-text.js +11 -0
  70. package/dist/public-slice-zone.d.ts +7 -0
  71. package/dist/public-slice-zone.js +6 -0
  72. package/dist/query-cache.d.ts +14 -0
  73. package/dist/query-cache.js +289 -0
  74. package/dist/query-preview-loader.d.ts +9 -0
  75. package/dist/query-preview-loader.js +28 -0
  76. package/dist/query-preview-runtime.d.ts +3 -0
  77. package/dist/query-preview-runtime.js +3 -0
  78. package/dist/rendering.d.ts +29 -0
  79. package/dist/rendering.js +17 -0
  80. package/dist/resize-edge.d.ts +8 -0
  81. package/dist/resize-edge.js +33 -0
  82. package/dist/rich-text.d.ts +150 -0
  83. package/dist/rich-text.js +618 -0
  84. package/dist/slice-render-failure.d.ts +8 -0
  85. package/dist/slice-render-failure.js +46 -0
  86. package/dist/slice-zone.d.ts +82 -0
  87. package/dist/slice-zone.js +273 -0
  88. package/dist/slice.d.ts +16 -0
  89. package/dist/slice.js +46 -0
  90. package/dist/storystick-mark.d.ts +3 -0
  91. package/dist/storystick-mark.js +6 -0
  92. package/dist/use-generated-query.d.ts +37 -0
  93. package/dist/use-generated-query.js +308 -0
  94. package/dist/use-momentary-preview-navigate-mode.d.ts +11 -0
  95. package/dist/use-momentary-preview-navigate-mode.js +158 -0
  96. package/dist/use-preview-perspective-intent.d.ts +53 -0
  97. package/dist/use-preview-perspective-intent.js +227 -0
  98. package/dist/use-preview-pointer-drag.d.ts +16 -0
  99. package/dist/use-preview-pointer-drag.js +98 -0
  100. package/dist/use-preview-toolbar-layout.d.ts +36 -0
  101. package/dist/use-preview-toolbar-layout.js +141 -0
  102. package/dist/use-render-mode-debounce.d.ts +31 -0
  103. package/dist/use-render-mode-debounce.js +143 -0
  104. package/dist/video.d.ts +21 -0
  105. package/dist/video.js +63 -0
  106. package/package.json +154 -3
@@ -0,0 +1,68 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import * as React from 'react';
4
+ /** Owns only the lifecycle required to remove a server-painted LQIP. */
5
+ export function StorystickImageElement({ blurDataURL, ref, onLoad, onError, src, style, ...props }) {
6
+ const imageRef = React.useRef(null);
7
+ const mountedRef = React.useRef(false);
8
+ const srcKey = typeof src === 'string' ? src : '';
9
+ const currentSrcRef = React.useRef(srcKey);
10
+ currentSrcRef.current = srcKey;
11
+ const [settledSrc, setSettledSrc] = React.useState(null);
12
+ const showsPlaceholder = settledSrc !== srcKey;
13
+ const settle = React.useCallback(async (image) => {
14
+ const expectedSrc = srcKey;
15
+ if (typeof image.decode === 'function') {
16
+ try {
17
+ await image.decode();
18
+ }
19
+ catch {
20
+ // A failed decode must not leave the placeholder over native error UI.
21
+ }
22
+ }
23
+ if (!mountedRef.current || currentSrcRef.current !== expectedSrc)
24
+ return;
25
+ setSettledSrc(expectedSrc);
26
+ }, [srcKey]);
27
+ React.useEffect(() => {
28
+ mountedRef.current = true;
29
+ const image = imageRef.current;
30
+ if (image?.complete) {
31
+ void settle(image);
32
+ }
33
+ return () => {
34
+ mountedRef.current = false;
35
+ };
36
+ }, [settle]);
37
+ const setImageRef = React.useCallback((element) => {
38
+ imageRef.current = element;
39
+ const cleanup = typeof ref === 'function' ? ref(element) : undefined;
40
+ if (ref && typeof ref !== 'function')
41
+ ref.current = element;
42
+ return () => {
43
+ imageRef.current = null;
44
+ if (typeof cleanup === 'function')
45
+ cleanup();
46
+ else if (typeof ref === 'function')
47
+ ref(null);
48
+ else if (ref)
49
+ ref.current = null;
50
+ };
51
+ }, [ref]);
52
+ const placeholderStyle = showsPlaceholder
53
+ ? {
54
+ backgroundImage: `url(${JSON.stringify(blurDataURL)})`,
55
+ backgroundPosition: style?.objectPosition ?? 'center',
56
+ backgroundRepeat: 'no-repeat',
57
+ backgroundSize: 'cover',
58
+ }
59
+ : undefined;
60
+ return (_jsx("img", { ...props, ref: setImageRef, src: src, style: placeholderStyle ? { ...style, ...placeholderStyle } : style, onLoad: (event) => {
61
+ const image = event.currentTarget;
62
+ void settle(image);
63
+ onLoad?.(event);
64
+ }, onError: (event) => {
65
+ setSettledSrc(srcKey);
66
+ onError?.(event);
67
+ } }));
68
+ }
@@ -0,0 +1,3 @@
1
+ /** The media resolver owns both autonomous and explicitly sourced values. */
2
+ export { resolveImage } from '@storystick/media';
3
+ export { StorystickImage, type StorystickImageComponentProps, type StorystickImageFailure, type StorystickImageFallback, } from './image-component.js';
package/dist/image.js ADDED
@@ -0,0 +1,3 @@
1
+ /** The media resolver owns both autonomous and explicitly sourced values. */
2
+ export { resolveImage } from '@storystick/media';
3
+ export { StorystickImage, } from './image-component.js';
@@ -0,0 +1 @@
1
+ export * from './rendering.js';
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ // Rendering and typed client primitives. Reactive reads use @storystick/react/query.
2
+ export * from './rendering.js';
@@ -0,0 +1,2 @@
1
+ /** Framework adapter boundary. Application code should use StorystickLink. */
2
+ export { StorystickLinkAdapter, type StorystickLinkAdapterProps, } from './link-component.js';
@@ -0,0 +1,2 @@
1
+ /** Framework adapter boundary. Application code should use StorystickLink. */
2
+ export { StorystickLinkAdapter, } from './link-component.js';
@@ -0,0 +1,53 @@
1
+ import { type LinkDocument as StorystickLinkDocument } from '@storystick/types';
2
+ import * as React from 'react';
3
+ import { type LinkOptions, type LinkValue } from '@storystick/types';
4
+ export type StorystickLinkValue = LinkValue & {
5
+ readonly text?: React.ReactNode;
6
+ readonly title?: string | null;
7
+ };
8
+ type LinkSource = {
9
+ readonly link: StorystickLinkValue | null | undefined;
10
+ readonly document?: never;
11
+ readonly href?: never;
12
+ };
13
+ type DocumentSource = {
14
+ readonly link?: never;
15
+ readonly document: StorystickLinkDocument | null | undefined;
16
+ readonly href?: never;
17
+ };
18
+ export type { LinkDocument as StorystickLinkDocument } from '@storystick/types';
19
+ type HrefSource = {
20
+ readonly link?: never;
21
+ readonly document?: never;
22
+ readonly href: string | null | undefined;
23
+ };
24
+ export type StorystickLinkOptions = LinkOptions & {
25
+ readonly fallbackLabel?: React.ReactNode;
26
+ };
27
+ type AnchorPassthroughProps = Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, 'children' | 'href' | 'rel' | 'target' | 'title' | 'suppressHydrationWarning'>;
28
+ type RegularComposition = {
29
+ readonly asChild?: false;
30
+ readonly children?: React.ReactNode;
31
+ };
32
+ type ChildComposition = {
33
+ readonly asChild: true;
34
+ readonly children: React.ReactElement;
35
+ };
36
+ type StorystickLinkCommonProps = AnchorPassthroughProps & {
37
+ readonly options?: StorystickLinkOptions;
38
+ readonly title?: string | null;
39
+ };
40
+ export type StorystickLinkProps = StorystickLinkCommonProps & (LinkSource | DocumentSource | HrefSource) & (RegularComposition | ChildComposition);
41
+ export type StorystickLinkAdapterProps = StorystickLinkProps & {
42
+ /** Router-aware component used only for resolved internal links. */
43
+ readonly internalComponent?: React.ElementType;
44
+ /** Adapter-owned props forwarded to the internal component. */
45
+ readonly internalComponentProps?: Readonly<Record<string, unknown>>;
46
+ };
47
+ /**
48
+ * Framework-neutral link renderer. Internal links use a native anchor unless
49
+ * a router adapter supplies `internalComponent`.
50
+ */
51
+ export declare function StorystickLink(input: StorystickLinkProps): React.ReactElement;
52
+ /** @internal Framework adapters may replace the renderer for internal links. */
53
+ export declare function StorystickLinkAdapter(input: StorystickLinkAdapterProps): React.ReactElement;
@@ -0,0 +1,97 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { resolveDocumentLink } from '@storystick/types';
3
+ import { cleanPreviewAttributes } from '@storystick/types/internal/preview-provenance';
4
+ import { stripPreviewProvenanceTextMarkers } from '@storystick/types/preview-provenance';
5
+ import * as React from 'react';
6
+ import { previewTextProps } from './preview-text-props.js';
7
+ import { resolveLink, } from '@storystick/types';
8
+ /**
9
+ * Framework-neutral link renderer. Internal links use a native anchor unless
10
+ * a router adapter supplies `internalComponent`.
11
+ */
12
+ export function StorystickLink(input) {
13
+ return _jsx(StorystickLinkAdapter, { ...input });
14
+ }
15
+ /** @internal Framework adapters may replace the renderer for internal links. */
16
+ export function StorystickLinkAdapter(input) {
17
+ const { asChild = false, children, options, title, internalComponent: InternalComponent = 'a', internalComponentProps, link: _link, document: _document, href: _href, ...anchorProps } = input;
18
+ const { fallbackLabel, ...linkOptions } = options ?? {};
19
+ const resolved = resolveInput(input, linkOptions);
20
+ const fieldLabel = 'link' in input ? input.link?.text : undefined;
21
+ const authoredTitle = 'link' in input ? input.link?.title : undefined;
22
+ const selectedTitle = Object.prototype.hasOwnProperty.call(input, 'title')
23
+ ? title
24
+ : authoredTitle;
25
+ const cleanTitle = cleanedTitle(selectedTitle);
26
+ const diagnosticStatus = linkDiagnosticStatus(resolved.status);
27
+ const { href: resolvedHref, ...resolvedAttrs } = resolved.props;
28
+ const commonProps = {
29
+ ...cleanPreviewAttributes(anchorProps),
30
+ ...resolvedAttrs,
31
+ 'data-ss-e': resolved.props['data-ss-e'],
32
+ 'data-ss-link-status': diagnosticStatus,
33
+ title: cleanTitle,
34
+ suppressHydrationWarning: true,
35
+ };
36
+ if (asChild) {
37
+ const child = React.Children.only(children);
38
+ const childProps = child.props;
39
+ const childLabel = childProps.children ?? fieldLabel ?? fallbackLabel;
40
+ const props = {
41
+ ...cleanPreviewAttributes({ ...childProps, ...anchorProps }),
42
+ ...resolvedAttrs,
43
+ href: resolvedHref,
44
+ 'data-ss-e': resolved.props['data-ss-e'],
45
+ 'data-ss-link-status': diagnosticStatus,
46
+ title: cleanTitle,
47
+ suppressHydrationWarning: true,
48
+ };
49
+ return React.cloneElement(child, { ...props, ...linkLabelProps(childLabel, props) });
50
+ }
51
+ const label = children ?? fieldLabel ?? fallbackLabel;
52
+ const labelProps = linkLabelProps(label, commonProps);
53
+ if (resolved.status === 'ok' || resolved.status === 'missing-anchor') {
54
+ return React.createElement(InternalComponent, {
55
+ ...internalComponentProps,
56
+ ...commonProps,
57
+ href: resolved.props.href,
58
+ ...labelProps,
59
+ });
60
+ }
61
+ if (resolved.status === 'external' || resolved.status === 'asset') {
62
+ return _jsx("a", { href: resolved.props.href, ...commonProps, ...labelProps });
63
+ }
64
+ return _jsx("a", { ...commonProps, ...labelProps });
65
+ }
66
+ function linkLabelProps(label, elementProps) {
67
+ if (typeof label !== 'string')
68
+ return { children: label };
69
+ const props = previewTextProps(label);
70
+ // The link/object field already owns this element when it has provenance.
71
+ // Cleaning its label must not add a competing text-field owner.
72
+ return elementProps['data-ss-e'] != null || elementProps['data-ss-p'] != null
73
+ ? { children: props.children }
74
+ : props;
75
+ }
76
+ function resolveInput(input, options) {
77
+ if ('link' in input)
78
+ return resolveLink(input.link, options);
79
+ if ('document' in input) {
80
+ if (!input.document)
81
+ return resolveLink(null, options);
82
+ return resolveDocumentLink(input.document, options);
83
+ }
84
+ return resolveLink(input.href === null || input.href === undefined
85
+ ? null
86
+ : { href: input.href }, options);
87
+ }
88
+ function cleanedTitle(value) {
89
+ if (typeof value !== 'string')
90
+ return undefined;
91
+ return stripPreviewProvenanceTextMarkers(value) || undefined;
92
+ }
93
+ function linkDiagnosticStatus(status) {
94
+ return status === 'ok' || status === 'external' || status === 'asset'
95
+ ? undefined
96
+ : status;
97
+ }
package/dist/link.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ /** One resolver for ordinary JS, React and framework adapters. */
2
+ export { resolveLink } from '@storystick/types';
3
+ export { StorystickLink, type StorystickLinkDocument, type StorystickLinkOptions, type StorystickLinkProps, type StorystickLinkValue, } from './link-component.js';
package/dist/link.js ADDED
@@ -0,0 +1,3 @@
1
+ /** One resolver for ordinary JS, React and framework adapters. */
2
+ export { resolveLink } from '@storystick/types';
3
+ export { StorystickLink, } from './link-component.js';
@@ -0,0 +1,21 @@
1
+ import type { Client, StorystickError as StorystickErrorType } from '@storystick/client';
2
+ export interface ViewGenerationSignal {
3
+ readonly generation: number;
4
+ readonly status: 'ready' | 'stale' | 'rebuilding' | 'error';
5
+ readonly projectedThroughSeq?: number;
6
+ readonly projectionHash?: string;
7
+ readonly lensHash?: string;
8
+ }
9
+ interface PreviewGenerationSnapshot {
10
+ readonly version: number;
11
+ readonly epoch: string;
12
+ readonly signal: ViewGenerationSignal | null;
13
+ readonly error: StorystickErrorType | null;
14
+ }
15
+ interface PreviewCoordinator {
16
+ getSnapshot(): PreviewGenerationSnapshot;
17
+ subscribe(listener: () => void): () => void;
18
+ }
19
+ export declare function currentQueryContextEpoch(client: Client): string;
20
+ export declare function previewGenerationFor(client: Client): PreviewCoordinator;
21
+ export {};
@@ -0,0 +1,134 @@
1
+ import { getInternalClientRuntime } from '@storystick/client/internal';
2
+ import { StorystickError } from '@storystick/client';
3
+ import { getPreviewAuthority, subscribePreviewAuthority, } from '@storystick/preview/runtime/authority';
4
+ const COORDINATORS = new WeakMap();
5
+ export function currentQueryContextEpoch(client) {
6
+ const state = getPreviewAuthority();
7
+ if (!state.httpClient || !state.sessionId)
8
+ return getInternalClientRuntime(client).contextEpoch;
9
+ return previewEpoch(state.sessionId, state.variantId, state.releaseId) + `:${state.readContext?.contextId ?? ''}:${state.readContext?.revision ?? ''}`;
10
+ }
11
+ export function previewGenerationFor(client) {
12
+ const existing = COORDINATORS.get(client);
13
+ if (existing)
14
+ return existing;
15
+ let snapshot = Object.freeze({
16
+ version: 0,
17
+ epoch: currentQueryContextEpoch(client),
18
+ signal: null,
19
+ error: null,
20
+ });
21
+ const listeners = new Set();
22
+ let stopRuntime = null;
23
+ let stopWatch = null;
24
+ let watchIdentity = '';
25
+ const notify = (patch) => {
26
+ snapshot = Object.freeze({
27
+ version: snapshot.version + 1,
28
+ epoch: patch.epoch ?? snapshot.epoch,
29
+ signal: patch.signal,
30
+ error: patch.error,
31
+ });
32
+ for (const listener of listeners)
33
+ listener();
34
+ };
35
+ const stopCurrentWatch = () => {
36
+ if (stopWatch)
37
+ stopWatch();
38
+ stopWatch = null;
39
+ watchIdentity = '';
40
+ };
41
+ const syncRuntime = () => {
42
+ const state = getPreviewAuthority();
43
+ const nextEpoch = currentQueryContextEpoch(client);
44
+ if (nextEpoch !== snapshot.epoch) {
45
+ stopCurrentWatch();
46
+ notify({ epoch: nextEpoch, signal: null, error: null });
47
+ }
48
+ if (!state.client || !state.sessionId) {
49
+ stopCurrentWatch();
50
+ return;
51
+ }
52
+ const nextWatchIdentity = nextEpoch;
53
+ if (nextWatchIdentity === watchIdentity)
54
+ return;
55
+ stopCurrentWatch();
56
+ watchIdentity = nextWatchIdentity;
57
+ // ConvexReactClient exposes this imperative watcher underneath its React helpers.
58
+ const watch = state.client.watchQuery?.('sdkQueries:viewGeneration', { sessionId: state.sessionId });
59
+ if (!watch)
60
+ return;
61
+ const onUpdate = () => {
62
+ try {
63
+ const value = watch.localQueryResult();
64
+ if (value === null || value === undefined) {
65
+ notify({ signal: null, error: null });
66
+ return;
67
+ }
68
+ if (!isViewGenerationSignal(value)) {
69
+ throw new Error('Invalid viewGeneration response.');
70
+ }
71
+ notify({
72
+ signal: Object.freeze(value),
73
+ error: value.status === 'error'
74
+ ? new StorystickError('The preview projection is unavailable.', {
75
+ code: 'storystick/DELIVERY_UNAVAILABLE',
76
+ category: 'system',
77
+ retryable: true,
78
+ })
79
+ : null,
80
+ });
81
+ }
82
+ catch (cause) {
83
+ notify({
84
+ signal: snapshot.signal,
85
+ error: new StorystickError('The preview projection signal failed.', {
86
+ code: 'storystick/DELIVERY_UNAVAILABLE',
87
+ category: 'system',
88
+ retryable: true,
89
+ payload: cause,
90
+ }),
91
+ });
92
+ }
93
+ };
94
+ const cleanup = watch.onUpdate(onUpdate);
95
+ stopWatch = typeof cleanup === 'function' ? cleanup : null;
96
+ onUpdate();
97
+ };
98
+ const coordinator = {
99
+ getSnapshot: () => snapshot,
100
+ subscribe(listener) {
101
+ listeners.add(listener);
102
+ if (listeners.size === 1) {
103
+ stopRuntime = subscribePreviewAuthority(syncRuntime);
104
+ syncRuntime();
105
+ }
106
+ return () => {
107
+ listeners.delete(listener);
108
+ if (listeners.size === 0) {
109
+ stopRuntime?.();
110
+ stopRuntime = null;
111
+ stopCurrentWatch();
112
+ }
113
+ };
114
+ },
115
+ };
116
+ COORDINATORS.set(client, coordinator);
117
+ return coordinator;
118
+ }
119
+ function previewEpoch(sessionId, variantId, releaseId) {
120
+ return `preview:${sessionId}:${variantId ?? ''}:${releaseId ?? ''}`;
121
+ }
122
+ function isViewGenerationSignal(value) {
123
+ if (!value || typeof value !== 'object' || Array.isArray(value))
124
+ return false;
125
+ const input = value;
126
+ return Number.isSafeInteger(input.generation) &&
127
+ (input.status === 'ready' || input.status === 'stale' ||
128
+ input.status === 'rebuilding' || input.status === 'error') &&
129
+ (input.projectedThroughSeq === undefined ||
130
+ (Number.isSafeInteger(input.projectedThroughSeq) &&
131
+ input.projectedThroughSeq >= 0)) &&
132
+ (input.projectionHash === undefined || typeof input.projectionHash === 'string') &&
133
+ (input.lensHash === undefined || typeof input.lensHash === 'string');
134
+ }
@@ -0,0 +1,9 @@
1
+ export interface PreviewAssistantNavigation {
2
+ projectId: string;
3
+ sessionId: string;
4
+ threadId: string | null;
5
+ open: boolean;
6
+ }
7
+ export declare function readPreviewAssistantNavigation(projectId: string | null, sessionId: string | null): PreviewAssistantNavigation | null;
8
+ export declare function rememberPreviewAssistantNavigation(value: PreviewAssistantNavigation): void;
9
+ export declare function clearPreviewAssistantNavigation(): void;
@@ -0,0 +1,39 @@
1
+ /** Only navigation coordinates survive a reload, never a composer or attachment. */
2
+ const KEY = '__storystick_assistant_navigation__';
3
+ export function readPreviewAssistantNavigation(projectId, sessionId) {
4
+ if (!projectId || !sessionId || typeof window === 'undefined')
5
+ return null;
6
+ try {
7
+ const value = JSON.parse(window.sessionStorage.getItem(KEY) ?? 'null');
8
+ return value?.projectId === projectId &&
9
+ value.sessionId === sessionId &&
10
+ (value.threadId === null || typeof value.threadId === 'string') &&
11
+ typeof value.open === 'boolean'
12
+ ? { projectId, sessionId, threadId: value.threadId, open: value.open }
13
+ : null;
14
+ }
15
+ catch {
16
+ return null;
17
+ }
18
+ }
19
+ export function rememberPreviewAssistantNavigation(value) {
20
+ try {
21
+ window.sessionStorage.setItem(KEY, JSON.stringify({
22
+ projectId: value.projectId,
23
+ sessionId: value.sessionId,
24
+ threadId: value.threadId,
25
+ open: value.open,
26
+ }));
27
+ }
28
+ catch {
29
+ /* Preview remains usable without storage. */
30
+ }
31
+ }
32
+ export function clearPreviewAssistantNavigation() {
33
+ try {
34
+ window.sessionStorage.removeItem(KEY);
35
+ }
36
+ catch {
37
+ /* No retained navigation. */
38
+ }
39
+ }
@@ -0,0 +1,12 @@
1
+ import { type PreviewAuthoringAdmission, type PreviewAuthoringRuntimeModule, type PreviewAuthoringSurface } from '@storystick/preview/authoring/runtime-contract';
2
+ declare global {
3
+ interface Window {
4
+ StorystickAuthoringRuntime?: PreviewAuthoringRuntimeModule;
5
+ }
6
+ }
7
+ export declare function authoringRuntimeUrl(studioUrl: string): string | null;
8
+ export declare function loadPreviewAuthoringRuntime(source: string, surface?: PreviewAuthoringSurface | null): Promise<PreviewAuthoringRuntimeModule>;
9
+ export declare function requestPreviewAuthoringAdmission(args: {
10
+ url: string;
11
+ previous?: PreviewAuthoringAdmission | null;
12
+ }): Promise<PreviewAuthoringAdmission>;
@@ -0,0 +1,108 @@
1
+ 'use client';
2
+ import { STORYSTICK_AUTHORING_RUNTIME_CONTRACT_VERSION, STORYSTICK_AUTHORING_RUNTIME_GLOBAL, STORYSTICK_AUTHORING_RUNTIME_PATH, isPreviewAuthoringAdmission, } from '@storystick/preview/authoring/runtime-contract';
3
+ const loads = new Map();
4
+ const attempts = new Map();
5
+ export function authoringRuntimeUrl(studioUrl) {
6
+ try {
7
+ const studio = new URL(studioUrl);
8
+ if (!['http:', 'https:'].includes(studio.protocol))
9
+ return null;
10
+ return new URL(STORYSTICK_AUTHORING_RUNTIME_PATH, studio.origin).href;
11
+ }
12
+ catch {
13
+ return null;
14
+ }
15
+ }
16
+ export function loadPreviewAuthoringRuntime(source, surface) {
17
+ const pending = loadRuntimeModule(source);
18
+ // Code preparation never holds admission/authentication behind a panel's
19
+ // dependencies. Rendering uses the same imports and its own Suspense boundary.
20
+ if (surface)
21
+ void pending.then(runtime => runtime.preload(surface)).catch(() => undefined);
22
+ return pending;
23
+ }
24
+ function loadRuntimeModule(source) {
25
+ const installed = readRuntime();
26
+ if (installed)
27
+ return Promise.resolve(installed);
28
+ const existing = loads.get(source);
29
+ if (existing)
30
+ return existing;
31
+ const attempt = attempts.get(source) ?? 0;
32
+ attempts.set(source, attempt + 1);
33
+ const pending = new Promise((resolve, reject) => {
34
+ const script = document.createElement('script');
35
+ const url = new URL(source, document.baseURI);
36
+ url.searchParams.set('v', String(STORYSTICK_AUTHORING_RUNTIME_CONTRACT_VERSION));
37
+ // A failed or incompatible module is retained in the browser's module
38
+ // map. A deliberate retry needs a fresh entry URL to execute registration.
39
+ if (attempt > 0)
40
+ url.searchParams.set('retry', String(attempt));
41
+ script.src = url.href;
42
+ script.type = 'module';
43
+ script.crossOrigin = 'anonymous';
44
+ script.async = true;
45
+ script.dataset.storystickAuthoringRuntime = '';
46
+ script.onload = () => {
47
+ const runtime = readRuntime();
48
+ if (runtime)
49
+ resolve(runtime);
50
+ else {
51
+ script.remove();
52
+ reject(new Error('AUTHORING_RUNTIME_CONTRACT_MISMATCH'));
53
+ }
54
+ };
55
+ script.onerror = () => {
56
+ script.remove();
57
+ reject(new Error('AUTHORING_RUNTIME_UNAVAILABLE'));
58
+ };
59
+ document.head.append(script);
60
+ }).finally(() => {
61
+ if (!readRuntime())
62
+ loads.delete(source);
63
+ });
64
+ loads.set(source, pending);
65
+ return pending;
66
+ }
67
+ function readRuntime() {
68
+ const runtime = window[STORYSTICK_AUTHORING_RUNTIME_GLOBAL];
69
+ return runtime?.contractVersion ===
70
+ STORYSTICK_AUTHORING_RUNTIME_CONTRACT_VERSION
71
+ ? runtime
72
+ : null;
73
+ }
74
+ export async function requestPreviewAuthoringAdmission(args) {
75
+ const response = await fetch(args.url, {
76
+ method: 'POST',
77
+ credentials: 'same-origin',
78
+ cache: 'no-store',
79
+ headers: { 'Content-Type': 'application/json' },
80
+ body: JSON.stringify(args.previous
81
+ ? {
82
+ admissionId: args.previous.connection.id,
83
+ fileDelegationId: args.previous.files.id,
84
+ }
85
+ : {}),
86
+ });
87
+ if (!response.ok)
88
+ throw await admissionError(response);
89
+ const admission = await response.json();
90
+ if (!isPreviewAuthoringAdmission(admission)) {
91
+ throw new Error('AUTHORING_ADMISSION_INVALID');
92
+ }
93
+ return admission;
94
+ }
95
+ async function admissionError(response) {
96
+ try {
97
+ const body = (await response.json());
98
+ const code = typeof body.error?.code === 'string'
99
+ ? body.error.code
100
+ : 'AUTHORING_ADMISSION_UNAVAILABLE';
101
+ const error = new Error(code);
102
+ error.name = code;
103
+ return error;
104
+ }
105
+ catch {
106
+ return new Error('AUTHORING_ADMISSION_UNAVAILABLE');
107
+ }
108
+ }
@@ -0,0 +1,13 @@
1
+ import type { PreviewAuthoringPresentation, PreviewAuthoringTarget } from '@storystick/preview/authoring/runtime-contract';
2
+ /** One navigation handoff, never a draft or an editing credential. */
3
+ export interface PreviewEditorContinuation {
4
+ id: string;
5
+ projectId: string;
6
+ sessionId: string;
7
+ href: string;
8
+ destination: PreviewAuthoringPresentation;
9
+ target: PreviewAuthoringTarget;
10
+ }
11
+ export declare function rememberPreviewEditorNavigation(projectId: string, sessionId: string, basePath: string, destination: PreviewAuthoringPresentation, target: PreviewAuthoringTarget): PreviewEditorContinuation;
12
+ export declare function readPreviewEditorNavigation(projectId: string | null, sessionId: string | null): PreviewEditorContinuation | null;
13
+ export declare function clearPreviewEditorNavigation(id: string): void;
@@ -0,0 +1,68 @@
1
+ import { previewDraftHref } from '@storystick/preview/identity';
2
+ import { createPreviewRequestId } from '@storystick/preview/runtime/request-id';
3
+ import { isPreviewPerspective } from '@storystick/types/preview-query-targeting';
4
+ const STORAGE_KEY = '__storystick_editor_navigation__';
5
+ let memory = null;
6
+ export function rememberPreviewEditorNavigation(projectId, sessionId, basePath, destination, target) {
7
+ const entry = {
8
+ id: createPreviewRequestId(), projectId, sessionId,
9
+ href: basePath + previewDraftHref(destination.pathname, destination.draftDocumentId),
10
+ destination,
11
+ target: {
12
+ documentId: target.documentId, locale: target.locale,
13
+ ...(target.releaseId ? { releaseId: target.releaseId } : {}),
14
+ ...(target.variantId ? { variantId: target.variantId } : {}),
15
+ },
16
+ };
17
+ memory = entry;
18
+ try {
19
+ sessionStorage.setItem(STORAGE_KEY, JSON.stringify(entry));
20
+ }
21
+ catch { /* Storage may be unavailable. */ }
22
+ return entry;
23
+ }
24
+ export function readPreviewEditorNavigation(projectId, sessionId) {
25
+ if (!projectId || !sessionId || typeof window === 'undefined')
26
+ return null;
27
+ let entry = memory;
28
+ if (!entry) {
29
+ try {
30
+ const value = JSON.parse(sessionStorage.getItem(STORAGE_KEY) ?? 'null');
31
+ if (value && typeof value.id === 'string' && typeof value.href === 'string' &&
32
+ typeof value.destination?.documentId === 'string' &&
33
+ typeof value.destination?.locale === 'string' &&
34
+ isPreviewPerspective(value.destination?.perspective) &&
35
+ typeof value.target?.documentId === 'string' &&
36
+ typeof value.target?.locale === 'string' &&
37
+ (value.target.variantId === undefined || typeof value.target.variantId === 'string') &&
38
+ (value.target.releaseId === undefined || typeof value.target.releaseId === 'string'))
39
+ entry = value;
40
+ }
41
+ catch { /* Invalid or unavailable hints do not affect navigation. */ }
42
+ }
43
+ if (!entry || entry.projectId !== projectId || entry.sessionId !== sessionId)
44
+ return null;
45
+ // Only the destination of the editor gesture may resume its panel. Ignore
46
+ // session/refresh query parameters: they carry no document identity.
47
+ try {
48
+ const location = new URL(window.location.href);
49
+ const destination = new URL(entry.href, location.origin);
50
+ return destination.origin === location.origin &&
51
+ destination.pathname === location.pathname &&
52
+ destination.searchParams.get('_ss_draft') === location.searchParams.get('_ss_draft')
53
+ ? entry : null;
54
+ }
55
+ catch {
56
+ return null;
57
+ }
58
+ }
59
+ export function clearPreviewEditorNavigation(id) {
60
+ if (memory?.id === id)
61
+ memory = null;
62
+ try {
63
+ if (JSON.parse(sessionStorage.getItem(STORAGE_KEY) ?? 'null')?.id === id) {
64
+ sessionStorage.removeItem(STORAGE_KEY);
65
+ }
66
+ }
67
+ catch { /* Nothing to clear when storage is unavailable. */ }
68
+ }