@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.
- package/LICENSE +112 -0
- package/NOTICE +17 -0
- package/PUBLIC-API.md +36 -0
- package/README.md +226 -2
- package/dist/.source-hash +1 -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 +100 -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 +518 -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-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 +13 -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 +18 -0
- package/dist/preview-toolbar-shell.js +138 -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 +5 -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 +146 -3
package/dist/index.js
ADDED
|
@@ -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,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 } 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): Promise<PreviewAuthoringRuntimeModule>;
|
|
9
|
+
export declare function requestPreviewAuthoringAdmission(args: {
|
|
10
|
+
url: string;
|
|
11
|
+
previous?: PreviewAuthoringAdmission | null;
|
|
12
|
+
}): Promise<PreviewAuthoringAdmission>;
|
|
@@ -0,0 +1,100 @@
|
|
|
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) {
|
|
17
|
+
const installed = readRuntime();
|
|
18
|
+
if (installed)
|
|
19
|
+
return Promise.resolve(installed);
|
|
20
|
+
const existing = loads.get(source);
|
|
21
|
+
if (existing)
|
|
22
|
+
return existing;
|
|
23
|
+
const attempt = attempts.get(source) ?? 0;
|
|
24
|
+
attempts.set(source, attempt + 1);
|
|
25
|
+
const pending = new Promise((resolve, reject) => {
|
|
26
|
+
const script = document.createElement('script');
|
|
27
|
+
const url = new URL(source, document.baseURI);
|
|
28
|
+
url.searchParams.set('v', String(STORYSTICK_AUTHORING_RUNTIME_CONTRACT_VERSION));
|
|
29
|
+
// A failed or incompatible module is retained in the browser's module
|
|
30
|
+
// map. A deliberate retry needs a fresh entry URL to execute registration.
|
|
31
|
+
if (attempt > 0)
|
|
32
|
+
url.searchParams.set('retry', String(attempt));
|
|
33
|
+
script.src = url.href;
|
|
34
|
+
script.type = 'module';
|
|
35
|
+
script.crossOrigin = 'anonymous';
|
|
36
|
+
script.async = true;
|
|
37
|
+
script.dataset.storystickAuthoringRuntime = '';
|
|
38
|
+
script.onload = () => {
|
|
39
|
+
const runtime = readRuntime();
|
|
40
|
+
if (runtime)
|
|
41
|
+
resolve(runtime);
|
|
42
|
+
else {
|
|
43
|
+
script.remove();
|
|
44
|
+
reject(new Error('AUTHORING_RUNTIME_CONTRACT_MISMATCH'));
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
script.onerror = () => {
|
|
48
|
+
script.remove();
|
|
49
|
+
reject(new Error('AUTHORING_RUNTIME_UNAVAILABLE'));
|
|
50
|
+
};
|
|
51
|
+
document.head.append(script);
|
|
52
|
+
}).finally(() => {
|
|
53
|
+
if (!readRuntime())
|
|
54
|
+
loads.delete(source);
|
|
55
|
+
});
|
|
56
|
+
loads.set(source, pending);
|
|
57
|
+
return pending;
|
|
58
|
+
}
|
|
59
|
+
function readRuntime() {
|
|
60
|
+
const runtime = window[STORYSTICK_AUTHORING_RUNTIME_GLOBAL];
|
|
61
|
+
return runtime?.contractVersion ===
|
|
62
|
+
STORYSTICK_AUTHORING_RUNTIME_CONTRACT_VERSION
|
|
63
|
+
? runtime
|
|
64
|
+
: null;
|
|
65
|
+
}
|
|
66
|
+
export async function requestPreviewAuthoringAdmission(args) {
|
|
67
|
+
const response = await fetch(args.url, {
|
|
68
|
+
method: 'POST',
|
|
69
|
+
credentials: 'same-origin',
|
|
70
|
+
cache: 'no-store',
|
|
71
|
+
headers: { 'Content-Type': 'application/json' },
|
|
72
|
+
body: JSON.stringify(args.previous
|
|
73
|
+
? {
|
|
74
|
+
admissionId: args.previous.connection.id,
|
|
75
|
+
fileDelegationId: args.previous.files.id,
|
|
76
|
+
}
|
|
77
|
+
: {}),
|
|
78
|
+
});
|
|
79
|
+
if (!response.ok)
|
|
80
|
+
throw await admissionError(response);
|
|
81
|
+
const admission = await response.json();
|
|
82
|
+
if (!isPreviewAuthoringAdmission(admission)) {
|
|
83
|
+
throw new Error('AUTHORING_ADMISSION_INVALID');
|
|
84
|
+
}
|
|
85
|
+
return admission;
|
|
86
|
+
}
|
|
87
|
+
async function admissionError(response) {
|
|
88
|
+
try {
|
|
89
|
+
const body = (await response.json());
|
|
90
|
+
const code = typeof body.error?.code === 'string'
|
|
91
|
+
? body.error.code
|
|
92
|
+
: 'AUTHORING_ADMISSION_UNAVAILABLE';
|
|
93
|
+
const error = new Error(code);
|
|
94
|
+
error.name = code;
|
|
95
|
+
return error;
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return new Error('AUTHORING_ADMISSION_UNAVAILABLE');
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type PreviewOverlayClickTarget } from '@storystick/preview/provenance';
|
|
2
|
+
import type { PreviewReadContext } from '@storystick/types/preview-query-targeting';
|
|
3
|
+
import type { PreviewAuthoringTarget } from '@storystick/preview/authoring/runtime-contract';
|
|
4
|
+
import { type ReactElement } from 'react';
|
|
5
|
+
import type { PreviewRuntimeAdapterSnapshot } from '@storystick/preview/framework/runtime-adapter-store';
|
|
6
|
+
export interface PreviewStandaloneControlsProps {
|
|
7
|
+
projectId: string | null;
|
|
8
|
+
state: PreviewRuntimeAdapterSnapshot;
|
|
9
|
+
basePath?: string;
|
|
10
|
+
pathname?: string | null;
|
|
11
|
+
recovery?: {
|
|
12
|
+
error: string | null;
|
|
13
|
+
retry: () => void;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
/** Minimal host: one sleeping bubble, one compact menu and one shared editor. */
|
|
17
|
+
export declare function PreviewStandaloneControls({ state, projectId, recovery, basePath, pathname, }: PreviewStandaloneControlsProps): ReactElement | null;
|
|
18
|
+
export declare function targetForOverlay(target: PreviewOverlayClickTarget, perspective: PreviewReadContext['perspective'] | null, presentationLocale?: string): PreviewAuthoringTarget | null;
|