camox 0.76.0 → 0.76.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/dist/core/editing/createEditableBlock.js +1077 -1681
  2. package/dist/core/editing/fieldSlot.js +251 -0
  3. package/dist/core/editing/useFieldSlot.js +159 -0
  4. package/dist/core/layoutContext.d.ts +2 -0
  5. package/dist/core/layoutContext.js +27 -1
  6. package/dist/features/canvas/CanvasPageFrame.js +2 -2
  7. package/dist/features/canvas/CanvasPreviewPageSlot.js +1 -1
  8. package/dist/features/canvas/canvasCameraController.js +595 -0
  9. package/dist/features/canvas/useCanvasCamera.js +141 -587
  10. package/dist/features/canvas/useCanvasFrame.js +1 -1
  11. package/dist/features/page/DerivedPageContent.js +4 -8
  12. package/dist/features/preview/components/AssetLightbox.js +6 -65
  13. package/dist/features/preview/components/CanvasPreview.js +2 -2
  14. package/dist/features/preview/components/Frame.js +3 -3
  15. package/dist/features/preview/components/ItemFieldsEditor.js +376 -460
  16. package/dist/features/preview/components/Overlays.js +4 -9
  17. package/dist/features/preview/components/PreviewPanel.js +3 -3
  18. package/dist/features/preview/components/RemovedBlockPlaceholder.js +2 -2
  19. package/dist/features/preview/components/SelectionEditor.js +285 -339
  20. package/dist/features/preview/components/ZoomableImage.js +105 -0
  21. package/dist/features/preview/components/contentFieldSchema.js +3 -1
  22. package/dist/features/preview/components/useFieldWriter.js +30 -51
  23. package/dist/features/preview/components/useRecordView.js +3 -3
  24. package/dist/features/preview/overlayMessages.js +20 -58
  25. package/dist/features/preview/previewStore.js +6 -123
  26. package/dist/features/preview/repeatableItemPath.js +47 -0
  27. package/dist/features/renderer/RendererApp.js +6 -6
  28. package/dist/features/renderer/RendererFrames.js +2 -2
  29. package/dist/features/renderer/rendererHost.js +1 -1
  30. package/dist/features/routes/pageRuntime.d.ts +2 -8
  31. package/dist/features/routes/pageRuntime.js +21 -105
  32. package/dist/features/runtime/pageApp.js +1 -1
  33. package/dist/features/runtime/pageRead.js +105 -0
  34. package/dist/features/runtime/reader.d.ts +25 -0
  35. package/dist/features/runtime/reader.js +142 -0
  36. package/dist/features/runtime/runtime.d.ts +9 -3
  37. package/dist/features/runtime/runtime.js +608 -776
  38. package/dist/features/sdkBundle/buildSdkBundle.d.ts +5 -0
  39. package/dist/features/sdkBundle/buildSdkBundle.js +9 -3
  40. package/dist/features/studio/components/DraftsSheet.js +124 -65
  41. package/dist/features/studio/components/ThreadMessages.js +5 -9
  42. package/dist/features/studio/showInCanvas.js +9 -19
  43. package/dist/features/studio/useEditorSelection.js +8 -19
  44. package/dist/package.js +1 -1
  45. package/dist/studio.css +1 -1
  46. package/package.json +4 -4
  47. package/dist/core/hooks/useFieldSelection.js +0 -67
  48. package/dist/core/hooks/useIsEditable.js +0 -21
  49. package/dist/core/hooks/useOverlayMessage.js +0 -70
@@ -1,16 +1,11 @@
1
1
  import { canvasStore } from "./canvasStore.js";
2
- import { canvasPageIframe } from "../preview/previewPageFrame.js";
3
2
  import { ViewportContinuityContext, captureViewportAnchor, resolveViewportAnchor } from "../preview/viewportContinuity.js";
3
+ import { canvasPageIframe } from "../preview/previewPageFrame.js";
4
4
  import { canvasSeamCover } from "./canvasFrameDocument.js";
5
- import { revealScrollDelta } from "../preview/selectionReveal.js";
6
- import { CANVAS_WHEEL_ZOOM_RATE, ZOOM_STRETCH, canvasMaximumScale, canvasMinimumScale, canvasOverview, canvasVerticalRail, canvasWheelDelta, constrainCanvasCamera, findCanvasSnapTarget, fitCanvasPage, isCanvasCameraSnapped, isCanvasPageScale, isInCanvasSnapWindow, placeCanvasPageAnchor, snapCanvasZoom, stretchCanvasZoom, unstretchCanvasZoom, zoomCanvasAt } from "./canvasCamera.js";
7
- import { createCanvasFlight, createCanvasPan } from "./canvasFlight.js";
5
+ import { createCanvasCameraController } from "./canvasCameraController.js";
8
6
  import * as React from "react";
9
7
 
10
8
  //#region src/features/canvas/useCanvasCamera.ts
