camox 0.43.0 → 0.44.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 (63) hide show
  1. package/bin/camox.mjs +9 -1
  2. package/dist/core/components/lexical/InlineStylesPlugin.js +22 -6
  3. package/dist/core/components/lexical/SelectionBroadcaster.js +4 -4
  4. package/dist/core/components/lexical/SidebarLexicalEditor.js +15 -15
  5. package/dist/core/createApp.d.ts +28 -2
  6. package/dist/core/createBlock.js +24 -10
  7. package/dist/core/editing/createEditableBlock.d.ts +135 -1
  8. package/dist/core/editing/createEditableBlock.js +209 -74
  9. package/dist/core/lib/contentType.d.ts +5 -0
  10. package/dist/core/lib/contentType.js +11 -0
  11. package/dist/core/lib/fieldTypes.js +1 -0
  12. package/dist/core/lib/iconTypes.d.ts +13 -0
  13. package/dist/core/lib/icons.d.ts +6 -0
  14. package/dist/core/lib/icons.js +130 -0
  15. package/dist/core/lib/lexicalState.js +6 -6
  16. package/dist/core/lib/modifierFormats.js +1 -1
  17. package/dist/core/lib/textStyles.d.ts +1 -1
  18. package/dist/core/lib/textStyles.js +3 -3
  19. package/dist/features/content/components/AssetCard.js +104 -68
  20. package/dist/features/preview/CamoxPreview.js +120 -95
  21. package/dist/features/preview/commentsEnabled.js +2 -2
  22. package/dist/features/preview/components/AssetLightbox.js +17 -6
  23. package/dist/features/preview/components/AttachedComments.js +224 -87
  24. package/dist/features/preview/components/CommentTargetQuote.js +22 -21
  25. package/dist/features/preview/components/DerivedLayoutDialog.js +36 -21
  26. package/dist/features/preview/components/FieldOverlayStyles.js +29 -9
  27. package/dist/features/preview/components/FieldToolbar.js +8 -8
  28. package/dist/features/preview/components/IconFieldEditor.js +231 -0
  29. package/dist/features/preview/components/ItemFieldsEditor.js +21 -14
  30. package/dist/features/preview/components/OverlayTracker.js +1 -1
  31. package/dist/features/preview/components/PagePicker.js +44 -44
  32. package/dist/features/preview/components/PreviewPanel.js +1 -1
  33. package/dist/features/preview/components/PreviewToolbar.js +224 -225
  34. package/dist/features/preview/components/SendFeedbackDialog.js +83 -0
  35. package/dist/features/preview/overlayHighlights.js +54 -0
  36. package/dist/features/preview/previewCommentsStore.js +54 -34
  37. package/dist/features/preview/previewSelection.js +28 -23
  38. package/dist/features/preview/usePageComments.js +39 -0
  39. package/dist/features/provider/CoreCamoxProvider.js +37 -5
  40. package/dist/features/provider/LocalhostPreviewProvider.js +81 -45
  41. package/dist/features/provider/components/CommandPalette.js +35 -84
  42. package/dist/features/runtime/pageApp.js +32 -28
  43. package/dist/features/runtime/pageServer.js +4 -1
  44. package/dist/features/runtime/runtime.d.ts +2 -0
  45. package/dist/features/runtime/runtime.js +25 -0
  46. package/dist/features/studio/components/EnvironmentMenu.js +75 -63
  47. package/dist/features/vite/auth.js +1 -50
  48. package/dist/features/vite/iconGeneration.js +16 -0
  49. package/dist/features/vite/vite.d.ts +4 -2
  50. package/dist/features/vite/vite.js +19 -2
  51. package/dist/lib/auth.js +39 -16
  52. package/dist/lib/preview-handoff.js +13 -0
  53. package/dist/lib/queries.js +12 -1
  54. package/dist/lib/use-project-room.js +5 -2
  55. package/dist/studio-overlays.css +19 -15
  56. package/dist/studio.css +1 -1
  57. package/package.json +5 -4
  58. package/skills/camox/SKILL.md +6 -0
  59. package/skills/camox/references/block-definitions.md +27 -0
  60. package/skills/camox/references/cli-common.md +14 -0
  61. package/skills/camox/references/cli-content.md +11 -0
  62. package/skills/camox/references/field-styling.md +4 -4
  63. package/skills/camoxify/SKILL.md +14 -0
@@ -21,9 +21,9 @@ import { Switch } from "@camox/ui/switch";
21
21
  //#region src/features/preview/components/AssetLightbox.tsx
