camox 0.63.0 → 0.65.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 (41) hide show
  1. package/dist/core/createApp.d.ts +28 -28
  2. package/dist/core/createLayout.d.ts +1 -1
  3. package/dist/core/derivedRoutes.js +3 -30
  4. package/dist/core/editing/createEditableBlock.d.ts +604 -604
  5. package/dist/features/canvas/canvasCamera.js +11 -2
  6. package/dist/features/canvas/useCanvasCamera.js +14 -10
  7. package/dist/features/page/CuratedPageContent.js +2 -1
  8. package/dist/features/page/UnplacedBlockContent.js +52 -0
  9. package/dist/features/preview/components/PreviewPanel.js +40 -12
  10. package/dist/features/preview/components/SidebarStack.js +78 -9
  11. package/dist/features/preview/strayCaretGuard.js +34 -0
  12. package/dist/features/provider/CoreCamoxProvider.js +5 -3
  13. package/dist/features/routes/pageRuntime.d.ts +2 -0
  14. package/dist/features/routes/pageRuntime.js +8 -5
  15. package/dist/features/runtime/pageApp.js +111 -74
  16. package/dist/features/runtime/pageClient.js +22 -2
  17. package/dist/features/runtime/runtime.d.ts +10 -0
  18. package/dist/features/runtime/runtime.js +314 -4
  19. package/dist/features/runtime/runtimeRoutes.d.ts +5 -0
  20. package/dist/features/runtime/runtimeRoutes.js +5 -0
  21. package/dist/features/studio/components/ChatWidget.js +88 -62
  22. package/dist/features/studio/components/ConnectAgent.js +313 -0
  23. package/dist/features/studio/components/Patrol.js +455 -205
  24. package/dist/features/studio/components/ReviewDraftSheet.js +431 -334
  25. package/dist/features/studio/components/Squareling.js +74 -41
  26. package/dist/features/studio/components/ThreadComposer.js +130 -107
  27. package/dist/features/studio/components/ThreadMessages.js +82 -13
  28. package/dist/features/studio/components/clientLogoPaths.js +10 -0
  29. package/dist/features/studio/findingSentence.js +37 -0
  30. package/dist/features/studio/reportSentence.js +15 -0
  31. package/dist/features/studio/studioStore.js +23 -1
  32. package/dist/features/studio/workingSentence.js +8 -0
  33. package/dist/lib/api-client.d.ts +1 -0
  34. package/dist/lib/auth.js +34 -32
  35. package/dist/lib/queries.js +9 -1
  36. package/dist/lib/server-auth-cookie.js +17 -4
  37. package/dist/lib/studio-reads.d.ts +1 -1
  38. package/dist/lib/studio-reads.js +5 -1
  39. package/dist/package.js +1 -1
  40. package/dist/studio.css +1 -1
  41. package/package.json +15 -4
@@ -23,6 +23,11 @@ const MAX_CANVAS_ZOOM = 2;
23
23
  * camera, so painting and panning slow down with the size of the site.
24
24
  */
25
25
  const MIN_CANVAS_ZOOM = .1;
26
+ /**
27
+ * The overview is at most this fraction of the page fit's zoom, so it always
28
+ * shows a Page as an item on the Canvas, even when the site is one short Page.
29
+ */
30
+ const CANVAS_OVERVIEW_PAGE_ZOOM = .6;
26
31
  /** Log-scale zoom per pixel of Ctrl/⌘ + wheel, including trackpad pinches. */
27
32
  const CANVAS_WHEEL_ZOOM_RATE = .0139392;
28
33
  /** Relative (log-scale) bands make the detent feel the same at every page width. */
@@ -178,7 +183,11 @@ function zoomCanvasAt(camera, point, scale, minimumScale = Number.EPSILON, maxim
178
183
  }