11
- const GESTURE_IDLE_MS = 180;
12
- /** A zoom ends once its input pauses this long; a stretch past a limit then springs back. */
13
- const ZOOM_END_MS = 120;
14
9
  const CAMERA_PROPERTIES = [
15
10
  "--canvas-x",
16
11
  "--canvas-y",
@@ -18,8 +13,6 @@ const CAMERA_PROPERTIES = [
18
13
  "--canvas-rest-zoom",
19
14
  "--canvas-pixel-ratio"
20
15
  ];
21
- /** The camera counts as moving until it has been still this long. */
22
- const CAMERA_SETTLE_MS = 150;
23
16
  /**
24
17
  * At rest, zooms where layers that scale with the camera stay promoted
25
18
  * (`data-canvas-promoted`, for `will-change: transform`), so a movement starts
@@ -38,20 +31,27 @@ function isControl(target) {
38
31
  function isTextEntry(target) {
39
32
  return target instanceof Element && (!!target.closest("input, select, textarea") || target instanceof HTMLElement && target.isContentEditable);
40
33
  }
34
+ /** The camera's keyboard commands, by key: a zoom key wins over Shift+1, which types "+" on some layouts. */
35
+ function keyCommand(event) {
36
+ switch (event.key) {
37
+ case "+":
38
+ case "=": return "zoomIn";
39
+ case "-": return "zoomOut";
40
+ case "0": return "zoomToActualSize";
41
+ }
42
+ if (event.key === "Home" || event.shiftKey && event.code === "Digit1") return "overview";
43
+ switch (event.key) {
44
+ case "ArrowLeft": return "panLeft";
45
+ case "ArrowRight": return "panRight";
46
+ case "ArrowUp": return "panUp";
47
+ case "ArrowDown": return "panDown";
48
+ }
49
+ }
41
50
  /**
42
- * The camera's y at a new scale, keeping the document point at the viewport's
43
- * top edge. Above the page top, the unscaled chrome keeps its screen position.
51
+ * Adapts the Canvas camera controller to the DOM: its input events, sizes, animation
52
+ * frames and timers, and the canvas store. Animate only the camera's CSS properties;
53
+ * page trees never rerender on pan/zoom.
44
54
  */
45
- function rescaleY(camera, scale) {
46
- return camera.y > 0 ? camera.y : camera.y * scale / camera.scale;
47
- }
48
- const NO_INSETS = {
49
- top: 0,
50
- right: 0,
51
- bottom: 0,
52
- left: 0
53
- };
54
- /** Animate only the camera's CSS properties; page trees never rerender on pan/zoom. */
55
55
  function useCanvasCamera(workspaceKey, initialTarget, row) {
56
56
  const continuity = React.useContext(ViewportContinuityContext);
57
57
  const mirrors = React.useRef(/* @__PURE__ */ new Set());
@@ -59,9 +59,9 @@ function useCanvasCamera(workspaceKey, initialTarget, row) {
59
59
  const contentRef = React.useRef(null);
60
60
  const initialTargetRef = React.useRef(initialTarget);
61
61
  const rowRef = React.useRef(row);
62
- const flightControls = React.useRef(null);
62
+ const controllerRef = React.useRef(null);
63
63
  const flyTo = React.useCallback((key) => {
64
- flightControls.current?.flyTo(key);
64
+ controllerRef.current?.flyTo(key);
65
65
  }, []);
66
66
  const paintMirror = React.useRef(null);
67
67
  /** Paints the camera onto an element outside the viewport until the returned cleanup. */
@@ -81,15 +81,24 @@ function useCanvasCamera(workspaceKey, initialTarget, row) {
81
81
  * unless enough of it already shows. A page flight or a drag in progress keeps the camera.
82
82
  */
83
83
  const reveal = React.useCallback((element, immediate = false) => {
84
- flightControls.current?.reveal(element, immediate);
84
+ const controller = controllerRef.current;
85
+ const viewport = viewportRef.current;
86
+ const frame = element.ownerDocument.defaultView?.frameElement;
87
+ if (!controller || !viewport || !frame) return;
88
+ const rect = element.getBoundingClientRect();
89
+ controller.reveal({
90
+ frameTop: frame.getBoundingClientRect().top - viewport.getBoundingClientRect().top,
91
+ top: rect.top,
92
+ height: rect.height
93
+ }, immediate);
85
94
  }, []);
86
95
  /** Zoom out to the whole Canvas, keeping the selection: its caller decides what it becomes. */
87
96
  const fitAll = React.useCallback(() => {
88
- flightControls.current?.fitAll();
97
+ controllerRef.current?.fitAll();
89
98
  }, []);
90
99
  React.useLayoutEffect(() => {
91
100
  rowRef.current = row;
92
- flightControls.current?.syncRow();
101
+ controllerRef.current?.setRow(row);
93
102
  });
94
103
  React.useLayoutEffect(() => {
95
104
  const viewport = viewportRef.current;
@@ -108,187 +117,23 @@ function useCanvasCamera(workspaceKey, initialTarget, row) {
108
117
  capture: true,
109
118
  passive: false
110
119
  });
111
- let insets = rowRef.current?.insets ?? NO_INSETS;
112
- const measureViewport = () => {
113
- insets = rowRef.current?.insets ?? NO_INSETS;
114
- return {
115
- width: Math.max(0, viewport.clientWidth - insets.left - insets.right),
116
- height: Math.max(0, viewport.clientHeight - insets.top - insets.bottom)
117
- };
118
- };
119
- let viewportSize = measureViewport();
120
- const page = initialTargetRef.current;
121
- const targets = () => rowRef.current?.targets ?? (page ? [{
122
- ...page,
123
- key: "initial"
124
- }] : []);
125
- const measureContent = () => {
126
- const widths = targets().map((target) => target.width);
127
- return {
128
- width: rowRef.current?.width ?? content.offsetWidth,
129
- height: content.offsetHeight,
130
- pageWidth: widths.length ? Math.min(...widths) : void 0
131
- };
132
- };
133
- let contentSize = measureContent();
134
- let minimumScale = canvasMinimumScale(viewportSize, contentSize);
135
- const updateGeometry = (nextViewport = measureViewport(), nextContent = measureContent()) => {
136
- viewportSize = nextViewport;
137
- contentSize = nextContent;
138
- minimumScale = canvasMinimumScale(viewportSize, contentSize);
139
- };
140
- const targetOf = (key) => key == null ? void 0 : targets().find((candidate) => candidate.key === key);
141
- let stretch;
142
- let zoomEnd = 0;
143
- const constrain = (next, stretched = !!stretch) => constrainCanvasCamera(next, viewportSize, contentSize, canvasMaximumScale(viewportSize, targets(), minimumScale), stretched);
144
- const saved = canvasStore.getSnapshot().context.views[workspaceKey];
145
- const savedCamera = saved && !saved.fitted ? saved.camera : void 0;
146
- let restoredCamera = savedCamera && (!page || isCanvasCameraSnapped(savedCamera, viewportSize, page)) ? savedCamera : void 0;
147
- let fittingInitialPage = !!page && !restoredCamera;
148
- const initialCamera = () => fittingInitialPage && page ? constrain(fitCanvasPage(viewportSize, page)) : canvasOverview(viewportSize, contentSize);
149
- let camera = restoredCamera ? constrain(restoredCamera) : initialCamera();
150
- let target = camera;
151
- let animation = 0;
152
- let previousTime = 0;
153
- let flight;
154
- let gesture;
155
- let requested = rowRef.current?.selected ?? null;
156
- let selection = requested ?? (page && targets().find((candidate) => candidate.left === page.left)?.key) ?? null;
157
- const select = (key) => {
158
- if (key === selection) return;
159
- selection = key;
160
- if (key) rowRef.current?.onSelect(key);
161
- else rowRef.current?.onDeselect?.();
162
- };
163
- const rest = (next) => {
164
- if (!stretch) return next;
165
- const maximumScale = canvasMaximumScale(viewportSize, targets(), minimumScale);
166
- const scale = Math.min(maximumScale, Math.max(minimumScale, next.scale));
167
- const settled = zoomCanvasAt(next, stretch.point, scale, minimumScale, maximumScale);
168
- const held = targetOf(selection);
169
- const fit = held && fitCanvasPage(viewportSize, held);
170
- return constrain(fit && isCanvasPageScale(scale, fit.scale) ? {
171
- ...settled,
172
- x: fit.x
173
- } : settled, false);
174
- };
175
- const releaseSelection = () => {
176
- const held = targetOf(selection);
177
- if (selection && !(held && isCanvasCameraSnapped(rest(target), viewportSize, held))) select(null);
178
- };
179
- const zoom = (base, point, factor, time, translation = {
180
- x: 0,
181
- y: 0
182
- }) => {
183
- const scale = base.scale;
184
- const maximumScale = canvasMaximumScale(viewportSize, targets(), minimumScale);
185
- const intendedScale = (gesture?.kind === "zoom" && time - gesture.time < GESTURE_IDLE_MS ? gesture.intendedScale : unstretchCanvasZoom(scale, minimumScale, maximumScale)) * factor;
186
- gesture = {
187
- kind: "zoom",
188
- time,
189
- intendedScale
190
- };
191
- const limited = Math.min(maximumScale, Math.max(minimumScale, intendedScale));
192
- const eligible = targets().filter((candidate) => findCanvasSnapTarget(base, viewportSize, [candidate]));
193
- const candidate = stretch && targetOf(selection) || findCanvasSnapTarget(camera, viewportSize, eligible);
194
- const fit = candidate && fitCanvasPage(viewportSize, candidate);
195
- const snapped = snapCanvasZoom(scale, limited, fit && fit.scale >= minimumScale ? fit.scale : void 0);
196
- const stretched = limited !== intendedScale;
197
- const nextScale = stretched ? stretchCanvasZoom(intendedScale, minimumScale, maximumScale) : snapped;
198
- ownerWindow.clearTimeout(zoomEnd);
199
- if (!pointers.size) zoomEnd = ownerWindow.setTimeout(endZoom, ZOOM_END_MS);
200
- stretch = stretched ? { point } : void 0;
201
- const anchored = zoomCanvasAt(base, point, nextScale, minimumScale / Math.exp(ZOOM_STRETCH), maximumScale * Math.exp(ZOOM_STRETCH));
202
- const next = {
203
- ...anchored,
204
- x: anchored.x + translation.x,
205
- y: anchored.y + translation.y
206
- };
207
- if (!candidate || !fit || !isCanvasPageScale(snapped, fit.scale)) return next;
208
- select(candidate.key);
209
- return {
210
- ...next,
211
- x: (viewportSize.width - candidate.width * nextScale) / 2 - candidate.left * nextScale
212
- };
213
- };
214
- const pan = (base, delta, time, source, rail = {
215
- mode: "free",
216
- samples: []
217
- }) => {
218
- const start = gesture?.kind === "pan" && gesture.source === source && time - gesture.time < GESTURE_IDLE_MS ? gesture.intendedX : base.x;
219
- const intended = constrain({
220
- ...base,
221
- x: start + delta.x,
222
- y: base.y + delta.y
223
- });
224
- gesture = {
225
- kind: "pan",
226
- time,
227
- rail,
228
- intendedX: intended.x,
229
- source
230
- };
231
- if (!delta.x) return {
232
- ...intended,
233
- x: base.x
234
- };
235
- const candidate = findCanvasSnapTarget(intended, viewportSize, targets());
236
- const fit = candidate && fitCanvasPage(viewportSize, candidate);
237
- if (!candidate || !fit || fit.scale < minimumScale) return intended;
238
- const held = isCanvasCameraSnapped(base, viewportSize, candidate);
239
- if (!isInCanvasSnapWindow(intended, viewportSize, candidate, held)) return intended;
240
- select(candidate.key);
241
- gesture.intendedX += (candidate.left + candidate.width / 2) * (intended.scale - fit.scale);
242
- return constrain({
243
- x: fit.x,
244
- y: rescaleY(intended, fit.scale),
245
- scale: fit.scale
246
- });
247
- };
248
- const endZoom = () => {
249
- ownerWindow.clearTimeout(zoomEnd);
250
- if (stretch) {
251
- const next = rest(target);
252
- stretch = void 0;
253
- gesture = void 0;
254
- markMoving();
255
- applyCamera(next);
256
- }
257
- };
258
- const pointers = /* @__PURE__ */ new Map();
259
120
  let spaceHeld = false;
260
- const paintPanState = () => {
261
- for (const element of [viewport, ...mirrors.current]) if (pointers.size) element.dataset.canvasPan = "dragging";
121
+ const paintPanState = (view) => {
122
+ for (const element of [viewport, ...mirrors.current]) if (view.dragging) element.dataset.canvasPan = "dragging";
262
123
  else if (spaceHeld) element.dataset.canvasPan = "ready";
263
124
  else delete element.dataset.canvasPan;
264
125
  };
265
126
  const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)");
266
- const isOverview = (view) => {
267
- const overview = canvasOverview(viewportSize, contentSize);
268
- return Math.abs(view.scale - overview.scale) < 1e-8 && Math.abs(view.x - overview.x) < .5 && Math.abs(view.y - overview.y) < .5;
269
- };
270
- const remember = () => {
271
- if (restoredCamera || fittingInitialPage || !viewportSize.width || !viewportSize.height) return;
272
- canvasStore.send({
273
- type: "rememberView",
274
- workspaceKey,
275
- view: {
276
- camera,
277
- fitted: isOverview(camera)
278
- }
279
- });
280
- };
281
- let settle = 0;
282
- const paintPromotion = () => {
283
- const promoted = !!settle || camera.scale >= PROMOTED_ZOOM_RANGE.min && camera.scale <= PROMOTED_ZOOM_RANGE.max;
127
+ const paintPromotion = ({ camera, moving }) => {
128
+ const promoted = moving || camera.scale >= PROMOTED_ZOOM_RANGE.min && camera.scale <= PROMOTED_ZOOM_RANGE.max;
284
129
  for (const element of [viewport, ...mirrors.current]) {
285
130
  if (promoted === "canvasPromoted" in element.dataset) continue;
286
131
  if (promoted) element.dataset.canvasPromoted = "";
287
132
  else delete element.dataset.canvasPromoted;
288
133
  }
289
134
  };
290
- const paintAtRest = () => {
291
- if (settle) return;
135
+ const paintAtRest = ({ camera, moving }) => {
136
+ if (moving) return;
292
137
  const seamCover = String(canvasSeamCover(camera.scale, ownerWindow.devicePixelRatio));
293
138
  const zoom = String(camera.scale);
294
139
  for (const element of [viewport, ...mirrors.current]) {
@@ -296,358 +141,131 @@ function useCanvasCamera(workspaceKey, initialTarget, row) {
296
141
  if (element.style.getPropertyValue("--canvas-rest-zoom") !== zoom) element.style.setProperty("--canvas-rest-zoom", zoom);
297
142
  }
298
143
  };
299
- const setMoving = (moving) => {
300
- paintPromotion();
301
- paintAtRest();
302
- canvasStore.send({
303
- type: "setCameraMoving",
304
- workspaceKey,
305
- moving
306
- });
307
- };
308
- const markMoving = () => {
309
- const wasMoving = !!settle;
310
- ownerWindow.clearTimeout(settle);
311
- settle = ownerWindow.setTimeout(() => {
312
- settle = 0;
313
- setMoving(false);
314
- }, CAMERA_SETTLE_MS);
315
- if (!wasMoving) setMoving(true);
316
- };
317
- const paintElement = (element) => {
144
+ const paintElement = (element, { camera, insets }) => {
318
145
  element.style.setProperty("--canvas-x", `${camera.x + insets.left}px`);
319
146
  element.style.setProperty("--canvas-y", `${camera.y + insets.top}px`);
320
147
  element.style.setProperty("--canvas-zoom", String(camera.scale));
321
148
  element.style.setProperty("--canvas-pixel-ratio", String(ownerWindow.devicePixelRatio));
322
149
  };
323
- paintMirror.current = (element) => {
324
- paintElement(element);
325
- paintPanState();
326
- paintPromotion();
327
- paintAtRest();
328
- };
329
- const paint = () => {
330
- paintElement(viewport);
331
- for (const element of mirrors.current) paintElement(element);
332
- paintPromotion();
333
- paintAtRest();
334
- remember();
335
- };
336
- const animate = (time) => {
337
- markMoving();
338
- if (flight) {
339
- flight.startedAt ??= time;
340
- const progress = reducedMotion.matches ? 1 : Math.min(1, (time - flight.startedAt) / flight.path.duration);
341
- camera = progress === 1 ? target : flight.path.at(progress);
342
- paint();
343
- if (progress < 1) {
344
- animation = requestAnimationFrame(animate);
345
- return;
346
- }
347
- flight = void 0;
348
- animation = 0;
349
- previousTime = 0;
350
- return;
351
- }
352
- const elapsed = previousTime ? Math.min(64, time - previousTime) : 16;
353
- previousTime = time;
354
- const amount = reducedMotion.matches ? 1 : 1 - Math.exp(-elapsed / 55);
355
- const approach = (from, to, epsilon) => {
356
- const next = from + (to - from) * amount;
357
- return Math.abs(next - to) < epsilon ? to : next;
358
- };
359
- camera = constrain({
360
- x: approach(camera.x, target.x, .05),
361
- y: approach(camera.y, target.y, .05),
362
- scale: approach(camera.scale, target.scale, 1e-4)
363
- }, true);
364
- const settled = Math.abs(camera.x - target.x) < .05 && Math.abs(camera.y - target.y) < .05 && Math.abs(camera.scale - target.scale) < 1e-4;
365
- if (settled) camera = target;
366
- paint();
367
- animation = settled ? 0 : requestAnimationFrame(animate);
368
- if (settled) previousTime = 0;
369
- };
370
- const applyCamera = (next, immediate = false) => {
371
- target = constrain(next);
372
- if (immediate) {
373
- cancelAnimationFrame(animation);
374
- animation = 0;
375
- previousTime = 0;
376
- camera = target;
377
- paint();
378
- return;
379
- }
380
- if (!animation) animation = requestAnimationFrame(animate);
381
- };
382
- const cancelCameraFlight = () => {
383
- if (!flight) return;
384
- flight = void 0;
385
- cancelAnimationFrame(animation);
386
- animation = 0;
387
- previousTime = 0;
388
- target = camera;
389
- gesture = void 0;
390
- };
391
- const startFlight = (key) => {
392
- cancelCameraFlight();
393
- const destination = targetOf(key);
394
- if (!destination) return;
395
- flyToCamera(() => fitCanvasPage(viewportSize, destination), destination);
396
- };
397
- const startOverviewFlight = () => {
398
- cancelCameraFlight();
399
- flyToCamera(() => canvasOverview(viewportSize, contentSize));
400
- };
401
- const startPan = (destination) => {
402
- cancelCameraFlight();
403
- flyToCamera(() => destination, void 0, true);
404
- };
405
- const flyToCamera = (destinationCamera, page, pan = false) => {
406
- cancelAnimationFrame(animation);
407
- animation = 0;
408
- previousTime = 0;
409
- restoredCamera = void 0;
410
- fittingInitialPage = false;
411
- gesture = void 0;
412
- ownerWindow.clearTimeout(zoomEnd);
413
- stretch = void 0;
414
- updateGeometry();
415
- camera = constrain(camera, true);
416
- target = constrain(destinationCamera());
417
- const path = pan ? createCanvasPan(camera, target, viewportSize) : createCanvasFlight(camera, target, viewportSize, contentSize);
418
- if (reducedMotion.matches || path.duration === 0) {
419
- applyCamera(target, true);
420
- return;
421
- }
422
- flight = {
423
- page: page && { ...page },
424
- pan,
425
- path
426
- };
427
- animation = requestAnimationFrame(animate);
428
- };
429
- let syncedTargets = targets();
430
- flightControls.current = {
431
- flyTo: startFlight,
432
- reveal: (element, immediate) => {
433
- const frame = element.ownerDocument.defaultView?.frameElement;
434
- if (!frame || flight && !flight.pan || pointers.size) return;
435
- const rect = element.getBoundingClientRect();
436
- const worldTop = (frame.getBoundingClientRect().top - viewport.getBoundingClientRect().top - insets.top + rect.top * camera.scale - camera.y) / camera.scale;
437
- const delta = revealScrollDelta({
438
- top: target.y + worldTop * target.scale,
439
- bottom: target.y + (worldTop + rect.height) * target.scale
440
- }, {
441
- top: 0,
442
- bottom: viewportSize.height
443
- });
444
- if (!delta) return;
445
- const next = {
446
- ...target,
447
- y: target.y - delta
448
- };
449
- if (immediate) move(next, true);
450
- else startPan(next);
451
- },
452
- syncRow: () => {
453
- const key = rowRef.current?.selected ?? null;
454
- const destination = targetOf(key);
455
- const previousTargets = syncedTargets;
456
- syncedTargets = targets();
457
- if (key !== requested) {
458
- requested = key;
459
- selection = key;
460
- if (destination && !isCanvasCameraSnapped(rest(target), viewportSize, destination)) startFlight(destination.key);
461
- return;
462
- }
463
- if (flight?.page) {
464
- const page = targetOf(flight.page.key);
465
- if (!page) cancelCameraFlight();
466
- else if (page.left !== flight.page.left || page.width !== flight.page.width) startFlight(page.key);
467
- return;
150
+ const saved = canvasStore.getSnapshot().context.views[workspaceKey];
151
+ const controller = createCanvasCameraController({
152
+ initialPage: initialTargetRef.current,
153
+ saved,
154
+ row: rowRef.current
155
+ }, {
156
+ measure: () => ({
157
+ viewport: {
158
+ width: viewport.clientWidth,
159
+ height: viewport.clientHeight
160
+ },
161
+ content: {
162
+ width: content.offsetWidth,
163
+ height: content.offsetHeight
468
164
  }
469
- const previous = previousTargets[syncedTargets.findIndex((candidate) => candidate.key === key)];
470
- if (destination && previous?.key === key && previous.left !== destination.left && previous.width === destination.width && selection === key && !flight && isCanvasCameraSnapped(rest(target), viewportSize, previous)) {
471
- updateGeometry();
472
- const fit = fitCanvasPage(viewportSize, destination);
473
- const next = {
474
- ...fit,
475
- y: rescaleY(restoredCamera ?? target, fit.scale)
476
- };
477
- if (restoredCamera || fittingInitialPage) restoredCamera = next;
478
- fittingInitialPage = false;
479
- camera = constrain(camera, true);
480
- applyCamera(next, true);
481
- return;
165
+ }),
166
+ clock: {
167
+ frame: (callback) => {
168
+ const id = requestAnimationFrame(callback);
169
+ return () => cancelAnimationFrame(id);
170
+ },
171
+ after: (ms, callback) => {
172
+ const id = ownerWindow.setTimeout(callback, ms);
173
+ return () => ownerWindow.clearTimeout(id);
482
174
  }
483
- if (destination && selection === key && !flight && !fittingInitialPage && !isCanvasCameraSnapped(rest(target), viewportSize, destination)) startFlight(destination.key);
484
175
  },
485
- fitAll: startOverviewFlight
486
- };
487
- const move = (next, immediate = false) => {
488
- markMoving();
489
- cancelCameraFlight();
490
- restoredCamera = void 0;
491
- fittingInitialPage = false;
492
- applyCamera(next, immediate);
493
- releaseSelection();
176
+ reducedMotion: () => reducedMotion.matches,
177
+ paint: (view) => {
178
+ paintElement(viewport, view);
179
+ for (const element of mirrors.current) paintElement(element, view);
180
+ paintPromotion(view);
181
+ paintAtRest(view);
182
+ },
183
+ moving: (view) => {
184
+ paintPromotion(view);
185
+ paintAtRest(view);
186
+ canvasStore.send({
187
+ type: "setCameraMoving",
188
+ workspaceKey,
189
+ moving: view.moving
190
+ });
191
+ },
192
+ remember: (view) => canvasStore.send({
193
+ type: "rememberView",
194
+ workspaceKey,
195
+ view
196
+ })
197
+ });
198
+ controllerRef.current = controller;
199
+ paintMirror.current = (element) => {
200
+ paintElement(element, controller);
201
+ paintPanState(controller);
202
+ paintPromotion(controller);
203
+ paintAtRest(controller);
494
204
  };
205
+ const pageElement = (key) => Array.from(content.querySelectorAll("[data-canvas-page]")).find((element) => element.dataset.canvasPage === key);
495
206
  const continuityBinding = {
496
207
  capture: () => {
497
- const held = flight ? void 0 : targetOf(selection);
498
- const element = held && Array.from(content.querySelectorAll("[data-canvas-page]")).find((element) => element.dataset.canvasPage === held.key);
208
+ const { camera, page } = controller.capture();
209
+ const element = page ? pageElement(page) : void 0;
499
210
  const doc = element ? canvasPageIframe(element)?.contentDocument : void 0;
500
- const anchor = doc && element ? captureViewportAnchor(doc, Math.max(0, (viewport.getBoundingClientRect().top + insets.top - element.getBoundingClientRect().top) / camera.scale)) : void 0;
211
+ const anchor = doc && element ? captureViewportAnchor(doc, Math.max(0, (viewport.getBoundingClientRect().top + controller.insets.top - element.getBoundingClientRect().top) / camera.scale)) : void 0;
501
212
  return {
502
- camera: { ...camera },
213
+ camera,
503
214
  pathname: element ? element.dataset.canvasPathname : void 0,
504
215
  anchor
505
216
  };
506
217
  },
507
218
  /** Snap to the selected page, at the anchor's reading point or the previous camera's. */
508
219
  restore: (anchor, previous) => {
509
- const destination = targetOf(selection);
510
- if (!destination) return;
511
- cancelCameraFlight();
512
- const page = Array.from(content.querySelectorAll("[data-canvas-page]")).find((element) => element.dataset.canvasPage === destination.key);
513
- const doc = page ? canvasPageIframe(page)?.contentDocument : void 0;
514
- updateGeometry();
515
- const fit = fitCanvasPage(viewportSize, destination);
516
- let next = previous ? {
517
- ...fit,
518
- y: rescaleY(previous, fit.scale)
519
- } : fit;
520
- if (anchor && doc) {
521
- const y = resolveViewportAnchor(doc, anchor);
522
- const frameOffsetY = page ? (page.getBoundingClientRect().top - viewport.getBoundingClientRect().top - insets.top - camera.y) / camera.scale : 0;
523
- next = placeCanvasPageAnchor(viewportSize, destination, y, fit.scale, frameOffsetY);
524
- }
525
- fittingInitialPage = false;
526
- restoredCamera = next;
527
- applyCamera(next, true);
220
+ controller.restore(previous, (key) => {
221
+ const page = pageElement(key);
222
+ const doc = page ? canvasPageIframe(page)?.contentDocument : void 0;
223
+ if (!anchor || !doc || !page) return void 0;
224
+ const { camera, insets } = controller;
225
+ return {
226
+ y: resolveViewportAnchor(doc, anchor),
227
+ frameOffsetY: (page.getBoundingClientRect().top - viewport.getBoundingClientRect().top - insets.top - camera.y) / camera.scale
228
+ };
229
+ });
528
230
  }
529
231
  };
530
232
  if (continuity) continuity.canvas = continuityBinding;
531
- const resize = new ResizeObserver(() => {
532
- const previousInsets = insets;
533
- const nextViewport = measureViewport();
534
- const nextContent = measureContent();
535
- if (insets.left === previousInsets.left && insets.top === previousInsets.top && nextViewport.width === viewportSize.width && nextViewport.height === viewportSize.height && nextContent.width === contentSize.width && nextContent.height === contentSize.height && nextContent.pageWidth === contentSize.pageWidth) return;
536
- const wasFitted = isOverview(target);
537
- gesture = void 0;
538
- const held = nextViewport.width !== viewportSize.width && !flight && !fittingInitialPage ? targetOf(selection) : void 0;
539
- updateGeometry(nextViewport, nextContent);
540
- if (held) {
541
- const fit = fitCanvasPage(viewportSize, held);
542
- const next = {
543
- ...fit,
544
- y: rescaleY(restoredCamera ?? target, fit.scale)
545
- };
546
- if (restoredCamera) restoredCamera = next;
547
- camera = constrain(camera, true);
548
- applyCamera(next, true);
549
- return;
550
- }
551
- if (flight) {
552
- camera = constrain(camera, true);
553
- if (flight.page) startFlight(flight.page.key);
554
- else if (flight.pan) startPan(target);
555
- else startOverviewFlight();
556
- return;
557
- }
558
- if (restoredCamera) {
559
- applyCamera(restoredCamera, true);
560
- return;
561
- }
562
- if (fittingInitialPage) {
563
- applyCamera(initialCamera(), true);
564
- return;
565
- }
566
- camera = constrain(camera, true);
567
- applyCamera(wasFitted ? canvasOverview(viewportSize, contentSize) : target, true);
568
- });
233
+ const resize = new ResizeObserver(() => controller.resize());
569
234
  resize.observe(viewport);
570
235
  resize.observe(content);
571
236
  const localPoint = (x, y) => {
572
237
  const rect = viewport.getBoundingClientRect();
573
238
  return {
574
- x: x - rect.left - insets.left,
575
- y: y - rect.top - insets.top
239
+ x: x - rect.left,
240
+ y: y - rect.top
576
241
  };
577
242
  };
578
243
  const onWheel = (event) => {
579
244
  event.preventDefault();
580
- const dx = canvasWheelDelta(event.deltaX, event.deltaMode, viewportSize.width);
581
- const dy = canvasWheelDelta(event.deltaY, event.deltaMode, viewportSize.height);
582
- if (!dx && !dy) return;
583
- rowRef.current?.onEditorMove?.();
584
- cancelCameraFlight();
585
- if (event.ctrlKey || event.metaKey) {
586
- const point = localPoint(event.clientX, event.clientY);
587
- move(zoom(target, point, Math.exp(-dy * CANVAS_WHEEL_ZOOM_RATE), event.timeStamp));
588
- return;
589
- }
590
- endZoom();
591
- const previous = gesture?.kind === "pan" && gesture.source === "wheel" && event.timeStamp - gesture.time < GESTURE_IDLE_MS ? gesture.rail : void 0;
592
- const rail = !event.shiftKey && !pointers.size && targets().length > 0 && isCanvasPageScale(target.scale, canvasMaximumScale(viewportSize, targets(), minimumScale)) ? canvasVerticalRail({
593
- x: dx,
594
- y: dy
595
- }, event.timeStamp, previous) : {
596
- mode: "free",
597
- samples: []
598
- };
599
- const vertical = rail.mode === "vertical";
600
- move(pan(target, {
601
- x: -(vertical ? 0 : event.shiftKey && dx === 0 ? dy : dx),
602
- y: -(event.shiftKey && dx === 0 ? 0 : dy)
603
- }, event.timeStamp, "wheel", rail));
245
+ controller.wheel({
246
+ deltaX: event.deltaX,
247
+ deltaY: event.deltaY,
248
+ deltaMode: event.deltaMode,
249
+ point: localPoint(event.clientX, event.clientY),
250
+ zoom: event.ctrlKey || event.metaKey,
251
+ shift: event.shiftKey,
252
+ time: event.timeStamp
253
+ });
604
254
  };
605
255
  const onPointerDown = (event) => {
606
256
  if (!(event.button === 1 || event.button === 0 && (spaceHeld || event.pointerType === "touch")) || !spaceHeld && isControl(event.target)) return;
607
- gesture = void 0;
608
257
  event.preventDefault();
609
258
  viewport.focus({ preventScroll: true });
610
- move(camera, true);
611
- pointers.set(event.pointerId, localPoint(event.clientX, event.clientY));
259
+ controller.pointerDown(event.pointerId, localPoint(event.clientX, event.clientY));
612
260
  viewport.setPointerCapture(event.pointerId);
613
- paintPanState();
261
+ paintPanState(controller);
614
262
  };
615
263
  const onPointerMove = (event) => {
616
- const previous = pointers.get(event.pointerId);
617
- if (!previous) return;
618
- const next = localPoint(event.clientX, event.clientY);
619
- const other = [...pointers.entries()].find(([id]) => id !== event.pointerId)?.[1];
620
- pointers.set(event.pointerId, next);
621
- rowRef.current?.onEditorMove?.();
622
- if (!other) {
623
- move(pan(camera, {
624
- x: next.x - previous.x,
625
- y: next.y - previous.y
626
- }, event.timeStamp, "pointer"), true);
627
- return;
628
- }
629
- const oldCenter = {
630
- x: (previous.x + other.x) / 2,
631
- y: (previous.y + other.y) / 2
632
- };
633
- const center = {
634
- x: (next.x + other.x) / 2,
635
- y: (next.y + other.y) / 2
636
- };
637
- const oldDistance = Math.hypot(previous.x - other.x, previous.y - other.y);
638
- const distance = Math.hypot(next.x - other.x, next.y - other.y);
639
- const zoomed = zoom(camera, oldCenter, distance / Math.max(1, oldDistance), event.timeStamp, {
640
- x: center.x - oldCenter.x,
641
- y: center.y - oldCenter.y
642
- });
643
- move(zoomed, true);
264
+ controller.pointerMove(event.pointerId, localPoint(event.clientX, event.clientY), event.timeStamp);
644
265
  };
645
266
  const onPointerEnd = (event) => {
646
- gesture = void 0;
647
- pointers.delete(event.pointerId);
648
- if (pointers.size < 2) endZoom();
649
- if (pointers.size) return;
650
- paintPanState();
267
+ controller.pointerUp(event.pointerId);
268
+ if (!controller.dragging) paintPanState(controller);
651
269
  };
652
270
  const ownsSpace = (target) => {
653
271
  const doc = viewport.ownerDocument;
@@ -661,96 +279,37 @@ function useCanvasCamera(workspaceKey, initialTarget, row) {
661
279
  event.preventDefault();
662
280
  if (spaceHeld) return;
663
281
  spaceHeld = true;
664
- paintPanState();
282
+ paintPanState(controller);
665
283
  };
666
284
  const onSpaceUp = (event) => {
667
285
  if (event.code !== "Space" || !spaceHeld) return;
668
286
  event.preventDefault();
669
287
  spaceHeld = false;
670
- paintPanState();
288
+ paintPanState(controller);
671
289
  };
672
290
  const releaseSpace = () => {
673
291
  spaceHeld = false;
674
- paintPanState();
292
+ paintPanState(controller);
675
293
  };
676
294
  const onFocusIn = (event) => {
677
295
  if (!isControl(event.target)) return;
678
296
  const rect = event.target.getBoundingClientRect();
679
- const viewportRect = viewport.getBoundingClientRect();
680
- const bounds = {
681
- left: viewportRect.left + insets.left,
682
- top: viewportRect.top + insets.top,
683
- right: viewportRect.left + insets.left + viewportSize.width,
684
- bottom: viewportRect.top + insets.top + viewportSize.height
685
- };
686
- const dx = Math.max(0, bounds.left + 16 - rect.left) - Math.max(0, rect.right - bounds.right + 16);
687
- const dy = Math.max(0, bounds.top + 16 - rect.top) - Math.max(0, rect.bottom - bounds.bottom + 16);
688
- if (!dx && !dy) return;
689
- gesture = void 0;
690
- move({
691
- ...camera,
692
- x: camera.x + dx,
693
- y: camera.y + dy
694
- }, true);
297
+ const { x: left, y: top } = localPoint(rect.left, rect.top);
298
+ const { x: right, y: bottom } = localPoint(rect.right, rect.bottom);
299
+ controller.bringIntoView({
300
+ left,
301
+ top,
302
+ right,
303
+ bottom
304
+ });
695
305
  };
696
306
  const onKeyDown = (event) => {
697
307
  if (isControl(event.target) || event.altKey || event.ctrlKey || event.metaKey) return;
698
- if ([
699
- "+",
700
- "=",
701
- "-",
702
- "0",
703
- "Home",
704
- "ArrowLeft",
705
- "ArrowRight",
706
- "ArrowUp",
707
- "ArrowDown"
708
- ].includes(event.key) || event.shiftKey && event.code === "Digit1") cancelCameraFlight();
709
- const center = {
710
- x: viewportSize.width / 2,
711
- y: viewportSize.height / 2
712
- };
713
- const factor = event.key === "+" || event.key === "=" ? 1.25 : event.key === "-" ? .8 : event.key === "0" ? 1 / target.scale : void 0;
714
- if (factor) {
715
- gesture = void 0;
716
- event.preventDefault();
717
- rowRef.current?.onEditorMove?.();
718
- move(zoom(target, center, factor, event.timeStamp));
719
- return;
720
- }
721
- if (event.key === "Home" || event.shiftKey && event.code === "Digit1") {
722
- endZoom();
723
- gesture = void 0;
724
- event.preventDefault();
725
- rowRef.current?.onEditorMove?.();
726
- move(canvasOverview(viewportSize, contentSize));
727
- return;
728
- }
729
- const direction = {
730
- ArrowLeft: {
731
- x: 100,
732
- y: 0
733
- },
734
- ArrowRight: {
735
- x: -100,
736
- y: 0
737
- },
738
- ArrowUp: {
739
- x: 0,
740
- y: 100
741
- },
742
- ArrowDown: {
743
- x: 0,
744
- y: -100
745
- }
746
- }[event.key];
747
- if (!direction) return;
308
+ const command = keyCommand(event);
309
+ if (!command) return;
748
310
  event.preventDefault();
749
- rowRef.current?.onEditorMove?.();
750
- endZoom();
751
- move(pan(target, direction, event.timeStamp, "keyboard"));
311
+ controller.key(command, event.timeStamp);
752
312
  };
753
- paint();
754
313
  viewport.addEventListener("wheel", onWheel, { passive: false });
755
314
  viewport.addEventListener("pointerdown", onPointerDown);
756
315
  viewport.addEventListener("pointermove", onPointerMove);
@@ -764,16 +323,11 @@ function useCanvasCamera(workspaceKey, initialTarget, row) {
764
323
  ownerWindow.addEventListener("blur", releaseSpace);
765
324
  return () => {
766
325
  if (continuity?.canvas === continuityBinding) continuity.canvas = null;
767
- flightControls.current = null;
326
+ controllerRef.current = null;
768
327
  ownerWindow.removeEventListener("wheel", preventHistorySwipe, { capture: true });
769
328
  if (overscrollX) rootStyle.setProperty("overscroll-behavior-x", overscrollX, overscrollPriority);
770
329
  else rootStyle.removeProperty("overscroll-behavior-x");
771
- remember();
772
- cancelAnimationFrame(animation);
773
- ownerWindow.clearTimeout(settle);
774
- settle = 0;
775
- ownerWindow.clearTimeout(zoomEnd);
776
- setMoving(false);
330
+ controller.dispose();
777
331
  resize.disconnect();
778
332
  viewport.removeEventListener("wheel", onWheel);
779
333
  viewport.removeEventListener("pointerdown", onPointerDown);