camox 0.68.0 → 0.71.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 (34) hide show
  1. package/dist/core/createBlock.js +14 -7
  2. package/dist/core/hooks/useArtifactHost.js +56 -46
  3. package/dist/core/lib/artifact.d.ts +4 -0
  4. package/dist/core/lib/artifactHost.js +40 -0
  5. package/dist/core/lib/artifactRuntime.js +20 -1
  6. package/dist/features/canvas/canvasFrameDocument.js +25 -2
  7. package/dist/features/canvas/useCanvasFrame.js +6 -2
  8. package/dist/features/preview/components/previewStyles.js +11 -2
  9. package/dist/features/provider/AuthenticatedCamoxProvider.js +33 -28
  10. package/dist/features/renderer/RendererApp.js +202 -154
  11. package/dist/features/renderer/RendererFrames.js +87 -55
  12. package/dist/features/renderer/definitionAnswers.js +1 -0
  13. package/dist/features/renderer/rendererHost.js +4 -0
  14. package/dist/features/renderer/studioConnection.js +9 -1
  15. package/dist/features/runtime/pageApp.js +7 -3
  16. package/dist/features/runtime/pageNavigation.js +93 -17
  17. package/dist/features/runtime/rendererClient.js +13 -6
  18. package/dist/features/studio/agentRunToast.js +71 -0
  19. package/dist/features/studio/components/ChatWidget.js +2 -0
  20. package/dist/features/studio/components/DraftBar.js +2 -3
  21. package/dist/features/studio/components/DraftsControl.js +181 -198
  22. package/dist/features/studio/components/LocalePicker.js +22 -4
  23. package/dist/features/studio/components/ReviewDraftSheet.js +345 -568
  24. package/dist/features/studio/draftLabel.js +1 -1
  25. package/dist/features/studio/studioStore.js +5 -17
  26. package/dist/features/studio/viewSwitch.js +62 -0
  27. package/dist/lib/auth.js +5 -1
  28. package/dist/lib/draft-scope.js +28 -15
  29. package/dist/lib/studio-reads.js +46 -13
  30. package/dist/lib/view-switch.js +64 -0
  31. package/dist/package.js +1 -1
  32. package/dist/studio.css +1 -1
  33. package/package.json +6 -4
  34. package/skills/camox/references/artifacts.md +85 -1
@@ -7,7 +7,7 @@ import { useProjectUuid } from "../lib/auth.js";
7
7
  import { isItemMarker, queryResultIds, resolveAssetValue, useNormalizedData } from "../lib/normalized-data.js";
8
8
  import { useLinkPages } from "../lib/link-pages.js";
9
9
  import { useBlockEditingRuntime } from "./editing/BlockEditingRuntime.js";
10
- import { ArtifactStudioContext, ArtifactsRunContext, useArtifactHost, useInitialArtifactData } from "./hooks/useArtifactHost.js";
10
+ import { ArtifactCaptureContext, ArtifactStudioContext, ArtifactsRunContext, useArtifactHost, useInitialArtifactData } from "./hooks/useArtifactHost.js";
11
11
  import { IconSvg } from "./lib/icons.js";
12
12
  import { renderImagePlaceholder } from "./lib/imagePlaceholder.js";
13
13
  import { buildImageSrcSet, getDefaultImageSizes, getDefaultImageWidth, transformImageUrl } from "./lib/imageTransform.js";
@@ -17,6 +17,7 @@ import { c } from "react/compiler-runtime";
17
17
  import * as React from "react";
18
18
  import { isQueryBacked } from "@camox/api-contract";
19
19
  import { Type } from "@sinclair/typebox";
20
+ import { ARTIFACT_FIELD_ATTRIBUTE, ARTIFACT_ITEM_ATTRIBUTE } from "@camox/api-contract/drafts";
20
21
  import { generateKeyBetween } from "fractional-indexing";
21
22
  import { jsx } from "react/jsx-runtime";
22
23
 
@@ -184,12 +185,18 @@ function createViewBlock(options) {
184
185
  });
185
186
  const value = useValue(name);
186
187
  const data = useArtifactData(name);
187
- const hostProps = useArtifactHost({
188
- studio: React.use(ArtifactStudioContext),
189
- data,
190
- size: useFieldSchema(name)?.size,
191
- navigate: useNavigate()
192
- });
188
+ const item = React.use(RepeaterContext);
189
+ const hostProps = {
190
+ ...useArtifactHost({
191
+ studio: React.use(ArtifactStudioContext),
192
+ data,
193
+ size: useFieldSchema(name)?.size,
194
+ navigate: useNavigate(),
195
+ capture: React.use(ArtifactCaptureContext)
196
+ }),
197
+ [ARTIFACT_FIELD_ATTRIBUTE]: name,
198
+ ...item?.itemId ? { [ARTIFACT_ITEM_ATTRIBUTE]: item.itemId } : {}
199
+ };
193
200
  const initialData = useInitialArtifactData(isArtifactSourceReference(value) ? value.artifactUrl : value, data);
194
201
  const fontLinks = useOptionalCamoxApp()?.getArtifactFontLinks();
195
202
  const runs = React.use(ArtifactsRunContext);
@@ -15,37 +15,43 @@ const ArtifactStudioContext = React.createContext(null);
15
15
  */
16
16
  const ArtifactsRunContext = React.createContext(true);
17
17
  /**
18
+ * Whether artifacts run in capture mode: in the look document (ADR 0032), where their Timeline's
19
+ * time only moves when the browser `look` drives seeks it.
20
+ */
21
+ const ArtifactCaptureContext = React.createContext(false);
22
+ /**
18
23
  * Connects an artifact's `<iframe>` to the `camox` runtime inside it: keeps sharing `studio` and
19
24
  * its Artifact Data as they change, sizes frames sized to their content, and routes the
20
25
  * artifact's navigation.
21
26
  */
22
27
  function useArtifactHost(t0) {
23
- const $ = c(18);
24
- const { studio, data: t1, size, navigate } = t0;
25
- let t2;
28
+ const $ = c(19);
29
+ const { studio, data: t1, size, navigate, capture: t2 } = t0;
30
+ let t3;
26
31
  if ($[0] !== t1) {
27
- t2 = t1 === void 0 ? {} : t1;
32
+ t3 = t1 === void 0 ? {} : t1;
28
33
  $[0] = t1;
29
- $[1] = t2;
30
- } else t2 = $[1];
31
- const data = t2;
34
+ $[1] = t3;
35
+ } else t3 = $[1];
36
+ const data = t3;
37
+ const capture = t2 === void 0 ? false : t2;
32
38
  const host = React.useRef(null);
33
- let t3;
39
+ let t4;
34
40
  if ($[2] !== data || $[3] !== studio) {
35
- t3 = {
41
+ t4 = {
36
42
  studio,
37
43
  data
38
44
  };
39
45
  $[2] = data;
40
46
  $[3] = studio;
41
- $[4] = t3;
42
- } else t3 = $[4];
43
- const latest = React.useRef(t3);
47
+ $[4] = t4;
48
+ } else t4 = $[4];
49
+ const latest = React.useRef(t4);
44
50
  const latestNavigate = React.useRef(navigate);