179
184
  function fitCanvas(viewport, content) {
180
185
  const padding = canvasInsets(viewport);
181
- const scale = Math.max(Number.EPSILON, Math.min(1, (viewport.width - padding.x * 2) / Math.max(1, content.width), (viewport.height - padding.top - padding.bottom) / Math.max(1, content.height)));
186
+ const pageZoom = content.pageWidth === void 0 ? Infinity : CANVAS_OVERVIEW_PAGE_ZOOM * fitCanvasPage(viewport, {
187
+ left: 0,
188
+ width: content.pageWidth
189
+ }).scale;
190
+ const scale = Math.max(Number.EPSILON, Math.min(1, pageZoom, (viewport.width - padding.x * 2) / Math.max(1, content.width), (viewport.height - padding.top - padding.bottom) / Math.max(1, content.height)));
182
191
  return {
183
192
  x: (viewport.width - content.width * scale) / 2,
184
193
  y: (viewport.height + padding.top - padding.bottom - content.height * scale) / 2,
@@ -241,4 +250,4 @@ function canvasWheelDelta(delta, mode, viewportSize) {
241
250
  }
242
251
 
243
252
  //#endregion
244
- export { CANVAS_OVERSCROLL, CANVAS_WHEEL_ZOOM_RATE, MIN_CANVAS_ZOOM, PAN_SNAP_CAPTURE, PAN_SNAP_RELEASE, ZOOM_SNAP_CAPTURE, ZOOM_SNAP_RELEASE, ZOOM_STRETCH, canvasInsets, canvasMaximumScale, canvasMinimumScale, canvasOverview, canvasTargetOffset, canvasVerticalRail, canvasWheelDelta, constrainCanvasCamera, findCanvasSnapTarget, fitCanvas, fitCanvasPage, isCanvasCameraSnapped, isCanvasPageScale, isInCanvasSnapWindow, placeCanvasPageAnchor, snapCanvasZoom, stretchCanvasZoom, unstretchCanvasZoom, zoomCanvasAt };
253
+ export { CANVAS_OVERSCROLL, CANVAS_OVERVIEW_PAGE_ZOOM, CANVAS_WHEEL_ZOOM_RATE, MIN_CANVAS_ZOOM, PAN_SNAP_CAPTURE, PAN_SNAP_RELEASE, ZOOM_SNAP_CAPTURE, ZOOM_SNAP_RELEASE, ZOOM_STRETCH, canvasInsets, canvasMaximumScale, canvasMinimumScale, canvasOverview, canvasTargetOffset, canvasVerticalRail, canvasWheelDelta, constrainCanvasCamera, findCanvasSnapTarget, fitCanvas, fitCanvasPage, isCanvasCameraSnapped, isCanvasPageScale, isInCanvasSnapWindow, placeCanvasPageAnchor, snapCanvasZoom, stretchCanvasZoom, unstretchCanvasZoom, zoomCanvasAt };
@@ -117,10 +117,19 @@ function useCanvasCamera(workspaceKey, initialTarget, row) {
117
117
  };
118
118
  };
119
119
  let viewportSize = measureViewport();
120
- const measureContent = () => ({
121
- width: rowRef.current?.width ?? content.offsetWidth,
122
- height: content.offsetHeight
123
- });
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
+ };
124
133
  let contentSize = measureContent();
125
134
  let minimumScale = canvasMinimumScale(viewportSize, contentSize);