22
22
  function DeliveredSize(t0) {
23
23
  const $ = c(16);
24
- if ($[0] !== "974fbfa56da78c6c8e188316efcb7f5517bffbf74578eecb8288c82aad4e9b63") {
24
+ if ($[0] !== "a37322f6f4a0473903954c8afefb928b001d0a8beede19e05fe56bf7cdbe1ecf") {
25
25
  for (let $i = 0; $i < 16; $i += 1) $[$i] = Symbol.for("react.memo_cache_sentinel");
26
- $[0] = "974fbfa56da78c6c8e188316efcb7f5517bffbf74578eecb8288c82aad4e9b63";
26
+ $[0] = "a37322f6f4a0473903954c8afefb928b001d0a8beede19e05fe56bf7cdbe1ecf";
27
27
  }
28
28
  const { bytes, raw } = t0;
29
29
  if (bytes == null) {
@@ -92,9 +92,9 @@ function DeliveredSize(t0) {
92
92
  }
93
93
  function DeliveredLabel(t0) {
94
94
  const $ = c(4);
95
- if ($[0] !== "974fbfa56da78c6c8e188316efcb7f5517bffbf74578eecb8288c82aad4e9b63") {
95
+ if ($[0] !== "a37322f6f4a0473903954c8afefb928b001d0a8beede19e05fe56bf7cdbe1ecf") {
96
96
  for (let $i = 0; $i < 4; $i += 1) $[$i] = Symbol.for("react.memo_cache_sentinel");
97
- $[0] = "974fbfa56da78c6c8e188316efcb7f5517bffbf74578eecb8288c82aad4e9b63";
97
+ $[0] = "a37322f6f4a0473903954c8afefb928b001d0a8beede19e05fe56bf7cdbe1ecf";
98
98
  }
99
99
  const { children } = t0;
100
100
  let t1;
@@ -171,6 +171,7 @@ const AssetLightbox = ({ open, onOpenChange, fileId }) => {
171
171
  }
172
172
  }, [open]);
173
173
  const isImage = file?.mimeType?.startsWith("image/") ?? false;
174
+ const isVideo = file?.mimeType?.startsWith("video/") ?? false;
174
175
  const canUseAiMetadata = isRasterImage(file?.mimeType);
175
176
  const fileUrl = file?.url;
176
177
  const fileMimeType = file?.mimeType;
@@ -367,6 +368,16 @@ const AssetLightbox = ({ open, onOpenChange, fileId }) => {
367
368
  className: "max-h-full max-w-full cursor-zoom-in object-contain shadow-lg",
368
369
  draggable: false
369
370
  })
371
+ }) : isVideo ? /* @__PURE__ */ jsx("div", {
372
+ className: "checkered absolute inset-0 flex items-center justify-center p-6",
373
+ children: open && /* @__PURE__ */ jsx("video", {
374
+ src: file.url,
375
+ controls: true,
376
+ playsInline: true,
377
+ preload: "metadata",
378
+ "aria-label": file.alt || file.filename || "Video preview",
379
+ className: "max-h-full max-w-full object-contain shadow-lg"
380
+ }, file.url)
370
381
  }) : /* @__PURE__ */ jsx("div", {
371
382
  className: "bg-muted/30 flex h-full min-h-[70vh] items-center justify-center p-6",
372
383
  children: /* @__PURE__ */ jsx(FileIcon, { className: "text-muted-foreground h-16 w-16" })
@@ -530,7 +541,7 @@ const AssetLightbox = ({ open, onOpenChange, fileId }) => {
530
541
  ref: replaceInputRef,
531
542
  type: "file",
532
543
  className: "hidden",
533
- accept: isImage ? "image/*" : "*/*",
544
+ accept: isImage ? "image/*" : isVideo ? "video/*" : "*/*",
534
545
  onChange: (e_0) => {
535
546
  if (e_0.target.files) handleReplaceDrop(e_0.target.files);
536
547
  e_0.target.value = "";
@@ -540,7 +551,7 @@ const AssetLightbox = ({ open, onOpenChange, fileId }) => {
540
551
  type: "button",
541
552
  variant: "secondary",
542
553
  onClick: () => replaceInputRef.current?.click(),
543
- children: isImage ? "Replace image" : "Replace file"
554
+ children: isImage ? "Replace image" : isVideo ? "Replace video" : "Replace file"
544
555
  })
545
556
  ]
546
557
  })
@@ -1,34 +1,148 @@
1
- import { useAuthContext } from "../../../lib/auth.js";
1
+ import { useCamoxApp } from "../../provider/components/CamoxAppContext.js";
2
+ import { blockQueries, commentMutations, commentQueries } from "../../../lib/queries.js";
3
+ import { areCommentsEnabled } from "../commentsEnabled.js";
2
4
  import { previewStore } from "../previewStore.js";
3
- import { previewCommentsStore, revealCommentTarget } from "../previewCommentsStore.js";
5
+ import { getCommentTargetFieldType, previewCommentsStore, revealCommentTarget } from "../previewCommentsStore.js";
6
+ import { usePageComments } from "../usePageComments.js";
4
7
  import { CommentHeader } from "./CommentHeader.js";
5
8
  import { CommentTargetQuote } from "./CommentTargetQuote.js";
9
+ import { SendFeedbackDialog } from "./SendFeedbackDialog.js";
6
10
  import { SidebarSection, SidebarSectionContent, SidebarSectionHeader } from "./SidebarSection.js";
11
+ import { c } from "react/compiler-runtime";
12
+ import { useMutation, useQueryClient } from "@tanstack/react-query";
7
13
  import * as React from "react";
8
14
  import { jsx, jsxs } from "react/jsx-runtime";
9
15
  import { useSelector } from "@xstate/store-react";
