camox 0.63.0 → 0.66.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 (60) 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/CamoxCanvas.js +195 -173
  6. package/dist/features/canvas/canvasCamera.js +11 -2
  7. package/dist/features/canvas/canvasFrameCapture.js +5 -12
  8. package/dist/features/canvas/canvasFrameDocument.js +90 -29
  9. package/dist/features/canvas/canvasItems.js +22 -6
  10. package/dist/features/canvas/sidebar/ContentSidebar.js +19 -21
  11. package/dist/features/canvas/sidebar/LayoutPageOverview.js +22 -49
  12. package/dist/features/canvas/sidebar/LayoutPageSeo.js +73 -76
  13. package/dist/features/canvas/sidebar/PageInspector.js +61 -50
  14. package/dist/features/canvas/sidebar/PageInspectorSkeleton.js +4 -4
  15. package/dist/features/canvas/useCanvasCamera.js +36 -15
  16. package/dist/features/page/CuratedPageContent.js +2 -1
  17. package/dist/features/page/UnplacedBlockContent.js +52 -0
  18. package/dist/features/preview/components/PageInfo.js +103 -100
  19. package/dist/features/preview/components/PreviewPanel.js +40 -12
  20. package/dist/features/preview/components/SidebarStack.js +78 -9
  21. package/dist/features/preview/strayCaretGuard.js +34 -0
  22. package/dist/features/provider/CoreCamoxProvider.js +5 -3
  23. package/dist/features/routes/pageRuntime.d.ts +2 -0
  24. package/dist/features/routes/pageRuntime.js +8 -5
  25. package/dist/features/runtime/RuntimeChrome.js +66 -40
  26. package/dist/features/runtime/pageApp.js +111 -74
  27. package/dist/features/runtime/pageClient.js +22 -2
  28. package/dist/features/runtime/runtime.d.ts +10 -0
  29. package/dist/features/runtime/runtime.js +314 -4
  30. package/dist/features/runtime/runtimeRoutes.d.ts +5 -0
  31. package/dist/features/runtime/runtimeRoutes.js +5 -0
  32. package/dist/features/studio/components/AgentSidebar.js +3 -3
  33. package/dist/features/studio/components/ChatWidget.js +106 -75
  34. package/dist/features/studio/components/ConnectAgent.js +313 -0
  35. package/dist/features/studio/components/DraftsControl.js +172 -159
  36. package/dist/features/studio/components/Patrol.js +605 -350
  37. package/dist/features/studio/components/ReviewDraftSheet.js +566 -430
  38. package/dist/features/studio/components/Squareling.js +74 -41
  39. package/dist/features/studio/components/ThreadComposer.js +192 -107
  40. package/dist/features/studio/components/ThreadMessages.js +95 -18
  41. package/dist/features/studio/components/UnnamedDraftBar.js +349 -0
  42. package/dist/features/studio/components/clientLogoPaths.js +10 -0
  43. package/dist/features/studio/findingSentence.js +37 -0
  44. package/dist/features/studio/patrollerBio.js +40 -0
  45. package/dist/features/studio/reportSentence.js +15 -0
  46. package/dist/features/studio/studioStore.js +36 -1
  47. package/dist/features/studio/workingSentence.js +10 -7
  48. package/dist/lib/api-client.d.ts +1 -0
  49. package/dist/lib/auth.js +34 -32
  50. package/dist/lib/queries.js +9 -1
  51. package/dist/lib/revisions.js +25 -3
  52. package/dist/lib/server-auth-cookie.js +17 -4
  53. package/dist/lib/studio-reads.d.ts +1 -1
  54. package/dist/lib/studio-reads.js +5 -1
  55. package/dist/lib/use-thread.js +1 -1
  56. package/dist/package.js +1 -1
  57. package/dist/studio.css +1 -1
  58. package/package.json +15 -4
  59. package/dist/features/canvas/sidebar/DerivedPageInspector.js +0 -49
  60. package/dist/features/canvas/sidebar/SingletonPageInspector.js +0 -39
@@ -3,7 +3,7 @@ import { Squareling } from "./Squareling.js";
3
3
  import { ThreadMarkdown } from "./ThreadMarkdown.js";
4
4
  import { c } from "react/compiler-runtime";
5
5
  import * as React from "react";
6
- import { jsx, jsxs } from "react/jsx-runtime";
6
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
7
7
  import { Avatar, AvatarFallback, AvatarImage } from "@camox/ui/avatar";
8
8
  import { Bubble, BubbleContent } from "@camox/ui/bubble";
9
9
  import { Message, MessageAvatar, MessageContent, MessageHeader } from "@camox/ui/message";
