camox 0.54.0 → 0.57.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 (50) hide show
  1. package/dist/features/canvas/CamoxCanvas.js +14 -11
  2. package/dist/features/canvas/CanvasLoadingScreen.js +1 -1
  3. package/dist/features/canvas/CanvasPageHeader.js +1 -1
  4. package/dist/features/canvas/canvasFrameDocument.js +13 -6
  5. package/dist/features/canvas/sidebar/CanvasViewHeader.js +1 -1
  6. package/dist/features/canvas/sidebar/{CanvasSidebar.js → ContentSidebar.js} +5 -5
  7. package/dist/features/canvas/sidebar/{CanvasSidebarFrame.js → ContentSidebarFrame.js} +4 -4
  8. package/dist/features/canvas/sidebar/{CanvasSidebarSkeleton.js → ContentSidebarSkeleton.js} +7 -7
  9. package/dist/features/preview/CamoxPreview.js +2 -2
  10. package/dist/features/preview/components/Frame.js +1 -18
  11. package/dist/features/preview/components/PreviewToolbar.js +1 -1
  12. package/dist/features/preview/components/SelectionEditor.js +2 -2
  13. package/dist/features/preview/components/SidebarStack.js +201 -116
  14. package/dist/features/preview/components/StudioEditingFallback.js +3 -3
  15. package/dist/features/preview/components/previewStyles.js +25 -1
  16. package/dist/features/renderer/RendererApp.js +55 -37
  17. package/dist/features/routes/pageRuntime.d.ts +1 -1
  18. package/dist/features/runtime/RuntimeChrome.js +4 -4
  19. package/dist/features/studio/components/AgentSidebar.js +56 -0
  20. package/dist/features/studio/components/ChatWidget.js +219 -0
  21. package/dist/features/studio/components/DraftsControl.js +17 -17
  22. package/dist/features/studio/components/Patrol.js +676 -0
  23. package/dist/features/studio/components/ReviewDraftSheet.js +38 -38
  24. package/dist/features/studio/components/Squareling.js +171 -84
  25. package/dist/features/studio/components/ThreadMessages.js +256 -136
  26. package/dist/features/studio/localeOptions.js +7 -3
  27. package/dist/features/studio/reportSentence.js +75 -0
  28. package/dist/features/studio/routes.js +2 -1
  29. package/dist/features/studio/showInCanvas.js +9 -2
  30. package/dist/features/studio/studioStore.js +5 -5
  31. package/dist/features/studio/workingSentence.js +55 -0
  32. package/dist/lib/queries.js +1 -1
  33. package/dist/lib/studio-reads.d.ts +1 -0
  34. package/dist/lib/studio-reads.js +5 -0
  35. package/dist/lib/use-thread.js +156 -190
  36. package/dist/package.js +1 -1
  37. package/dist/studio.css +1 -1
  38. package/package.json +4 -4
  39. package/skills/camox/SKILL.md +3 -10
  40. package/skills/camox/references/artifacts.md +2 -2
  41. package/skills/camox/references/block-definitions.md +1 -1
  42. package/skills/camox/references/layout-definitions.md +1 -1
  43. package/skills/camox/references/routed-layouts.md +2 -2
  44. package/skills/camox-cli/SKILL.md +15 -0
  45. package/skills/{camox → camox-cli}/references/cli-common.md +1 -1
  46. package/skills/{camox → camox-cli}/references/cli-content.md +2 -2
  47. package/skills/{camox → camox-cli}/references/cli-publishing.md +4 -4
  48. package/skills/camox-expand/SKILL.md +1 -1
  49. package/skills/camoxify/SKILL.md +1 -1
  50. package/dist/features/studio/components/ChatSidebar.js +0 -346
@@ -1,6 +1,8 @@
1
+ import { cn } from "../../../lib/utils.js";
1
2
  import { Squareling } from "./Squareling.js";
2
3
  import { ThreadMarkdown } from "./ThreadMarkdown.js";
3
4
  import { c } from "react/compiler-runtime";
5
+ import * as React from "react";
4
6
  import { jsx, jsxs } from "react/jsx-runtime";
5
7
  import { Avatar, AvatarFallback, AvatarImage } from "@camox/ui/avatar";
6
8
  import { Bubble, BubbleContent } from "@camox/ui/bubble";