16
+ import { COMMENT_MESSAGE_MAX_LENGTH } from "@camox/api-contract";
10
17
  import { Button } from "@camox/ui/button";
11
- import { ArrowUp, X } from "lucide-react";
18
+ import { Archive, ArrowUp, CornerLeftUp, Eye, X } from "lucide-react";
19
+ import { toast } from "@camox/ui/toaster";
12
20
  import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupTextarea } from "@camox/ui/input-group";
21
+ import { ButtonGroup } from "@camox/ui/button-group";
13
22
 
14
23
  //#region src/features/preview/components/AttachedComments.tsx
15
24
  function resizeComposer(element) {
16
25
  element.style.height = "auto";
17
26
  element.style.height = `${element.scrollHeight}px`;
18
27
  }
28
+ function editorTarget({ blockId, itemId, fieldName }) {
29
+ if (blockId == null) return { kind: "page" };
30
+ if (itemId != null) {
31
+ if (fieldName != null) return {
32
+ kind: "item-field",
33
+ blockId,
34
+ itemId,
35
+ fieldName
36
+ };
37
+ return {
38
+ kind: "item",
39
+ blockId,
40
+ itemId
41
+ };
42
+ }
43
+ if (fieldName != null) return {
44
+ kind: "block-field",
45
+ blockId,
46
+ fieldName
47
+ };
48
+ return {
49
+ kind: "block",
50
+ blockId
51
+ };
52
+ }
53
+ /** Gate before mounting queries, mutations, or composers. */
54
+ function AttachedComments(props) {
55
+ const $ = c(3);
56
+ if ($[0] !== "8254f8bf23ff0138d71d56295c89e5b6eb791bb39cd29713ed38f79fecd96403") {
57
+ for (let $i = 0; $i < 3; $i += 1) $[$i] = Symbol.for("react.memo_cache_sentinel");
58
+ $[0] = "8254f8bf23ff0138d71d56295c89e5b6eb791bb39cd29713ed38f79fecd96403";
59
+ }
60
+ if (!areCommentsEnabled() || props.pageId == null) return null;
61
+ let t0;
62
+ if ($[1] !== props) {
63
+ t0 = /* @__PURE__ */ jsx(EnabledAttachedComments, {
64
+ ...props,
65
+ pageId: props.pageId
66
+ });
67
+ $[1] = props;
68
+ $[2] = t0;
69
+ } else t0 = $[2];
70
+ return t0;
71
+ }
19
72
  /** Shared comment list and composer for editors and the Feedback sidebar. */