@@ -18,7 +18,7 @@ import { MessageScroller, MessageScrollerButton, MessageScrollerContent, Message
18
18
  * agent does, never how: thinking, looking around, making changes, building, fixing the build,
19
19
  * or starting a draft. Each editor's message shows its
20
20
  * sender's avatar, without their name. A Live turn handed off to a draft ends on the draft's
21
- * card, which `onOpenDraft` opens.
21
+ * card, which `onOpenDraft` opens. The thread keeps its end in view, unless the editor scrolls up.
22
22
  */
23
23
  function ThreadMessages(t0) {
24
24
  const $ = c(20);
@@ -105,10 +105,13 @@ function ThreadMessages(t0) {
105
105
  } else t5 = $[17];
106
106
  let t6;
107
107
  if ($[18] !== t4) {
108
- t6 = /* @__PURE__ */ jsx(MessageScrollerProvider, { children: /* @__PURE__ */ jsxs(MessageScroller, {
109
- className: "min-h-0 flex-1",
110
- children: [t4, t5]
111
- }) });
108
+ t6 = /* @__PURE__ */ jsx(MessageScrollerProvider, {
109
+ autoScroll: true,
110
+ children: /* @__PURE__ */ jsxs(MessageScroller, {
111
+ className: "min-h-0 flex-1",
112
+ children: [t4, t5]
113
+ })
114
+ });
112
115
  $[18] = t4;
113
116
  $[19] = t6;
114
117
  } else t6 = $[19];
@@ -274,6 +277,7 @@ const SQUARELING_STATUSES = {
274
277
  exploring: "exploring",
275
278
  changing: "changing",
276
279
  building: "building",
280
+ checking: "checking",
277
281
  fixing: "fixing",
278
282
  startingDraft: "changing"
279
283
  };
@@ -282,6 +286,7 @@ const ACTIVITY_LINES = {
282
286
  exploring: "Looking around…",
283
287
  changing: "Making changes…",
284
288
  building: "Building…",
289
+ checking: "Checking the design…",
285
290
  fixing: "Fixing the build…",
286
291
  startingDraft: "Starting a draft…"
287
292
  };
@@ -419,10 +424,7 @@ function ThreadMessageRow(t0) {
419
424
  t4 = /* @__PURE__ */ jsx(Bubble, {
420
425
  variant: "muted",
421
426
  className: "max-w-full",
422
- children: /* @__PURE__ */ jsx(BubbleContent, {
423
- className: "whitespace-pre-wrap",
424
- children: message.text
425
- })
427
+ children: /* @__PURE__ */ jsx(BubbleContent, { children: /* @__PURE__ */ jsx(CroppedText, { text: message.text }) })
426
428
  });
427
429
  $[29] = message.text;
428
430
  $[30] = t4;
@@ -450,10 +452,7 @@ function ThreadMessageRow(t0) {
450
452
  t2 = /* @__PURE__ */ jsx(Message, { children: /* @__PURE__ */ jsxs(MessageContent, { children: [t1, /* @__PURE__ */ jsx(Bubble, {
451
453
  variant: "muted",
452
454
  className: "max-w-full",
453
- children: /* @__PURE__ */ jsx(BubbleContent, {
454
- className: "whitespace-pre-wrap",
455
- children: message.text
456
- })
455
+ children: /* @__PURE__ */ jsx(BubbleContent, { children: /* @__PURE__ */ jsx(CroppedText, { text: message.text }) })
457
456
  })] }) });
458
457
  $[35] = message.text;
459
458
  $[36] = t2;
@@ -472,10 +471,7 @@ function ThreadMessageRow(t0) {
472
471
  align: "end",
473
472
  variant: "muted",
474
473
  className: "max-w-full",
475
- children: /* @__PURE__ */ jsx(BubbleContent, {
476
- className: "whitespace-pre-wrap",
477
- children: message.text
478
- })
474
+ children: /* @__PURE__ */ jsx(BubbleContent, { children: /* @__PURE__ */ jsx(CroppedText, { text: message.text }) })
479
475
  }) });
480
476
  $[39] = message.text;
481
477
  $[40] = t2;
@@ -498,6 +494,87 @@ function _temp(subject, index) {
498
494
  children: subject
499
495
  }, index);
500
496
  }
