camox 0.45.0 → 0.47.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.
Files changed (45) hide show
  1. package/dist/core/hooks/useArtifactHost.js +2 -2
  2. package/dist/features/canvas/CamoxCanvas.js +438 -771
  3. package/dist/features/canvas/CanvasPageFrame.js +1 -3
  4. package/dist/features/canvas/CanvasPreviewPageSlot.js +26 -6
  5. package/dist/features/canvas/canvasCamera.js +24 -9
  6. package/dist/features/canvas/canvasFrameDocument.js +4 -19
  7. package/dist/features/canvas/canvasItems.js +12 -102
  8. package/dist/features/canvas/canvasStore.js +3 -92
  9. package/dist/features/canvas/sidebar/CanvasNavigator.js +81 -183
  10. package/dist/features/canvas/sidebar/CanvasSidebar.js +22 -28
  11. package/dist/features/canvas/sidebar/CuratedPageInspector.js +2 -2
  12. package/dist/features/canvas/useCanvasCamera.js +16 -31
  13. package/dist/features/canvas/useCanvasFrame.js +5 -6
  14. package/dist/features/preview/CamoxPreview.js +137 -120
  15. package/dist/features/preview/components/{AddBlockDialog.js → AddBlockSheet.js} +35 -32
  16. package/dist/features/preview/components/BlockActionsPopover.js +6 -6
  17. package/dist/features/preview/components/BlockThumbnail.js +31 -33
  18. package/dist/features/preview/components/Overlays.js +1 -1
  19. package/dist/features/preview/components/PageTree.js +1 -1
  20. package/dist/features/preview/components/PreviewPanel.js +1 -1
  21. package/dist/features/preview/components/PreviewToolbar.js +74 -85
  22. package/dist/features/preview/previewStore.js +9 -9
  23. package/dist/features/provider/CamoxProvider.js +8 -16
  24. package/dist/features/provider/CoreCamoxProvider.js +10 -12
  25. package/dist/features/provider/signInShortcut.js +60 -0
  26. package/dist/features/routes/pageRuntime.js +8 -8
  27. package/dist/features/runtime/RuntimeChrome.js +1 -1
  28. package/dist/features/runtime/pageApp.js +11 -39
  29. package/dist/features/runtime/runtime.d.ts +2 -7
  30. package/dist/features/runtime/runtime.js +6 -19
  31. package/dist/features/studio/FollowAgentProvider.js +11 -22
  32. package/dist/features/studio/components/FaviconDialog.js +3 -3
  33. package/dist/features/studio/followAgent.js +5 -14
  34. package/dist/features/studio/routes.js +1 -23
  35. package/dist/lib/auth.js +36 -24
  36. package/dist/lib/studio-reads.js +0 -20
  37. package/dist/package.js +1 -1
  38. package/dist/studio.css +1 -1
  39. package/package.json +4 -4
  40. package/skills/camox-expand/SKILL.md +5 -4
  41. package/dist/features/canvas/CanvasBlockFrame.js +0 -353
  42. package/dist/features/canvas/CanvasBlockHeader.js +0 -55
  43. package/dist/features/canvas/CanvasShelfMarker.js +0 -255
  44. package/dist/features/canvas/sidebar/CatalogBlockInspector.js +0 -189
  45. package/dist/features/provider/LocalhostPreviewProvider.js +0 -158
@@ -1,224 +1,122 @@
1
- import { cn } from "../../../lib/utils.js";
2
1
  import { previewStore } from "../../preview/previewStore.js";
3
2
  import { canvasPagePath } from "../canvasPages.js";
4
3
  import { c } from "react/compiler-runtime";
5
4
  import * as React from "react";
6
5
  import { jsx, jsxs } from "react/jsx-runtime";
7
6
  import { Button } from "@camox/ui/button";
8
- import { ChevronsUpDown, Plus } from "lucide-react";
7
+ import { Plus } from "lucide-react";
9
8
  import { Command as Command$1, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, commandFilter } from "@camox/ui/command";
10
9
 
11
10
  //#region src/features/canvas/sidebar/CanvasNavigator.tsx