20
- function AttachedComments({ pageId, blockId, itemId, fieldName, fieldType, allPageComments = false }) {
21
- const { authClient } = useAuthContext();
22
- const { data: session } = authClient.useSession();
23
- const { comments, draft, activeId, focusTarget } = useSelector(previewCommentsStore, (state) => state.context);
24
- const matches = (entry) => entry.pageId === pageId && (allPageComments || entry.target.blockId === blockId && entry.target.itemId === itemId && entry.target.fieldName === fieldName);
25
- const attached = comments.filter(matches);
73
+ function EnabledAttachedComments({ pageId, blockId, itemId, fieldName, allPageComments = false }) {
74
+ const camoxApp = useCamoxApp();
75
+ const queryClient = useQueryClient();
76
+ const commentsQuery = usePageComments(pageId);
77
+ const { draft, activeId, focusTarget } = useSelector(previewCommentsStore, (state) => state.context);
78
+ const target = editorTarget({
79
+ blockId,
80
+ itemId,
81
+ fieldName
82
+ });
83
+ const matches = (entry) => entry.pageId === pageId && (allPageComments || entry.target != null && entry.target.kind === target.kind && (!("blockId" in entry.target) || entry.target.blockId === blockId) && (!("itemId" in entry.target) || entry.target.itemId === itemId) && (!("fieldName" in entry.target) || entry.target.fieldName === fieldName));
84
+ const attached = (commentsQuery.data ?? []).filter(matches);
85
+ const visible = attached.filter((comment) => !comment.resolved);
26
86
  const currentDraft = draft && matches(draft) ? draft : null;
27
- const selected = attached.find((comment) => comment.id === activeId);
87
+ const selected = attached.find((comment_0) => comment_0.id === activeId);
28
88
  const textarea = React.useRef(null);
29
89
  const activeComment = React.useRef(null);
30
90
  const draftTarget = currentDraft?.target;
31
91
  const message = currentDraft?.message ?? "";
92
+ const submitting = React.useRef(false);
93
+ const setResolved = useMutation({
94
+ ...commentMutations.setResolved(),
95
+ onSuccess: (comment_1, submitted) => {
96
+ const queryKey = commentQueries.list(submitted.pageId).queryKey;
97
+ queryClient.setQueryData(queryKey, (comments) => comments?.map((entry_0) => entry_0.id === comment_1.id ? comment_1 : entry_0));
98
+ queryClient.invalidateQueries({ queryKey });
99
+ },
100
+ onError: () => toast.error("Could not update feedback. Please try again.")
101
+ });
102
+ const createComment = useMutation({
103
+ ...commentMutations.create(),
104
+ onSuccess: (comment_2, submitted_0) => {
105
+ const queryKey_0 = commentQueries.list(submitted_0.pageId).queryKey;
106
+ queryClient.setQueryData(queryKey_0, (comments_0) => {
107
+ if (!comments_0) return void 0;
108
+ return [...comments_0.filter((entry_1) => entry_1.id !== comment_2.id), comment_2].sort((a, b) => a.createdAt - b.createdAt || a.id.localeCompare(b.id));
109
+ });
110
+ queryClient.invalidateQueries({ queryKey: queryKey_0 });
111
+ previewCommentsStore.send({
112
+ type: "postSucceeded",
113
+ draft: submitted_0
114
+ });
115
+ },
116
+ onSettled: () => {
117
+ submitting.current = false;
118
+ }
119
+ });
120
+ const selectComment = async (id, target_0) => {
121
+ previewCommentsStore.send({
122
+ type: "selectComment",
123
+ id
124
+ });
125
+ if (target_0.kind === "page") {
126
+ revealCommentTarget(target_0);
127
+ return;
128
+ }
129
+ try {
130
+ const bundle = await queryClient.fetchQuery(blockQueries.get(target_0.blockId));
131
+ if (previewCommentsStore.getSnapshot().context.activeId !== id) return;
132
+ if ("itemId" in target_0 && !bundle.repeatableItems.some((item) => item.id === target_0.itemId)) {
133
+ toast.error("This feedback target is no longer available.");
134
+ return;
135
+ }
136
+ const fieldType = getCommentTargetFieldType(target_0, bundle, camoxApp);
137
+ if ("fieldName" in target_0 && !fieldType) {
138
+ toast.error("This feedback field is no longer available.");
139
+ return;
140
+ }
141
+ revealCommentTarget(target_0, fieldType);
142
+ } catch {
143
+ toast.error("This feedback target is no longer available.");
144
+ }
145
+ };
32
146
  React.useLayoutEffect(() => {
33
147
  if (textarea.current) resizeComposer(textarea.current);
34
148
  }, [message, pageId]);
@@ -45,17 +159,9 @@ function AttachedComments({ pageId, blockId, itemId, fieldName, fieldType, allPa
45
159
  return () => observer.disconnect();
46
160
  }, [pageId]);
47
161
  const submit = () => {
48
- if (!message.trim()) return;
49
- previewCommentsStore.send({
50
- type: "postComment",
51
- id: crypto.randomUUID(),
52
- author: {
53
- name: session?.user.name || "You",
54
- image: session?.user.image ?? null
55
- },
56
- createdAt: Date.now()
57
- });
58
- textarea.current?.focus({ preventScroll: true });
162
+ if (!areCommentsEnabled() || !currentDraft || !message.trim() || submitting.current) return;
163
+ submitting.current = true;
164
+ createComment.mutate(currentDraft);
59
165
  };
60
166
  React.useEffect(() => {
61
167
  if (!draftTarget || focusTarget !== draftTarget || !textarea.current) return;
@@ -67,7 +173,6 @@ function AttachedComments({ pageId, blockId, itemId, fieldName, fieldType, allPa
67
173
  if (!selected) return;
68
174
  activeComment.current?.scrollIntoView({ block: "nearest" });
69
175
  }, [selected]);
70
- if (pageId == null) return null;
71
176
  return /* @__PURE__ */ jsxs(SidebarSection, {
72
177
  "aria-label": allPageComments ? "Feedback" : blockId == null ? "Page comments" : fieldName ? `Comments on ${fieldName}` : itemId == null ? "Block comments" : "Item comments",
73
178
  divider: allPageComments ? "none" : "top",
@@ -88,50 +193,92 @@ function AttachedComments({ pageId, blockId, itemId, fieldName, fieldType, allPa
88
193
  children: /* @__PURE__ */ jsx(X, { className: "text-muted-foreground" })
89
194
  })]
90
195
  }) : /* @__PURE__ */ jsx(SidebarSectionHeader, { children: "Feedback" }), /* @__PURE__ */ jsxs(SidebarSectionContent, { children: [
91
- allPageComments && attached.length === 0 && /* @__PURE__ */ jsx("p", {
196
+ commentsQuery.isPending && /* @__PURE__ */ jsx("p", {
92
197
  className: "text-muted-foreground text-sm",
93
- children: "No feedback yet. Select something on the page to comment on it."
198
+ children: "Loading feedback…"
94
199
  }),
95
- attached.map((comment_0) => /* @__PURE__ */ jsxs("div", {
96
- ref: comment_0.id === activeId ? activeComment : void 0,
97
- ...allPageComments && {
98
- role: "button",
99
- tabIndex: 0,
100
- className: "hover:bg-card focus-visible:ring-ring rounded-md p-1 outline-none focus-visible:ring-2",
101
- onClick: () => {
102
- previewCommentsStore.send({
103
- type: "selectComment",
104
- id: comment_0.id
105
- });
106
- revealCommentTarget(comment_0.target);
107
- },
108
- onKeyDown: (event) => {
109
- if (event.key !== "Enter" && event.key !== " ") return;
110
- event.preventDefault();
111
- event.currentTarget.click();
112
- }
113
- },
114
- children: [/* @__PURE__ */ jsx(CommentHeader, {
115
- author: comment_0.author,
116
- createdAt: comment_0.createdAt
200
+ commentsQuery.isError && /* @__PURE__ */ jsxs("div", {
201
+ role: "alert",
202
+ className: "text-sm",
203
+ children: [
204
+ "Could not load feedback.",
205
+ " ",
206
+ /* @__PURE__ */ jsx(Button, {
207
+ type: "button",
208
+ variant: "link",
209
+ onClick: () => void commentsQuery.refetch(),
210
+ children: "Retry"
211
+ })
212
+ ]
213
+ }),
214
+ allPageComments && commentsQuery.isSuccess && visible.length === 0 && /* @__PURE__ */ jsxs("p", {
215
+ className: "text-muted-foreground text-sm",
216
+ children: [attached.length === 0 ? "No feedback yet." : "No unresolved feedback.", " Select something on the page to comment on it."]
217
+ }),
218
+ visible.map((comment_3) => /* @__PURE__ */ jsxs("div", {
219
+ ref: comment_3.id === activeId ? activeComment : void 0,
220
+ className: "rounded-md p-1",
221
+ children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx(CommentHeader, {
222
+ author: comment_3.author,
223
+ createdAt: comment_3.createdAt
117
224
  }), /* @__PURE__ */ jsxs("div", {
118
225
  className: "pl-8",
119
- children: [allPageComments && /* @__PURE__ */ jsx(CommentTargetQuote, {
226
+ children: [allPageComments && (comment_3.target ? /* @__PURE__ */ jsx(CommentTargetQuote, {
120
227
  pageId,
121
- target: comment_0.target
122
- }), /* @__PURE__ */ jsx("p", {
228
+ target: comment_3.target
229
+ }) : /* @__PURE__ */ jsx("p", {
230
+ className: "text-muted-foreground text-sm",
231
+ children: "Target no longer available"
232
+ })), /* @__PURE__ */ jsx("p", {
123
233
  className: "text-sm wrap-break-word whitespace-pre-wrap",
124
- children: comment_0.message
234
+ children: comment_3.message
125
235
  })]
236
+ })] }), /* @__PURE__ */ jsxs(ButtonGroup, {
237
+ className: "mt-2 ml-8",
238
+ "aria-label": "Comment actions",
239
+ children: [
240
+ allPageComments && /* @__PURE__ */ jsxs(Button, {
241
+ type: "button",
242
+ variant: "outline",
243
+ size: "sm",
244
+ disabled: comment_3.target == null,
245
+ onClick: () => {
246
+ if (comment_3.target) selectComment(comment_3.id, comment_3.target);
247
+ },
248
+ children: [/* @__PURE__ */ jsx(Eye, { className: "text-muted-foreground" }), "View"]
249
+ }),
250
+ !allPageComments && /* @__PURE__ */ jsxs(Button, {
251
+ type: "button",
252
+ variant: "outline",
253
+ size: "sm",
254
+ onClick: () => previewStore.send({
255
+ type: "setCommentMode",
256
+ enabled: true
257
+ }),
258
+ children: [/* @__PURE__ */ jsx(CornerLeftUp, { className: "text-muted-foreground" }), "See all"]
259
+ }),
260
+ /* @__PURE__ */ jsxs(Button, {
261
+ type: "button",
262
+ variant: "outline",
263
+ size: "sm",
264
+ "aria-label": "Archive comment",
265
+ disabled: setResolved.isPending,
266
+ onClick: () => {
267
+ setResolved.mutate({
268
+ pageId,
269
+ id: comment_3.id,
270
+ resolved: true
271
+ });
272
+ },
273
+ children: [/* @__PURE__ */ jsx(Archive, { className: "text-muted-foreground" }), "Archive"]
274
+ })
275
+ ]
126
276
  })]
127
- }, comment_0.id)),
128
- allPageComments && /* @__PURE__ */ jsx(Button, {
129
- type: "button",
130
- variant: "secondary",
131
- className: "w-full",
132
- disabled: attached.length === 0,
133
- children: "Send feedback to agent..."
134
- }),
277
+ }, comment_3.id)),
278
+ allPageComments && /* @__PURE__ */ jsx(SendFeedbackDialog, {
279
+ pageId,
280
+ disabled: visible.length === 0
281
+ }, pageId),
135
282
  !allPageComments && /* @__PURE__ */ jsxs(InputGroup, { children: [/* @__PURE__ */ jsx(InputGroupTextarea, {
136
283
  ref: textarea,
137
284
  "aria-label": "Comment text",
@@ -139,37 +286,22 @@ function AttachedComments({ pageId, blockId, itemId, fieldName, fieldType, allPa
139
286
  className: "block field-sizing-fixed min-h-0 flex-none overflow-hidden leading-6",
140
287
  rows: 1,
141
288
  value: message,
142
- onKeyDown: (event_0) => {
143
- if (event_0.key !== "Enter" || event_0.shiftKey || event_0.nativeEvent.isComposing) return;
144
- event_0.preventDefault();
145
- event_0.stopPropagation();
289
+ maxLength: COMMENT_MESSAGE_MAX_LENGTH,
290
+ onKeyDown: (event) => {
291
+ if (event.key !== "Enter" || event.shiftKey || event.nativeEvent.isComposing) return;
292
+ event.preventDefault();
293
+ event.stopPropagation();
146
294
  submit();
147
295
  },
148
- onChange: (event_1) => {
149
- if (!currentDraft) {
150
- const fieldId = [
151
- blockId,
152
- itemId,
153
- fieldName
154
- ].filter((part) => part != null).join("__");
155
- previewCommentsStore.send({
156
- type: "startComment",
157
- pageId,
158
- target: {
159
- blockId,
160
- itemId,
161
- fieldName,
162
- fieldType,
163
- selector: blockId == null ? "body" : fieldName != null ? `[data-camox-field-id="${CSS.escape(fieldId)}"]` : itemId != null ? `[data-camox-repeater-item-id="${itemId}"]` : `[data-camox-block-id="${blockId}"]`,
164
- label: blockId == null ? `Page · ${pageId}` : fieldName != null ? `Field · ${fieldName}` : itemId != null ? `Item · ${itemId}` : `Block · ${blockId}`,
165
- x: .5,
166
- y: .5
167
- }
168
- });
169
- }
296
+ onChange: (event_0) => {
297
+ if (!currentDraft) previewCommentsStore.send({
298
+ type: "startComment",
299
+ pageId,
300
+ target
301
+ });
170
302
  previewCommentsStore.send({
171
303
  type: "setMessage",
172
- message: event_1.target.value
304
+ message: event_0.target.value
173
305
  });
174
306
  }
175
307
  }), /* @__PURE__ */ jsx(InputGroupAddon, {
@@ -181,11 +313,16 @@ function AttachedComments({ pageId, blockId, itemId, fieldName, fieldType, allPa
181
313
  variant: "default",
182
314
  className: "rounded-full",
183
315
  "aria-label": "Post comment",
184
- disabled: !message.trim(),
316
+ disabled: !message.trim() || createComment.isPending,
185
317
  onClick: submit,
186
318
  children: /* @__PURE__ */ jsx(ArrowUp, { size: 16 })
187
319
  })
188
- })] })
320
+ })] }),
321
+ createComment.isError && currentDraft?.id === createComment.variables?.id && /* @__PURE__ */ jsx("p", {
322
+ role: "alert",
323
+ className: "text-destructive text-sm",
324
+ children: "Could not post feedback. Your draft is saved here; try posting again."
325
+ })
189
326
  ] })]
190
327
  });
191
328
  }
@@ -1,5 +1,7 @@
1
1
  import { isLexicalState, lexicalStateToPlainText } from "../../../core/lib/lexicalState.js";
2
+ import { useCamoxApp } from "../../provider/components/CamoxAppContext.js";
2
3
  import { blockQueries, pageQueries } from "../../../lib/queries.js";
4
+ import { getCommentTargetFieldType } from "../previewCommentsStore.js";
3
5
  import { c } from "react/compiler-runtime";
4
6
  import { useQuery } from "@tanstack/react-query";
5
7
  import { jsx } from "react/jsx-runtime";
@@ -7,9 +9,9 @@ import { jsx } from "react/jsx-runtime";
7
9
  //#region src/features/preview/components/CommentTargetQuote.tsx
8
10
  function Quote(t0) {
9
11
  const $ = c(3);
10
- if ($[0] !== "c7d7da293bb0be577af3f96a804786ef95d893a2ac7e10598c27ec5225cf9e92") {
12
+ if ($[0] !== "3d839e9320d85d713ee71a099780efa90c475e3bad1d5b3892ff6d87762000ab") {
11
13
  for (let $i = 0; $i < 3; $i += 1) $[$i] = Symbol.for("react.memo_cache_sentinel");
12
- $[0] = "c7d7da293bb0be577af3f96a804786ef95d893a2ac7e10598c27ec5225cf9e92";
14
+ $[0] = "3d839e9320d85d713ee71a099780efa90c475e3bad1d5b3892ff6d87762000ab";
13
15
  }
14
16
  const { children } = t0;
15
17
  let t1;
@@ -25,9 +27,9 @@ function Quote(t0) {
25
27
  }
26
28
  function PageQuote(t0) {
27
29
  const $ = c(5);
28
- if ($[0] !== "c7d7da293bb0be577af3f96a804786ef95d893a2ac7e10598c27ec5225cf9e92") {
30
+ if ($[0] !== "3d839e9320d85d713ee71a099780efa90c475e3bad1d5b3892ff6d87762000ab") {
29
31
  for (let $i = 0; $i < 5; $i += 1) $[$i] = Symbol.for("react.memo_cache_sentinel");
30
- $[0] = "c7d7da293bb0be577af3f96a804786ef95d893a2ac7e10598c27ec5225cf9e92";
32
+ $[0] = "3d839e9320d85d713ee71a099780efa90c475e3bad1d5b3892ff6d87762000ab";
31
33
  }
32
34
  const { pageId } = t0;
33
35
  let t1;
@@ -48,11 +50,12 @@ function PageQuote(t0) {
48
50
  }
49
51
  function BlockQuote(t0) {
50
52
  const $ = c(19);
51
- if ($[0] !== "c7d7da293bb0be577af3f96a804786ef95d893a2ac7e10598c27ec5225cf9e92") {
53
+ if ($[0] !== "3d839e9320d85d713ee71a099780efa90c475e3bad1d5b3892ff6d87762000ab") {
52
54
  for (let $i = 0; $i < 19; $i += 1) $[$i] = Symbol.for("react.memo_cache_sentinel");
53
- $[0] = "c7d7da293bb0be577af3f96a804786ef95d893a2ac7e10598c27ec5225cf9e92";
55
+ $[0] = "3d839e9320d85d713ee71a099780efa90c475e3bad1d5b3892ff6d87762000ab";
54
56
  }
55
57
  const { target } = t0;
58
+ const app = useCamoxApp();
56
59
  let t1;
57
60
  if ($[1] !== target.blockId) {
58
61
  t1 = blockQueries.get(target.blockId);
@@ -71,11 +74,11 @@ function BlockQuote(t0) {
71
74
  return t3;
72
75
  }
73
76
  let t2;
74
- if ($[5] !== bundle.block || $[6] !== bundle.repeatableItems || $[7] !== target.itemId) {
75
- t2 = target.itemId == null ? bundle.block : bundle.repeatableItems.find((item) => item.id === target.itemId);
77
+ if ($[5] !== bundle.block || $[6] !== bundle.repeatableItems || $[7] !== target) {
78
+ t2 = "itemId" in target ? bundle.repeatableItems.find((item) => item.id === target.itemId) : bundle.block;
76
79
  $[5] = bundle.block;
77
80
  $[6] = bundle.repeatableItems;
78
- $[7] = target.itemId;
81
+ $[7] = target;
79
82
  $[8] = t2;
80
83
  } else t2 = $[8];
81
84
  const subject = t2;
@@ -87,9 +90,10 @@ function BlockQuote(t0) {
87
90
  } else t3 = $[9];
88
91
  return t3;
89
92
  }
90
- if (target.fieldName != null) {
93
+ if ("fieldName" in target) {
94
+ const fieldType = getCommentTargetFieldType(target, bundle, app);
91
95
  const value = subject.content[target.fieldName];
92
- if (target.fieldType === "String" || target.fieldType == null) {
96
+ if (fieldType === "String") {
93
97
  if (typeof value === "string" || isLexicalState(value)) {
94
98
  let t3;
95
99
  if ($[10] !== value) {
@@ -105,7 +109,7 @@ function BlockQuote(t0) {
105
109
  } else t4 = $[13];
106
110
  return t4;
107
111
  }
108
- if (value == null && target.fieldType === "String") {
112
+ if (value == null) {
109
113
  let t3;
110
114
  if ($[14] === Symbol.for("react.memo_cache_sentinel")) {
111
115
  t3 = /* @__PURE__ */ jsx(Quote, { children: "Empty text" });
@@ -114,7 +118,7 @@ function BlockQuote(t0) {
114
118
  return t3;
115
119
  }
116
120
  }
117
- if (target.fieldType === "Link" && value && typeof value === "object" && "text" in value && typeof value.text === "string") {
121
+ if (fieldType === "Link" && value && typeof value === "object" && "text" in value && typeof value.text === "string") {
118
122
  const t3 = value.text || "Empty link text";
119
123
  let t4;
120
124
  if ($[15] !== t3) {
@@ -125,7 +129,7 @@ function BlockQuote(t0) {
125
129
  return t4;
126
130
  }
127
131
  }
128
- const t3 = subject.summary || (target.itemId == null ? "Untitled block" : "Untitled item");
132
+ const t3 = subject.summary || ("itemId" in target ? "Untitled item" : "Untitled block");
129
133
  let t4;
130
134
  if ($[17] !== t3) {
131
135
  t4 = /* @__PURE__ */ jsx(Quote, { children: t3 });
@@ -136,12 +140,12 @@ function BlockQuote(t0) {
136
140
  }
137
141
  function CommentTargetQuote(t0) {
138
142
  const $ = c(5);
139
- if ($[0] !== "c7d7da293bb0be577af3f96a804786ef95d893a2ac7e10598c27ec5225cf9e92") {
143
+ if ($[0] !== "3d839e9320d85d713ee71a099780efa90c475e3bad1d5b3892ff6d87762000ab") {
140
144
  for (let $i = 0; $i < 5; $i += 1) $[$i] = Symbol.for("react.memo_cache_sentinel");
141
- $[0] = "c7d7da293bb0be577af3f96a804786ef95d893a2ac7e10598c27ec5225cf9e92";
145
+ $[0] = "3d839e9320d85d713ee71a099780efa90c475e3bad1d5b3892ff6d87762000ab";
142
146
  }
143
147
  const { pageId, target } = t0;
144
- if (target.blockId == null) {
148
+ if (target.kind === "page") {
145
149
  let t1;
146
150
  if ($[1] !== pageId) {
147
151
  t1 = /* @__PURE__ */ jsx(PageQuote, { pageId });
@@ -152,10 +156,7 @@ function CommentTargetQuote(t0) {
152
156
  }
153
157
  let t1;
154
158
  if ($[3] !== target) {
155
- t1 = /* @__PURE__ */ jsx(BlockQuote, { target: {
156
- ...target,
157
- blockId: target.blockId
158
- } });
159
+ t1 = /* @__PURE__ */ jsx(BlockQuote, { target });
159
160
  $[3] = target;
160
161
  $[4] = t1;
161
162
  } else t1 = $[4];
@@ -5,7 +5,7 @@ import { jsx, jsxs } from "react/jsx-runtime";
5
5
  import { Button } from "@camox/ui/button";
6
6
  import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@camox/ui/dialog";
7
7
  import { Label } from "@camox/ui/label";
8
- import { Input } from "@camox/ui/input";
8
+ import { InputGroup, InputGroupAddon, InputGroupInput, InputGroupText } from "@camox/ui/input-group";
9
9
 
10
10
  //#region src/features/preview/components/DerivedLayoutDialog.tsx
11
11
  function DerivedLayoutDialog({ layout, projectSlug, currentParams, onClose }) {
@@ -28,11 +28,12 @@ function DerivedLayoutDialog({ layout, projectSlug, currentParams, onClose }) {
28
28
  const value = values[param]?.trim();
29
29
  return value && !value.includes("/") && value !== "." && value !== "..";
30
30
  });
31
- const path = `/${segments.map((segment_1) => {
31
+ const resolvedSegments = segments.map((segment_1) => {
32
32
  if (!segment_1.startsWith("$")) return encodeURIComponent(segment_1);
33
33
  const value_0 = values[segment_1.slice(1)]?.trim();
34
34
  return value_0 ? encodeURIComponent(value_0) : segment_1;
35
- }).join("/")}`;
35
+ });
36
+ const path = `/${resolvedSegments.join("/")}`;
36
37
  return /* @__PURE__ */ jsx(Dialog, {
37
38
  open: true,
38
39
  onOpenChange: (open) => !open && onClose(),
@@ -57,24 +58,38 @@ function DerivedLayoutDialog({ layout, projectSlug, currentParams, onClose }) {
57
58
  },
58
59
  children: [
59
60
  /* @__PURE__ */ jsxs(DialogHeader, { children: [/* @__PURE__ */ jsxs(DialogTitle, { children: ["Preview ", layout._internal.title] }), /* @__PURE__ */ jsx(DialogDescription, { children: "Enter the path parameters for the page you want to preview." })] }),
60
- params.map((param_0) => /* @__PURE__ */ jsxs("div", {
61
- className: "space-y-2",
62
- children: [/* @__PURE__ */ jsx(Label, {
63
- htmlFor: `${id}-${param_0}`,
64
- children: param_0
65
- }), /* @__PURE__ */ jsx(Input, {
66
- id: `${id}-${param_0}`,
67
- required: true,
68
- value: values[param_0] ?? "",
69
- onChange: (event_0) => setValues((previous) => ({
70
- ...previous,
71
- [param_0]: event_0.target.value
72
- }))
73
- })]
74
- }, param_0)),
75
- /* @__PURE__ */ jsx("p", {
76
- className: "text-muted-foreground font-mono text-sm break-all",
77
- children: path
61
+ params.map((param_0) => {
62
+ const index = segments.indexOf(`$${param_0}`);
63
+ const prefix = resolvedSegments.slice(0, index).join("/");
64
+ const suffix = segments.slice(index + 1).every((segment_2) => !segment_2.startsWith("$")) ? resolvedSegments.slice(index + 1).join("/") : "";
65
+ return /* @__PURE__ */ jsxs("div", {
66
+ className: "space-y-2",
67
+ children: [/* @__PURE__ */ jsx(Label, {
68
+ htmlFor: `${id}-${param_0}`,
69
+ children: param_0
70
+ }), /* @__PURE__ */ jsxs(InputGroup, { children: [
71
+ /* @__PURE__ */ jsx(InputGroupAddon, { children: /* @__PURE__ */ jsxs(InputGroupText, {
72
+ className: "font-mono",
73
+ children: ["/", prefix && `${prefix}/`]
74
+ }) }),
75
+ /* @__PURE__ */ jsx(InputGroupInput, {
76
+ id: `${id}-${param_0}`,
77
+ required: true,
78
+ value: values[param_0] ?? "",
79
+ onChange: (event_0) => setValues((previous) => ({
80
+ ...previous,
81
+ [param_0]: event_0.target.value
82
+ }))
83
+ }),
84
+ suffix && /* @__PURE__ */ jsx(InputGroupAddon, {
85
+ align: "inline-end",
86
+ children: /* @__PURE__ */ jsxs(InputGroupText, {
87
+ className: "font-mono",
88
+ children: ["/", suffix]
89
+ })
90
+ })
91
+ ] })]
92
+ }, param_0);
78
93
  }),
79
94
  error && /* @__PURE__ */ jsx("p", {
80
95
  role: "alert",