@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
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useEffect, useLayoutEffect, useMemo, useSyncExternalStore } from 'react';
|
|
3
|
+
import { QueryExecutionError, StorystickError } from '@storystick/client';
|
|
4
|
+
import { prepareContentValues, rawContentValues, bindClientSchemaToAuthority, clientMatchesPreviewAuthority, getExecutableQueryDescriptor, getInternalClientRuntime, getPreviewQueryReceiptId, } from '@storystick/client/internal';
|
|
5
|
+
import { getPreviewAuthority, subscribePreviewAuthority, } from '@storystick/preview/runtime/authority';
|
|
6
|
+
import { getQueryPreviewState, subscribeQueryPreview, loadQueryPreview } from './query-preview-loader.js';
|
|
7
|
+
import { createRefCountedObserver, convergeQueryProjection, previewQueryHasConfirmedCoverage, previewQueryIsAwaitingTargetedProjection, queryCacheKey, registerPreviewQueryReceipt, subscribeRefCounted, } from './query-cache.js';
|
|
8
|
+
import { currentQueryContextEpoch, previewGenerationFor, } from './preview-generation.js';
|
|
9
|
+
const AUTHORITY_IDENTITIES = new WeakMap();
|
|
10
|
+
let nextAuthorityIdentity = 1;
|
|
11
|
+
function authorityIdentity(client) {
|
|
12
|
+
const existing = AUTHORITY_IDENTITIES.get(client);
|
|
13
|
+
if (existing !== undefined)
|
|
14
|
+
return existing;
|
|
15
|
+
const created = nextAuthorityIdentity++;
|
|
16
|
+
AUTHORITY_IDENTITIES.set(client, created);
|
|
17
|
+
return created;
|
|
18
|
+
}
|
|
19
|
+
const SERVER_PENDING = Object.freeze({
|
|
20
|
+
status: 'pending',
|
|
21
|
+
data: undefined,
|
|
22
|
+
pageInfo: undefined,
|
|
23
|
+
error: null,
|
|
24
|
+
isFetching: false,
|
|
25
|
+
refetch: async () => {
|
|
26
|
+
throw new Error('storystick/QUERY_DISABLED: Cannot refetch before the query is enabled.');
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
export function useQuery(executableQuery, options = {}) {
|
|
30
|
+
const enabled = options.enabled !== false;
|
|
31
|
+
const compiled = executableQuery.compile();
|
|
32
|
+
const descriptor = getExecutableQueryDescriptor(executableQuery);
|
|
33
|
+
const client = descriptor.host.client;
|
|
34
|
+
if (!client || typeof client.projectId !== 'string') {
|
|
35
|
+
throw new TypeError('storystick/QUERY_INVALID: The executable query is not owned by a Client.');
|
|
36
|
+
}
|
|
37
|
+
const previewState = useSyncExternalStore(subscribePreviewAuthority, getPreviewAuthority, getPreviewAuthority);
|
|
38
|
+
const previewModules = useSyncExternalStore(subscribeQueryPreview, getQueryPreviewState, getQueryPreviewState);
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
if (enabled && previewState.sessionId)
|
|
41
|
+
void loadQueryPreview();
|
|
42
|
+
}, [enabled, previewState.sessionId]);
|
|
43
|
+
const previewAuthority = previewState.httpClient &&
|
|
44
|
+
previewAuthorityMatchesState(previewState.httpClient, client, previewState)
|
|
45
|
+
? previewState.httpClient
|
|
46
|
+
: null;
|
|
47
|
+
const hasExecutionAuthority = previewState.sessionId === null || previewAuthority !== null;
|
|
48
|
+
const executionClient = useMemo(() => previewAuthority
|
|
49
|
+
? bindClientSchemaToAuthority(previewAuthority, client.schema)
|
|
50
|
+
: client, [client, previewAuthority]);
|
|
51
|
+
const contextEpoch = previewAuthority
|
|
52
|
+
? `${currentQueryContextEpoch(client)}:authority:${authorityIdentity(previewAuthority)}`
|
|
53
|
+
: getInternalClientRuntime(client).contextEpoch;
|
|
54
|
+
const key = queryCacheKey(executionClient, compiled.canonical, compiled.query.locale.code, compiled.query.page?.after ?? null, contextEpoch);
|
|
55
|
+
const keyIdentity = JSON.stringify(key);
|
|
56
|
+
const store = useMemo(() => {
|
|
57
|
+
if (typeof window === 'undefined' || !enabled || !hasExecutionAuthority || (previewAuthority && !previewModules.runtime))
|
|
58
|
+
return disabledStore();
|
|
59
|
+
return deferredStore(() => {
|
|
60
|
+
const observer = createRefCountedObserver(executionClient, {
|
|
61
|
+
queryKey: key,
|
|
62
|
+
queryFn: ({ signal }) => {
|
|
63
|
+
if (!executionAuthorityIsCurrent(previewAuthority, client)) {
|
|
64
|
+
throw new StorystickError('The query authority changed before execution.', {
|
|
65
|
+
code: 'storystick/QUERY_ABORTED',
|
|
66
|
+
category: 'cancel',
|
|
67
|
+
retryable: false,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
return getInternalClientRuntime(executionClient).executionHost.executeCompiled(compiled.query, compiled.canonical, { signal });
|
|
71
|
+
},
|
|
72
|
+
enabled: true,
|
|
73
|
+
retry: false,
|
|
74
|
+
staleTime: Infinity,
|
|
75
|
+
gcTime: 5 * 60 * 1_000,
|
|
76
|
+
refetchOnMount: false,
|
|
77
|
+
refetchOnWindowFocus: false,
|
|
78
|
+
refetchOnReconnect: false,
|
|
79
|
+
});
|
|
80
|
+
let previousCore = null;
|
|
81
|
+
let previousGenerationVersion = -1;
|
|
82
|
+
let previousTargetingState = '';
|
|
83
|
+
let previousSnapshot = null;
|
|
84
|
+
let previousLiveProjection = null;
|
|
85
|
+
let liveOverlayHandle = null;
|
|
86
|
+
let liveOverlayRetainCount = 0;
|
|
87
|
+
let paintProjection = null;
|
|
88
|
+
const generation = previewGenerationFor(client);
|
|
89
|
+
const currentQueryResult = () => observer.getCurrentResult().data;
|
|
90
|
+
const registerCommittedReceipt = () => {
|
|
91
|
+
if (executionClient === client)
|
|
92
|
+
return;
|
|
93
|
+
registerPreviewQueryReceipt(client, executionClient, key, currentQueryResult());
|
|
94
|
+
};
|
|
95
|
+
const syncLiveQueryReceipt = () => {
|
|
96
|
+
liveOverlayHandle?.updateReceipt(getPreviewQueryReceiptId(currentQueryResult()));
|
|
97
|
+
};
|
|
98
|
+
const converge = () => {
|
|
99
|
+
const signal = generation.getSnapshot().signal;
|
|
100
|
+
if (signal?.status === 'ready' &&
|
|
101
|
+
signal.projectionHash !== undefined &&
|
|
102
|
+
!previewQueryHasConfirmedCoverage(executionClient, currentQueryResult())) {
|
|
103
|
+
convergeQueryProjection(executionClient, key, signal.projectionHash);
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
const snapshot = () => {
|
|
107
|
+
const current = observer.getCurrentResult();
|
|
108
|
+
const generationSnapshot = generation.getSnapshot();
|
|
109
|
+
const hasConfirmedTargetingCoverage = previewQueryHasConfirmedCoverage(executionClient, current.data);
|
|
110
|
+
const isAwaitingTargetedProjection = previewQueryIsAwaitingTargetedProjection(executionClient, current.data);
|
|
111
|
+
const targetingState = [
|
|
112
|
+
hasConfirmedTargetingCoverage,
|
|
113
|
+
isAwaitingTargetedProjection,
|
|
114
|
+
].join(':');
|
|
115
|
+
const liveProjection = liveOverlayHandle?.getSnapshot() ?? null;
|
|
116
|
+
if (current === previousCore &&
|
|
117
|
+
generationSnapshot.version === previousGenerationVersion &&
|
|
118
|
+
targetingState === previousTargetingState &&
|
|
119
|
+
liveProjection === previousLiveProjection &&
|
|
120
|
+
previousSnapshot)
|
|
121
|
+
return previousSnapshot;
|
|
122
|
+
previousCore = current;
|
|
123
|
+
previousGenerationVersion = generationSnapshot.version;
|
|
124
|
+
previousTargetingState = targetingState;
|
|
125
|
+
previousLiveProjection = liveProjection;
|
|
126
|
+
const rawRead = rawContentValues(current.data);
|
|
127
|
+
const projectedRead = current.error === null && generationSnapshot.error === null
|
|
128
|
+
? (previewModules.runtime?.applyPreviewLiveQueryProjection(rawRead, liveProjection) ?? rawRead)
|
|
129
|
+
: rawRead;
|
|
130
|
+
const visibleQueryResult = projectedRead === rawRead ? current.data : prepareContentValues(projectedRead, false, previewAuthority !== null);
|
|
131
|
+
paintProjection =
|
|
132
|
+
visibleQueryResult !== current.data ? liveProjection : null;
|
|
133
|
+
previousSnapshot = toPublicSnapshot(current, observer, generationSnapshot.error, visibleQueryResult);
|
|
134
|
+
return previousSnapshot;
|
|
135
|
+
};
|
|
136
|
+
return {
|
|
137
|
+
subscribe: (listener) => {
|
|
138
|
+
const onUpdate = () => {
|
|
139
|
+
registerCommittedReceipt();
|
|
140
|
+
syncLiveQueryReceipt();
|
|
141
|
+
converge();
|
|
142
|
+
listener();
|
|
143
|
+
};
|
|
144
|
+
// `useSyncExternalStore.getSnapshot` must stay pure. Register the
|
|
145
|
+
// transport receipt only once React commits this subscription, then
|
|
146
|
+
// keep it aligned with committed observer notifications.
|
|
147
|
+
registerCommittedReceipt();
|
|
148
|
+
if (executionClient !== client) {
|
|
149
|
+
if (liveOverlayRetainCount === 0) {
|
|
150
|
+
liveOverlayHandle = previewModules.runtime.previewLiveQueryOverlay.retain(keyIdentity);
|
|
151
|
+
}
|
|
152
|
+
liveOverlayRetainCount += 1;
|
|
153
|
+
syncLiveQueryReceipt();
|
|
154
|
+
}
|
|
155
|
+
const stopLiveOverlay = liveOverlayHandle?.subscribe(onUpdate);
|
|
156
|
+
const stopObserver = subscribeRefCounted(executionClient, observer, onUpdate, client);
|
|
157
|
+
const stopGeneration = generation.subscribe(onUpdate);
|
|
158
|
+
converge();
|
|
159
|
+
return () => {
|
|
160
|
+
stopLiveOverlay?.();
|
|
161
|
+
if (liveOverlayRetainCount > 0) {
|
|
162
|
+
liveOverlayRetainCount -= 1;
|
|
163
|
+
if (liveOverlayRetainCount === 0) {
|
|
164
|
+
liveOverlayHandle?.release();
|
|
165
|
+
liveOverlayHandle = null;
|
|
166
|
+
paintProjection = null;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
stopGeneration();
|
|
170
|
+
stopObserver();
|
|
171
|
+
};
|
|
172
|
+
},
|
|
173
|
+
snapshot,
|
|
174
|
+
paintProjection: () => paintProjection,
|
|
175
|
+
};
|
|
176
|
+
});
|
|
177
|
+
// The canonical identity deliberately replaces the ephemeral inline builder identity.
|
|
178
|
+
}, [client, enabled, executionClient, hasExecutionAuthority, keyIdentity, previewModules]);
|
|
179
|
+
const result = useSyncExternalStore(store.subscribe, store.snapshot, () => SERVER_PENDING);
|
|
180
|
+
const paintProjection = store.paintProjection?.() ?? null;
|
|
181
|
+
useLayoutEffect(() => {
|
|
182
|
+
if (paintProjection?.editorMutationAt === undefined ||
|
|
183
|
+
typeof window === 'undefined')
|
|
184
|
+
return;
|
|
185
|
+
const frame = window.requestAnimationFrame(() => {
|
|
186
|
+
const firstPaintOpportunity = window.performance.timeOrigin + window.performance.now();
|
|
187
|
+
previewModules.runtime?.recordPreviewPerfMetric(window, {
|
|
188
|
+
name: 'preview.liveQuery.firstPaintOpportunity',
|
|
189
|
+
durationMs: Math.max(0, firstPaintOpportunity - paintProjection.editorMutationAt),
|
|
190
|
+
detail: {
|
|
191
|
+
queryInstanceId: paintProjection.queryInstanceId,
|
|
192
|
+
sourceRevision: paintProjection.sourceRevision,
|
|
193
|
+
payloadBytes: previewModules.runtime?.estimatePreviewPayloadBytes(window, paintProjection),
|
|
194
|
+
projectionToPaintMs: paintProjection.projectedAt === undefined
|
|
195
|
+
? null
|
|
196
|
+
: Math.max(0, firstPaintOpportunity - paintProjection.projectedAt),
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
return () => window.cancelAnimationFrame(frame);
|
|
201
|
+
}, [
|
|
202
|
+
paintProjection?.queryInstanceId,
|
|
203
|
+
paintProjection?.sourceEpoch,
|
|
204
|
+
paintProjection?.sourceRevision,
|
|
205
|
+
]);
|
|
206
|
+
if (enabled && previewAuthority && previewModules.error) {
|
|
207
|
+
return { ...SERVER_PENDING, status: 'error', error: previewModules.error,
|
|
208
|
+
refetch: async () => {
|
|
209
|
+
await loadQueryPreview();
|
|
210
|
+
const failure = getQueryPreviewState().error;
|
|
211
|
+
if (failure)
|
|
212
|
+
throw failure;
|
|
213
|
+
if (!executionAuthorityIsCurrent(previewAuthority, client)) {
|
|
214
|
+
throw new StorystickError('The query authority changed before retry.', {
|
|
215
|
+
code: 'storystick/QUERY_ABORTED', category: 'cancel', retryable: false,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
return getInternalClientRuntime(executionClient).executionHost.executeCompiled(compiled.query, compiled.canonical);
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
return result;
|
|
223
|
+
}
|
|
224
|
+
function previewAuthorityMatchesState(authority, owner, state) {
|
|
225
|
+
if (!state.sessionId || !state.previewToken)
|
|
226
|
+
return false;
|
|
227
|
+
return clientMatchesPreviewAuthority(authority, {
|
|
228
|
+
projectId: owner.projectId,
|
|
229
|
+
previewSessionId: state.sessionId,
|
|
230
|
+
previewObservationId: state.sessionId,
|
|
231
|
+
previewToken: state.previewToken,
|
|
232
|
+
readContext: state.readContext,
|
|
233
|
+
variantId: state.variantId,
|
|
234
|
+
releaseId: state.releaseId,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
function executionAuthorityIsCurrent(previewAuthority, owner) {
|
|
238
|
+
const current = getPreviewAuthority();
|
|
239
|
+
if (current.sessionId === null)
|
|
240
|
+
return previewAuthority === null;
|
|
241
|
+
return previewAuthority !== null && previewAuthorityMatchesState(previewAuthority, owner, current);
|
|
242
|
+
}
|
|
243
|
+
function disabledStore() {
|
|
244
|
+
const snapshot = SERVER_PENDING;
|
|
245
|
+
return {
|
|
246
|
+
subscribe: () => () => { },
|
|
247
|
+
snapshot: () => snapshot,
|
|
248
|
+
paintProjection: () => null,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
function toPublicSnapshot(current, observer, generationError, visibleQueryResult = current.data) {
|
|
252
|
+
const refetch = async () => {
|
|
253
|
+
const result = await observer.refetch();
|
|
254
|
+
if (result.error)
|
|
255
|
+
throw result.error;
|
|
256
|
+
if (!result.data)
|
|
257
|
+
throw new Error('storystick/QUERY_PROTOCOL: Refetch completed without a result.');
|
|
258
|
+
if (result.data.exactness !== 'exact')
|
|
259
|
+
throw new QueryExecutionError(result.data);
|
|
260
|
+
return result.data.data;
|
|
261
|
+
};
|
|
262
|
+
const visibleError = current.error ?? generationError;
|
|
263
|
+
if (visibleError) {
|
|
264
|
+
const last = visibleQueryResult?.exactness === 'exact' ? visibleQueryResult : undefined;
|
|
265
|
+
return {
|
|
266
|
+
status: 'error',
|
|
267
|
+
data: last?.data,
|
|
268
|
+
pageInfo: last?.pageInfo,
|
|
269
|
+
error: visibleError,
|
|
270
|
+
isFetching: current.isFetching,
|
|
271
|
+
refetch,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
const result = visibleQueryResult;
|
|
275
|
+
if (!result) {
|
|
276
|
+
return {
|
|
277
|
+
status: 'pending', data: undefined, pageInfo: undefined,
|
|
278
|
+
error: null, isFetching: current.isFetching,
|
|
279
|
+
refetch,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
if (result.exactness !== 'exact') {
|
|
283
|
+
return {
|
|
284
|
+
status: 'error', data: undefined, pageInfo: undefined,
|
|
285
|
+
error: new QueryExecutionError(result), isFetching: current.isFetching,
|
|
286
|
+
refetch,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
return {
|
|
290
|
+
status: 'success', data: result.data, pageInfo: result.pageInfo,
|
|
291
|
+
error: null, isFetching: current.isFetching, refetch,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
function deferredStore(create) {
|
|
295
|
+
let active = null;
|
|
296
|
+
let subscribers = 0;
|
|
297
|
+
return {
|
|
298
|
+
subscribe(listener) {
|
|
299
|
+
active ??= create();
|
|
300
|
+
subscribers++;
|
|
301
|
+
const stop = active.subscribe(listener);
|
|
302
|
+
return () => { stop(); if (--subscribers === 0)
|
|
303
|
+
active = null; };
|
|
304
|
+
},
|
|
305
|
+
snapshot: () => active?.snapshot() ?? SERVER_PENDING,
|
|
306
|
+
paintProjection: () => active?.paintProjection() ?? null,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type RefObject } from 'react';
|
|
2
|
+
import { isMomentaryNavigateKeyDown } from '@storystick/preview/runtime/momentary-navigate';
|
|
3
|
+
export { isMomentaryNavigateKeyDown };
|
|
4
|
+
type InteractionMode = 'edit' | 'browse';
|
|
5
|
+
interface UseMomentaryPreviewInteractionOverrideOptions {
|
|
6
|
+
enabled: boolean;
|
|
7
|
+
persistentMode: InteractionMode;
|
|
8
|
+
previewIframeRef?: RefObject<HTMLIFrameElement | null>;
|
|
9
|
+
isLocalPreviewEvent?: (event: Event) => boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare function useMomentaryPreviewInteractionOverride({ enabled, persistentMode, previewIframeRef, isLocalPreviewEvent, }: UseMomentaryPreviewInteractionOverrideOptions): InteractionMode | null;
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { isMomentaryNavigateKeyDown, isMomentaryNavigateKeySignal, } from '@storystick/preview/runtime/momentary-navigate';
|
|
3
|
+
export { isMomentaryNavigateKeyDown };
|
|
4
|
+
export function useMomentaryPreviewInteractionOverride({ enabled, persistentMode, previewIframeRef, isLocalPreviewEvent, }) {
|
|
5
|
+
const [shortcutActive, setShortcutActive] = useState(false);
|
|
6
|
+
const timerRef = useRef(null);
|
|
7
|
+
const lastPointerClientPositionRef = useRef(null);
|
|
8
|
+
const altDownRef = useRef(false);
|
|
9
|
+
const localEngaged = useRef(true);
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
const cancelTimer = () => {
|
|
12
|
+
if (timerRef.current === null)
|
|
13
|
+
return;
|
|
14
|
+
clearTimeout(timerRef.current);
|
|
15
|
+
timerRef.current = null;
|
|
16
|
+
};
|
|
17
|
+
const schedule = (callback, delay) => {
|
|
18
|
+
cancelTimer();
|
|
19
|
+
timerRef.current = setTimeout(() => {
|
|
20
|
+
timerRef.current = null;
|
|
21
|
+
callback();
|
|
22
|
+
}, delay);
|
|
23
|
+
};
|
|
24
|
+
const clear = () => {
|
|
25
|
+
cancelTimer();
|
|
26
|
+
altDownRef.current = false;
|
|
27
|
+
setShortcutActive(false);
|
|
28
|
+
};
|
|
29
|
+
if (!enabled) {
|
|
30
|
+
clear();
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (typeof window === 'undefined' || typeof document === 'undefined') {
|
|
34
|
+
clear();
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const getPreviewIframe = () => previewIframeRef?.current ?? null;
|
|
38
|
+
const isPointerInsidePreviewFrame = () => {
|
|
39
|
+
const iframe = getPreviewIframe();
|
|
40
|
+
const point = lastPointerClientPositionRef.current;
|
|
41
|
+
if (iframe === null || point === null)
|
|
42
|
+
return false;
|
|
43
|
+
const rect = iframe.getBoundingClientRect();
|
|
44
|
+
if (rect.width <= 0 || rect.height <= 0)
|
|
45
|
+
return false;
|
|
46
|
+
return (point.x >= rect.left &&
|
|
47
|
+
point.x <= rect.right &&
|
|
48
|
+
point.y >= rect.top &&
|
|
49
|
+
point.y <= rect.bottom);
|
|
50
|
+
};
|
|
51
|
+
const isPreviewFrameEngaged = () => {
|
|
52
|
+
const iframe = getPreviewIframe();
|
|
53
|
+
if (iframe === null)
|
|
54
|
+
return isLocalPreviewEvent ? localEngaged.current : false;
|
|
55
|
+
return isPointerInsidePreviewFrame() || document.activeElement === iframe;
|
|
56
|
+
};
|
|
57
|
+
const syncActiveFromScope = () => {
|
|
58
|
+
setShortcutActive(altDownRef.current && isPreviewFrameEngaged());
|
|
59
|
+
};
|
|
60
|
+
const onPointerMove = (event) => {
|
|
61
|
+
if (isLocalPreviewEvent)
|
|
62
|
+
localEngaged.current = isLocalPreviewEvent(event);
|
|
63
|
+
lastPointerClientPositionRef.current = {
|
|
64
|
+
x: event.clientX,
|
|
65
|
+
y: event.clientY,
|
|
66
|
+
};
|
|
67
|
+
syncActiveFromScope();
|
|
68
|
+
};
|
|
69
|
+
const onPointerOut = (event) => {
|
|
70
|
+
if (event.relatedTarget !== null)
|
|
71
|
+
return;
|
|
72
|
+
lastPointerClientPositionRef.current = null;
|
|
73
|
+
syncActiveFromScope();
|
|
74
|
+
};
|
|
75
|
+
const onKeyDown = (event) => {
|
|
76
|
+
if (!isMomentaryNavigateKeyDown(event))
|
|
77
|
+
return;
|
|
78
|
+
if (isLocalPreviewEvent && lastPointerClientPositionRef.current === null)
|
|
79
|
+
localEngaged.current = isLocalPreviewEvent(event);
|
|
80
|
+
altDownRef.current = true;
|
|
81
|
+
// Keyboard focus can remain in a Studio field while the pointer is
|
|
82
|
+
// over the iframe. The shortcut scope is the preview frame, not the
|
|
83
|
+
// currently focused parent-document input.
|
|
84
|
+
if (!isPreviewFrameEngaged())
|
|
85
|
+
return;
|
|
86
|
+
cancelTimer();
|
|
87
|
+
setShortcutActive(true);
|
|
88
|
+
};
|
|
89
|
+
const onKeyUp = (event) => {
|
|
90
|
+
if (event.key === 'Alt' || !event.altKey) {
|
|
91
|
+
altDownRef.current = false;
|
|
92
|
+
clear();
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
const onWindowBlur = () => {
|
|
96
|
+
schedule(() => {
|
|
97
|
+
const iframe = getPreviewIframe();
|
|
98
|
+
if (iframe !== null &&
|
|
99
|
+
document.activeElement === iframe &&
|
|
100
|
+
altDownRef.current) {
|
|
101
|
+
setShortcutActive(true);
|
|
102
|
+
iframe.blur();
|
|
103
|
+
window.focus();
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
clear();
|
|
107
|
+
}, 0);
|
|
108
|
+
};
|
|
109
|
+
const onVisibilityChange = () => {
|
|
110
|
+
if (document.visibilityState !== 'visible')
|
|
111
|
+
clear();
|
|
112
|
+
};
|
|
113
|
+
const onMessage = (event) => {
|
|
114
|
+
if (!isMomentaryNavigateKeySignal(event.data))
|
|
115
|
+
return;
|
|
116
|
+
const iframe = getPreviewIframe();
|
|
117
|
+
if (iframe === null)
|
|
118
|
+
return;
|
|
119
|
+
const iframeWindow = iframe.contentWindow;
|
|
120
|
+
if (iframeWindow === null || event.source !== iframeWindow)
|
|
121
|
+
return;
|
|
122
|
+
if (event.data.active) {
|
|
123
|
+
altDownRef.current = true;
|
|
124
|
+
cancelTimer();
|
|
125
|
+
setShortcutActive(true);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
altDownRef.current = false;
|
|
129
|
+
clear();
|
|
130
|
+
};
|
|
131
|
+
document.addEventListener('pointermove', onPointerMove, true);
|
|
132
|
+
document.addEventListener('mousemove', onPointerMove, true);
|
|
133
|
+
document.addEventListener('pointerout', onPointerOut, true);
|
|
134
|
+
document.addEventListener('mouseout', onPointerOut, true);
|
|
135
|
+
document.addEventListener('keydown', onKeyDown, true);
|
|
136
|
+
document.addEventListener('keyup', onKeyUp, true);
|
|
137
|
+
window.addEventListener('blur', onWindowBlur);
|
|
138
|
+
window.addEventListener('message', onMessage);
|
|
139
|
+
document.addEventListener('visibilitychange', onVisibilityChange);
|
|
140
|
+
return () => {
|
|
141
|
+
cancelTimer();
|
|
142
|
+
altDownRef.current = false;
|
|
143
|
+
lastPointerClientPositionRef.current = null;
|
|
144
|
+
document.removeEventListener('pointermove', onPointerMove, true);
|
|
145
|
+
document.removeEventListener('mousemove', onPointerMove, true);
|
|
146
|
+
document.removeEventListener('pointerout', onPointerOut, true);
|
|
147
|
+
document.removeEventListener('mouseout', onPointerOut, true);
|
|
148
|
+
document.removeEventListener('keydown', onKeyDown, true);
|
|
149
|
+
document.removeEventListener('keyup', onKeyUp, true);
|
|
150
|
+
window.removeEventListener('blur', onWindowBlur);
|
|
151
|
+
window.removeEventListener('message', onMessage);
|
|
152
|
+
document.removeEventListener('visibilitychange', onVisibilityChange);
|
|
153
|
+
};
|
|
154
|
+
}, [enabled, previewIframeRef, isLocalPreviewEvent]);
|
|
155
|
+
if (!enabled || !shortcutActive)
|
|
156
|
+
return null;
|
|
157
|
+
return persistentMode === 'edit' ? 'browse' : 'edit';
|
|
158
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { PreviewPerspectiveCommandResult } from '@storystick/preview';
|
|
2
|
+
import type { PreviewPerspective, PreviewReadContext } from '@storystick/types/preview-query-targeting';
|
|
3
|
+
import type { PreviewLifecyclePayload } from '@storystick/preview';
|
|
4
|
+
import type { PreviewPerspectiveIntentObservation } from '@storystick/preview/runtime/preview-perspective-intent-state';
|
|
5
|
+
type PreviewPerspectiveCommandStatus = PreviewPerspectiveCommandResult['status'];
|
|
6
|
+
export type AcceptedPreviewPerspectiveIntentResult = {
|
|
7
|
+
readonly status: 'local-noop';
|
|
8
|
+
readonly context: PreviewReadContext;
|
|
9
|
+
} | {
|
|
10
|
+
readonly status: Extract<PreviewPerspectiveCommandStatus, 'applied'>;
|
|
11
|
+
readonly commandId: string;
|
|
12
|
+
readonly context: PreviewReadContext;
|
|
13
|
+
readonly appliedRevision: number;
|
|
14
|
+
};
|
|
15
|
+
export type PreviewPerspectiveIntentResult = AcceptedPreviewPerspectiveIntentResult | {
|
|
16
|
+
readonly status: Extract<PreviewPerspectiveCommandStatus, 'conflict'>;
|
|
17
|
+
readonly commandId: string;
|
|
18
|
+
readonly context: PreviewReadContext;
|
|
19
|
+
} | {
|
|
20
|
+
readonly status: 'unavailable' | 'superseded';
|
|
21
|
+
readonly commandId: string | null;
|
|
22
|
+
};
|
|
23
|
+
export declare function isAcceptedPreviewPerspectiveIntent(result: PreviewPerspectiveIntentResult): result is AcceptedPreviewPerspectiveIntentResult;
|
|
24
|
+
interface PreviewPerspectiveIntentInputs<Command extends {
|
|
25
|
+
generation: number;
|
|
26
|
+
}> {
|
|
27
|
+
readonly getSessionId: () => string | null;
|
|
28
|
+
readonly getReadContext: () => PreviewReadContext | null;
|
|
29
|
+
readonly beginTransition: (perspective: PreviewPerspective) => Command;
|
|
30
|
+
readonly dispatch: (perspective: PreviewPerspective) => Promise<{
|
|
31
|
+
commandId: string | null;
|
|
32
|
+
}>;
|
|
33
|
+
readonly registerCommand: (commandId: string, command: Command) => void;
|
|
34
|
+
readonly clearTransition: () => void;
|
|
35
|
+
readonly onTerminalFailure?: (status: 'conflict' | 'unavailable') => void;
|
|
36
|
+
/** Runs synchronously with acceptance, before React can reconcile effects. */
|
|
37
|
+
readonly onAccepted?: (context: PreviewReadContext) => void;
|
|
38
|
+
readonly onStateChange?: (observation: PreviewPerspectiveIntentObservation) => void;
|
|
39
|
+
readonly timeoutMs?: number;
|
|
40
|
+
}
|
|
41
|
+
export interface PreviewPerspectiveIntentLifecycle {
|
|
42
|
+
readonly cancelPending: () => void;
|
|
43
|
+
readonly changePerspective: (perspective: PreviewPerspective) => Promise<PreviewPerspectiveIntentResult>;
|
|
44
|
+
readonly observeLifecycle: (payload: PreviewLifecyclePayload) => boolean;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Event-owned lifecycle for an explicit perspective gesture. React effects
|
|
48
|
+
* may reconcile the accepted read context, but they never create this intent.
|
|
49
|
+
*/
|
|
50
|
+
export declare function usePreviewPerspectiveIntent<Command extends {
|
|
51
|
+
generation: number;
|
|
52
|
+
}>(inputs: PreviewPerspectiveIntentInputs<Command>): PreviewPerspectiveIntentLifecycle;
|
|
53
|
+
export {};
|