126
135
  const updateGeometry = (nextViewport = measureViewport(), nextContent = measureContent()) => {
@@ -128,11 +137,6 @@ function useCanvasCamera(workspaceKey, initialTarget, row) {
128
137
  contentSize = nextContent;
129
138
  minimumScale = canvasMinimumScale(viewportSize, contentSize);
130
139
  };
131
- const page = initialTargetRef.current;
132
- const targets = () => rowRef.current?.targets ?? (page ? [{
133
- ...page,
134
- key: "initial"
135
- }] : []);
136
140
  const targetOf = (key) => key == null ? void 0 : targets().find((candidate) => candidate.key === key);
137
141
  let stretch;
138
142
  let zoomEnd = 0;
@@ -511,7 +515,7 @@ function useCanvasCamera(workspaceKey, initialTarget, row) {
511
515
  const previousInsets = insets;
512
516
  const nextViewport = measureViewport();
513
517
  const nextContent = measureContent();
514
- 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) return;
518
+ 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;
515
519
  const wasFitted = isOverview(target);
516
520
  gesture = void 0;
517
521
  const held = nextViewport.width !== viewportSize.width && !flight && !fittingInitialPage ? targetOf(selection) : void 0;
@@ -12,6 +12,7 @@ import * as React from "react";
12
12
  import { jsx } from "react/jsx-runtime";
13
13
 
14
14
  //#region src/features/page/CuratedPageContent.tsx
15
+ /** A block read with its bundle, rendered by its definition's code as a page shows it. */
15
16
  function PageBlock(t0) {
16
17
  const $ = c(19);
17
18
  const { blockId, mode } = t0;
@@ -177,4 +178,4 @@ function _temp(block) {
177
178
  }
178
179
 
179
180
  //#endregion
180
- export { CuratedPageContent };
181
+ export { CuratedPageContent, PageBlock };
@@ -0,0 +1,52 @@
1
+ import { reads, useRead } from "../../lib/studio-reads.js";
2
+ import { BlockErrorBoundary } from "../preview/components/BlockErrorBoundary.js";
3
+ import { PageBlock } from "./CuratedPageContent.js";
4
+ import { c } from "react/compiler-runtime";
5
+ import { jsx } from "react/jsx-runtime";
6
+
7
+ //#region src/features/page/UnplacedBlockContent.tsx
8
+ /**
9
+ * A block placed nowhere, alone, as a page would show it: the look document's Block Definition
10
+ * mode (ADR 0032). Its bundle is read like a page's block, so it renders and hydrates as one.
11
+ * Thumbnails render a Block Definition their own way (`BlockPeek`); only the block render is
12
+ * shared.
13
+ */
14
+ function UnplacedBlockContent(t0) {
15
+ const $ = c(8);
16
+ const { blockId } = t0;
17
+ let t1;
18
+ if ($[0] !== blockId) {
19
+ t1 = reads.blockBundle(blockId);
20
+ $[0] = blockId;
21
+ $[1] = t1;
22
+ } else t1 = $[1];
23
+ const { block } = useRead(t1);
24
+ let t2;
25
+ if ($[2] !== blockId) {
26
+ t2 = /* @__PURE__ */ jsx(PageBlock, {
27
+ blockId,
28
+ mode: "site"
29
+ });
30
+ $[2] = blockId;
31
+ $[3] = t2;
32
+ } else t2 = $[3];
33
+ let t3;
34
+ if ($[4] !== block.type || $[5] !== blockId || $[6] !== t2) {
35
+ t3 = /* @__PURE__ */ jsx("main", {
36
+ className: "flex flex-col",
37
+ children: /* @__PURE__ */ jsx(BlockErrorBoundary, {
38
+ blockId,
39
+ blockType: block.type,
40
+ children: t2
41
+ })
42
+ });
43
+ $[4] = block.type;
44
+ $[5] = blockId;
45
+ $[6] = t2;
46
+ $[7] = t3;
47
+ } else t3 = $[7];
48
+ return t3;
49
+ }
50
+
51
+ //#endregion
52
+ export { UnplacedBlockContent };
@@ -11,6 +11,7 @@ import { previewPageIframeProps, usePreviewPageFrame } from "../previewPageFrame
11
11
  import { adoptCanvasDocument, pauseCanvasDocument } from "../../canvas/canvasFrameDocument.js";
12
12
  import { forwardCanvasWheel, useCanvasFrameInput } from "../../canvas/useCanvasFrameInput.js";
13
13
  import { useActionShortcuts } from "../../provider/useAdminShortcuts.js";
14
+ import { guardStrayCaret } from "../strayCaretGuard.js";
14
15
  import { useBlockActionsShortcuts } from "./BlockActionsPopover.js";
15
16
  import { FieldOverlayStyles } from "./FieldOverlayStyles.js";
16
17
  import { MobilePreviewDrawer } from "./MobilePreviewDrawer.js";
@@ -60,6 +61,7 @@ const PreviewFrameEffects = () => {
60
61
  t0 = /* @__PURE__ */ jsxs(Fragment, { children: [
61
62
  /* @__PURE__ */ jsx(FieldOverlayStyles, {}),
62
63
  /* @__PURE__ */ jsx(KeyDownForwarder, {}),
64
+ /* @__PURE__ */ jsx(StrayCaretGuard, {}),
63
65
  /* @__PURE__ */ jsx(OverlayTracker, {})
64
66
  ] });
65
67
  $[0] = t0;
@@ -72,6 +74,27 @@ const KeyDownForwarder = () => {
72
74
  useActionShortcuts(inIframe ? iframeWindow : null);
73
75
  return null;
74
76
  };
77
+ const StrayCaretGuard = () => {
78
+ const $ = c(3);
79
+ const { window: iframeWindow } = useFrame();
80
+ let t0;
81
+ let t1;
82
+ if ($[0] !== iframeWindow) {
83
+ t0 = () => {
84
+ if (!iframeWindow) return;
85
+ return guardStrayCaret(iframeWindow.document);
86
+ };
87
+ t1 = [iframeWindow];
88
+ $[0] = iframeWindow;
89
+ $[1] = t0;
90
+ $[2] = t1;
91
+ } else {
92
+ t0 = $[1];
93
+ t1 = $[2];
94
+ }
95
+ React.useEffect(t0, t1);
96
+ return null;
97
+ };
75
98
  /** The preview's read-only tree is never editable, whether or not the Canvas shows. */
76
99
  const READ_ONLY_ARTIFACT_STUDIO = {
77
100
  editing: false,
@@ -91,7 +114,7 @@ function CuratedBlockShortcuts() {
91
114
  * when used, pauses with the camera, and draws its editing overlays.
92
115
  */
93
116
  function PreviewPageCanvasEffects(t0) {
94
- const $ = c(30);
117
+ const $ = c(31);
95
118
  const { owner } = t0;
96
119
  const { window: frameWindow, iframeElement } = useFrame();
97
120
  const { frame, slot, inCanvas } = usePreviewPageFrame();
@@ -225,33 +248,38 @@ function PreviewPageCanvasEffects(t0) {
225
248
  if (!placed) return null;
226
249
  let t11;
227
250
  let t12;
251
+ let t13;
228
252
  if ($[24] === Symbol.for("react.memo_cache_sentinel")) {
229
253
  t11 = /* @__PURE__ */ jsx(FieldOverlayStyles, {});
230
- t12 = /* @__PURE__ */ jsx(OverlayTracker, {});
254
+ t12 = /* @__PURE__ */ jsx(StrayCaretGuard, {});
255
+ t13 = /* @__PURE__ */ jsx(OverlayTracker, {});
231
256
  $[24] = t11;
232
257
  $[25] = t12;
258
+ $[26] = t13;
233
259
  } else {
234
260
  t11 = $[24];
235
261
  t12 = $[25];
262
+ t13 = $[26];
236
263
  }
237
- const t13 = owner?.kind === "page";
238
- let t14;
239
- if ($[26] !== iframeElement || $[27] !== owner || $[28] !== t13) {
240
- t14 = /* @__PURE__ */ jsxs(Fragment, { children: [
264
+ const t14 = owner?.kind === "page";
265
+ let t15;
266
+ if ($[27] !== iframeElement || $[28] !== owner || $[29] !== t14) {
267
+ t15 = /* @__PURE__ */ jsxs(Fragment, { children: [
241
268
  t11,
242
269
  t12,
270
+ t13,
243
271
  /* @__PURE__ */ jsx(Overlays, {
244
272
  frame: iframeElement,
245
273
  owner,
246
- canAddBlocks: t13
274
+ canAddBlocks: t14
247
275
  })
248
276
  ] });
249
- $[26] = iframeElement;
250
- $[27] = owner;
251
- $[28] = t13;
277
+ $[27] = iframeElement;
278
+ $[28] = owner;
252
279
  $[29] = t14;
253
- } else t14 = $[29];
254
- return t14;
280
+ $[30] = t15;
281
+ } else t15 = $[30];
282
+ return t15;
255
283
  }
256
284
  function _temp(event) {
257
285
  if (event.target?.closest?.("a")) event.preventDefault();
@@ -143,6 +143,8 @@ function SidebarStack(t0) {
143
143
  if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
144
144
  t1 = {
145
145
  ready: false,
146
+ boxes: /* @__PURE__ */ new Map(),
147
+ observer: null,
146
148
  front: null,
147
149
  behind: null,
148
150
  inFront: null
@@ -156,8 +158,16 @@ function SidebarStack(t0) {
156
158
  t2 = () => {
157
159
  const current = motion.current;
158
160
  current.ready = true;
161
+ if (typeof ResizeObserver !== "undefined") {
162
+ const observer = new ResizeObserver(() => measureCards(current));
163
+ current.observer = observer;
164
+ for (const card of current.boxes.keys()) observer.observe(card);
165
+ if (current.inFront?.parentElement) observer.observe(current.inFront.parentElement);
166
+ }
159
167
  return () => {
160
168
  current.ready = false;
169
+ current.observer?.disconnect();
170
+ current.observer = null;
161
171
  };
162
172
  };
163
173
  t3 = [];
@@ -233,20 +243,57 @@ function SidebarStack(t0) {
233
243
  } else t9 = $[12];
234
244
  return t9;
235
245
  }
236
- /** A still copy of a card, where it was, so it can move away once the card is gone. */
237
- function copyCard(card, layer) {
238
- const layerRect = layer.getBoundingClientRect();
246
+ /**
247
+ * Where a card is laid out relative to `stack`, whose box its layers fill. Offsets leave out the
248
+ * transforms that move cards as they come.
249
+ */
250
+ function layoutBox(card, stack) {
251
+ let left = 0;
252
+ let top = 0;
253
+ let node = card;
254
+ while (node && node !== stack) {
255
+ left += node.offsetLeft;
256
+ top += node.offsetTop;
257
+ const parent = node.offsetParent;
258
+ if (parent && parent !== stack) {
259
+ left += parent.clientLeft;
260
+ top += parent.clientTop;
261
+ }
262
+ node = parent;
263
+ }
264
+ if (node === stack) return {
265
+ left,
266
+ top,
267
+ width: card.offsetWidth,
268
+ height: card.offsetHeight
269
+ };
270
+ const stackRect = stack.getBoundingClientRect();
239
271
  const rect = card.getBoundingClientRect();
272
+ return {
273
+ left: rect.left - stackRect.left,
274
+ top: rect.top - stackRect.top,
275
+ width: rect.width,
276
+ height: rect.height
277
+ };
278
+ }
279
+ /** Notes where each card of the stack is laid out now. */
280
+ function measureCards(motion) {
281
+ const stack = motion.inFront?.parentElement;
282
+ if (!stack) return;
283
+ for (const card of motion.boxes.keys()) if (card.isConnected) motion.boxes.set(card, layoutBox(card, stack));
284
+ }
285
+ /** A still copy of a card, at `box`, so it can move away once the card is gone. */
286
+ function copyCard(card, box) {
240
287
  const copy = card.cloneNode(true);
241
288
  copy.setAttribute("aria-hidden", "true");
242
289
  copy.setAttribute("inert", "");
243
290
  Object.assign(copy.style, {
244
291
  position: "absolute",
245
292
  margin: "0",
246
- left: `${rect.left - layerRect.left}px`,
247
- top: `${rect.top - layerRect.top}px`,
248
- width: `${rect.width}px`,
249
- height: `${rect.height}px`
293
+ left: `${box.left}px`,
294
+ top: `${box.top}px`,
295
+ width: `${box.width}px`,
296
+ height: `${box.height}px`
250
297
  });
251
298
  const scrolled = [];
252
299
  const walk = (node, path) => {
@@ -276,6 +323,13 @@ function useStackCard(ref, depth, isView) {
276
323
  const card = ref.current;
277
324
  const motion = stack?.current;
278
325
  if (!card || !motion || depth == null) return;
326
+ if (!motion.boxes.has(card)) motion.boxes.set(card, {
327
+ left: 0,
328
+ top: 0,
329
+ width: 0,
330
+ height: 0
331
+ });
332
+ motion.observer?.observe(card);
279
333
  const previousFront = motion.front;
280
334
  if (isView) motion.front = depth;
281
335
  if (motion.ready && previousFront != null && prefersMotion()) {
@@ -289,10 +343,21 @@ function useStackCard(ref, depth, isView) {
289
343
  }
290
344
  return () => {
291
345
  const { behind, inFront } = motion;
292
- if (!motion.ready || !behind || !inFront || !prefersMotion()) return;
293
- const { copy, restoreScroll } = copyCard(card, inFront);
346
+ const box = motion.boxes.get(card);
347
+ const forget = () => {
348
+ motion.boxes.delete(card);
349
+ motion.observer?.unobserve(card);
350
+ };
351
+ if (!motion.ready || !behind || !inFront || !box || !prefersMotion()) {
352
+ queueMicrotask(() => {
353
+ if (!card.isConnected) forget();
354
+ });
355
+ return;
356
+ }
357
+ const { copy, restoreScroll } = copyCard(card, box);
294
358
  queueMicrotask(() => {
295
359
  if (card.isConnected) return;
360
+ forget();
296
361
  const how = motionFor(depth, motion.front ?? depth);
297
362
  (how === "front" ? inFront : behind).append(copy);
298
363
  restoreScroll();
@@ -303,6 +368,10 @@ function useStackCard(ref, depth, isView) {
303
368
  });
304
369
  };
305
370
  }, []);
371
+ React.useLayoutEffect(() => {
372
+ const motion = stack?.current;
373
+ if (motion) measureCards(motion);
374
+ });
306
375
  }
307
376
  /**
308
377
  * Eases a header's card and content from where they were laid out before `shape` changed to
@@ -0,0 +1,34 @@
1
+ //#region src/features/preview/strayCaretGuard.ts
2
+ /** Where the browser would put the caret for a click at this point. */
3
+ function caretNodeAt(doc, x, y) {
4
+ if (typeof doc.caretPositionFromPoint === "function") return doc.caretPositionFromPoint(x, y)?.offsetNode ?? null;
5
+ return doc.caretRangeFromPoint?.(x, y)?.startContainer ?? null;
6
+ }
7
+ function isEditable(node) {
8
+ return (node?.nodeType === 1 ? node : node?.parentElement ?? null)?.closest("[contenteditable=\"\"], [contenteditable=\"true\"]") != null;
9
+ }
10
+ /**
11
+ * Keep a click beside an editable field from landing in it. On a click over
12
+ * non-editable content, browsers put the caret at the nearest text, and when that
13
+ * text is in a field, they focus it: clicking a block's padding next to a heading
14
+ * starts editing the heading instead of selecting the block. Such a click now only
15
+ * leaves the field being edited, as a click on content without text would.
16
+ */
17
+ function guardStrayCaret(doc) {
18
+ const onMouseDown = (event) => {
19
+ if (event.button !== 0) return;
20
+ const target = event.target;
21
+ if (!target || isEditable(target)) return;
22
+ if (target.closest?.("input, textarea, select, button")) return;
23
+ if (!isEditable(caretNodeAt(doc, event.clientX, event.clientY))) return;
24
+ event.preventDefault();
25
+ const active = doc.activeElement;
26
+ if (active?.isContentEditable) active.blur();
27
+ doc.getSelection()?.removeAllRanges();
28
+ };
29
+ doc.addEventListener("mousedown", onMouseDown, true);
30
+ return () => doc.removeEventListener("mousedown", onMouseDown, true);
31
+ }
32
+
33
+ //#endregion
34
+ export { guardStrayCaret };
@@ -9,7 +9,7 @@ import { jsx } from "react/jsx-runtime";
9
9
  import { replaceEqualDeep, useQueryClient } from "@tanstack/react-query";
10
10
 
11
11
  //#region src/features/provider/CoreCamoxProvider.tsx
12
- function CoreCamoxProvider({ apiUrl, authenticationUrl, camoxApp, children, projectUuid, initialAuthenticated = false, initialProject }) {
12
+ function CoreCamoxProvider({ apiUrl, authenticationUrl, camoxApp, children, projectUuid, initialAuthenticated = false, initialProject, sessionless = false }) {
13
13
  const queryClient = useQueryClient();
14
14
  const authClient = React.useMemo(() => createCamoxAuthClient(apiUrl), [apiUrl]);
15
15
  const providedScope = React.useContext(DraftScopeContext);
@@ -48,14 +48,16 @@ function CoreCamoxProvider({ apiUrl, authenticationUrl, camoxApp, children, proj
48
48
  apiUrl,
49
49
  projectUuid,
50
50
  initialAuthenticated,
51
- exchangingToken
51
+ exchangingToken,
52
+ sessionless
52
53
  }), [
53
54
  authClient,
54
55
  authenticationUrl,
55
56
  apiUrl,
56
57
  projectUuid,
57
58
  initialAuthenticated,
58
- exchangingToken
59
+ exchangingToken,
60
+ sessionless
59
61
  ]);
60
62
  const [isPreviewHandoff, setIsPreviewHandoff] = React.useState(false);
61
63
  React.useEffect(() => {
@@ -46,6 +46,8 @@ export interface DraftRead {
46
46
  }
47
47
  export declare function createServerApiClient(apiUrl: string, options?: {
48
48
  authCookieHeader?: string;
49
+ /** A look token, which reads the draft it was signed for without a session (ADR 0032). */
50
+ lookToken?: string;
49
51
  draftId?: string;
50
52
  locale?: LocaleView;
51
53
  }): RouterClient<Router>;
@@ -6,7 +6,7 @@ import { PAGE_IMAGE_SIZE, pageImageUrl } from "../og/pageImage.js";
6
6
  import { curatedLayoutContext } from "../../core/layoutContext.js";
7
7
  import { isArticle } from "../../core/structuredData.js";
8
8
  import { publishingDraft } from "../runtime/publishingDraft.js";
9
- import { DRAFT_ENDED_ERROR_CODE, DRAFT_HEADER, PUBLISHING_DRAFT_HEADER } from "@camox/api-contract/drafts";
9
+ import { DRAFT_ENDED_ERROR_CODE, DRAFT_HEADER, LOOK_TOKEN_HEADER, PUBLISHING_DRAFT_HEADER } from "@camox/api-contract/drafts";
10
10
  import { createORPCClient } from "@orpc/client";
11
11
  import { RPCLink } from "@orpc/client/fetch";
12
12
 
@@ -38,7 +38,8 @@ function parseMarkdownPath(pathname) {
38
38
  }
39
39
  function createServerApiClient(apiUrl, options) {
40
40
  const headers = localeHeaders(options?.locale);
41
- if (options?.draftId && options.authCookieHeader) headers[DRAFT_HEADER] = options.draftId;
41
+ if (options?.lookToken) headers[LOOK_TOKEN_HEADER] = options.lookToken;
42
+ if (options?.draftId && (options.authCookieHeader || options.lookToken)) headers[DRAFT_HEADER] = options.draftId;
42
43
  else {
43
44
  const publishing = publishingDraft();
44
45
  if (publishing) headers[PUBLISHING_DRAFT_HEADER] = publishing;
@@ -115,13 +116,14 @@ async function readDraftOrLive(draftId, member, read) {
115
116
  function renderedHref(url, read) {
116
117
  return read.refusedDraft ? withoutDraft(url) : url;
117
118
  }
118
- async function loadCamoxPage({ apiUrl, authCookieHeader, draftId, locale, pathname, projectUuid, queryClient }) {
119
+ async function loadCamoxPage({ apiUrl, authCookieHeader, lookToken, draftId, locale, pathname, projectUuid, queryClient }) {
119
120
  const studioCache = createServerStudioCache(queryClient, {
120
121
  apiUrl,
121
122
  authCookieHeader,
123
+ lookToken,
122
124
  locale
123
125
  });
124
- const inDraft = { in: draftId && authCookieHeader ? draftId : null };
126
+ const inDraft = { in: draftId && (authCookieHeader || lookToken) ? draftId : null };
125
127
  const fallback = locale && linkFallbackLocale(locale);
126
128
  const [page] = await Promise.all([
127
129
  studioCache.ensure(reads.pageStructure({
@@ -140,12 +142,13 @@ async function loadCamoxPage({ apiUrl, authCookieHeader, draftId, locale, pathna
140
142
  * The studio's reads for one server request, read with its session. Each operation names the
141
143
  * draft it acts in, once the request settled which one it reads.
142
144
  */
143
- function createServerStudioCache(queryClient, { apiUrl, authCookieHeader, locale }) {
145
+ function createServerStudioCache(queryClient, { apiUrl, authCookieHeader, lookToken, locale }) {
144
146
  return createStudioCache({
145
147
  queryClient,
146
148
  scope: createDraftScope({ queryClient }),
147
149
  api: (draft, inLocale) => createServerApiClient(apiUrl, {
148
150
  authCookieHeader,
151
+ lookToken,
149
152
  draftId: draft ?? void 0,
150
153
  locale: inLocale
151
154
  }),