45
51
  const [height, setHeight] = React.useState(null);
46
- let t4;
52
+ let t5;
47
53
  if ($[5] !== data || $[6] !== navigate || $[7] !== studio) {
48
- t4 = () => {
54
+ t5 = () => {
49
55
  latest.current = {
50
56
  studio,
51
57
  data
@@ -59,55 +65,59 @@ function useArtifactHost(t0) {
59
65
  $[5] = data;
60
66
  $[6] = navigate;
61
67
  $[7] = studio;
62
- $[8] = t4;
63
- } else t4 = $[8];
64
- React.useEffect(t4);
65
- let t5;
66
- if ($[9] !== size) {
67
- t5 = (frame) => {
68
+ $[8] = t5;
69
+ } else t5 = $[8];
70
+ React.useEffect(t5);
71
+ let t6;
72
+ if ($[9] !== capture || $[10] !== size) {
73
+ t6 = (frame) => {
68
74
  if (!frame) return;
69
75
  const options = {
70
76
  size,
71
77
  onHeight: setHeight,
72
78
  navigate: (path) => void latestNavigate.current({ to: path })
73
79
  };
74
- const connected = connectArtifactHost(frame, latest.current, options);
80
+ const connected = connectArtifactHost(frame, {
81
+ ...latest.current,
82
+ capture
83
+ }, options);
75
84
  host.current = connected;
76
85
  return () => {
77
86
  connected.disconnect();
78
87
  if (host.current === connected) host.current = null;
79
88
  };
80
89
  };
81
- $[9] = size;
82
- $[10] = t5;
83
- } else t5 = $[10];
84
- const ref = t5;
90
+ $[9] = capture;
91
+ $[10] = size;
92
+ $[11] = t6;
93
+ } else t6 = $[11];
94
+ const ref = t6;
85
95
  if (size !== "content" || height === null) {
86
- let t6;
87
- if ($[11] !== ref) {
88
- t6 = { ref };
89
- $[11] = ref;
90
- $[12] = t6;
91
- } else t6 = $[12];
92
- return t6;
96
+ let t7;
97
+ if ($[12] !== ref) {
98
+ t7 = { ref };
99
+ $[12] = ref;
100
+ $[13] = t7;
101
+ } else t7 = $[13];
102
+ return t7;
93
103
  }
94
- let t6;
95
- if ($[13] !== height) {
96
- t6 = { height };
97
- $[13] = height;
98
- $[14] = t6;
99
- } else t6 = $[14];
100
104
  let t7;
101
- if ($[15] !== ref || $[16] !== t6) {
102
- t7 = {
105
+ if ($[14] !== height) {
106
+ t7 = { height };
107
+ $[14] = height;
108
+ $[15] = t7;
109
+ } else t7 = $[15];
110
+ let t8;
111
+ if ($[16] !== ref || $[17] !== t7) {
112
+ t8 = {
103
113
  ref,
104
- style: t6
114
+ style: t7
105
115
  };
106
- $[15] = ref;
107
- $[16] = t6;
116
+ $[16] = ref;
108
117
  $[17] = t7;
109
- } else t7 = $[17];
110
- return t7;
118
+ $[18] = t8;
119
+ } else t8 = $[18];
120
+ return t8;
111
121
  }
112
122
  /**
113
123
  * The Artifact Data to inline in an artifact's `srcDoc`: the data its source first rendered
@@ -136,4 +146,4 @@ function useInitialArtifactData(source, data) {
136
146
  }
137
147
 
138
148
  //#endregion
139
- export { ArtifactStudioContext, ArtifactsRunContext, useArtifactHost, useInitialArtifactData };
149
+ export { ArtifactCaptureContext, ArtifactStudioContext, ArtifactsRunContext, useArtifactHost, useInitialArtifactData };
@@ -26,6 +26,10 @@ export type ArtifactRenderProps = ArtifactFrameProps & {
26
26
  ref: React.RefCallback<HTMLIFrameElement>;
27
27
  /** For `size: "content"`, the height the artifact's content needs, once it reports it. */
28
28
  style?: React.CSSProperties;
29
+ /** The artifact field's name, so the look document's browser finds the frame. */
30
+ "data-camox-artifact"?: string;
31
+ /** The id of the Repeatable Item holding the artifact field, when an item's. */
32
+ "data-camox-item"?: string;
29
33
  };
30
34
  //#endregion
31
35
  export {};
@@ -1,5 +1,6 @@
1
1
  import { artifactThemeColors } from "./artifactColors.js";
2
2
  import { observeArtifactTheme, readArtifactTheme } from "./artifactTheme.js";
3
+ import { LOOK_ARTIFACT_CAPTURE } from "@camox/api-contract/drafts";
3
4
 
4
5
  //#region src/core/lib/artifactHost.ts
5
6
  function isRuntimeMessage(data) {
@@ -134,6 +135,34 @@ function connectArtifactHost(frame, initialState, options = {}) {
134
135
  channel
135
136
  }, "*");
136
137
  };
138
+ const answers = /* @__PURE__ */ new Map();
139
+ let lastRequest = 0;
140
+ let whenReady = [];
141
+ const ask = (type, payload = {}) => new Promise((resolve) => {
142
+ const id = ++lastRequest;
143
+ answers.set(id, resolve);
144
+ const send = () => post(type, {
145
+ ...payload,
146
+ id
147
+ });
148
+ if (ready) send();
149
+ else whenReady.push(send);
150
+ });
151
+ const capture = {
152
+ timeline: async () => (await ask("timeline")).timeline ?? null,
153
+ seek: async (t) => {
154
+ const { error } = await ask("seek", { t });
155
+ return typeof error === "string" ? { error } : {};
156
+ },
157
+ guardrail: async () => {
158
+ const { findings } = await ask("guardrail");
159
+ return Array.isArray(findings) ? findings : [];
160
+ }
161
+ };
162
+ if (initialState.capture) Object.defineProperty(frame, LOOK_ARTIFACT_CAPTURE, {
163
+ value: capture,
164
+ configurable: true
165
+ });
137
166
  const sendStudio = () => post("studio", { studio: state.studio });
138
167
  const sendData = () => post("data", { data: state.data ?? {} });
139
168
  let sentTheme = null;
@@ -163,10 +192,20 @@ function connectArtifactHost(frame, initialState, options = {}) {
163
192
  if (message.camox === "ready") {
164
193
  ready = true;
165
194
  observeViewport(false);
195
+ if (initialState.capture) post("capture");
166
196
  sendStudio();
167
197
  sendData();
168
198
  sendTheme(true);
169
199
  if (options.size === "content") post("measure");
200
+ for (const send of whenReady) send();
201
+ whenReady = [];
202
+ return;
203
+ }
204
+ if (message.camox === "timeline" || message.camox === "seeked" || message.camox === "guardrail") {
205
+ const answer = typeof message.id === "number" ? answers.get(message.id) : void 0;
206
+ if (!answer) return;
207
+ answers.delete(message.id);
208
+ answer(message);
170
209
  return;
171
210
  }
172
211
  if (message.camox === "height") {
@@ -197,6 +236,7 @@ function connectArtifactHost(frame, initialState, options = {}) {
197
236
  page.removeEventListener("message", onMessage);
198
237
  observeViewport(false);
199
238
  stopObservingTheme();
239
+ if (initialState.capture) Reflect.deleteProperty(frame, LOOK_ARTIFACT_CAPTURE);
200
240
  }
201
241
  };
202
242
  }
@@ -22,6 +22,25 @@
22
22
  * - host → frame: `measure`, for frames sized to their content. The runtime then reports the
23
23
  * document's `height` whenever it changes.
24
24
  * - runtime → page: `navigate` with an `href`. The host acts only with transient user activation.
25
+ * - host → frame: `capture`, from the look document's hosts only: the runtime enters capture mode,
26
+ * where real time never advances and the Timeline only moves when seeked. Only in capture mode
27
+ * does it answer `timeline` (→ `timeline` with the same `id` and the declared `timeline`, or
28
+ * `null` without one) and `seek` with `t` in ms (→ `seeked` with the same `id`, once the frame at
29
+ * that time painted, and an `error` when `render` threw) and `guardrail` (→ `guardrail` with the
30
+ * same `id` and its `findings`, see `LookGuardrailFinding`). On the website and in the studio
31
+ * they go unanswered.
32
+ *
33
+ * The Timeline (`camox.timeline`) is declared once, synchronously, by the source, which draws its
34
+ * first frame at once, before the body is revealed. On the website its clock calls `render(t)` in
35
+ * `requestAnimationFrame` with real time, wrapped at `duration` when looping and held at the end
36
+ * otherwise. A looping Timeline plays from load, a non-looping one from when its frame is first
37
+ * visible; both pause while the frame is out of the viewport (the runtime observes it as
38
+ * `onViewport` does) and resume where they were. With `prefers-reduced-motion: reduce`, it shows
39
+ * its poster's frame and never plays. Its Tracks are compiled to Web Animations, one per
40
+ * element and property, which play on the compositor from the same start time and loop with it;
41
+ * in capture mode they're paused, and seeking sets their `currentTime`. The `type` and `count`
42
+ * helpers register with it and are drawn from the same `t`, before `render`, so they restart with
43
+ * each loop and seek exactly.
25
44
  *
26
45
  * The theme part applies the tokens to the artifact's `:root` and keeps the body hidden until the
27
46
  * first theme arrives, so the artifact never paints in the wrong colors, or until a short fallback
@@ -32,7 +51,7 @@
32
51
  * `plugins/minifyArtifactRuntime.ts`), so it must stay one template literal without
33
52
  * interpolations or backslashes.
34
53
  */
35
- const ARTIFACT_RUNTIME = "(()=>{\"use strict\";let e=window.parent,t=null,n=null,r,i,a=new Set,o=location.hash.slice(1),s=()=>{if(i!==void 0)return r;let e=document.querySelector(`script[data-camox-data]`);try{r=JSON.parse(e?e.textContent:decodeURIComponent(o)),(!r||typeof r!=`object`||Array.isArray(r))&&(r={})}catch{r={}}return i=JSON.stringify(r),r},c=(()=>{let e=new Set,t=[],n=Object.freeze({tokens:Object.freeze({}),colors:Object.freeze({})}),r=document.createElement(`style`);r.textContent=`body { visibility: hidden !important; }`,document.head.append(r);let i=()=>r.remove();setTimeout(i,300);let a=e=>Number.isFinite(e)?Math.min(255,Math.max(0,Math.round(e))):0;return{get current(){return n},receive(r){let o={},s={},c=r.colors||{};for(let[e,t]of Object.entries(r.tokens||{})){if(!e.startsWith(`--`)||typeof t!=`string`)continue;o[e]=t;let n=Object.hasOwn(c,e)?c[e]:null;n&&typeof n==`object`&&(s[e]=Object.freeze({r:a(n.r),g:a(n.g),b:a(n.b)}))}let l=document.documentElement.style;for(let e of t)Object.hasOwn(o,e)||l.removeProperty(e);for(let[e,t]of Object.entries(o))l.setProperty(e,t);t=Object.keys(o),i(),n=Object.freeze({tokens:Object.freeze(o),colors:Object.freeze(s)});for(let t of[...e])t(n)},subscribe(t){return e.add(t),()=>{e.delete(t)}}}})(),l=new Set,u=null,d=null,f=null,p=()=>{let e=Math.ceil(document.documentElement.getBoundingClientRect().height);e!==f&&(f=e,m(`height`,{height:f}))},m=(n,r)=>{t&&e.postMessage({...r,camox:n,channel:t},`*`)},h=()=>m(`observe-viewport`,{observe:l.size>0}),g={studio(e){let t=e.studio;n=t?Object.freeze({editing:!!t.editing,canvas:!!t.canvas}):null},data(e){s();let t=JSON.stringify(e.data??{});if(t!==i){r=JSON.parse(t),i=t;for(let e of[...a])e(r)}},theme:c.receive,measure(){f=null,d||(d=new ResizeObserver(p),d.observe(document.documentElement)),p()},viewport(e){let{progress:t,visibleRatio:n}=e;if(typeof t==`number`&&typeof n==`number`){u=Object.freeze({progress:t,visibleRatio:n}),document.documentElement.style.setProperty(`--camox-progress`,String(t));for(let e of l)e(u)}}};window.addEventListener(`message`,e=>{let n=e.data;if(n&&typeof n.camox==`string`){if(n.camox===`connect`){if(typeof n.channel!=`string`||n.channel===t)return;t=n.channel,m(`ready`),l.size>0&&h()}else t&&n.channel===t&&Object.hasOwn(g,n.camox)&&g[n.camox](n)}});let _=Object.freeze({get studio(){return n},get data(){return s()},onData(e){return a.add(e),()=>{a.delete(e)}},get theme(){return c.current},onTheme:c.subscribe,navigate(e){m(`navigate`,{href:String(e)})},onViewport(e){if(typeof e!=`function`)throw TypeError(`camox.onViewport needs a callback`);let t=t=>e(t);return l.add(t),l.size===1?h():u&&queueMicrotask(()=>l.has(t)&&e(u)),()=>{l.delete(t)&&l.size===0&&h()}}});Object.defineProperty(window,\"camox\",{value:_,enumerable:!0}),e!==window&&e.postMessage({camox:`hello`},`*`)})();";
54
+ const ARTIFACT_RUNTIME = "(()=>{\"use strict\";let e=window.parent,t=null,n=null,r,i,a=new Set,o=location.hash.slice(1),s=()=>{if(i!==void 0)return r;let e=document.querySelector(`script[data-camox-data]`);try{r=JSON.parse(e?e.textContent:decodeURIComponent(o)),(!r||typeof r!=`object`||Array.isArray(r))&&(r={})}catch{r={}}return i=JSON.stringify(r),r},c=(()=>{let e={setTimeout:window.setTimeout.bind(window),setInterval:window.setInterval.bind(window),clearTimeout:window.clearTimeout.bind(window)},t=new Map;window.setTimeout=function(n,r,...i){if(typeof n!=`function`)return e.setTimeout(n,r,...i);let a=e.setTimeout(function(){return t.delete(a),n.apply(this,arguments)},r,...i);return t.set(a,{kind:`setTimeout`,delay:Number(r)||0}),a},window.setInterval=function(n,r,...i){let a=e.setInterval(n,r,...i);return typeof n==`function`&&t.set(a,{kind:`setInterval`,delay:Number(r)||0}),a};let n=function(n){t.delete(n),e.clearTimeout(n)};return window.clearTimeout=n,window.clearInterval=n,{later:e.setTimeout,get live(){return[...t.values()]}}})(),l=(()=>{let e=new Set,t=[],n=Object.freeze({tokens:Object.freeze({}),colors:Object.freeze({})}),r=document.createElement(`style`);r.textContent=`body { visibility: hidden !important; }`,document.head.append(r);let i=()=>r.remove();c.later(i,300);let a=e=>Number.isFinite(e)?Math.min(255,Math.max(0,Math.round(e))):0;return{get current(){return n},receive(r){let o={},s={},c=r.colors||{};for(let[e,t]of Object.entries(r.tokens||{})){if(!e.startsWith(`--`)||typeof t!=`string`)continue;o[e]=t;let n=Object.hasOwn(c,e)?c[e]:null;n&&typeof n==`object`&&(s[e]=Object.freeze({r:a(n.r),g:a(n.g),b:a(n.b)}))}let l=document.documentElement.style;for(let e of t)Object.hasOwn(o,e)||l.removeProperty(e);for(let[e,t]of Object.entries(o))l.setProperty(e,t);t=Object.keys(o),i(),n=Object.freeze({tokens:Object.freeze(o),colors:Object.freeze(s)});for(let t of[...e])t(n)},subscribe(t){return e.add(t),()=>{e.delete(t)}}}})(),u=new Set,d=null,f=null,p=null,m=()=>{let e=Math.ceil(document.documentElement.getBoundingClientRect().height);e!==p&&(p=e,h(`height`,{height:p}))},h=(n,r)=>{t&&e.postMessage({...r,camox:n,channel:t},`*`)},g=()=>h(`observe-viewport`,{observe:u.size>0}),_=(e,t,n,r)=>{let i=(e,t,n)=>3*(1-n)*(1-n)*n*e+3*(1-n)*n*n*t+n*n*n,a=(e,t,n)=>3*(1-n)*(1-n)*e+6*(1-n)*n*(t-e)+3*n*n*(1-t);return o=>{if(o<=0)return 0;if(o>=1)return 1;let s=o;for(let c=0;c<8;c++){let c=i(e,n,s)-o;if(Math.abs(c)<1e-7)return i(t,r,s);let l=a(e,n,s);if(Math.abs(l)<1e-6)break;s-=c/l}let c=0,l=1;s=o;for(let t=0;t<40;t++){let t=i(e,n,s);if(Math.abs(t-o)<1e-7)break;t<o?c=s:l=s,s=(c+l)/2}return i(t,r,s)}},v=Object.freeze({travel:[.45,0,.1,1],arrive:[.25,.8,.25,1]}),y=Object.freeze({linear:e=>Math.min(1,Math.max(0,e)),travel:_(...v.travel),arrive:_(...v.arrive)}),b=e=>typeof e==`string`&&Object.hasOwn(y,e),x=e=>{if(e===void 0)return y.linear;if(typeof e==`function`)return e;if(b(e))return y[e];throw TypeError(`Unknown easing `+String(e)+`: use travel, arrive, linear or a function`)},S=(()=>{let e=[`transform`,`opacity`],t=e=>e===`linear`?`linear`:`cubic-bezier(`+v[e].join(`, `)+`)`,n=()=>Object.freeze({width:window.innerWidth,height:window.innerHeight}),r=(e,t)=>({index:e.index,text:e.name+` is skipped, so nothing it animates moves: `+t+`.`}),i=(e,t)=>{if(!e||typeof e!=`object`)return`a Track is an object`;let{el:n,at:r,keyframes:i}=e;if(!n||typeof n.animate!=`function`)return`it needs an el, the element it moves`;if(typeof r!=`number`||!(r>=0))return`it needs an at, when it starts in ms`;let a=e.duration;if(typeof a!=`number`||!(a>=0)||!(r+a<=t))return`it needs a duration in ms, and must end by the Timeline's duration (`+t+` ms)`;if(e.ease!==void 0&&!b(e.ease))return`it has an unknown easing `+String(e.ease)+`: use travel, arrive or linear`;if(typeof i!=`function`&&(!i||typeof i!=`object`))return`it needs keyframes, or a function of the artifact's size giving them`},a=(e,t)=>{if(e===void 0)return{tracks:[],skipped:[]};if(!Array.isArray(e))throw TypeError(`A Timeline's tracks must be an array of Tracks`);let n=[],a=[];return e.forEach((e,o)=>{let s=`Track `+o,c=i(e,t);if(c){a.push(r({index:o,name:s},c));return}let{el:l,at:u,keyframes:d}=e,f=e.ease===void 0?`linear`:e.ease;n.push({name:s,index:o,el:l,at:u,end:u+e.duration,ease:f,keyframes:d})}),{tracks:n,skipped:a}},o=(t,n)=>{let r=typeof t.keyframes==`function`?t.keyframes(n):t.keyframes;if(!r||typeof r!=`object`)return{problem:`its keyframes must be an object`};let i={};for(let[t,n]of Object.entries(r)){if(!e.includes(t))return{problem:`Tracks animate transform and opacity only, not `+t+`: draw it from render(t)`};if(!Array.isArray(n)||n.length!==2)return{problem:`its `+t+` needs two values, [from, to]`};i[t]=n}return{frames:i}},s=(e,i,a)=>{let s=new Map,c=[],l=n();for(let t of[...e].sort((e,t)=>e.at-t.at||e.index-t.index)){let{frames:e,problem:n}=o(t,l);if(n)c.push(r(t,n));else for(let[n,[r,i]]of Object.entries(e)){s.has(t.el)||s.set(t.el,new Map);let e=s.get(t.el);e.has(n)||e.set(n,[]),e.get(n).push({track:t,from:r,to:i})}}let u=[],d=[];for(let[e,n]of s)for(let[r,o]of n){let n=(e,n,a)=>({offset:e/i,[r]:n,easing:t(a)}),s=[n(0,o[0].from,`linear`)],c=0,l=null,f=null;for(let{track:t,from:i,to:a}of o){if(f&&t.at<f.end){let n=Math.min(f.end,t.end);d.push({el:e,property:r,tracks:[f.index,t.index],from:t.at,to:n})}(!f||t.end>f.end)&&(f=t);let o=Math.max(t.at,c),u=Math.max(t.end,o);l&&s.push(n(o,l.to,`linear`)),s.push(n(o,i,t.ease),n(u,a,`linear`)),c=u,l={track:t,to:a}}s.push(n(i,l.to,`linear`)),u.push(e.animate(s,{duration:i,iterations:a?1/0:1,fill:`both`}))}return{animations:u,overlaps:d,skipped:c}};return(e,t,n)=>{let{tracks:r,skipped:i}=a(e,t),o=s(r,t,n),c=null,l=0,u=()=>{for(let e of o.animations)c===null?(e.pause(),e.currentTime=Math.min(t,Math.max(0,l))):e.startTime=c};return r.some(e=>typeof e.keyframes==`function`)&&window.addEventListener(`resize`,()=>{for(let e of o.animations)e.cancel();o=s(r,t,n),u()}),{get animations(){return o.animations},get overlaps(){return o.overlaps},get skipped(){return[...i,...o.skipped].sort((e,t)=>e.index-t.index)},play(e){c=e,u()},seek(e){c=null,l=e,u()}}}})(),C=(()=>{let e=null,t=null,n=!1,r=0,i=null,a=0,o=null,s=!1,l=t=>{let{duration:n,loop:r}=e;return r?(t%n+n)%n:Math.min(n,Math.max(0,t))},d=[],f=0,p=t=>{f=t;for(let e of d)e(t);e.render&&e.render(t)},m=e=>{t.seek(e),p(e)},_=()=>{let e=document.timeline?document.timeline.currentTime:null;return typeof e==`number`?e:performance.now()},v=()=>{r&&cancelAnimationFrame(r),r=0},y=t=>{r=0;let n=l(t-i);p(n),e.loop||n<e.duration?r=requestAnimationFrame(y):(a=e.duration,i=null)},b=()=>{v(),i=_()-a,t.play(i),r=requestAnimationFrame(y)},x=()=>{i!==null&&(a=_()-i,i=null,v(),m(l(a)))},C=()=>{if(n||s)return;let{loop:t,duration:r}=e;(o===null?t:o)&&(t||a<r)?i===null&&b():x()},w=e=>{o=e.visibleRatio>0,C()},T=()=>{let{poster:t,marks:n,loop:r,duration:i}=e;return t===null?r?0:i:n[t]},E=e=>{let t=!1,n=()=>{t||(t=!0,e())};requestAnimationFrame(()=>requestAnimationFrame(n)),c.later(n,200)};return{inspect(n){if(!e)return{animations:[],skipped:[],overlaps:[],ends:null};let{animations:r,skipped:i,overlaps:a}=t;if(!e.loop||!n)return{animations:r,skipped:i,overlaps:a,ends:null};let o=f,s=e.duration-1;try{m(0);let e=n();return m(s),{animations:r,skipped:i,overlaps:a,ends:{first:e,last:n(),at:s}}}finally{m(o)}},register(t,n){if(!e)throw Error(`camox.`+t+` needs a Timeline: call camox.timeline first`);d.push(n),n(f)},declare(r){if(e)throw Error(`camox.timeline can be called once per artifact`);if(!r||typeof r!=`object`)throw TypeError(`camox.timeline needs its options: duration, marks, render`);let{duration:i}=r;if(typeof i!=`number`||!Number.isFinite(i)||i<=0)throw TypeError(`camox.timeline needs a duration: a positive number of milliseconds`);let a={};for(let[e,t]of Object.entries(r.marks||{})){if(typeof t!=`number`||!(t>=0&&t<=i))throw RangeError(`The Mark `+e+` must be a time from 0 to the duration`);a[e]=t}let o=r.poster===void 0?null:String(r.poster);if(o!==null&&!Object.hasOwn(a,o))throw RangeError(`The poster `+o+` must name a Mark`);let{render:c}=r;if(c!==void 0&&typeof c!=`function`)throw TypeError(`A Timeline's render must be a function of the time`);let l=r.loop===!0;t=S(r.tracks,i,l),e=Object.freeze({duration:i,loop:l,marks:Object.freeze(a),poster:o,render:c}),s=!n&&typeof matchMedia==`function`&&matchMedia(`(prefers-reduced-motion: reduce)`).matches,m(s?T():0),!(n||s)&&(u.add(w),u.size===1&&g(),C())},capture(){n=!0,e&&(v(),i=null,u.delete(w)&&u.size===0&&g(),m(0))},describe(t){if(!n)return;let{duration:r,loop:i,marks:a,poster:o}=e||{};h(`timeline`,{id:t.id,timeline:e?{duration:r,loop:i,marks:a,poster:o}:null})},seek(t){if(!n)return;let r=Number(t.t),i;if(e&&Number.isFinite(r))try{m(l(r))}catch(e){i=`render(`+r+`) threw: `+String(e&&e.message?e.message:e)}E(()=>h(`seeked`,i===void 0?{id:t.id}:{id:t.id,error:i}))}}})(),w=(()=>{let e=(e,t)=>{if(!(t instanceof Element))throw TypeError(`camox.`+e+` needs an element to write in`);return t},t=(e,t,n,r,i)=>{if(n===void 0&&r!==void 0)return r;if(typeof n!=`number`||!Number.isFinite(n)||n<0||i&&n===0)throw TypeError(`camox.`+e+`'s `+t+` must be a `+(i?`positive `:``)+`number`);return n},n=e=>{let[,t=``]=String(e).split(`.`);return/^[0-9]+$/.test(t)?Math.min(20,t.length):0};return{type(n,r,i={}){let a=e(`type`,n),o=t(`type`,`at`,i.at,0),s=t(`type`,`cps`,i.cps,20,!0),c=typeof Intl.Segmenter==`function`?Array.from(new Intl.Segmenter().segment(String(r)),e=>e.segment):Array.from(String(r)),l=o+c.length*1e3/s,u=document.createElement(`span`),d=document.createTextNode(``),f=document.createElement(`span`);f.setAttribute(`aria-hidden`,`true`),f.setAttribute(`data-camox-caret`,``),f.style.cssText=`border-left: max(1px, 0.07em) solid currentColor; margin-right: calc(-1 * max(1px, 0.07em));`,u.append(d,f),a.replaceChildren(u);let p=!0;C.register(`type`,e=>{let t=e<o?0:Math.min(c.length,Math.floor((e-o)*s/1e3)+1),n=c.slice(0,t).join(``);d.data!==n&&(d.data=n);let r=e<o?o-e:e-l,i=r<0||Math.floor(r/530)%2==0;i!==p&&(p=i,f.style.visibility=i?``:`hidden`)})},count(r,i){let a=e(`count`,r);if(!i||typeof i.to!=`number`||!Number.isFinite(i.to))throw TypeError(`camox.count needs the number to count to`);let{to:o}=i,s=i.from===void 0?0:i.from;if(typeof s!=`number`||!Number.isFinite(s))throw TypeError(`camox.count's from must be a number`);let c=t(`count`,`at`,i.at,0),l=t(`count`,`duration`,i.duration,1e3,!0),u=x(i.ease===void 0?`arrive`:i.ease),d=i.format;if(d===void 0){let e=Math.max(n(s),n(o)),t=new Intl.NumberFormat(void 0,{minimumFractionDigits:e,maximumFractionDigits:e});d=e=>t.format(e)}else if(typeof d!=`function`)throw TypeError(`camox.count's format must be a function of the value`);let f=null;C.register(`count`,e=>{let t=e>=c+l?o:s+(o-s)*u(Math.max(0,e-c)/l),n=String(d(t));n!==f&&(f=n,a.textContent=n)})}}})(),T=(()=>{let e=!1,t=new Set,n=/-?(?:[0-9]+[.]?[0-9]*|[.][0-9]+)(?:e[-+]?[0-9]+)?/g,r=e=>{if(!e||!e.tagName)return`an element`;let t=e.tagName.toLowerCase();if(e.id)return t+`#`+e.id;let n=Array.from(e.classList).slice(0,2);if(n.length)return t+`.`+n.join(`.`);let i=e.parentElement;return!i||i===document.documentElement?t:r(i)+` > `+t+`:nth-child(`+(Array.from(i.children).indexOf(e)+1)+`)`},i=e=>{let t=e.effect,n=t&&t.target,i=(n?r(n):`no element`)+(t&&t.pseudoElement||``);return typeof CSSAnimation==`function`&&e instanceof CSSAnimation?`The CSS animation `+e.animationName+` on `+i+` plays on its own, outside the Timeline, so captures can't seek it: make it a Track, or draw it from render(t).`:typeof CSSTransition==`function`&&e instanceof CSSTransition?`A CSS transition of `+e.transitionProperty+` on `+i+` ran without a visitor's hover or press: motion that plays on its own goes through the Timeline, as a Track or from render(t).`:`A Web Animation on `+i+` was made by the source and plays outside the Timeline: declare it as a Track, or pause it and set its currentTime from render(t).`},a=()=>{if(!e||typeof document.getAnimations!=`function`)return;let n=new Set(C.inspect().animations);for(let e of document.getAnimations())n.has(e)||e.playState===`paused`||e.playState===`idle`||t.add(i(e))},o=()=>{let e=new Map;for(let{kind:t,delay:n}of c.live){let r=t+` `+n;e.has(r)||e.set(r,{kind:t,delay:n,count:0}),e.get(r).count++}return[...e.values()].map(({kind:e,delay:t,count:n})=>({kind:`timer`,text:(e===`setInterval`?`A setInterval every `+t+` ms is still running`:`A setTimeout of `+t+` ms is still pending`)+(n>1?` (`+n+` of them)`:``)+`: time stands still in captures, so what timers move never shows there, and they don't follow the Timeline's loop. Draw it from render(t) instead.`}))},s=e=>{let t=new Map,n=(r,i,a)=>{let o=getComputedStyle(r),s=a&&o.display!==`none`,c=i*Number(o.opacity),l=s&&o.visibility!==`hidden`&&c>.01;if(!r.hasAttribute(`data-camox-caret`)){let n={};for(let e of r.attributes)n[e.name+` attribute`]=e.value;let i=``;for(let e of r.childNodes)e.nodeType===Node.TEXT_NODE&&(i+=e.data);if(i&&(n.text=i),e.has(r)&&(n.transform=new DOMMatrix(o.transform).toString(),n.opacity=o.opacity),r instanceof HTMLCanvasElement)try{n[`canvas drawing`]=r.toDataURL()}catch{}t.set(r,{visible:l,values:n})}for(let e of r.children)n(e,c,s)};return n(document.documentElement,1,!0),t},l=(e,t)=>{if(e===t)return!0;if(typeof e!=`string`||typeof t!=`string`||e.replace(n,`0`)!==t.replace(n,`0`))return!1;let r=t.match(n)||[];return(e.match(n)||[]).every((e,t)=>{let n=Number(e),i=Number(r[t]);return Math.abs(n-i)<=.01*Math.max(1,Math.abs(n),Math.abs(i))})},u=e=>JSON.stringify(e.length>40?e.slice(0,39)+`…`:e),d=()=>{let e=new Set(C.inspect().animations.map(e=>e.effect&&e.effect.target)),t;try{t=C.inspect(()=>s(e)).ends}catch{return null}if(!t)return null;let{first:n,last:i,at:a}=t,o=[];for(let e of new Set([...n.keys(),...i.keys()])){let t=n.get(e),s=i.get(e);if(!t||!s)(t||s).visible&&o.push(r(e)+` is there at only one end`);else if(t.visible||s.visible)for(let n of new Set([...Object.keys(t.values),...Object.keys(s.values)])){let i=t.values[n],c=s.values[n];l(i,c)||o.push(n===`canvas drawing`?r(e)+`'s canvas drawing`:r(e)+`'s `+n+` (`+(i===void 0?`none`:u(i))+` at 0 ms, `+(c===void 0?`none`:u(c))+` at `+a+` ms)`)}}if(!o.length)return null;let c=o.slice(0,4).join(`; `)+(o.length>4?`; and `+(o.length-4)+` more`:``);return{kind:`loop-cut`,text:`Loop cut: the last frame (`+a+` ms) differs from the first (0 ms), so the loop jumps at its restart: `+c+`. Bring them back to how they start before the end, or hide them while they reset.`}};return{get watching(){return e},watch(){e=!0,a()},sample:a,report(){let e=d();a();let n=[...t].map(e=>({kind:`animation`,text:e}));n.push(...o());let{skipped:i,overlaps:s}=C.inspect();for(let{text:e}of i)n.push({kind:`track`,text:e});for(let{el:e,property:t,tracks:i,from:a,to:o}of s)n.push({kind:`overlap`,text:`Tracks `+i[0]+` and `+i[1]+` both animate the `+t+` of `+r(e)+`, from `+a+` to `+o+` ms: the later one waits for the earlier to end, so it jumps. Start it once the earlier one ends.`});return e&&n.push(e),n}}})(),E={studio(e){let t=e.studio;n=t?Object.freeze({editing:!!t.editing,canvas:!!t.canvas}):null},data(e){s();let t=JSON.stringify(e.data??{});if(t!==i){r=JSON.parse(t),i=t;for(let e of[...a])e(r)}},theme:l.receive,measure(){p=null,f||(f=new ResizeObserver(m),f.observe(document.documentElement)),m()},viewport(e){let{progress:t,visibleRatio:n}=e;if(typeof t==`number`&&typeof n==`number`){d=Object.freeze({progress:t,visibleRatio:n}),document.documentElement.style.setProperty(`--camox-progress`,String(t));for(let e of u)e(d)}},capture(e){C.capture(e),T.watch()},timeline:C.describe,seek(e){C.seek(e),T.sample()},guardrail(e){T.watching&&h(`guardrail`,{id:e.id,findings:T.report()})}};window.addEventListener(`message`,e=>{let n=e.data;if(n&&typeof n.camox==`string`){if(n.camox===`connect`){if(typeof n.channel!=`string`||n.channel===t)return;t=n.channel,h(`ready`),u.size>0&&g()}else t&&n.channel===t&&Object.hasOwn(E,n.camox)&&E[n.camox](n)}});let D=Object.freeze({get studio(){return n},get data(){return s()},onData(e){return a.add(e),()=>{a.delete(e)}},get theme(){return l.current},onTheme:l.subscribe,navigate(e){h(`navigate`,{href:String(e)})},onViewport(e){if(typeof e!=`function`)throw TypeError(`camox.onViewport needs a callback`);let t=t=>e(t);return u.add(t),u.size===1?g():d&&queueMicrotask(()=>u.has(t)&&e(d)),()=>{u.delete(t)&&u.size===0&&g()}},timeline:C.declare,interpolate(e,t,n,r){let[i,a]=t,[o,s]=n,c=x(r&&r.ease),l=a===i?e<i?0:1:(e-i)/(a-i);return o+(s-o)*c(Math.min(1,Math.max(0,l)))},random(e){let t=String(e),n=2166136261;for(let e=0;e<t.length;e++)n=Math.imul(n^t.charCodeAt(e),16777619);return n=Math.imul(n^n>>>16,2246822507),n=Math.imul(n^n>>>13,3266489909),((n^n>>>16)>>>0)/4294967296},type:w.type,count:w.count});Object.defineProperty(window,\"camox\",{value:D,enumerable:!0}),e!==window&&e.postMessage({camox:`hello`},`*`)})();";
36
55
 
37
56
  //#endregion
38
57
  export { ARTIFACT_RUNTIME };
@@ -1,5 +1,5 @@
1
1
  import { PREVIEW_PAUSED_ATTRIBUTE } from "../../core/lib/previewPaused.js";
2
- import { EMPTY_PREVIEW_DOCUMENT, isSiteStyle } from "../preview/components/previewStyles.js";
2
+ import { EMPTY_PREVIEW_DOCUMENT, isSiteStyle, restyle } from "../preview/components/previewStyles.js";
3
3
 
4
4
  //#region src/features/canvas/canvasFrameDocument.ts
5
5
  /** Freezes CSS animations, including ones that start while the frame is paused. */
@@ -167,6 +167,29 @@ function createCanvasDocument(previewDocument, href) {
167
167
  return `<!doctype html>${doc.documentElement.outerHTML}`;
168
168
  }
169
169
  /**
170
+ * Brings the document a Canvas frame loaded up to `canvasDocument`, which `createCanvasDocument`
171
+ * built for the same frame since, without loading it again: the frame keeps showing its page
172
+ * while another draft or locale's takes its place. Its links resolve against the new base, and
173
+ * it takes the new stylesheets, language and direction.
174
+ */
175
+ function refreshCanvasDocument(frame, canvasDocument) {
176
+ const doc = frame.document;
177
+ const next = new DOMParser().parseFromString(canvasDocument, "text/html");
178
+ const href = next.querySelector("base")?.getAttribute("href");
179
+ const base = doc.querySelector("base");
180
+ if (href && base && base.getAttribute("href") !== href) base.setAttribute("href", href);
181
+ syncAttributes(doc.documentElement, next.documentElement);
182
+ syncAttributes(doc.body, next.body);
183
+ restyle(frame, canvasDocument);
184
+ }
185
+ /** Attributes of a Canvas frame's root and body the Canvas writes while it shows the frame. */
186
+ const CANVAS_ATTRIBUTES = /* @__PURE__ */ new Set(["style", PREVIEW_PAUSED_ATTRIBUTE]);
187
+ /** Gives `element` the attributes of `from`, but those the Canvas writes. */
188
+ function syncAttributes(element, from) {
189
+ for (const name of element.getAttributeNames()) if (!CANVAS_ATTRIBUTES.has(name) && !from.hasAttribute(name)) element.removeAttribute(name);
190
+ for (const { name, value } of from.attributes) if (!CANVAS_ATTRIBUTES.has(name) && element.getAttribute(name) !== value) element.setAttribute(name, value);
191
+ }
192
+ /**
170
193
  * Give a loaded document what `createCanvasDocument` builds into a Canvas frame's,
171
194
  * for as long as it shows in the Canvas: no swipe navigation, CSS animations that
172
195
  * pause with the frame, and a page as tall as its content. Returns how to undo
@@ -367,4 +390,4 @@ function observeCanvasDocument(iframe, root, viewportHeight, suspended = () => f
367
390
  }
368
391
 
369
392
  //#endregion
370
- export { adaptCanvasStyleSheets, adoptCanvasDocument, canvasPageHref, canvasSeamCover, createCanvasDocument, logicalViewportValue, measureCanvasDocument, observeCanvasDocument, pauseCanvasDocument };
393
+ export { adaptCanvasStyleSheets, adoptCanvasDocument, canvasPageHref, canvasSeamCover, createCanvasDocument, logicalViewportValue, measureCanvasDocument, observeCanvasDocument, pauseCanvasDocument, refreshCanvasDocument };
@@ -1,6 +1,6 @@
1
1
  import { NavigationProvider } from "../navigation/navigation.js";
2
2
  import { FrameContext } from "../preview/components/Frame.js";
3
- import { createCanvasDocument, observeCanvasDocument, pauseCanvasDocument } from "./canvasFrameDocument.js";
3
+ import { createCanvasDocument, observeCanvasDocument, pauseCanvasDocument, refreshCanvasDocument } from "./canvasFrameDocument.js";
4
4
  import { useCanvasFrameInput } from "./useCanvasFrameInput.js";
5
5
  import { c } from "react/compiler-runtime";
6
6
  import * as React from "react";
@@ -28,9 +28,12 @@ function useCanvasFrame({ previewDocument, href, width, viewportHeight, mode })
28
28
  const [failure, setFailure] = React.useState(null);
29
29
  const live = mode === "live";
30
30
  const resting = mode === "resting";
31
+ const frameWindow = React.useRef(null);
31
32
  React.useEffect(() => {
32
33
  try {
33
- setSrcDoc(createCanvasDocument(previewDocument, href));
34
+ const built = createCanvasDocument(previewDocument, href);
35
+ if (frameWindow.current && previewDocument) return refreshCanvasDocument(frameWindow.current, built);
36
+ setSrcDoc(built);
34
37
  } catch (error) {
35
38
  setFailure(toError(error));
36
39
  }
@@ -75,6 +78,7 @@ function useCanvasFrame({ previewDocument, href, width, viewportHeight, mode })
75
78
  if (!root) throw new Error("The page preview document has no content root.");
76
79
  if (root === mount) return;
77
80
  root.replaceChildren();
81
+ frameWindow.current = root.ownerDocument.defaultView;
78
82
  setMount(root);
79
83
  setIframe(loaded);
80
84
  } catch (error) {
@@ -13,14 +13,23 @@ const EMPTY_PREVIEW_DOCUMENT = "<!doctype html><html><head></head><body><div id=
13
13
  */
14
14
  const SITE_STYLESHEETS = "link[rel~=\"stylesheet\"], style";
15
15
  const siteStylesheetsOf = (head) => Array.from(head.querySelectorAll(SITE_STYLESHEETS)).filter((element) => element.parentElement === head && isSiteStyle(element) && !element.hasAttribute("data-precedence"));
16
+ /** The attributes of a site document's root that say how its page reads: its language and direction. */
17
+ const ROOT_ATTRIBUTES = ["lang", "dir"];
16
18
  /**
17
19
  * Gives a frame's document the site stylesheets of `html`, a site document: the ones it no
18
- * longer has go, and its new ones are added, the rest of the frame left as it is.
20
+ * longer has go, and its new ones are added, the rest of the frame left as it is. Its root takes
21
+ * the document's language and direction, as a page in another locale has its own.
19
22
  */
20
23
  function restyle(frame, html) {
21
24
  const doc = frame.document;
22
25
  const { DOMParser } = frame;
23
- const wanted = siteStylesheetsOf(new DOMParser().parseFromString(html, "text/html").head);
26
+ const parsed = new DOMParser().parseFromString(html, "text/html");
27
+ for (const name of ROOT_ATTRIBUTES) {
28
+ const value = parsed.documentElement.getAttribute(name);
29
+ if (value === null) doc.documentElement.removeAttribute(name);
30
+ else if (doc.documentElement.getAttribute(name) !== value) doc.documentElement.setAttribute(name, value);
31
+ }
32
+ const wanted = siteStylesheetsOf(parsed.head);
24
33
  const held = siteStylesheetsOf(doc.head);
25
34
  const wantedMarkup = new Set(wanted.map((element) => element.outerHTML));
26
35
  const heldMarkup = new Set(held.map((element) => element.outerHTML));
@@ -1,11 +1,12 @@
1
1
  import { useDraftScope } from "../../lib/draft-scope.js";
2
- import { AuthContext, useAuthActions } from "../../lib/auth.js";
2
+ import { AuthContext, signedInUserId, useAuthActions } from "../../lib/auth.js";
3
3
  import { reads, useReadQuery, useStudioCache } from "../../lib/studio-reads.js";
4
4
  import { RevisionsContext, createRevisions } from "../../lib/revisions.js";
5
5
  import { useAdminShortcuts } from "./useAdminShortcuts.js";
6
6
  import { useApplyTheme, useThemeActions } from "../studio/useTheme.js";
7
7
  import { usePreviewPagesActions, useShowCanvasAction } from "../preview/CamoxPreview.js";
8
8
  import { useProjectRoom } from "../../lib/use-project-room.js";
9
+ import { useLeaveDraft } from "../studio/viewSwitch.js";
9
10
  import { useNavbarActions } from "../studio/components/Navbar.js";
10
11
  import { draftNoticeMessage } from "../studio/draftNotice.js";
11
12
  import { CommandPalette, useCommandPaletteActions } from "./components/CommandPalette.js";
@@ -17,7 +18,7 @@ import studioCssUrl from "virtual:camox-studio-css";
17
18
 
18
19
  //#region src/features/provider/AuthenticatedCamoxProvider.tsx
19
20
  function AuthenticatedCamoxProvider(t0) {
20
- const $ = c(18);
21
+ const $ = c(20);
21
22
  const { children } = t0;
22
23
  useAdminShortcuts();
23
24
  useCommandPaletteActions();
@@ -26,7 +27,7 @@ function AuthenticatedCamoxProvider(t0) {
26
27
  useNavbarActions();
27
28
  usePreviewPagesActions();
28
29
  useShowCanvasAction();
29
- const { apiUrl, projectUuid } = React.useContext(AuthContext);
30
+ const { apiUrl, projectUuid, authClient } = React.useContext(AuthContext);
30
31
  let t1;
31
32
  if ($[0] !== projectUuid) {
32
33
  t1 = reads.project({ projectUuid });
@@ -50,49 +51,53 @@ function AuthenticatedCamoxProvider(t0) {
50
51
  }
51
52
  React.useEffect(t2, t3);
52
53
  const [revisions] = React.useState(createRevisions);
54
+ const leaveDraft = useLeaveDraft();
53
55
  let t4;
54
- if ($[5] !== draftScope || $[6] !== revisions) {
56
+ if ($[5] !== authClient || $[6] !== draftScope || $[7] !== leaveDraft || $[8] !== revisions) {
55
57
  let t5;
56
- if ($[8] !== revisions) {
58
+ if ($[10] !== revisions) {
57
59
  t5 = (revision) => revisions.announce(revision);
58
- $[8] = revisions;
59
- $[9] = t5;
60
- } else t5 = $[9];
60
+ $[10] = revisions;
61
+ $[11] = t5;
62
+ } else t5 = $[11];
61
63
  t4 = {
62
64
  onDraftEnded(ended) {
63
65
  if (ended.draftId !== draftScope.current()) return;
66
+ if (ended.endedBy.id === signedInUserId(authClient)) return leaveDraft(ended);
64
67
  draftScope.leave(ended, { notice: true });
65
68
  },
66
69
  onRevision: t5
67
70
  };
68
- $[5] = draftScope;
69
- $[6] = revisions;
70
- $[7] = t4;
71
- } else t4 = $[7];
71
+ $[5] = authClient;
72
+ $[6] = draftScope;
73
+ $[7] = leaveDraft;
74
+ $[8] = revisions;
75
+ $[9] = t4;
76
+ } else t4 = $[9];
72
77
  useProjectRoom(apiUrl, project?.id, studioCache, t4);
73
78
  const { resolvedTheme } = useApplyTheme();
74
79
  let t5;
75
- if ($[10] === Symbol.for("react.memo_cache_sentinel")) {
80
+ if ($[12] === Symbol.for("react.memo_cache_sentinel")) {
76
81
  t5 = /* @__PURE__ */ jsx("link", {
77
82
  rel: "stylesheet",
78
83
  href: studioCssUrl,
79
84
  "data-camox-studio": true
80
85
  });
81
- $[10] = t5;
82
- } else t5 = $[10];
86
+ $[12] = t5;
87
+ } else t5 = $[12];
83
88
  let t6;
84
- if ($[11] !== resolvedTheme) {
89
+ if ($[13] !== resolvedTheme) {
85
90
  t6 = /* @__PURE__ */ jsx(Toaster, { theme: resolvedTheme });
86
- $[11] = resolvedTheme;
87
- $[12] = t6;
88
- } else t6 = $[12];
91
+ $[13] = resolvedTheme;
92
+ $[14] = t6;
93
+ } else t6 = $[14];
89
94
  let t7;
90
- if ($[13] === Symbol.for("react.memo_cache_sentinel")) {
95
+ if ($[15] === Symbol.for("react.memo_cache_sentinel")) {
91
96
  t7 = /* @__PURE__ */ jsx(CommandPalette, {});
92
- $[13] = t7;
93
- } else t7 = $[13];
97
+ $[15] = t7;
98
+ } else t7 = $[15];
94
99
  let t8;
95
- if ($[14] !== children || $[15] !== revisions || $[16] !== t6) {
100
+ if ($[16] !== children || $[17] !== revisions || $[18] !== t6) {
96
101
  t8 = /* @__PURE__ */ jsxs(RevisionsContext, {
97
102
  value: revisions,
98
103
  children: [
@@ -102,11 +107,11 @@ function AuthenticatedCamoxProvider(t0) {
102
107
  t7
103
108
  ]
104
109
  });
105
- $[14] = children;
106
- $[15] = revisions;
107
- $[16] = t6;
108
- $[17] = t8;
109
- } else t8 = $[17];
110
+ $[16] = children;
111
+ $[17] = revisions;
112
+ $[18] = t6;
113
+ $[19] = t8;
114
+ } else t8 = $[19];
110
115
  return t8;
111
116
  }
112
117
  function _temp(notice) {