@@ -9,38 +11,43 @@ import { MessageScroller, MessageScrollerButton, MessageScrollerContent, Message
9
11
 
10
12
  //#region src/features/studio/components/ThreadMessages.tsx
11
13
  /**
12
- * A thread's messages, and the agent's squareling at work while a run is going or a message is
13
- * still `sending`: on its way to the thread, which hasn't accepted it yet. Its face says whether
14
- * the agent thinks or acts (uses its tools, or builds its code). While the end of a turn builds
15
- * the agent's code, or the agent fixes a build error, it says so in words too. Each editor's message shows its sender's
16
- * avatar, without their name. A fresh Live thread links to the draft the editor's previous one
17
- * continued in (`continuedIn`).
14
+ * A thread's messages, and the agent's squareling, which always lives in it. An empty thread has
15
+ * it greet the editor; when the first message arrives, it moves to the end of the thread, and
16
+ * stays there after each message. While a run is going or a message is still `sending` (on its
17
+ * way to the thread, which hasn't accepted it yet), its face says whether the agent thinks or
18
+ * acts (uses its tools, or builds its code). While the end of a turn builds the agent's code, or
19
+ * the agent fixes a build error, it says so in words too. Each editor's message shows its
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.
18
22
  */
19
23
  function ThreadMessages(t0) {
20
24
  const $ = c(20);
21
- const { view, sending: t1, continuedIn } = t0;
25
+ const { view, sending: t1, onOpenDraft } = t0;
22
26
  const sending = t1 === void 0 ? false : t1;
23
27
  const working = view.running || sending;
24
- if (view.messages.length === 0 && !working) {
28
+ const empty = view.messages.length === 0 && !working;
29
+ const squareling = useSquarelingMorph(empty);
30
+ if (empty) {
25
31
  let t2;
26
- if ($[0] !== continuedIn) {
27
- t2 = continuedIn && /* @__PURE__ */ jsx(ContinuedIn, { link: continuedIn });
28
- $[0] = continuedIn;
32
+ if ($[0] !== squareling) {
33
+ t2 = /* @__PURE__ */ jsx(Squareling, {
34
+ ref: squareling,
35
+ status: "idle",
36
+ className: "size-10"
37
+ });
38
+ $[0] = squareling;
29
39
  $[1] = t2;
30
40
  } else t2 = $[1];
31
41
  let t3;
32
42
  if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
33
- t3 = /* @__PURE__ */ jsx("p", {
34
- "data-thread-empty": true,
35
- className: "text-muted-foreground flex min-h-0 flex-1 items-center justify-center p-4 text-center text-sm",
36
- children: "Ask Camox about your website’s content."
37
- });
43
+ t3 = /* @__PURE__ */ jsx("p", { children: "How can I help?" });
38
44
  $[2] = t3;
39
45
  } else t3 = $[2];
40
46
  let t4;
41
47
  if ($[3] !== t2) {
42
48
  t4 = /* @__PURE__ */ jsxs("div", {
43
- className: "flex min-h-0 flex-1 flex-col",
49
+ "data-thread-empty": true,
50
+ className: "text-muted-foreground flex min-h-0 flex-1 flex-col items-center justify-center gap-3 p-4 text-center text-sm",
44
51
  children: [t2, t3]
45
52
  });
46
53
  $[3] = t2;
@@ -49,108 +56,155 @@ function ThreadMessages(t0) {
49
56
  return t4;
50
57
  }
51
58
  let t2;
52
- if ($[5] !== continuedIn) {
53
- t2 = continuedIn && /* @__PURE__ */ jsx(ContinuedIn, { link: continuedIn });
54
- $[5] = continuedIn;
55
- $[6] = t2;
56
- } else t2 = $[6];
57
- let t3;
58
- if ($[7] !== view || $[8] !== working) {
59
- t3 = view.messages.map((message, index) => /* @__PURE__ */ jsxs(MessageScrollerItem, {
59
+ if ($[5] !== onOpenDraft || $[6] !== squareling || $[7] !== view || $[8] !== working) {
60
+ t2 = view.messages.map((message, index) => /* @__PURE__ */ jsxs(MessageScrollerItem, {
60
61
  messageId: message.id,
61
62
  scrollAnchor: message.from === "editor",
62
63
  className: "flex flex-col gap-4",
63
- children: [/* @__PURE__ */ jsx(ThreadMessageRow, { message }), working && index === view.messages.length - 1 && /* @__PURE__ */ jsx(Working, { view })]
64
+ children: [/* @__PURE__ */ jsx(ThreadMessageRow, {
65
+ message,
66
+ onOpenDraft
67
+ }), index === view.messages.length - 1 && /* @__PURE__ */ jsx(AgentPresence, {
68
+ view,
69
+ working,
70
+ squareling
71
+ })]
64
72
  }, message.id));
73
+ $[5] = onOpenDraft;
74
+ $[6] = squareling;
65
75
  $[7] = view;
66
76
  $[8] = working;
67
- $[9] = t3;
68
- } else t3 = $[9];
77
+ $[9] = t2;
78
+ } else t2 = $[9];
79
+ let t3;
80
+ if ($[10] !== squareling || $[11] !== view || $[12] !== working) {
81
+ t3 = view.messages.length === 0 && /* @__PURE__ */ jsx(AgentPresence, {
82
+ view,
83
+ working,
84
+ squareling
85
+ });
86
+ $[10] = squareling;
87
+ $[11] = view;
88
+ $[12] = working;
89
+ $[13] = t3;
90
+ } else t3 = $[13];
69
91
  let t4;
70
- if ($[10] !== view || $[11] !== working) {
71
- t4 = working && view.messages.length === 0 && /* @__PURE__ */ jsx(Working, { view });
72
- $[10] = view;
73
- $[11] = working;
74
- $[12] = t4;
75
- } else t4 = $[12];
76
- let t5;
77
- if ($[13] !== t2 || $[14] !== t3 || $[15] !== t4) {
78
- t5 = /* @__PURE__ */ jsx(MessageScrollerViewport, { children: /* @__PURE__ */ jsxs(MessageScrollerContent, {
92
+ if ($[14] !== t2 || $[15] !== t3) {
93
+ t4 = /* @__PURE__ */ jsx(MessageScrollerViewport, { children: /* @__PURE__ */ jsxs(MessageScrollerContent, {
79
94
  className: "flex flex-col gap-4 px-2 py-3",
80
- children: [
81
- t2,
82
- t3,
83
- t4
84
- ]
95
+ children: [t2, t3]
85
96
  }) });
86
- $[13] = t2;
87
- $[14] = t3;
88
- $[15] = t4;
89
- $[16] = t5;
90
- } else t5 = $[16];
91
- let t6;
97
+ $[14] = t2;
98
+ $[15] = t3;
99
+ $[16] = t4;
100
+ } else t4 = $[16];
101
+ let t5;
92
102
  if ($[17] === Symbol.for("react.memo_cache_sentinel")) {
93
- t6 = /* @__PURE__ */ jsx(MessageScrollerButton, {});
94
- $[17] = t6;
95
- } else t6 = $[17];
96
- let t7;
97
- if ($[18] !== t5) {
98
- t7 = /* @__PURE__ */ jsx(MessageScrollerProvider, { children: /* @__PURE__ */ jsxs(MessageScroller, {
103
+ t5 = /* @__PURE__ */ jsx(MessageScrollerButton, {});
104
+ $[17] = t5;
105
+ } else t5 = $[17];
106
+ let t6;
107
+ if ($[18] !== t4) {
108
+ t6 = /* @__PURE__ */ jsx(MessageScrollerProvider, { children: /* @__PURE__ */ jsxs(MessageScroller, {
99
109
  className: "min-h-0 flex-1",
100
- children: [t5, t6]
110
+ children: [t4, t5]
101
111
  }) });
102
- $[18] = t5;
103
- $[19] = t7;
104
- } else t7 = $[19];
105
- return t7;
112
+ $[18] = t4;
113
+ $[19] = t6;
114
+ } else t6 = $[19];
115
+ return t6;
106
116
  }
117
+ /** How long the greeting's squareling takes to reach the end of the thread, in ms. */
118
+ const MORPH_DURATION = 350;
119
+ /** A strong ease-in-out, for moving across the screen. */
120
+ const MORPH_EASING = "cubic-bezier(0.77, 0, 0.175, 1)";
107
121
  /**
108
- * Shown in the last message, not after it: the scroller only brings a message the editor just
109
- * sent into view when it's appended last, and it arrives while the agent is already working.
122
+ * Moves the squareling from where the greeting showed it to the end of the thread, as the first
123
+ * message arrives, so it reads as the same one. Without motion for editors who reduce it.
110
124
  */
111
- function Working(t0) {
112
- const $ = c(10);
113
- const { view: t1 } = t0;
125
+ function useSquarelingMorph(empty) {
126
+ const $ = c(2);
127
+ const node = React.useRef(null);
128
+ const greeting = React.useRef(null);
129
+ let t0;
130
+ if ($[0] !== empty) {
131
+ t0 = () => {
132
+ const element = node.current;
133
+ if (!element) return;
134
+ if (empty) {
135
+ greeting.current = element.getBoundingClientRect();
136
+ return;
137
+ }
138
+ const from = greeting.current;
139
+ greeting.current = null;
140
+ if (!from || matchMedia("(prefers-reduced-motion: reduce)").matches) return;
141
+ const to = element.getBoundingClientRect();
142
+ if (to.width === 0) return;
143
+ const dx = from.left + from.width / 2 - (to.left + to.width / 2);
144
+ const dy = from.top + from.height / 2 - (to.top + to.height / 2);
145
+ element.animate([{ transform: `translate(${dx}px, ${dy}px) scale(${from.width / to.width})` }, { transform: "none" }], {
146
+ duration: MORPH_DURATION,
147
+ easing: MORPH_EASING
148
+ });
149
+ };
150
+ $[0] = empty;
151
+ $[1] = t0;
152
+ } else t0 = $[1];
153
+ React.useLayoutEffect(t0);
154
+ return node;
155
+ }
156
+ /**
157
+ * The squareling at the end of the thread: idle between turns, at work during one. Shown in the
158
+ * last message, not after it: the scroller only brings a message the editor just sent into view
159
+ * when it's appended last, and it arrives while the agent is already working.
160
+ */
161
+ function AgentPresence(t0) {
162
+ const $ = c(13);
163
+ const { view: t1, working, squareling } = t0;
114
164
  const { status, usingTools } = t1;
115
165
  let t2;
116
- if ($[0] !== status || $[1] !== usingTools) {
117
- t2 = squarelingStatus(status, usingTools);
166
+ if ($[0] !== status || $[1] !== usingTools || $[2] !== working) {
167
+ t2 = working ? squarelingStatus(status, usingTools) : "idle";
118
168
  $[0] = status;
119
169
  $[1] = usingTools;
120
- $[2] = t2;
121
- } else t2 = $[2];
170
+ $[2] = working;
171
+ $[3] = t2;
172
+ } else t2 = $[3];
122
173
  let t3;
123
- if ($[3] !== t2) {
174
+ if ($[4] !== squareling || $[5] !== t2) {
124
175
  t3 = /* @__PURE__ */ jsx(Squareling, {
176
+ ref: squareling,
125
177
  status: t2,
126
178
  className: "size-6"
127
179
  });
128
- $[3] = t2;
129
- $[4] = t3;
130
- } else t3 = $[4];
180
+ $[4] = squareling;
181
+ $[5] = t2;
182
+ $[6] = t3;
183
+ } else t3 = $[6];
131
184
  let t4;
132
- if ($[5] !== status) {
133
- t4 = status ? /* @__PURE__ */ jsx("span", {
185
+ if ($[7] !== status || $[8] !== working) {
186
+ t4 = working && (status ? /* @__PURE__ */ jsx("span", {
134
187
  className: "shimmer",
135
188
  children: STATUS_LINES[status]
136
189
  }) : /* @__PURE__ */ jsx("span", {
137
190
  className: "sr-only",
138
191
  children: "Working…"
139
- });
140
- $[5] = status;
141
- $[6] = t4;
142
- } else t4 = $[6];
192
+ }));
193
+ $[7] = status;
194
+ $[8] = working;
195
+ $[9] = t4;
196
+ } else t4 = $[9];
143
197
  let t5;
144
- if ($[7] !== t3 || $[8] !== t4) {
198
+ if ($[10] !== t3 || $[11] !== t4) {
145
199
  t5 = /* @__PURE__ */ jsxs("div", {
146
200
  role: "status",
147
201
  className: "text-muted-foreground flex items-center gap-2 text-sm",
148
202
  children: [t3, t4]
149
203
  });
150
- $[7] = t3;
151
- $[8] = t4;
152
- $[9] = t5;
153
- } else t5 = $[9];
204
+ $[10] = t3;
205
+ $[11] = t4;
206
+ $[12] = t5;
207
+ } else t5 = $[12];
154
208
  return t5;
155
209
  }
156
210
  function squarelingStatus(status, usingTools) {
@@ -162,41 +216,54 @@ const STATUS_LINES = {
162
216
  fixing: "Fixing a build error…"
163
217
  };
164
218
  function ThreadMessageRow(t0) {
165
- const $ = c(14);
166
- const { message } = t0;
219
+ const $ = c(17);
220
+ const { message, onOpenDraft } = t0;
221
+ if (message.from === "draft") {
222
+ let t1;
223
+ if ($[0] !== message || $[1] !== onOpenDraft) {
224
+ t1 = /* @__PURE__ */ jsx(Message, { children: /* @__PURE__ */ jsx(MessageContent, { children: /* @__PURE__ */ jsx(DraftCard, {
225
+ message,
226
+ onOpenDraft
227
+ }) }) });
228
+ $[0] = message;
229
+ $[1] = onOpenDraft;
230
+ $[2] = t1;
231
+ } else t1 = $[2];
232
+ return t1;
233
+ }
167
234
  if (message.from === "stopped" || message.from === "failed") {
168
235
  const t1 = message.from === "stopped" ? `Stopped by ${message.sender.name}` : "Camox couldn’t finish this. Send your message again to retry.";
169
236
  let t2;
170
- if ($[0] !== t1) {
237
+ if ($[3] !== t1) {
171
238
  t2 = /* @__PURE__ */ jsx(Message, { children: /* @__PURE__ */ jsx(MessageContent, { children: /* @__PURE__ */ jsx("p", {
172
239
  className: "text-muted-foreground text-sm",
173
240
  children: t1
174
241
  }) }) });
175
- $[0] = t1;
176
- $[1] = t2;
177
- } else t2 = $[1];
242
+ $[3] = t1;
243
+ $[4] = t2;
244
+ } else t2 = $[4];
178
245
  return t2;
179
246
  }
180
247
  if (message.from === "agent") {
181
248
  let t1;
182
- if ($[2] !== message.text) {
249
+ if ($[5] !== message.text) {
183
250
  t1 = /* @__PURE__ */ jsx(Message, { children: /* @__PURE__ */ jsx(MessageContent, { children: /* @__PURE__ */ jsx(ThreadMarkdown, { text: message.text }) }) });
184
- $[2] = message.text;
185
- $[3] = t1;
186
- } else t1 = $[3];
251
+ $[5] = message.text;
252
+ $[6] = t1;
253
+ } else t1 = $[6];
187
254
  return t1;
188
255
  }
189
256
  if (message.from === "camox") {
190
257
  let t1;
191
- if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
258
+ if ($[7] === Symbol.for("react.memo_cache_sentinel")) {
192
259
  t1 = /* @__PURE__ */ jsx(MessageHeader, {
193
260
  className: "text-muted-foreground text-xs",
194
261
  children: "Camox"
195
262
  });
196
- $[4] = t1;
197
- } else t1 = $[4];
263
+ $[7] = t1;
264
+ } else t1 = $[7];
198
265
  let t2;
199
- if ($[5] !== message.text) {
266
+ if ($[8] !== message.text) {
200
267
  t2 = /* @__PURE__ */ jsx(Message, { children: /* @__PURE__ */ jsxs(MessageContent, { children: [t1, /* @__PURE__ */ jsx(Bubble, {
201
268
  variant: "muted",
202
269
  className: "max-w-full",
@@ -205,19 +272,19 @@ function ThreadMessageRow(t0) {
205
272
  children: message.text
206
273
  })
207
274
  })] }) });
208
- $[5] = message.text;
209
- $[6] = t2;
210
- } else t2 = $[6];
275
+ $[8] = message.text;
276
+ $[9] = t2;
277
+ } else t2 = $[9];
211
278
  return t2;
212
279
  }
213
280
  let t1;
214
- if ($[7] !== message.sender) {
281
+ if ($[10] !== message.sender) {
215
282
  t1 = /* @__PURE__ */ jsx(MessageAvatar, { children: /* @__PURE__ */ jsx(SenderAvatar, { sender: message.sender }) });
216
- $[7] = message.sender;
217
- $[8] = t1;
218
- } else t1 = $[8];
283
+ $[10] = message.sender;
284
+ $[11] = t1;
285
+ } else t1 = $[11];
219
286
  let t2;
220
- if ($[9] !== message.text) {
287
+ if ($[12] !== message.text) {
221
288
  t2 = /* @__PURE__ */ jsx(MessageContent, { children: /* @__PURE__ */ jsx(Bubble, {
222
289
  align: "end",
223
290
  variant: "muted",
@@ -227,47 +294,100 @@ function ThreadMessageRow(t0) {
227
294
  children: message.text
228
295
  })
229
296
  }) });
230
- $[9] = message.text;
231
- $[10] = t2;
232
- } else t2 = $[10];
297
+ $[12] = message.text;
298
+ $[13] = t2;
299
+ } else t2 = $[13];
233
300
  let t3;
234
- if ($[11] !== t1 || $[12] !== t2) {
301
+ if ($[14] !== t1 || $[15] !== t2) {
235
302
  t3 = /* @__PURE__ */ jsxs(Message, {
236
303
  align: "end",
237
304
  children: [t1, t2]
238
305
  });
239
- $[11] = t1;
240
- $[12] = t2;
241
- $[13] = t3;
242
- } else t3 = $[13];
306
+ $[14] = t1;
307
+ $[15] = t2;
308
+ $[16] = t3;
309
+ } else t3 = $[16];
243
310
  return t3;
244
311
  }
245
- function ContinuedIn(t0) {
246
- const $ = c(4);
247
- const { link } = t0;
312
+ const initials = (name) => name.split(" ").map((part) => part[0]).join("").toUpperCase().slice(0, 2);
313
+ 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";
314
+ /**
315
+ * The card of the draft the agent on Live handed the request off to, where its agent does the
316
+ * work: it opens the draft. Once the draft ended, there's nothing to open: it says how it ended.
317
+ */
318
+ function DraftCard(t0) {
319
+ const $ = c(15);
320
+ const { message, onOpenDraft } = t0;
248
321
  let t1;
249
- if ($[0] !== link.href || $[1] !== link.label || $[2] !== link.onOpen) {
250
- t1 = /* @__PURE__ */ jsxs("p", {
251
- className: "text-muted-foreground px-2 pt-3 text-center text-sm",
252
- children: [
253
- "Continued in",
254
- " ",
255
- /* @__PURE__ */ jsx("a", {
256
- href: link.href,
257
- onClick: link.onOpen,
258
- className: "text-foreground underline",
259
- children: link.label
260
- })
261
- ]
322
+ if ($[0] !== message.name) {
323
+ t1 = /* @__PURE__ */ jsx("span", {
324
+ className: "font-medium",
325
+ children: message.name
262
326
  });
263
- $[0] = link.href;
264
- $[1] = link.label;
265
- $[2] = link.onOpen;
266
- $[3] = t1;
267
- } else t1 = $[3];
268
- return t1;
327
+ $[0] = message.name;
328
+ $[1] = t1;
329
+ } else t1 = $[1];
330
+ const name = t1;
331
+ if (message.ended) {
332
+ const t2 = message.ended === "published" ? "Published" : "Deleted";
333
+ let t3;
334
+ if ($[2] !== t2) {
335
+ t3 = /* @__PURE__ */ jsx("span", {
336
+ className: "text-muted-foreground text-xs",
337
+ children: t2
338
+ });
339
+ $[2] = t2;
340
+ $[3] = t3;
341
+ } else t3 = $[3];
342
+ let t4;
343
+ if ($[4] !== name || $[5] !== t3) {
344
+ t4 = /* @__PURE__ */ jsxs("div", {
345
+ "data-thread-draft-card": true,
346
+ className: DRAFT_CARD_CLASSES,
347
+ children: [t3, name]
348
+ });
349
+ $[4] = name;
350
+ $[5] = t3;
351
+ $[6] = t4;
352
+ } else t4 = $[6];
353
+ return t4;
354
+ }
355
+ let t2;
356
+ if ($[7] !== message.draftId || $[8] !== onOpenDraft) {
357
+ t2 = () => onOpenDraft?.(message.draftId);
358
+ $[7] = message.draftId;
359
+ $[8] = onOpenDraft;
360
+ $[9] = t2;
361
+ } else t2 = $[9];
362
+ let t3;
363
+ let t4;
364
+ if ($[10] === Symbol.for("react.memo_cache_sentinel")) {
365
+ t3 = cn(DRAFT_CARD_CLASSES, "hover:bg-muted focus-visible:ring-ring transition-colors focus-visible:ring-2 focus-visible:outline-none");
366
+ t4 = /* @__PURE__ */ jsx("span", {
367
+ className: "text-muted-foreground text-xs",
368
+ children: "Started a draft"
369
+ });
370
+ $[10] = t3;
371
+ $[11] = t4;
372
+ } else {
373
+ t3 = $[10];
374
+ t4 = $[11];
375
+ }
376
+ let t5;
377
+ if ($[12] !== name || $[13] !== t2) {
378
+ t5 = /* @__PURE__ */ jsxs("button", {
379
+ type: "button",
380
+ "data-thread-draft-card": true,
381
+ onClick: t2,
382
+ className: t3,
383
+ children: [t4, name]
384
+ });
385
+ $[12] = name;
386
+ $[13] = t2;
387
+ $[14] = t5;
388
+ } else t5 = $[14];
389
+ return t5;
269
390
  }
270
- const initials = (name) => name.split(" ").map((part) => part[0]).join("").toUpperCase().slice(0, 2);
271
391
  function SenderAvatar(t0) {
272
392
  const $ = c(10);
273
393
  const { sender } = t0;
@@ -2,9 +2,13 @@
2
2
  const localeNames = new Intl.DisplayNames(["en"], { type: "language" });
3
3
  /** A locale's name as the studio shows it, "German (de)", or its tag alone when unnamed. */
4
4
  function localeLabel(tag) {
5
+ const name = localeName(tag);
6
+ return name !== tag ? `${name} (${tag})` : tag;
7
+ }
8
+ /** A locale's name in the studio's language, "German", or its tag alone when unnamed. */
9
+ function localeName(tag) {
5
10
  try {
6
- const name = localeNames.of(tag);
7
- return name && name !== tag ? `${name} (${tag})` : tag;
11
+ return localeNames.of(tag) ?? tag;
8
12
  } catch {
9
13
  return tag;
10
14
  }
@@ -115,4 +119,4 @@ function localeOptions(existing) {
115
119
  }
116
120
 
117
121
  //#endregion
118
- export { localeLabel, localeOptions };
122
+ export { localeLabel, localeName, localeOptions };
@@ -0,0 +1,75 @@
1
+ import { localeName } from "./localeOptions.js";
2
+ import { reportLocale } from "@camox/api-contract/patrol";
3
+
4
+ //#region src/features/studio/reportSentence.ts
5
+ const PHRASES = {
6
+ translated: {
7
+ done: "Translated",
8
+ failed: "Couldn't translate",
9
+ locale: "to"
10
+ },
11
+ mergedWithLive: {
12
+ done: "Translated",
13
+ failed: "Couldn't translate",
14
+ locale: "to"
15
+ },
16
+ wroteSeo: {
17
+ done: "Wrote SEO for",
18
+ failed: "Couldn't write SEO for",
19
+ locale: "in"
20
+ },
21
+ describedFile: {
22
+ done: "Described",
23
+ failed: "Couldn't describe",
24
+ locale: "in"
25
+ },
26
+ publishedCatchUp: {
27
+ done: "Published the caught-up translations",
28
+ failed: "Couldn't publish the caught-up translations"
29
+ }
30
+ };
31
+ /** A report as a sentence: "Wrote SEO for Pricing in German", or the text an AI wrote. */
32
+ function reportSentence(summary, outcome) {
33
+ if (summary.kind === "text") return summary.text;
34
+ const phrase = PHRASES[summary.verb];
35
+ const parts = [phrase[outcome]];
36
+ if (summary.subject) parts.push(summary.subject.name);
37
+ if (summary.locale && phrase.locale) parts.push(phrase.locale, localeName(summary.locale));
38
+ return parts.join(" ");
39
+ }
40
+ /**
41
+ * Where clicking a report leads, as its accessible name ends: "show it on the Canvas in German",
42
+ * "open the file on Live". Null for a report that leads nowhere: one without a place.
43
+ */
44
+ function whereReportLeads({ summary, place, live }, inDraft) {
45
+ if (!place) return null;
46
+ const locale = reportLocale(summary);
47
+ const inLocale = locale ? ` in ${localeName(locale)}` : "";
48
+ const onLive = inDraft && live;
49
+ if (place.kind === "file") return `open the file${onLive ? " on Live" : ""}`;
50
+ if (place.kind === "record") return `open it${inLocale}${onLive ? " on Live" : ""}`;
51
+ return `show it on ${onLive ? "Live's" : "the"} Canvas${inLocale}`;
52
+ }
53
+ /** The reports not shown, by locale: "…and 212 more in German, 3 in French". */
54
+ function moreSentence(more) {
55
+ return `…and ${more.map(({ locale, count }, index) => {
56
+ const where = locale === null ? "" : ` in ${localeName(locale)}`;
57
+ return `${count}${index === 0 ? " more" : ""}${where}`;
58
+ }).join(", ")}`;
59
+ }
60
+ const SHORT_UNITS = [
61
+ ["y", 31536e6],
62
+ ["w", 6048e5],
63
+ ["d", 864e5],
64
+ ["h", 36e5],
65
+ ["m", 6e4]
66
+ ];
67
+ /** How long ago a moment was, briefly: "now", "2m", "1h". */
68
+ function shortTimeAgo(at, now = Date.now()) {
69
+ const elapsed = now - at;
70
+ for (const [unit, size] of SHORT_UNITS) if (elapsed >= size) return `${Math.floor(elapsed / size)}${unit}`;
71
+ return "now";
72
+ }
73
+
74
+ //#endregion
75
+ export { moreSentence, reportSentence, shortTimeAgo, whereReportLeads };
@@ -6,6 +6,7 @@ const STUDIO_ASSETS_PATH = `${STUDIO_CONTENT_PATH}/assets`;
6
6
  /** The Settings Section, where members change Project Settings. */
7
7
  const STUDIO_SETTINGS_PATH = `${STUDIO_BASE_PATH}/settings`;
8
8
  const collectionContentPath = (collectionId) => `${STUDIO_CONTENT_PATH}/collections/${encodeURIComponent(collectionId)}`;
9
+ const editCollectionItemPath = (collectionId, id) => `${collectionContentPath(collectionId)}/${encodeURIComponent(id)}/edit`;
9
10
  function matchCollectionContentPath(pathname) {
10
11
  const match = /^\/camox\/content\/collections\/([^/]+)(?:(\/new)|\/([^/]+)\/edit)?$/.exec(pathname);
11
12
  if (!match) return null;
@@ -31,4 +32,4 @@ function matchCollectionContentPath(pathname) {
31
32
  const STUDIO_CANVAS_PATH = `${STUDIO_BASE_PATH}/canvas`;
32
33
 
33
34
  //#endregion
34
- export { STUDIO_ASSETS_PATH, STUDIO_BASE_PATH, STUDIO_CANVAS_PATH, STUDIO_CONTENT_PATH, STUDIO_ROUTE_SEGMENT, STUDIO_SETTINGS_PATH, collectionContentPath, matchCollectionContentPath };
35
+ export { STUDIO_ASSETS_PATH, STUDIO_BASE_PATH, STUDIO_CANVAS_PATH, STUDIO_CONTENT_PATH, STUDIO_ROUTE_SEGMENT, STUDIO_SETTINGS_PATH, collectionContentPath, editCollectionItemPath, matchCollectionContentPath };
@@ -97,7 +97,11 @@ function useShowInCanvas() {
97
97
  if ($[1] !== cache || $[2] !== navigate || $[3] !== projectUuid || $[4] !== search || $[5] !== view.locale || $[6] !== view.locales) {
98
98
  t1 = async (target) => {
99
99
  const locale = view.locales.find((each) => each.tag === target.locale) ?? view.locale;
100
- const page = (await cache.ensure(reads.pageList({ projectUuid }), { locale }).catch(_temp2)).find((each_0) => each_0.id === target.pageId);
100
+ const inDraft = target.draft === void 0 ? {} : { in: target.draft };
101
+ const page = (await cache.ensure(reads.pageList({ projectUuid }), {
102
+ locale,
103
+ ...inDraft
104
+ }).catch(_temp2)).find((each_0) => each_0.id === target.pageId);
101
105
  if (!page) return;
102
106
  const selection = selectionOf(target);
103
107
  if (selection) selectOnceShown({
@@ -106,7 +110,10 @@ function useShowInCanvas() {
106
110
  }, selection);
107
111
  else stopSelecting?.();
108
112
  previewStore.send({ type: "showCanvas" });
109
- await navigate({ to: `${localizedPath(page.fullPath, locale)}${search}` });
113
+ await navigate({
114
+ to: `${localizedPath(page.fullPath, locale)}${search}`,
115
+ ...target.draft === void 0 ? {} : { draft: target.draft }
116
+ });
110
117
  };
111
118
  $[1] = cache;
112
119
  $[2] = navigate;