497
+ /**
498
+ * A message's text, cropped past five lines and a half, fading out gently from its second line,
499
+ * above a button that shows it all. A message that fits shows as is.
500
+ */
501
+ function CroppedText(t0) {
502
+ const $ = c(17);
503
+ const { text } = t0;
504
+ const node = React.useRef(null);
505
+ const [overflows, setOverflows] = React.useState(false);
506
+ const [expanded, setExpanded] = React.useState(false);
507
+ let t1;
508
+ if ($[0] !== expanded) {
509
+ t1 = () => {
510
+ const element = node.current;
511
+ if (!element || expanded) return;
512
+ const measure = () => setOverflows(element.scrollHeight > element.clientHeight + 1);
513
+ measure();
514
+ const resizes = new ResizeObserver(measure);
515
+ resizes.observe(element);
516
+ const views = new IntersectionObserver(measure);
517
+ views.observe(element);
518
+ return () => {
519
+ resizes.disconnect();
520
+ views.disconnect();
521
+ };
522
+ };
523
+ $[0] = expanded;
524
+ $[1] = t1;
525
+ } else t1 = $[1];
526
+ let t2;
527
+ if ($[2] !== expanded || $[3] !== text) {
528
+ t2 = [text, expanded];
529
+ $[2] = expanded;
530
+ $[3] = text;
531
+ $[4] = t2;
532
+ } else t2 = $[4];
533
+ React.useLayoutEffect(t1, t2);
534
+ const cropped = !expanded && overflows;
535
+ const t3 = cropped || void 0;
536
+ const t4 = !expanded && "max-h-[5.5lh] overflow-hidden";
537
+ const t5 = cropped && "mask-[linear-gradient(to_bottom,black_1.5lh,transparent)]";
538
+ let t6;
539
+ if ($[5] !== t4 || $[6] !== t5) {
540
+ t6 = cn("whitespace-pre-wrap", t4, t5);
541
+ $[5] = t4;
542
+ $[6] = t5;
543
+ $[7] = t6;
544
+ } else t6 = $[7];
545
+ let t7;
546
+ if ($[8] !== t3 || $[9] !== t6 || $[10] !== text) {
547
+ t7 = /* @__PURE__ */ jsx("div", {
548
+ ref: node,
549
+ "data-thread-cropped": t3,
550
+ className: t6,
551
+ children: text
552
+ });
553
+ $[8] = t3;
554
+ $[9] = t6;
555
+ $[10] = text;
556
+ $[11] = t7;
557
+ } else t7 = $[11];
558
+ let t8;
559
+ if ($[12] !== cropped) {
560
+ t8 = cropped && /* @__PURE__ */ jsx("button", {
561
+ type: "button",
562
+ onClick: () => setExpanded(true),
563
+ className: "focus-visible:ring-ring mt-1 rounded-sm font-medium hover:underline focus-visible:ring-2 focus-visible:outline-none",
564
+ children: "Show full message"
565
+ });
566
+ $[12] = cropped;
567
+ $[13] = t8;
568
+ } else t8 = $[13];
569
+ let t9;
570
+ if ($[14] !== t7 || $[15] !== t8) {
571
+ t9 = /* @__PURE__ */ jsxs(Fragment, { children: [t7, t8] });
572
+ $[14] = t7;
573
+ $[15] = t8;
574
+ $[16] = t9;
575
+ } else t9 = $[16];
576
+ return t9;
577
+ }
501
578
  const initials = (name) => name.split(" ").map((part) => part[0]).join("").toUpperCase().slice(0, 2);
502
579
  const DRAFT_CARD_CLASSES = "bg-muted/50 flex w-full flex-col items-start gap-0.5 rounded-lg border px-3 py-2 text-left text-sm";