12
- /** Blocks behind a bordered toggle, collapsed until opened. */
13
- function CollapsedBlocks(t0) {
14
- const $ = c(14);
15
- const { label, children } = t0;
16
- const [expanded, setExpanded] = React.useState(false);
17
- let t1;
18
- let t2;
19
- let t3;
20
- if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
21
- t1 = cn("hover:bg-accent/75 flex w-full cursor-default items-center gap-2 px-3 py-2 text-left text-sm", "focus-visible:rounded-none focus-visible:ring-inset");
22
- t2 = () => setExpanded(_temp);
23
- t3 = /* @__PURE__ */ jsx(ChevronsUpDown, { className: "text-muted-foreground size-4 shrink-0" });
24
- $[0] = t1;
25
- $[1] = t2;
26
- $[2] = t3;
27
- } else {
28
- t1 = $[0];
29
- t2 = $[1];
30
- t3 = $[2];
31
- }
32
- let t4;
33
- if ($[3] !== label) {
34
- t4 = /* @__PURE__ */ jsx("span", {
35
- className: "truncate",
36
- children: label
37
- });
38
- $[3] = label;
39
- $[4] = t4;
40
- } else t4 = $[4];
41
- let t5;
42
- if ($[5] !== expanded || $[6] !== t4) {
43
- t5 = /* @__PURE__ */ jsxs("button", {
44
- type: "button",
45
- "aria-expanded": expanded,
46
- className: t1,
47
- onClick: t2,
48
- children: [t3, t4]
49
- });
50
- $[5] = expanded;
51
- $[6] = t4;
52
- $[7] = t5;
53
- } else t5 = $[7];
54
- let t6;
55
- if ($[8] !== children || $[9] !== expanded) {
56
- t6 = expanded && /* @__PURE__ */ jsx("div", {
57
- className: "border-input border-t p-1",
58
- children
59
- });
60
- $[8] = children;
61
- $[9] = expanded;
62
- $[10] = t6;
63
- } else t6 = $[10];
64
- let t7;
65
- if ($[11] !== t5 || $[12] !== t6) {
66
- t7 = /* @__PURE__ */ jsxs("div", {
67
- className: "border-input flex flex-col overflow-hidden rounded-lg border shadow-xs",
68
- children: [t5, t6]
69
- });
70
- $[11] = t5;
71
- $[12] = t6;
72
- $[13] = t7;
73
- } else t7 = $[13];
74
- return t7;
75
- }
76
- function _temp(value) {
77
- return !value;
78
- }
79
- function canvasItemLabel(item) {
80
- return item.kind === "page" ? item.page.title : item.block._internal.title;
81
- }
82
- /** Search an item by its label, and a Page by its path too, never by its key. */
83
- function canvasItemKeywords(item) {
84
- const label = canvasItemLabel(item);
85
- const path = item.kind === "page" ? canvasPagePath(item.page) : null;
86
- return path ? [label, path] : [label];
87
- }
88
11
  /**
89
- * The Canvas Navigator, shown while nothing is selected: every item of the Canvas,
90
- * in Canvas order, grouped under its shelf with its count, and a search that takes
91
- * focus. Pages show with their path; blocks start collapsed, though a search still
92
- * finds them. Picking an item shows it: the camera goes to it.
12
+ * The Canvas Navigator, shown while nothing is selected: every Page of the Canvas,
13
+ * in Canvas order, with its path, and a search that takes focus. Picking a Page
14
+ * shows it: the camera goes to it.
93
15
  */
