@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,227 @@
|
|
|
1
|
+
import { useCallback, useEffect, useLayoutEffect, useRef } from 'react';
|
|
2
|
+
import { previewPerspectivesEqual } from '@storystick/preview/runtime/preview-perspective-alignment';
|
|
3
|
+
const DEFAULT_PERSPECTIVE_INTENT_TIMEOUT_MS = 10_000;
|
|
4
|
+
export function isAcceptedPreviewPerspectiveIntent(result) {
|
|
5
|
+
return result.status === 'applied' || result.status === 'local-noop';
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Event-owned lifecycle for an explicit perspective gesture. React effects
|
|
9
|
+
* may reconcile the accepted read context, but they never create this intent.
|
|
10
|
+
*/
|
|
11
|
+
export function usePreviewPerspectiveIntent(inputs) {
|
|
12
|
+
const inputsRef = useRef(inputs);
|
|
13
|
+
useLayoutEffect(() => {
|
|
14
|
+
inputsRef.current = inputs;
|
|
15
|
+
}, [inputs]);
|
|
16
|
+
const pendingRef = useRef(null);
|
|
17
|
+
const earlyLifecycleRef = useRef(new Map());
|
|
18
|
+
const observeLifecycleRef = useRef(null);
|
|
19
|
+
const settle = useCallback((pending, result, options) => {
|
|
20
|
+
if (pending.settled)
|
|
21
|
+
return;
|
|
22
|
+
pending.settled = true;
|
|
23
|
+
clearTimeout(pending.timeout);
|
|
24
|
+
earlyLifecycleRef.current.clear();
|
|
25
|
+
if (pendingRef.current === pending)
|
|
26
|
+
pendingRef.current = null;
|
|
27
|
+
if (options.clearTransition)
|
|
28
|
+
inputsRef.current.clearTransition();
|
|
29
|
+
if (options.terminalFailure) {
|
|
30
|
+
inputsRef.current.onTerminalFailure?.(options.terminalFailure);
|
|
31
|
+
}
|
|
32
|
+
const commandId = ('commandId' in result ? result.commandId : null) ?? pending.commandId;
|
|
33
|
+
switch (result.status) {
|
|
34
|
+
case 'applied':
|
|
35
|
+
case 'local-noop':
|
|
36
|
+
inputsRef.current.onStateChange?.({
|
|
37
|
+
kind: 'accepted',
|
|
38
|
+
acceptance: result.status,
|
|
39
|
+
perspective: pending.perspective,
|
|
40
|
+
generation: pending.transition.generation,
|
|
41
|
+
commandId,
|
|
42
|
+
context: result.context,
|
|
43
|
+
});
|
|
44
|
+
break;
|
|
45
|
+
case 'conflict':
|
|
46
|
+
inputsRef.current.onStateChange?.({
|
|
47
|
+
kind: 'conflict',
|
|
48
|
+
perspective: pending.perspective,
|
|
49
|
+
generation: pending.transition.generation,
|
|
50
|
+
commandId,
|
|
51
|
+
context: result.context,
|
|
52
|
+
});
|
|
53
|
+
break;
|
|
54
|
+
case 'unavailable':
|
|
55
|
+
case 'superseded':
|
|
56
|
+
inputsRef.current.onStateChange?.({
|
|
57
|
+
kind: result.status,
|
|
58
|
+
perspective: pending.perspective,
|
|
59
|
+
generation: pending.transition.generation,
|
|
60
|
+
commandId,
|
|
61
|
+
});
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
pending.resolve(result);
|
|
65
|
+
}, []);
|
|
66
|
+
const dispatch = useCallback((pending) => {
|
|
67
|
+
earlyLifecycleRef.current.clear();
|
|
68
|
+
void inputsRef.current
|
|
69
|
+
.dispatch(pending.perspective)
|
|
70
|
+
.then(({ commandId }) => {
|
|
71
|
+
if (commandId !== null) {
|
|
72
|
+
inputsRef.current.registerCommand(commandId, pending.transition);
|
|
73
|
+
}
|
|
74
|
+
if (pendingRef.current !== pending || pending.settled)
|
|
75
|
+
return;
|
|
76
|
+
if (commandId === null) {
|
|
77
|
+
settle(pending, { status: 'unavailable', commandId: null }, { clearTransition: true, terminalFailure: 'unavailable' });
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
pending.commandId = commandId;
|
|
81
|
+
inputsRef.current.onStateChange?.({
|
|
82
|
+
kind: 'pending',
|
|
83
|
+
perspective: pending.perspective,
|
|
84
|
+
generation: pending.transition.generation,
|
|
85
|
+
commandId,
|
|
86
|
+
});
|
|
87
|
+
const earlyLifecycle = earlyLifecycleRef.current.get(commandId);
|
|
88
|
+
earlyLifecycleRef.current.clear();
|
|
89
|
+
if (earlyLifecycle !== undefined) {
|
|
90
|
+
observeLifecycleRef.current?.(earlyLifecycle);
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
.catch(() => {
|
|
94
|
+
if (pendingRef.current !== pending || pending.settled)
|
|
95
|
+
return;
|
|
96
|
+
settle(pending, { status: 'unavailable', commandId: pending.commandId }, { clearTransition: true, terminalFailure: 'unavailable' });
|
|
97
|
+
});
|
|
98
|
+
}, [settle]);
|
|
99
|
+
const changePerspective = useCallback((perspective) => {
|
|
100
|
+
const previous = pendingRef.current;
|
|
101
|
+
if (previous !== null &&
|
|
102
|
+
previewPerspectivesEqual(previous.perspective, perspective)) {
|
|
103
|
+
return previous.promise;
|
|
104
|
+
}
|
|
105
|
+
if (previous !== null) {
|
|
106
|
+
settle(previous, { status: 'superseded', commandId: previous.commandId }, { clearTransition: false });
|
|
107
|
+
}
|
|
108
|
+
const sessionId = inputsRef.current.getSessionId();
|
|
109
|
+
const context = inputsRef.current.getReadContext();
|
|
110
|
+
if (sessionId === null || context === null) {
|
|
111
|
+
inputsRef.current.onTerminalFailure?.('unavailable');
|
|
112
|
+
inputsRef.current.onStateChange?.({
|
|
113
|
+
kind: 'unavailable',
|
|
114
|
+
perspective,
|
|
115
|
+
generation: null,
|
|
116
|
+
commandId: null,
|
|
117
|
+
});
|
|
118
|
+
return Promise.resolve({ status: 'unavailable', commandId: null });
|
|
119
|
+
}
|
|
120
|
+
if (previous === null &&
|
|
121
|
+
previewPerspectivesEqual(context.perspective, perspective)) {
|
|
122
|
+
inputsRef.current.onAccepted?.(context);
|
|
123
|
+
inputsRef.current.onStateChange?.({
|
|
124
|
+
kind: 'accepted',
|
|
125
|
+
acceptance: 'local-noop',
|
|
126
|
+
perspective,
|
|
127
|
+
generation: null,
|
|
128
|
+
commandId: null,
|
|
129
|
+
context,
|
|
130
|
+
});
|
|
131
|
+
return Promise.resolve({ status: 'local-noop', context });
|
|
132
|
+
}
|
|
133
|
+
const transition = inputsRef.current.beginTransition(perspective);
|
|
134
|
+
let resolve;
|
|
135
|
+
const promise = new Promise((done) => {
|
|
136
|
+
resolve = done;
|
|
137
|
+
});
|
|
138
|
+
const timeoutMs = inputsRef.current.timeoutMs ?? DEFAULT_PERSPECTIVE_INTENT_TIMEOUT_MS;
|
|
139
|
+
const pending = {
|
|
140
|
+
perspective,
|
|
141
|
+
transition,
|
|
142
|
+
promise,
|
|
143
|
+
resolve,
|
|
144
|
+
commandId: null,
|
|
145
|
+
contextId: context.contextId,
|
|
146
|
+
timeout: setTimeout(() => {
|
|
147
|
+
settle(pending, { status: 'unavailable', commandId: pending.commandId }, { clearTransition: true, terminalFailure: 'unavailable' });
|
|
148
|
+
}, timeoutMs),
|
|
149
|
+
settled: false,
|
|
150
|
+
};
|
|
151
|
+
pendingRef.current = pending;
|
|
152
|
+
inputsRef.current.onStateChange?.({
|
|
153
|
+
kind: 'pending',
|
|
154
|
+
perspective,
|
|
155
|
+
generation: transition.generation,
|
|
156
|
+
commandId: null,
|
|
157
|
+
});
|
|
158
|
+
dispatch(pending);
|
|
159
|
+
return promise;
|
|
160
|
+
}, [dispatch, settle]);
|
|
161
|
+
const observeLifecycle = useCallback((payload) => {
|
|
162
|
+
const pending = pendingRef.current;
|
|
163
|
+
const outcome = payload.contextCommand;
|
|
164
|
+
if (pending === null || outcome === undefined) {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
if (pending.commandId === null) {
|
|
168
|
+
earlyLifecycleRef.current.set(outcome.commandId, payload);
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
if (outcome.commandId !== pending.commandId)
|
|
172
|
+
return false;
|
|
173
|
+
if (outcome.status === 'unavailable' ||
|
|
174
|
+
payload.readContext === undefined) {
|
|
175
|
+
settle(pending, { status: 'unavailable', commandId: outcome.commandId }, { clearTransition: true, terminalFailure: 'unavailable' });
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
// Application and current authority are separate facts. A replay may
|
|
179
|
+
// acknowledge r while the shared context has already advanced to r+1.
|
|
180
|
+
const current = inputsRef.current.getReadContext();
|
|
181
|
+
const context = current?.contextId === payload.readContext.contextId &&
|
|
182
|
+
current.revision > payload.readContext.revision
|
|
183
|
+
? current
|
|
184
|
+
: payload.readContext;
|
|
185
|
+
if ((current && current.contextId !== pending.contextId) ||
|
|
186
|
+
context.contextId !== pending.contextId ||
|
|
187
|
+
(outcome.status === 'applied' &&
|
|
188
|
+
outcome.contextId !== pending.contextId)) {
|
|
189
|
+
settle(pending, { status: 'superseded', commandId: outcome.commandId }, { clearTransition: true });
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
if (outcome.status === 'applied' &&
|
|
193
|
+
context.revision >= outcome.appliedRevision &&
|
|
194
|
+
previewPerspectivesEqual(context.perspective, pending.perspective)) {
|
|
195
|
+
inputsRef.current.onAccepted?.(context);
|
|
196
|
+
settle(pending, {
|
|
197
|
+
status: 'applied',
|
|
198
|
+
commandId: outcome.commandId,
|
|
199
|
+
context,
|
|
200
|
+
appliedRevision: outcome.appliedRevision,
|
|
201
|
+
}, { clearTransition: false });
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
if (previewPerspectivesEqual(context.perspective, pending.perspective)) {
|
|
205
|
+
inputsRef.current.onAccepted?.(context);
|
|
206
|
+
settle(pending, { status: 'local-noop', context }, { clearTransition: true });
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
// A conflict observes another accepted gesture. Never silently replay ours
|
|
210
|
+
// against its newer revision: that would overwrite the other editor.
|
|
211
|
+
settle(pending, {
|
|
212
|
+
status: 'conflict', commandId: outcome.commandId, context,
|
|
213
|
+
}, { clearTransition: true, terminalFailure: 'conflict' });
|
|
214
|
+
return true;
|
|
215
|
+
}, [dispatch, settle]);
|
|
216
|
+
useLayoutEffect(() => {
|
|
217
|
+
observeLifecycleRef.current = observeLifecycle;
|
|
218
|
+
}, [observeLifecycle]);
|
|
219
|
+
const cancelPending = useCallback(() => {
|
|
220
|
+
const pending = pendingRef.current;
|
|
221
|
+
if (pending === null)
|
|
222
|
+
return;
|
|
223
|
+
settle(pending, { status: 'superseded', commandId: pending.commandId }, { clearTransition: false });
|
|
224
|
+
}, [settle]);
|
|
225
|
+
useEffect(() => cancelPending, [cancelPending]);
|
|
226
|
+
return { changePerspective, observeLifecycle, cancelPending };
|
|
227
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type PointerEvent as ReactPointerEvent, type MouseEvent as ReactMouseEvent } from 'react';
|
|
2
|
+
import type { Point } from './preview-toolbar-layout.js';
|
|
3
|
+
/** One captured pointer, one paint per frame, one committed position on release. */
|
|
4
|
+
export declare function usePreviewPointerDrag({ onStart, onMove, onEnd, onCancel }: {
|
|
5
|
+
onStart(event: ReactPointerEvent<HTMLElement>): void;
|
|
6
|
+
onMove(delta: Point): void;
|
|
7
|
+
onEnd(delta: Point): void;
|
|
8
|
+
onCancel(): void;
|
|
9
|
+
}): {
|
|
10
|
+
onPointerDown(event: ReactPointerEvent<HTMLElement>): void;
|
|
11
|
+
onPointerMove(event: ReactPointerEvent<HTMLElement>): void;
|
|
12
|
+
onPointerUp(event: ReactPointerEvent<HTMLElement>): void;
|
|
13
|
+
onPointerCancel(event: ReactPointerEvent<HTMLElement>): void;
|
|
14
|
+
onLostPointerCapture(event: ReactPointerEvent<HTMLElement>): void;
|
|
15
|
+
onClickCapture(event: ReactMouseEvent<HTMLElement>): void;
|
|
16
|
+
};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
/** One captured pointer, one paint per frame, one committed position on release. */
|
|
3
|
+
export function usePreviewPointerDrag({ onStart, onMove, onEnd, onCancel }) {
|
|
4
|
+
const callbacks = useRef({ onStart, onMove, onEnd, onCancel });
|
|
5
|
+
callbacks.current = { onStart, onMove, onEnd, onCancel };
|
|
6
|
+
const session = useRef(null);
|
|
7
|
+
const suppressClick = useRef(false);
|
|
8
|
+
function finish(cancel) {
|
|
9
|
+
const current = session.current;
|
|
10
|
+
if (!current)
|
|
11
|
+
return;
|
|
12
|
+
session.current = null;
|
|
13
|
+
if (current.frame !== null)
|
|
14
|
+
current.window.cancelAnimationFrame(current.frame);
|
|
15
|
+
current.cleanup();
|
|
16
|
+
if (current.element.hasPointerCapture(current.pointer))
|
|
17
|
+
current.element.releasePointerCapture(current.pointer);
|
|
18
|
+
if (current.dragging) {
|
|
19
|
+
suppressClick.current = true;
|
|
20
|
+
if (cancel)
|
|
21
|
+
callbacks.current.onCancel();
|
|
22
|
+
else
|
|
23
|
+
callbacks.current.onEnd(current.delta);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
useEffect(() => () => finish(true), []);
|
|
27
|
+
return {
|
|
28
|
+
onPointerDown(event) {
|
|
29
|
+
if (event.button !== 0 || event.isPrimary === false || session.current)
|
|
30
|
+
return;
|
|
31
|
+
suppressClick.current = false;
|
|
32
|
+
const element = event.currentTarget;
|
|
33
|
+
const win = element.ownerDocument.defaultView;
|
|
34
|
+
const cancel = () => finish(true);
|
|
35
|
+
const escape = (e) => {
|
|
36
|
+
if (e.key !== 'Escape')
|
|
37
|
+
return;
|
|
38
|
+
e.preventDefault();
|
|
39
|
+
e.stopPropagation();
|
|
40
|
+
cancel();
|
|
41
|
+
};
|
|
42
|
+
element.setPointerCapture(event.pointerId);
|
|
43
|
+
session.current = { pointer: event.pointerId, start: { x: event.clientX, y: event.clientY },
|
|
44
|
+
delta: { x: 0, y: 0 }, dragging: false, element, window: win, frame: null,
|
|
45
|
+
cleanup: () => {
|
|
46
|
+
win.removeEventListener('blur', cancel);
|
|
47
|
+
win.removeEventListener('resize', cancel);
|
|
48
|
+
win.visualViewport?.removeEventListener('resize', cancel);
|
|
49
|
+
win.visualViewport?.removeEventListener('scroll', cancel);
|
|
50
|
+
win.removeEventListener('keydown', escape, true);
|
|
51
|
+
} };
|
|
52
|
+
win.addEventListener('blur', cancel);
|
|
53
|
+
win.addEventListener('resize', cancel);
|
|
54
|
+
win.visualViewport?.addEventListener('resize', cancel);
|
|
55
|
+
win.visualViewport?.addEventListener('scroll', cancel);
|
|
56
|
+
win.addEventListener('keydown', escape, true);
|
|
57
|
+
},
|
|
58
|
+
onPointerMove(event) {
|
|
59
|
+
const current = session.current;
|
|
60
|
+
if (!current || current.pointer !== event.pointerId)
|
|
61
|
+
return;
|
|
62
|
+
current.delta = { x: event.clientX - current.start.x, y: event.clientY - current.start.y };
|
|
63
|
+
if (!current.dragging) {
|
|
64
|
+
if (Math.hypot(current.delta.x, current.delta.y) < 5)
|
|
65
|
+
return;
|
|
66
|
+
current.dragging = true;
|
|
67
|
+
callbacks.current.onStart(event);
|
|
68
|
+
}
|
|
69
|
+
event.preventDefault();
|
|
70
|
+
if (current.frame === null)
|
|
71
|
+
current.frame = current.window.requestAnimationFrame(() => {
|
|
72
|
+
current.frame = null;
|
|
73
|
+
callbacks.current.onMove(current.delta);
|
|
74
|
+
});
|
|
75
|
+
},
|
|
76
|
+
onPointerUp(event) {
|
|
77
|
+
if (session.current?.pointer !== event.pointerId)
|
|
78
|
+
return;
|
|
79
|
+
session.current.delta = { x: event.clientX - session.current.start.x, y: event.clientY - session.current.start.y };
|
|
80
|
+
finish(false);
|
|
81
|
+
},
|
|
82
|
+
onPointerCancel(event) {
|
|
83
|
+
if (session.current?.pointer === event.pointerId)
|
|
84
|
+
finish(true);
|
|
85
|
+
},
|
|
86
|
+
onLostPointerCapture(event) {
|
|
87
|
+
if (session.current?.pointer === event.pointerId)
|
|
88
|
+
finish(true);
|
|
89
|
+
},
|
|
90
|
+
onClickCapture(event) {
|
|
91
|
+
if (!suppressClick.current || event.detail === 0)
|
|
92
|
+
return;
|
|
93
|
+
suppressClick.current = false;
|
|
94
|
+
event.preventDefault();
|
|
95
|
+
event.stopPropagation();
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type KeyboardEvent } from 'react';
|
|
2
|
+
import { type Rect, type ResizeEdge } from './preview-toolbar-layout.js';
|
|
3
|
+
export declare function usePreviewToolbarLayout(closeMenu: () => void): {
|
|
4
|
+
bubble: import("react").RefObject<HTMLButtonElement | null>;
|
|
5
|
+
panel: import("react").RefObject<HTMLElement | null>;
|
|
6
|
+
dock: "bottom" | "left" | "right";
|
|
7
|
+
bubbleDrag: {
|
|
8
|
+
onPointerDown(event: import("react").PointerEvent<HTMLElement>): void;
|
|
9
|
+
onPointerMove(event: import("react").PointerEvent<HTMLElement>): void;
|
|
10
|
+
onPointerUp(event: import("react").PointerEvent<HTMLElement>): void;
|
|
11
|
+
onPointerCancel(event: import("react").PointerEvent<HTMLElement>): void;
|
|
12
|
+
onLostPointerCapture(event: import("react").PointerEvent<HTMLElement>): void;
|
|
13
|
+
onClickCapture(event: import("react").MouseEvent<HTMLElement>): void;
|
|
14
|
+
};
|
|
15
|
+
panelDrag: {
|
|
16
|
+
onPointerDown(event: import("react").PointerEvent<HTMLElement>): void;
|
|
17
|
+
onPointerMove(event: import("react").PointerEvent<HTMLElement>): void;
|
|
18
|
+
onPointerUp(event: import("react").PointerEvent<HTMLElement>): void;
|
|
19
|
+
onPointerCancel(event: import("react").PointerEvent<HTMLElement>): void;
|
|
20
|
+
onLostPointerCapture(event: import("react").PointerEvent<HTMLElement>): void;
|
|
21
|
+
onClickCapture(event: import("react").MouseEvent<HTMLElement>): void;
|
|
22
|
+
};
|
|
23
|
+
onBubbleKeyDown: (event: KeyboardEvent) => void;
|
|
24
|
+
onPanelKeyDown: (event: KeyboardEvent) => void;
|
|
25
|
+
panelBounds: Rect;
|
|
26
|
+
resizeEdges: readonly ResizeEdge[];
|
|
27
|
+
resizeHandleProps: (edge: ResizeEdge) => {
|
|
28
|
+
onKeyDown(event: KeyboardEvent): void;
|
|
29
|
+
onPointerDown(event: import("react").PointerEvent<HTMLElement>): void;
|
|
30
|
+
onPointerMove(event: import("react").PointerEvent<HTMLElement>): void;
|
|
31
|
+
onPointerUp(event: import("react").PointerEvent<HTMLElement>): void;
|
|
32
|
+
onPointerCancel(event: import("react").PointerEvent<HTMLElement>): void;
|
|
33
|
+
onLostPointerCapture(event: import("react").PointerEvent<HTMLElement>): void;
|
|
34
|
+
onClickCapture(event: import("react").MouseEvent<HTMLElement>): void;
|
|
35
|
+
};
|
|
36
|
+
};
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
2
|
+
import { bubbleRect, panelRect, panelResizeEdges, parseToolbarLayout, placePanel, resizePanel, snapBubble } from './preview-toolbar-layout.js';
|
|
3
|
+
import { usePreviewPointerDrag } from './use-preview-pointer-drag.js';
|
|
4
|
+
const STORAGE_KEY = 'storystick.preview.layout.v2';
|
|
5
|
+
function readViewport() {
|
|
6
|
+
const v = window.visualViewport;
|
|
7
|
+
return { x: v?.offsetLeft ?? 0, y: v?.offsetTop ?? 0, width: v?.width ?? window.innerWidth, height: v?.height ?? window.innerHeight };
|
|
8
|
+
}
|
|
9
|
+
function paint(element, rect) {
|
|
10
|
+
if (!element)
|
|
11
|
+
return;
|
|
12
|
+
element.style.transform = `translate3d(${rect.x}px, ${rect.y}px, 0)`;
|
|
13
|
+
element.style.width = `${rect.width}px`;
|
|
14
|
+
element.style.height = `${rect.height}px`;
|
|
15
|
+
}
|
|
16
|
+
const add = (a, b) => ({ x: a.x + b.x, y: a.y + b.y });
|
|
17
|
+
function keyboardDelta(event) {
|
|
18
|
+
const step = event.shiftKey ? 80 : 20;
|
|
19
|
+
return { ArrowLeft: { x: -step, y: 0 }, ArrowRight: { x: step, y: 0 }, ArrowUp: { x: 0, y: -step }, ArrowDown: { x: 0, y: step } }[event.key];
|
|
20
|
+
}
|
|
21
|
+
export function usePreviewToolbarLayout(closeMenu) {
|
|
22
|
+
const bubble = useRef(null);
|
|
23
|
+
const panel = useRef(null);
|
|
24
|
+
const [viewport, setViewport] = useState(readViewport);
|
|
25
|
+
const [layout, setLayout] = useState(() => {
|
|
26
|
+
try {
|
|
27
|
+
return parseToolbarLayout(localStorage.getItem(STORAGE_KEY));
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return parseToolbarLayout(null);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
const bubbleStart = useRef({ x: 0, y: 0 });
|
|
34
|
+
const panelStart = useRef({ x: 0, y: 0 });
|
|
35
|
+
const resizeStart = useRef(null);
|
|
36
|
+
const bubbleBounds = bubbleRect(layout.bubble, viewport);
|
|
37
|
+
const panelBounds = panelRect(layout.panel, viewport, layout.bubble);
|
|
38
|
+
function commit(next) {
|
|
39
|
+
setLayout(next);
|
|
40
|
+
try {
|
|
41
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(next));
|
|
42
|
+
}
|
|
43
|
+
catch { /* Positioning works without storage. */ }
|
|
44
|
+
}
|
|
45
|
+
function restore() {
|
|
46
|
+
bubble.current?.removeAttribute('data-dragging');
|
|
47
|
+
panel.current?.removeAttribute('data-dragging');
|
|
48
|
+
panel.current?.removeAttribute('data-resizing');
|
|
49
|
+
paint(bubble.current, bubbleBounds);
|
|
50
|
+
paint(panel.current, panelBounds);
|
|
51
|
+
}
|
|
52
|
+
useLayoutEffect(restore, [layout, viewport]);
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
const resize = () => setViewport(readViewport());
|
|
55
|
+
window.addEventListener('resize', resize);
|
|
56
|
+
window.visualViewport?.addEventListener('resize', resize);
|
|
57
|
+
window.visualViewport?.addEventListener('scroll', resize);
|
|
58
|
+
return () => {
|
|
59
|
+
window.removeEventListener('resize', resize);
|
|
60
|
+
window.visualViewport?.removeEventListener('resize', resize);
|
|
61
|
+
window.visualViewport?.removeEventListener('scroll', resize);
|
|
62
|
+
};
|
|
63
|
+
}, []);
|
|
64
|
+
const bubbleDrag = usePreviewPointerDrag({
|
|
65
|
+
onStart() { bubbleStart.current = bubbleBounds; bubble.current?.setAttribute('data-dragging', ''); closeMenu(); },
|
|
66
|
+
onMove(delta) {
|
|
67
|
+
const p = add(bubbleStart.current, delta);
|
|
68
|
+
paint(bubble.current, { ...bubbleBounds,
|
|
69
|
+
x: Math.max(viewport.x, Math.min(viewport.x + viewport.width - bubbleBounds.width, p.x)),
|
|
70
|
+
y: Math.max(viewport.y, Math.min(viewport.y + viewport.height - bubbleBounds.height, p.y)) });
|
|
71
|
+
},
|
|
72
|
+
onEnd(delta) {
|
|
73
|
+
bubble.current?.removeAttribute('data-dragging');
|
|
74
|
+
commit({ ...layout, bubble: snapBubble(add(bubbleStart.current, delta), viewport) });
|
|
75
|
+
},
|
|
76
|
+
onCancel: restore,
|
|
77
|
+
});
|
|
78
|
+
const panelDrag = usePreviewPointerDrag({
|
|
79
|
+
onStart() { panelStart.current = panelBounds; panel.current?.setAttribute('data-dragging', ''); closeMenu(); },
|
|
80
|
+
onMove(delta) { paint(panel.current, panelRect(placePanel(add(panelStart.current, delta), layout.panel, viewport, layout.bubble, false), viewport, layout.bubble)); },
|
|
81
|
+
onEnd(delta) {
|
|
82
|
+
panel.current?.removeAttribute('data-dragging');
|
|
83
|
+
commit({ ...layout, panel: placePanel(add(panelStart.current, delta), layout.panel, viewport, layout.bubble, true) });
|
|
84
|
+
},
|
|
85
|
+
onCancel: restore,
|
|
86
|
+
});
|
|
87
|
+
function resized(delta) {
|
|
88
|
+
const start = resizeStart.current;
|
|
89
|
+
return resizePanel(layout.panel, start.rect, start.edge, delta, viewport, layout.bubble);
|
|
90
|
+
}
|
|
91
|
+
const panelResize = usePreviewPointerDrag({
|
|
92
|
+
onStart(event) {
|
|
93
|
+
resizeStart.current = { rect: panelBounds, edge: event.currentTarget.dataset.resizeEdge };
|
|
94
|
+
panel.current?.setAttribute('data-resizing', '');
|
|
95
|
+
closeMenu();
|
|
96
|
+
},
|
|
97
|
+
onMove(delta) { paint(panel.current, panelRect(resized(delta), viewport, layout.bubble)); },
|
|
98
|
+
onEnd(delta) {
|
|
99
|
+
panel.current?.removeAttribute('data-resizing');
|
|
100
|
+
commit({ ...layout, panel: resized(delta) });
|
|
101
|
+
},
|
|
102
|
+
onCancel: restore,
|
|
103
|
+
});
|
|
104
|
+
function resizeHandleProps(edge) {
|
|
105
|
+
return { ...panelResize, onKeyDown(event) {
|
|
106
|
+
const delta = keyboardDelta(event);
|
|
107
|
+
if (!delta)
|
|
108
|
+
return;
|
|
109
|
+
event.preventDefault();
|
|
110
|
+
commit({ ...layout, panel: resizePanel(layout.panel, panelBounds, edge, delta, viewport, layout.bubble) });
|
|
111
|
+
} };
|
|
112
|
+
}
|
|
113
|
+
function onBubbleKeyDown(event) {
|
|
114
|
+
if (!event.altKey || !['ArrowLeft', 'ArrowRight', 'ArrowDown', 'ArrowUp'].includes(event.key))
|
|
115
|
+
return;
|
|
116
|
+
event.preventDefault();
|
|
117
|
+
closeMenu();
|
|
118
|
+
const edge = event.key === 'ArrowLeft' ? 'left' : event.key === 'ArrowRight' ? 'right' : event.key === 'ArrowDown' ? 'bottom' : layout.bubble.edge;
|
|
119
|
+
commit({ ...layout, bubble: { edge, offset: event.key === 'ArrowUp' ? Math.max(0, layout.bubble.offset - .1) : .5 } });
|
|
120
|
+
}
|
|
121
|
+
function onPanelKeyDown(event) {
|
|
122
|
+
if (event.altKey && (event.key === 'ArrowUp' || event.key === 'ArrowDown')) {
|
|
123
|
+
event.preventDefault();
|
|
124
|
+
const x = layout.panel.kind === 'docked' ? layout.panel.edge === 'left' ? 0 : 1 : layout.panel.x;
|
|
125
|
+
commit({ ...layout, panel: { width: layout.panel.width, height: layout.panel.height, kind: 'stretched', x } });
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (event.key === 'Home' || event.key === 'End') {
|
|
129
|
+
event.preventDefault();
|
|
130
|
+
commit({ ...layout, panel: { width: layout.panel.width, height: layout.panel.height, kind: 'docked', edge: event.key === 'Home' ? 'left' : 'right' } });
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const delta = keyboardDelta(event);
|
|
134
|
+
if (!delta)
|
|
135
|
+
return;
|
|
136
|
+
event.preventDefault();
|
|
137
|
+
commit({ ...layout, panel: placePanel(add(panelBounds, delta), layout.panel, viewport, layout.bubble, false) });
|
|
138
|
+
}
|
|
139
|
+
return { bubble, panel, dock: layout.bubble.edge, bubbleDrag, panelDrag, onBubbleKeyDown, onPanelKeyDown,
|
|
140
|
+
panelBounds, resizeEdges: panelResizeEdges(layout.panel), resizeHandleProps };
|
|
141
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADR-125 D3 — synchronous best-effort field-Proxy wrap at the client
|
|
3
|
+
* query seam. Co-located with the preview-module loader so there is ONE
|
|
4
|
+
* dynamic-import discipline + ONE "production never loads preview" doc
|
|
5
|
+
* (anti-spaghetti). Behaviour:
|
|
6
|
+
*
|
|
7
|
+
* - Module not cached yet (first renders, or non-preview where it
|
|
8
|
+
* never loads) → returns `data` unchanged. `useRenderModeDebounce`
|
|
9
|
+
* (called by the generated-query hook) kicks the async load;
|
|
10
|
+
* once cached, subsequent renders wrap. The preview iframe re-renders
|
|
11
|
+
* on `contentRevision` so the model populates well within the
|
|
12
|
+
* first-visible window.
|
|
13
|
+
* - Cached but the SDK reports tracking inactive (not embedded, SSR,
|
|
14
|
+
* production) → `wrapWithProxy` itself is a referential identity
|
|
15
|
+
* no-op (Lane G). Zero overhead, zero allocation.
|
|
16
|
+
*
|
|
17
|
+
* RSC residual (ADR-125 §5/§7): a pure-RSC customer (Next App Router
|
|
18
|
+
* `createServerClient()` — the next-marketing/Coast dogfood) never
|
|
19
|
+
* calls this hook, so its field model is never registered. Field
|
|
20
|
+
* auto-annotation there degrades to `data-ss-doc` (Lane A, serialized)
|
|
21
|
+
* + section overlays (PR1b) + the `data-ss` developer escape hatch
|
|
22
|
+
* (Lane C). Closing it needs the SDK to observe the resolved doc
|
|
23
|
+
* client-side — an ADR-031 Pattern-C extension, deliberately out of PR2.
|
|
24
|
+
*/
|
|
25
|
+
export declare function tryWrapResultForPreview<T>(docs: T): T;
|
|
26
|
+
export declare function useRenderModeDebounce<T>(value: T): T;
|
|
27
|
+
/**
|
|
28
|
+
* Test-only — flushes the dynamic-import cache so tests can inject
|
|
29
|
+
* fake preview modules. Not exported from index.ts.
|
|
30
|
+
*/
|
|
31
|
+
export declare function _resetPreviewModuleCacheForTests(): void;
|