503
580
  /**
@@ -0,0 +1,349 @@
1
+ import { reads, useReadQuery } from "../../../lib/studio-reads.js";
2
+ import { cn } from "../../../lib/utils.js";
3
+ import { studioStore } from "../studioStore.js";
4
+ import { plural } from "../draftLabel.js";
5
+ import { useRenameDraft } from "./ReviewDraftSheet.js";
6
+ import { useDrafts } from "./DraftsControl.js";
7
+ import { c } from "react/compiler-runtime";
8
+ import * as React from "react";
9
+ import { jsx, jsxs } from "react/jsx-runtime";
10
+ import { useSelector } from "@xstate/store-react";
11
+ import { Button } from "@camox/ui/button";
12
+ import { Input } from "@camox/ui/input";
13
+ import { toast } from "@camox/ui/toaster";
14
+ import { Spinner } from "@camox/ui/spinner";
15
+ import { Popover, PopoverContent, PopoverDescription, PopoverHeader, PopoverTitle, PopoverTrigger } from "@camox/ui/popover";
16
+ import { FloatingToolbar } from "@camox/ui/floating-toolbar";
17
+
18
+ //#region src/features/studio/components/UnnamedDraftBar.tsx
19
+ /**
20
+ * How long a draft goes without a change before the bar shows, so it doesn't pop up mid-edit.
21
+ */
22
+ const SETTLE_MS = 1500;
23
+ /**
24
+ * Asks what to do with an unnamed draft, which only editing on Live starts: discard its
25
+ * changes, keep it as a draft by naming it, or publish it. It stays up until one of them ends
26
+ * the draft or names it, and stands in for the navbar's pending changes until then.
27
+ */
28
+ function UnnamedDraftBar(t0) {
29
+ const $ = c(23);
30
+ const { stacked } = t0;
31
+ const { projectId, current } = useDrafts();
32
+ const unnamed = current?.name === null ? current : null;
33
+ const keeping = useSelector(studioStore, _temp);
34
+ const settled = useSettled(unnamed?.id ?? null, unnamed?.lastChangedAt ?? null);
35
+ if (!projectId || !unnamed || !(settled || keeping)) return null;
36
+ const empty = unnamed.changeCount === 0;
37
+ const t1 = stacked ? "bottom-16" : "bottom-2";
38
+ let t2;
39
+ if ($[0] !== t1) {
40
+ t2 = cn("animate-in fade-in slide-in-from-bottom-4 fixed w-max gap-6 duration-300", t1);
41
+ $[0] = t1;
42
+ $[1] = t2;
43
+ } else t2 = $[1];
44
+ let t3;
45
+ if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
46
+ t3 = /* @__PURE__ */ jsx("span", {
47
+ "aria-hidden": true,
48
+ className: "size-2 shrink-0 rounded-full bg-blue-500"
49
+ });
50
+ $[2] = t3;
51
+ } else t3 = $[2];
52
+ let t4;
53
+ if ($[3] !== empty || $[4] !== unnamed.changeCount) {
54
+ t4 = empty ? "No changes yet" : plural(unnamed.changeCount, "pending change");
55
+ $[3] = empty;
56
+ $[4] = unnamed.changeCount;
57
+ $[5] = t4;
58
+ } else t4 = $[5];
59
+ let t5;
60
+ if ($[6] !== t4) {
61
+ t5 = /* @__PURE__ */ jsxs("span", {
62
+ className: "flex items-center gap-2 px-2 text-sm",
63
+ children: [t3, t4]
64
+ });
65
+ $[6] = t4;
66
+ $[7] = t5;
67
+ } else t5 = $[7];
68
+ let t6;
69
+ if ($[8] === Symbol.for("react.memo_cache_sentinel")) {
70
+ t6 = /* @__PURE__ */ jsx(Button, {
71
+ type: "button",
72
+ variant: "outline",
73
+ onClick: _temp2,
74
+ children: "Discard"
75
+ });
76
+ $[8] = t6;
77
+ } else t6 = $[8];
78
+ let t7;
79
+ if ($[9] !== empty || $[10] !== keeping || $[11] !== projectId || $[12] !== unnamed) {
80
+ t7 = /* @__PURE__ */ jsx(KeepDraft, {
81
+ projectId,
82
+ draft: unnamed,
83
+ open: keeping,
84
+ disabled: empty
85
+ });
86
+ $[9] = empty;
87
+ $[10] = keeping;
88
+ $[11] = projectId;
89
+ $[12] = unnamed;
90
+ $[13] = t7;
91
+ } else t7 = $[13];
92
+ let t8;
93
+ if ($[14] !== empty) {
94
+ t8 = /* @__PURE__ */ jsx(Button, {
95
+ type: "button",
96
+ disabled: empty,
97
+ onClick: _temp3,
98
+ children: "Publish…"
99
+ });
100
+ $[14] = empty;
101
+ $[15] = t8;
102
+ } else t8 = $[15];
103
+ let t9;
104
+ if ($[16] !== t7 || $[17] !== t8) {
105
+ t9 = /* @__PURE__ */ jsxs("div", {
106
+ className: "flex items-center gap-2",
107
+ children: [
108
+ t6,
109
+ t7,
110
+ t8
111
+ ]
112
+ });
113
+ $[16] = t7;
114
+ $[17] = t8;
115
+ $[18] = t9;
116
+ } else t9 = $[18];
117
+ let t10;
118
+ if ($[19] !== t2 || $[20] !== t5 || $[21] !== t9) {
119
+ t10 = /* @__PURE__ */ jsxs(FloatingToolbar, {
120
+ "aria-label": "Unnamed draft",
121
+ className: t2,
122
+ children: [t5, t9]
123
+ });
124
+ $[19] = t2;
125
+ $[20] = t5;
126
+ $[21] = t9;
127
+ $[22] = t10;
128
+ } else t10 = $[22];
129
+ return t10;
130
+ }
131
+ function _temp3() {
132
+ return studioStore.send({ type: "openReviewDraftSheet" });
133
+ }
134
+ function _temp2() {
135
+ return studioStore.send({ type: "openDeleteDraftDialog" });
136
+ }
137
+ function _temp(state) {
138
+ return state.context.isKeepDraftOpen;
139
+ }
140
+ /**
141
+ * Whether the draft went without a change for `SETTLE_MS`. Once settled it stays so for that
142
+ * draft, so the bar doesn't hide again on the next edit.
143
+ */
144
+ function useSettled(draftId, lastChangedAt) {
145
+ const $ = c(5);
146
+ const [settledDraft, setSettledDraft] = React.useState(null);
147
+ let t0;
148
+ let t1;
149
+ if ($[0] !== draftId || $[1] !== lastChangedAt || $[2] !== settledDraft) {
150
+ t0 = () => {
151
+ if (!draftId || settledDraft === draftId) return;
152
+ const wait = Math.max(0, (lastChangedAt ?? 0) + SETTLE_MS - Date.now());
153
+ const timer = setTimeout(() => setSettledDraft(draftId), wait);
154
+ return () => clearTimeout(timer);
155
+ };
156
+ t1 = [
157
+ draftId,
158
+ lastChangedAt,
159
+ settledDraft
160
+ ];
161
+ $[0] = draftId;
162
+ $[1] = lastChangedAt;
163
+ $[2] = settledDraft;
164
+ $[3] = t0;
165
+ $[4] = t1;
166
+ } else {
167
+ t0 = $[3];
168
+ t1 = $[4];
169
+ }
170
+ React.useEffect(t0, t1);
171
+ return !!draftId && settledDraft === draftId;
172
+ }
173
+ /** "Keep as draft": names the draft, which keeps it and ends the bar. */
174
+ function KeepDraft(t0) {
175
+ const $ = c(15);
176
+ const { projectId, draft, open, disabled } = t0;
177
+ let t1;
178
+ if ($[0] !== disabled) {
179
+ t1 = /* @__PURE__ */ jsx(Button, {
180
+ type: "button",
181
+ variant: "outline",
182
+ disabled
183
+ });
184
+ $[0] = disabled;
185
+ $[1] = t1;
186
+ } else t1 = $[1];
187
+ let t2;
188
+ if ($[2] !== disabled || $[3] !== t1) {
189
+ t2 = /* @__PURE__ */ jsx(PopoverTrigger, {
190
+ disabled,
191
+ render: t1,
192
+ children: "Keep as draft"
193
+ });
194
+ $[2] = disabled;
195
+ $[3] = t1;
196
+ $[4] = t2;
197
+ } else t2 = $[4];
198
+ let t3;
199
+ if ($[5] !== draft || $[6] !== open || $[7] !== projectId) {
200
+ t3 = open && /* @__PURE__ */ jsx(KeepDraftForm, {
201
+ projectId,
202
+ draft
203
+ });
204
+ $[5] = draft;
205
+ $[6] = open;
206
+ $[7] = projectId;
207
+ $[8] = t3;
208
+ } else t3 = $[8];
209
+ let t4;
210
+ if ($[9] !== t3) {
211
+ t4 = /* @__PURE__ */ jsx(PopoverContent, {
212
+ side: "top",
213
+ sideOffset: 16,
214
+ className: "w-80",
215
+ children: t3
216
+ });
217
+ $[9] = t3;
218
+ $[10] = t4;
219
+ } else t4 = $[10];
220
+ let t5;
221
+ if ($[11] !== open || $[12] !== t2 || $[13] !== t4) {
222
+ t5 = /* @__PURE__ */ jsxs(Popover, {
223
+ open,
224
+ onOpenChange: _temp4,
225
+ children: [t2, t4]
226
+ });
227
+ $[11] = open;
228
+ $[12] = t2;
229
+ $[13] = t4;
230
+ $[14] = t5;
231
+ } else t5 = $[14];
232
+ return t5;
233
+ }
234
+ function _temp4(next) {
235
+ return studioStore.send({ type: next ? "openKeepDraft" : "closeKeepDraft" });
236
+ }
237
+ /**
238
+ * The name to keep the draft under, filled with the short name its Draft Overview suggests until
239
+ * the editor types: AI names a few changes well, and better than a name typed before making them.
240
+ */
241
+ function KeepDraftForm(t0) {
242
+ const $ = c(24);
243
+ const { projectId, draft } = t0;
244
+ let t1;
245
+ if ($[0] !== draft.id || $[1] !== projectId) {
246
+ t1 = reads.publishPreview({
247
+ projectId,
248
+ draftId: draft.id
249
+ });
250
+ $[0] = draft.id;
251
+ $[1] = projectId;
252
+ $[2] = t1;
253
+ } else t1 = $[2];
254
+ const preview = useReadQuery(t1);
255
+ const overview = preview.data?.overview;
256
+ const suggested = overview?.status === "ready" ? overview.name : null;
257
+ const summarizing = !suggested && (preview.isPending || overview?.status === "pending");
258
+ const [typed, setTyped] = React.useState(null);
259
+ const name = typed ?? suggested ?? "";
260
+ const rename = useRenameDraft(projectId);
261
+ let t2;
262
+ if ($[3] !== draft.id || $[4] !== name || $[5] !== rename) {
263
+ t2 = (event) => {
264
+ event.preventDefault();
265
+ const kept = name.trim();
266
+ if (!kept) return;
267
+ rename.mutate({
268
+ draftId: draft.id,
269
+ name: kept
270
+ }, { onSuccess: () => {
271
+ studioStore.send({ type: "closeKeepDraft" });
272
+ toast.success(`Kept as "${kept}"`);
273
+ } });
274
+ };
275
+ $[3] = draft.id;
276
+ $[4] = name;
277
+ $[5] = rename;
278
+ $[6] = t2;
279
+ } else t2 = $[6];
280
+ let t3;
281
+ if ($[7] === Symbol.for("react.memo_cache_sentinel")) {
282
+ t3 = /* @__PURE__ */ jsxs(PopoverHeader, { children: [/* @__PURE__ */ jsx(PopoverTitle, { children: "Keep as draft" }), /* @__PURE__ */ jsx(PopoverDescription, { children: "Name it for what it changes, so you and your team can find it later." })] });
283
+ $[7] = t3;
284
+ } else t3 = $[7];
285
+ const t4 = summarizing ? "Summarizing your changes…" : "What does this draft change?";
286
+ let t5;
287
+ if ($[8] === Symbol.for("react.memo_cache_sentinel")) {
288
+ t5 = (event_0) => setTyped(event_0.target.value);
289
+ $[8] = t5;
290
+ } else t5 = $[8];
291
+ let t6;
292
+ if ($[9] !== name || $[10] !== t4) {
293
+ t6 = /* @__PURE__ */ jsx(Input, {
294
+ autoFocus: true,
295
+ "aria-label": "Draft name",
296
+ value: name,
297
+ maxLength: 200,
298
+ placeholder: t4,
299
+ onChange: t5
300
+ });
301
+ $[9] = name;
302
+ $[10] = t4;
303
+ $[11] = t6;
304
+ } else t6 = $[11];
305
+ let t7;
306
+ if ($[12] !== name || $[13] !== rename.isPending) {
307
+ t7 = !name.trim() || rename.isPending;
308
+ $[12] = name;
309
+ $[13] = rename.isPending;
310
+ $[14] = t7;
311
+ } else t7 = $[14];
312
+ let t8;
313
+ if ($[15] !== rename.isPending) {
314
+ t8 = rename.isPending ? /* @__PURE__ */ jsx(Spinner, {}) : null;
315
+ $[15] = rename.isPending;
316
+ $[16] = t8;
317
+ } else t8 = $[16];
318
+ let t9;
319
+ if ($[17] !== t7 || $[18] !== t8) {
320
+ t9 = /* @__PURE__ */ jsxs(Button, {
321
+ type: "submit",
322
+ disabled: t7,
323
+ children: [t8, "Keep draft"]
324
+ });
325
+ $[17] = t7;
326
+ $[18] = t8;
327
+ $[19] = t9;
328
+ } else t9 = $[19];
329
+ let t10;
330
+ if ($[20] !== t2 || $[21] !== t6 || $[22] !== t9) {
331
+ t10 = /* @__PURE__ */ jsxs("form", {
332
+ className: "flex flex-col gap-3",
333
+ onSubmit: t2,
334
+ children: [
335
+ t3,
336
+ t6,
337
+ t9
338
+ ]
339
+ });
340
+ $[20] = t2;
341
+ $[21] = t6;
342
+ $[22] = t9;
343
+ $[23] = t10;
344
+ } else t10 = $[23];
345
+ return t10;
346
+ }
347
+
348
+ //#endregion
349
+ export { SETTLE_MS, UnnamedDraftBar };
@@ -0,0 +1,10 @@
1
+ //#region src/features/studio/components/clientLogoPaths.ts
2
+ /** Each client's mark, as one SVG path on a 24×24 box. */
3
+ const CLIENT_LOGO_PATHS = {
4
+ claude: "m4.7144 15.9555 4.7174-2.6471.079-.2307-.079-.1275h-.2307l-.7893-.0486-2.6956-.0729-2.3375-.0971-2.2646-.1214-.5707-.1215-.5343-.7042.0546-.3522.4797-.3218.686.0608 1.5179.1032 2.2767.1578 1.6514.0972 2.4468.255h.3886l.0546-.1579-.1336-.0971-.1032-.0972L6.973 9.8356l-2.55-1.6879-1.3356-.9714-.7225-.4918-.3643-.4614-.1578-1.0078.6557-.7225.8803.0607.2246.0607.8925.686 1.9064 1.4754 2.4893 1.8336.3643.3035.1457-.1032.0182-.0728-.164-.2733-1.3539-2.4467-1.445-2.4893-.6435-1.032-.17-.6194c-.0607-.255-.1032-.4674-.1032-.7285L6.287.1335 6.6997 0l.9957.1336.419.3642.6192 1.4147 1.0018 2.2282 1.5543 3.0296.4553.8985.2429.8318.091.255h.1579v-.1457l.1275-1.706.2368-2.0947.2307-2.6957.0789-.7589.3764-.9107.7468-.4918.5828.2793.4797.686-.0668.4433-.2853 1.8517-.5586 2.9021-.3643 1.9429h.2125l.2429-.2429.9835-1.3053 1.6514-2.0643.7286-.8196.85-.9046.5464-.4311h1.0321l.759 1.1293-.34 1.1657-1.0625 1.3478-.8804 1.1414-1.2628 1.7-.7893 1.36.0729.1093.1882-.0183 2.8535-.607 1.5421-.2794 1.8396-.3157.8318.3886.091.3946-.3278.8075-1.967.4857-2.3072.4614-3.4364.8136-.0425.0304.0486.0607 1.5482.1457.6618.0364h1.621l3.0175.2247.7892.522.4736.6376-.079.4857-1.2142.6193-1.6393-.3886-3.825-.9107-1.3113-.3279h-.1822v.1093l1.0929 1.0686 2.0035 1.8092 2.5075 2.3314.1275.5768-.3218.4554-.34-.0486-2.2039-1.6575-.85-.7468-1.9246-1.621h-.1275v.17l.4432.6496 2.3436 3.5214.1214 1.0807-.17.3521-.6071.2125-.6679-.1214-1.3721-1.9246L14.38 17.959l-1.1414-1.9428-.1397.079-.674 7.2552-.3156.3703-.7286.2793-.6071-.4614-.3218-.7468.3218-1.4753.3886-1.9246.3157-1.53.2853-1.9004.17-.6314-.0121-.0425-.1397.0182-1.4328 1.9672-2.1796 2.9446-1.7243 1.8456-.4128.164-.7164-.3704.0667-.6618.4008-.5889 2.386-3.0357 1.4389-1.882.929-1.0868-.0062-.1579h-.0546l-6.3385 4.1164-1.1293.1457-.4857-.4554.0608-.7467.2307-.2429 1.9064-1.3114Z",
5
+ chatgpt: "M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z",
6
+ copilot: "M23.922 16.997C23.061 18.492 18.063 22.02 12 22.02 5.937 22.02.939 18.492.078 16.997A.641.641 0 0 1 0 16.741v-2.869a.883.883 0 0 1 .053-.22c.372-.935 1.347-2.292 2.605-2.656.167-.429.414-1.055.644-1.517a10.098 10.098 0 0 1-.052-1.086c0-1.331.282-2.499 1.132-3.368.397-.406.89-.717 1.474-.952C7.255 2.937 9.248 1.98 11.978 1.98c2.731 0 4.767.957 6.166 2.093.584.235 1.077.546 1.474.952.85.869 1.132 2.037 1.132 3.368 0 .368-.014.733-.052 1.086.23.462.477 1.088.644 1.517 1.258.364 2.233 1.721 2.605 2.656a.841.841 0 0 1 .053.22v2.869a.641.641 0 0 1-.078.256Zm-11.75-5.992h-.344a4.359 4.359 0 0 1-.355.508c-.77.947-1.918 1.492-3.508 1.492-1.725 0-2.989-.359-3.782-1.259a2.137 2.137 0 0 1-.085-.104L4 11.746v6.585c1.435.779 4.514 2.179 8 2.179 3.486 0 6.565-1.4 8-2.179v-6.585l-.098-.104s-.033.045-.085.104c-.793.9-2.057 1.259-3.782 1.259-1.59 0-2.738-.545-3.508-1.492a4.359 4.359 0 0 1-.355-.508Zm2.328 3.25c.549 0 1 .451 1 1v2c0 .549-.451 1-1 1-.549 0-1-.451-1-1v-2c0-.549.451-1 1-1Zm-5 0c.549 0 1 .451 1 1v2c0 .549-.451 1-1 1-.549 0-1-.451-1-1v-2c0-.549.451-1 1-1Zm3.313-6.185c.136 1.057.403 1.913.878 2.497.442.544 1.134.938 2.344.938 1.573 0 2.292-.337 2.657-.751.384-.435.558-1.15.558-2.361 0-1.14-.243-1.847-.705-2.319-.477-.488-1.319-.862-2.824-1.025-1.487-.161-2.192.138-2.533.529-.269.307-.437.808-.438 1.578v.021c0 .265.021.562.063.893Zm-1.626 0c.042-.331.063-.628.063-.894v-.02c-.001-.77-.169-1.271-.438-1.578-.341-.391-1.046-.69-2.533-.529-1.505.163-2.347.537-2.824 1.025-.462.472-.705 1.179-.705 2.319 0 1.211.175 1.926.558 2.361.365.414 1.084.751 2.657.751 1.21 0 1.902-.394 2.344-.938.475-.584.742-1.44.878-2.497Z"
7
+ };
8
+
9
+ //#endregion
10
+ export { CLIENT_LOGO_PATHS };
@@ -0,0 +1,37 @@
1
+ import { localeName } from "./localeOptions.js";
2
+ import { findingSentence } from "@camox/api-contract/patrol";
3
+
4
+ //#region src/features/studio/findingSentence.ts
5
+ /** Quoted as editors read it. */
6
+ const quoted = (text) => `“${text}”`;
7
+ /** What a dismissal dismissed, briefly: "Links to About", or the text it was about. */
8
+ function dismissedSentence(about) {
9
+ switch (about.kind) {
10
+ case "missingPage": return `Links to ${about.page ?? "a missing page"}`;
11
+ case "brokenLink": return `Links to ${about.url}`;
12
+ default: return quoted(about.text);
13
+ }
14
+ }
15
+ /** What dismissing a finding says, as its button says it: a broken link's is "This link is fine". */
16
+ function dismissLabel(about) {
17
+ return about.kind === "brokenLink" ? "This link is fine" : "Dismiss";
18
+ }
19
+ /** What applying a fix does, as its button says it. */
20
+ function fixLabel(fix) {
21
+ switch (fix.kind) {
22
+ case "removeLink": return "Remove the link";
23
+ case "replaceText": return `Replace with ${quoted(fix.replacement)}`;
24
+ }
25
+ }
26
+ /**
27
+ * What "Ask the agent" fills the draft's chat with: who found what, where, and the fix it
28
+ * suggests, for the agent to find among the draft's findings and fix.
29
+ */
30
+ function askAgentMessage(finding, role) {
31
+ const where = `(in ${localeName(finding.locale)}, field ${quoted(finding.field)})`;
32
+ const fix = finding.fix ? ` Suggested fix: ${fixLabel(finding.fix)}.` : "";
33
+ return `${role} finding: ${findingSentence(finding)} ${where}.${fix}`;
34
+ }
35
+
36
+ //#endregion
37
+ export { askAgentMessage, dismissLabel, dismissedSentence, findingSentence, fixLabel };
@@ -0,0 +1,40 @@
1
+ //#region src/features/studio/patrollerBio.ts
2
+ /** What each patroller does, wherever it works. */
3
+ const ABOUT = {
4
+ seoManager: "Writes what search engines read: each page's SEO, and each file's alt text and filename.",
5
+ translator: "Translates your website from its default language into every other one, so that each stays complete.",
6
+ proofreader: "Reads the text you write for spelling and grammar mistakes, and suggests a fix for each.",
7
+ linkChecker: "Looks for links that lead nowhere, to your own pages or elsewhere, and suggests a fix when it can."
8
+ };
9
+ /** What a patroller does, as its profile card introduces it. */
10
+ function aboutSentence(id) {
11
+ return ABOUT[id];
12
+ }
13
+ /**
14
+ * When a patroller works where the editor is, by its duty there, in a draft or on Live, as its
15
+ * card's tag says it, and whether it runs there at all. In a project with a single locale, the
16
+ * Translator waits for a second one.
17
+ */
18
+ function whenTag(id, duty, inDraft, singleLocale) {
19
+ const runs = (label) => ({
20
+ label,
21
+ runs: true
22
+ });
23
+ if (id === "translator" && singleLocale) return {
24
+ label: "Runs once you add a language",
25
+ runs: false
26
+ };
27
+ if (duty === "atPublish") return runs("Runs when you publish");
28
+ if (duty === "asYouGo") {
29
+ if (inDraft) return runs("Runs as you edit");
30
+ if (id === "linkChecker") return runs("Runs every 6 hours");
31
+ return runs("Runs while a release is translated");
32
+ }
33
+ return {
34
+ label: "Doesn't run on Live",
35
+ runs: false
36
+ };
37
+ }
38
+
39
+ //#endregion
40
+ export { aboutSentence, whenTag };
@@ -26,6 +26,21 @@ const PHRASES = {
26
26
  publishedCatchUp: {
27
27
  done: "Published the caught-up translations",
28
28
  failed: "Couldn't publish the caught-up translations"
29
+ },
30
+ removedLink: {
31
+ done: "Removed a link in",
32
+ failed: "Couldn't remove a link in",
33
+ locale: "in"
34
+ },
35
+ fixedLink: {
36
+ done: "Fixed a link in",
37
+ failed: "Couldn't fix a link in",
38
+ locale: "in"
39
+ },
40
+ fixedMistake: {
41
+ done: "Fixed a typo in",
42
+ failed: "Couldn't fix a typo in",
43
+ locale: "in"
29
44
  }
30
45
  };