94
16
  function CanvasNavigator(t0) {
95
- const $ = c(20);
96
- const { shelves, selectedKey, onShowItem } = t0;
17
+ const $ = c(17);
18
+ const { pages, selectedKey, onShowPage } = t0;
97
19
  const [search, setSearch] = React.useState("");
98
20
  let t1;
99
21
  if ($[0] !== search) {
100
- t1 = search.trim();
101
- $[0] = search;
102
- $[1] = t1;
103
- } else t1 = $[1];
104
- const searching = t1 !== "";
105
- let t2;
106
- if ($[2] !== onShowItem || $[3] !== selectedKey) {
107
- t2 = (item) => {
108
- const selected = item.key === selectedKey;
109
- const path = item.kind === "page" ? canvasPagePath(item.page) : null;
110
- return /* @__PURE__ */ jsx(CommandItem, {
111
- value: item.key,
112
- keywords: canvasItemKeywords(item),
113
- "data-checked": selected,
114
- "aria-current": selected ? "true" : void 0,
115
- onSelect: () => onShowItem(item.key),
116
- children: item.kind === "page" ? /* @__PURE__ */ jsxs("div", {
117
- className: "flex min-w-0 flex-col py-0.5",
118
- children: [/* @__PURE__ */ jsx("span", {
119
- className: "truncate font-medium",
120
- children: canvasItemLabel(item)
121
- }), path && /* @__PURE__ */ jsx("span", {
122
- className: "text-muted-foreground truncate font-mono text-xs",
123
- children: path
124
- })]
125
- }) : /* @__PURE__ */ jsx("span", {
126
- className: "truncate",
127
- children: canvasItemLabel(item)
128
- })
129
- }, item.key);
130
- };
131
- $[2] = onShowItem;
132
- $[3] = selectedKey;
133
- $[4] = t2;
134
- } else t2 = $[4];
135
- const renderItem = t2;
136
- let t3;
137
- if ($[5] !== search) {
138
- t3 = /* @__PURE__ */ jsx(CommandInput, {
139
- placeholder: "Search the Canvas…",
22
+ t1 = /* @__PURE__ */ jsx(CommandInput, {
23
+ placeholder: "Search pages…",
140
24
  autoFocus: true,
141
25
  value: search,
142
26
  onValueChange: setSearch
143
27
  });
144
- $[5] = search;
145
- $[6] = t3;
146
- } else t3 = $[6];
28
+ $[0] = search;
29
+ $[1] = t1;
30
+ } else t1 = $[1];
31
+ let t2;
32
+ if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
33
+ t2 = /* @__PURE__ */ jsx(CommandEmpty, { children: "Nothing found." });
34
+ $[2] = t2;
35
+ } else t2 = $[2];
36
+ const t3 = `Pages (${pages.length})`;
147
37
  let t4;
148
- if ($[7] === Symbol.for("react.memo_cache_sentinel")) {
149
- t4 = /* @__PURE__ */ jsx(CommandEmpty, { children: "Nothing found." });
150
- $[7] = t4;
151
- } else t4 = $[7];
152
- let t5;
153
- if ($[8] !== renderItem || $[9] !== searching || $[10] !== shelves) {
154
- let t6;
155
- if ($[12] !== renderItem || $[13] !== searching) {
156
- t6 = (shelf) => {
157
- const heading = `${shelf.label} (${shelf.items.length})`;
158
- if (shelf.key === "shelf:pages") return /* @__PURE__ */ jsxs(CommandGroup, {
159
- heading,
160
- children: [shelf.items.map(renderItem), /* @__PURE__ */ jsx("div", {
161
- className: "p-1",
162
- children: /* @__PURE__ */ jsxs(Button, {
163
- type: "button",
164
- variant: "secondary",
165
- className: "w-full",
166
- onClick: _temp3,
167
- children: [/* @__PURE__ */ jsx(Plus, {}), "Create page"]
168
- })
169
- })]
170
- }, shelf.key);
171
- return /* @__PURE__ */ jsx(CommandGroup, {
172
- heading: searching ? heading : void 0,
173
- children: searching || shelf.items.length === 0 ? shelf.items.map(renderItem) : /* @__PURE__ */ jsx("div", {
174
- className: "p-1",
175
- children: /* @__PURE__ */ jsx(CollapsedBlocks, {
176
- label: `${shelf.items.length} ${shelf.items.length === 1 ? "block" : "blocks"}`,
177
- children: shelf.items.map(renderItem)
178
- })
38
+ if ($[3] !== onShowPage || $[4] !== pages || $[5] !== selectedKey) {
39
+ let t5;
40
+ if ($[7] !== onShowPage || $[8] !== selectedKey) {
41
+ t5 = (page) => {
42
+ const selected = page.key === selectedKey;
43
+ const path = canvasPagePath(page);
44
+ return /* @__PURE__ */ jsx(CommandItem, {
45
+ value: page.key,
46
+ keywords: path ? [page.title, path] : [page.title],
47
+ "data-checked": selected,
48
+ "aria-current": selected ? "true" : void 0,
49
+ onSelect: () => onShowPage(page.key),
50
+ children: /* @__PURE__ */ jsxs("div", {
51
+ className: "flex min-w-0 flex-col py-0.5",
52
+ children: [/* @__PURE__ */ jsx("span", {
53
+ className: "truncate font-medium",
54
+ children: page.title
55
+ }), path && /* @__PURE__ */ jsx("span", {
56
+ className: "text-muted-foreground truncate font-mono text-xs",
57
+ children: path
58
+ })]
179
59
  })
180
- }, shelf.key);
60
+ }, page.key);
181
61
  };
182
- $[12] = renderItem;
183
- $[13] = searching;
184
- $[14] = t6;
185
- } else t6 = $[14];
186
- t5 = shelves.map(t6);
187
- $[8] = renderItem;
188
- $[9] = searching;
189
- $[10] = shelves;
190
- $[11] = t5;
191
- } else t5 = $[11];
62
+ $[7] = onShowPage;
63
+ $[8] = selectedKey;
64
+ $[9] = t5;
65
+ } else t5 = $[9];
66
+ t4 = pages.map(t5);
67
+ $[3] = onShowPage;
68
+ $[4] = pages;
69
+ $[5] = selectedKey;
70
+ $[6] = t4;
71
+ } else t4 = $[6];
72
+ let t5;
73
+ if ($[10] === Symbol.for("react.memo_cache_sentinel")) {
74
+ t5 = /* @__PURE__ */ jsx("div", {
75
+ className: "p-1",
76
+ children: /* @__PURE__ */ jsxs(Button, {
77
+ type: "button",
78
+ variant: "secondary",
79
+ className: "w-full",
80
+ onClick: _temp2,
81
+ children: [/* @__PURE__ */ jsx(Plus, {}), "Create page"]
82
+ })
83
+ });
84
+ $[10] = t5;
85
+ } else t5 = $[10];
192
86
  let t6;
193
- if ($[15] !== t5) {
87
+ if ($[11] !== t3 || $[12] !== t4) {
194
88
  t6 = /* @__PURE__ */ jsxs(CommandList, {
195
89
  className: "max-h-none flex-1",
196
- children: [t4, t5]
90
+ children: [t2, /* @__PURE__ */ jsxs(CommandGroup, {
91
+ heading: t3,
92
+ children: [t4, t5]
93
+ })]
197
94
  });
198
- $[15] = t5;
199
- $[16] = t6;
200
- } else t6 = $[16];
95
+ $[11] = t3;
96
+ $[12] = t4;
97
+ $[13] = t6;
98
+ } else t6 = $[13];
201
99
  let t7;
202
- if ($[17] !== t3 || $[18] !== t6) {
100
+ if ($[14] !== t1 || $[15] !== t6) {
203
101
  t7 = /* @__PURE__ */ jsx("div", {
204
102
  "data-canvas-navigator": true,
205
103
  className: "flex min-h-0 flex-1 flex-col",
206
104
  children: /* @__PURE__ */ jsxs(Command$1, {
207
105
  className: "rounded-none! bg-transparent",
208
- filter: _temp2,
209
- children: [t3, t6]
106
+ filter: _temp,
107
+ children: [t1, t6]
210
108
  })
211
109
  });
212
- $[17] = t3;
213
- $[18] = t6;
214
- $[19] = t7;
215
- } else t7 = $[19];
110
+ $[14] = t1;
111
+ $[15] = t6;
112
+ $[16] = t7;
113
+ } else t7 = $[16];
216
114
  return t7;
217
115
  }
218
- function _temp3() {
116
+ function _temp2() {
219
117
  return previewStore.send({ type: "openCreatePageModal" });
220
118
  }
221
- function _temp2(_value, search_0, keywords) {
119
+ function _temp(_value, search_0, keywords) {
222
120
  return commandFilter(keywords?.join(" ") ?? "", search_0);
223
121
  }
224
122
 
@@ -1,7 +1,6 @@
1
1
  import { CanvasSidebarFrame } from "./CanvasSidebarFrame.js";
2
2
  import { CreatePageModal } from "../../preview/components/CreatePageModal.js";
3
3
  import { CanvasNavigator } from "./CanvasNavigator.js";
4
- import { CatalogBlockInspector } from "./CatalogBlockInspector.js";
5
4
  import { PageInspector } from "./PageInspector.js";
6
5
  import { c } from "react/compiler-runtime";
7
6
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -9,50 +8,45 @@ import { jsx, jsxs } from "react/jsx-runtime";
9
8
  //#region src/features/canvas/sidebar/CanvasSidebar.tsx
10
9
  /**
11
10
  * The sidebar beside the Canvas. Without a selection, it shows the Canvas Navigator;
12
- * with one, the Canvas Inspector of the selected Page or block of the Block Catalog.
11
+ * with one, the Canvas Inspector of the selected Page.
13
12
  */
14
13
  function CanvasSidebar(t0) {
15
- const $ = c(12);
16
- const { page, catalog, navigator, onShowWholeCanvas, onRenamePage, owner: t1, pageRecord } = t0;
14
+ const $ = c(11);
15
+ const { page, navigator, onShowWholeCanvas, onRenamePage, owner: t1, pageRecord } = t0;
17
16
  const owner = t1 === void 0 ? null : t1;
18
- const t2 = catalog?.block._internal.title ?? page?.title ?? "Canvas navigator";
17
+ const t2 = page?.title ?? "Canvas navigator";
19
18
  let t3;
20
- if ($[0] !== catalog || $[1] !== navigator || $[2] !== onRenamePage || $[3] !== onShowWholeCanvas || $[4] !== owner || $[5] !== page || $[6] !== pageRecord) {
21
- t3 = catalog ? /* @__PURE__ */ jsx(CatalogBlockInspector, {
22
- block: catalog.block,
23
- projectId: catalog.projectId,
24
- onBack: onShowWholeCanvas
25
- }) : page ? /* @__PURE__ */ jsx(PageInspector, {
19
+ if ($[0] !== navigator || $[1] !== onRenamePage || $[2] !== onShowWholeCanvas || $[3] !== owner || $[4] !== page || $[5] !== pageRecord) {
20
+ t3 = page ? /* @__PURE__ */ jsx(PageInspector, {
26
21
  page,
27
22
  owner,
28
23
  pageRecord,
29
24
  onBack: onShowWholeCanvas,
30
25
  onRenamePage
31
26
  }) : /* @__PURE__ */ jsx(CanvasNavigator, { ...navigator });
32
- $[0] = catalog;
33
- $[1] = navigator;
34
- $[2] = onRenamePage;
35
- $[3] = onShowWholeCanvas;
36
- $[4] = owner;
37
- $[5] = page;
38
- $[6] = pageRecord;
39
- $[7] = t3;
40
- } else t3 = $[7];
27
+ $[0] = navigator;
28
+ $[1] = onRenamePage;
29
+ $[2] = onShowWholeCanvas;
30
+ $[3] = owner;
31
+ $[4] = page;
32
+ $[5] = pageRecord;
33
+ $[6] = t3;
34
+ } else t3 = $[6];
41
35
  let t4;
42
- if ($[8] === Symbol.for("react.memo_cache_sentinel")) {
36
+ if ($[7] === Symbol.for("react.memo_cache_sentinel")) {
43
37
  t4 = /* @__PURE__ */ jsx(CreatePageModal, {});
44
- $[8] = t4;
45
- } else t4 = $[8];
38
+ $[7] = t4;
39
+ } else t4 = $[7];
46
40
  let t5;
47
- if ($[9] !== t2 || $[10] !== t3) {
41
+ if ($[8] !== t2 || $[9] !== t3) {
48
42
  t5 = /* @__PURE__ */ jsxs(CanvasSidebarFrame, {
49
43
  label: t2,
50
44
  children: [t3, t4]
51
45
  });
52
- $[9] = t2;
53
- $[10] = t3;
54
- $[11] = t5;
55
- } else t5 = $[11];
46
+ $[8] = t2;
47
+ $[9] = t3;
48
+ $[10] = t5;
49
+ } else t5 = $[10];
56
50
  return t5;
57
51
  }
58
52
 
@@ -1,7 +1,7 @@
1
1
  import { useProjectUuid } from "../../../lib/auth.js";
2
2
  import { reads, useRead } from "../../../lib/studio-reads.js";
3
3
  import { useLocalePath } from "../../../lib/view-locale.js";
4
- import { AddBlockDialog } from "../../preview/components/AddBlockDialog.js";
4
+ import { AddBlockSheet } from "../../preview/components/AddBlockSheet.js";
5
5
  import { PreviewEditingOwnerContext } from "../../preview/previewSelection.js";
6
6
  import { PageInfo } from "../../preview/components/PageInfo.js";
7
7
  import { PageTree } from "../../preview/components/PageTree.js";
@@ -51,7 +51,7 @@ function CuratedPageInspector() {
51
51
  const t3 = owner?.kind === "page" && owner.pageId === pageId;
52
52
  let t4;
53
53
  if ($[6] !== t3) {
54
- t4 = /* @__PURE__ */ jsx(AddBlockDialog, { focusCreatedBlock: t3 });
54
+ t4 = /* @__PURE__ */ jsx(AddBlockSheet, { focusCreatedBlock: t3 });
55
55
  $[6] = t3;
56
56
  $[7] = t4;
57
57
  } else t4 = $[7];
@@ -2,7 +2,7 @@ import { canvasStore } from "./canvasStore.js";
2
2
  import { ViewportContinuityContext, captureViewportAnchor, resolveViewportAnchor } from "../preview/viewportContinuity.js";
3
3
  import { canvasPageIframe } from "../preview/previewPageFrame.js";
4
4
  import { canvasBlockOverlap } from "./canvasFrameDocument.js";
5
- import { CANVAS_WHEEL_ZOOM_RATE, PAN_SNAP_CAPTURE, PAN_SNAP_RELEASE, ZOOM_STRETCH, canvasMaximumScale, canvasMinimumScale, canvasOverview, canvasVerticalRail, canvasWheelDelta, constrainCanvasCamera, findCanvasSnapTarget, fitCanvasPage, isCanvasCameraSnapped, isCanvasPageScale, placeCanvasPageAnchor, snapCanvasZoom, stretchCanvasZoom, unstretchCanvasZoom, zoomCanvasAt } from "./canvasCamera.js";
5
+ 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";
6
6
  import { revealScrollDelta } from "../preview/selectionReveal.js";
7
7
  import { createCanvasFlight, createCanvasPan } from "./canvasFlight.js";
8
8
  import * as React from "react";
@@ -71,13 +71,6 @@ function useCanvasCamera(workspaceKey, initialTarget, row) {
71
71
  };
72
72
  }, []);
73
73
  /**
74
- * Pan immediately so a world x sits at or right of a screen x; never pans
75
- * otherwise, nor while a selection holds the camera.
76
- */
77
- const keepInView = React.useCallback((worldX, screenX) => {
78
- flightControls.current?.keepInView(worldX, screenX);
79
- }, []);
80
- /**
81
74
  * Pan vertically to an element inside a Canvas frame, such as a selected block,
82
75
  * unless enough of it already shows. A page flight or a drag in progress keeps the camera.
83
76
  */
@@ -240,16 +233,17 @@ function useCanvasCamera(workspaceKey, initialTarget, row) {
240
233
  };
241
234
  const candidate = findCanvasSnapTarget(intended, viewportSize, targets());
242
235
  const fit = candidate && fitCanvasPage(viewportSize, candidate);
243
- if (!candidate || !fit || !isCanvasPageScale(base.scale, fit.scale)) return intended;
244
- const held = Math.abs(base.x - fit.x) < 1e-8;
245
- const band = viewportSize.width * (held ? PAN_SNAP_RELEASE : PAN_SNAP_CAPTURE);
246
- if (Math.abs(intended.x - fit.x) > band) return intended;
236
+ if (!candidate || !fit || fit.scale < minimumScale) return intended;
237
+ const held = isCanvasCameraSnapped(base, viewportSize, candidate);
238
+ if (!isInCanvasSnapWindow(intended, viewportSize, candidate, held)) return intended;
247
239
  if (selection !== candidate.key) rowRef.current?.onSnap?.();
248
240
  select(candidate.key);
249
- return {
250
- ...intended,
251
- x: fit.x
252
- };
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
+ });
253
247
  };
254
248
  const endZoom = () => {
255
249
  ownerWindow.clearTimeout(zoomEnd);
@@ -271,6 +265,10 @@ function useCanvasCamera(workspaceKey, initialTarget, row) {
271
265
  else delete element.dataset.canvasPan;
272
266
  };
273
267
  const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)");
268
+ const isOverview = (view) => {
269
+ const overview = canvasOverview(viewportSize, contentSize);
270
+ return Math.abs(view.scale - overview.scale) < 1e-8 && Math.abs(view.x - overview.x) < .5 && Math.abs(view.y - overview.y) < .5;
271
+ };
274
272
  const remember = () => {
275
273
  if (restoredCamera || fittingInitialPage || !viewportSize.width || !viewportSize.height) return;
276
274
  canvasStore.send({
@@ -278,7 +276,7 @@ function useCanvasCamera(workspaceKey, initialTarget, row) {
278
276
  workspaceKey,
279
277
  view: {
280
278
  camera,
281
- fitted: Math.abs(camera.scale - minimumScale) < 1e-8
279
+ fitted: isOverview(camera)
282
280
  }
283
281
  });
284
282
  };
@@ -433,18 +431,6 @@ function useCanvasCamera(workspaceKey, initialTarget, row) {
433
431
  };
434
432
  flightControls.current = {
435
433
  flyTo: startFlight,
436
- keepInView: (worldX, screenX) => {
437
- const x = screenX - worldX * camera.scale;
438
- if (selection || camera.x >= x) return;
439
- cancelCameraFlight();
440
- restoredCamera = void 0;
441
- fittingInitialPage = false;
442
- gesture = void 0;
443
- applyCamera({
444
- ...camera,
445
- x
446
- }, true);
447
- },
448
434
  reveal: (element, immediate) => {
449
435
  const frame = element.ownerDocument.defaultView?.frameElement;
450
436
  if (!frame || flight && !flight.pan || pointers.size) return;
@@ -535,7 +521,7 @@ function useCanvasCamera(workspaceKey, initialTarget, row) {
535
521
  };
536
522
  const nextContent = measureContent();
537
523
  if (nextViewport.width === viewportSize.width && nextViewport.height === viewportSize.height && nextContent.width === contentSize.width && nextContent.height === contentSize.height) return;
538
- const wasFitted = Math.abs(target.scale - minimumScale) < 1e-8;
524
+ const wasFitted = isOverview(target);
539
525
  gesture = void 0;
540
526
  const held = nextViewport.width !== viewportSize.width && !flight && !fittingInitialPage ? targetOf(selection) : void 0;
541
527
  updateGeometry(nextViewport, nextContent);
@@ -793,7 +779,6 @@ function useCanvasCamera(workspaceKey, initialTarget, row) {
793
779
  contentRef,
794
780
  flyTo,
795
781
  mirrorCamera,
796
- keepInView,
797
782
  reveal,
798
783
  fitAll
799
784
  };
@@ -20,7 +20,7 @@ function toError(error) {
20
20
  * Unless live, the frame's document is paused. A resting frame keeps its size:
21
21
  * its content is hidden, and must not shrink the frame and move the Canvas.
22
22
  */
23
- function useCanvasFrame({ previewDocument, href, width, viewportHeight, fit, subject, mode }) {
23
+ function useCanvasFrame({ previewDocument, href, width, viewportHeight, mode }) {
24
24
  const iframeRef = React.useRef(null);
25
25
  const [srcDoc, setSrcDoc] = React.useState();
26
26
  const [mount, setMount] = React.useState(null);
@@ -44,7 +44,7 @@ function useCanvasFrame({ previewDocument, href, width, viewportHeight, fit, sub
44
44
  React.useLayoutEffect(() => {
45
45
  const iframe = iframeRef.current;
46
46
  if (!iframe || !mount) return;
47
- const stop = observeCanvasDocument(iframe, mount, viewportHeight, fit, () => restingRef.current);
47
+ const stop = observeCanvasDocument(iframe, mount, viewportHeight, () => restingRef.current);
48
48
  sizing.current = stop;
49
49
  return () => {
50
50
  stop();
@@ -53,8 +53,7 @@ function useCanvasFrame({ previewDocument, href, width, viewportHeight, fit, sub
53
53
  }, [
54
54
  mount,
55
55
  viewportHeight,
56
- width,
57
- fit
56
+ width
58
57
  ]);
59
58
  React.useEffect(() => {
60
59
  if (!mount || live) return;
@@ -73,7 +72,7 @@ function useCanvasFrame({ previewDocument, href, width, viewportHeight, fit, sub
73
72
  try {
74
73
  const loaded = iframeRef.current;
75
74
  const root = (loaded?.contentDocument)?.querySelector("[data-camox-preview-root]");
76
- if (!root) throw new Error(`The ${subject} preview document has no content root.`);
75
+ if (!root) throw new Error("The page preview document has no content root.");
77
76
  if (root === mount) return;
78
77
  root.replaceChildren();
79
78
  setMount(root);
@@ -82,7 +81,7 @@ function useCanvasFrame({ previewDocument, href, width, viewportHeight, fit, sub
82
81
  setFailure(toError(error));
83
82
  }
84
83
  },
85
- onError: () => setFailure(/* @__PURE__ */ new Error(`The ${subject} preview document could not load.`))
84
+ onError: () => setFailure(/* @__PURE__ */ new Error("The page preview document could not load."))
86
85
  }
87
86
  };
88
87
  }