31
46
  /** A report as a sentence: "Wrote SEO for Pricing in German", or the text an AI wrote. */
@@ -10,8 +10,11 @@ const studioStore = createStore({
10
10
  startRenaming: false
11
11
  },
12
12
  isDeleteDraftDialogOpen: false,
13
+ isKeepDraftOpen: false,
13
14
  localeDialog: null,
14
- websiteLocation: null
15
+ websiteLocation: null,
16
+ agentAsk: null,
17
+ agentAsks: 0
15
18
  },
16
19
  on: {
17
20
  openCommandPalette: (context) => {
@@ -76,6 +79,18 @@ const studioStore = createStore({
76
79
  isDeleteDraftDialogOpen: false
77
80
  };
78
81
  },
82
+ openKeepDraft: (context) => {
83
+ return {
84
+ ...context,
85
+ isKeepDraftOpen: true
86
+ };
87
+ },
88
+ closeKeepDraft: (context) => {
89
+ return {
90
+ ...context,
91
+ isKeepDraftOpen: false
92
+ };
93
+ },
79
94
  openLocaleDialog: (context, event) => {
80
95
  return {
81
96
  ...context,
@@ -88,6 +103,26 @@ const studioStore = createStore({
88
103
  localeDialog: null
89
104
  };
90
105
  },
106
+ askAgent: (context, event) => {
107
+ return {
108
+ ...context,
109
+ reviewDraftSheet: {
110
+ open: false,
111
+ startRenaming: false
112
+ },
113
+ agentAsk: {
114
+ text: event.text,
115
+ id: context.agentAsks + 1
116
+ },
117
+ agentAsks: context.agentAsks + 1
118
+ };
119
+ },
120
+ takeAgentAsk: (context) => {
121
+ return {
122
+ ...context,
123
+ agentAsk: null
124
+ };
125
+ },
91
126
  rememberWebsiteLocation: (context, event) => {
92
127
  const previous = context.websiteLocation;
93
128
  const { locale, path, search, hash } = event.location;