camox 0.13.0 → 0.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -57,6 +57,26 @@ const getSchemaForItem = (contentSchema, itemId, itemsMap) => {
57
57
  return schema;
58
58
  };
59
59
  /**
60
+ * Like `getSchemaForItem` but returns the **array** schema (one level above
61
+ * the items schema), where per-item settings metadata lives.
62
+ */
63
+ const getArraySchemaForItem = (contentSchema, itemId, itemsMap) => {
64
+ const path = [];
65
+ let current = itemsMap.get(itemId);
66
+ while (current) {
67
+ path.unshift(current.fieldName);
68
+ current = current.parentItemId ? itemsMap.get(current.parentItemId) : void 0;
69
+ }
70
+ let schema = contentSchema;
71
+ for (let i = 0; i < path.length; i++) {
72
+ const prop = schema?.properties?.[path[i]];
73
+ if (!prop?.items) return null;
74
+ if (i === path.length - 1) return prop;
75
+ schema = prop.items;
76
+ }
77
+ return null;
78
+ };
79
+ /**
60
80
  * Builds the ancestor chain from root to this item (inclusive).
61
81
  * Returns items in order from root-most ancestor to the item itself.
62
82
  */
@@ -74,6 +94,7 @@ const PageContentSheet = () => {
74
94
  const updateContent = useMutation(blockMutations.updateContent());
75
95
  const updateSettings = useMutation(blockMutations.updateSettings());
76
96
  const updateRepeatableContent = useMutation(repeatableItemMutations.updateContent());
97
+ const updateRepeatableSettings = useMutation(repeatableItemMutations.updateSettings());
77
98
  const isOpen = useSelector(previewStore, (state) => state.context.isPageContentSheetOpen);
78
99
  const selection = useSelector(previewStore, (state_0) => state_0.context.selection);
79
100
  const iframeElement = useSelector(previewStore, (state_1) => state_1.context.iframeElement);
@@ -109,6 +130,17 @@ const PageContentSheet = () => {
109
130
  const settingsFields = React.useMemo(() => {
110
131
  return blockDef ? getSettingsFields(blockDef._internal.settingsSchema) : [];
111
132
  }, [blockDef]);
133
+ const itemArraySchema = React.useMemo(() => {
134
+ if (!blockDef || currentItemId == null) return null;
135
+ return getArraySchemaForItem(blockDef._internal.contentSchema, currentItemId, itemsMap);
136
+ }, [
137
+ blockDef,
138
+ currentItemId,
139
+ itemsMap
140
+ ]);
141
+ const itemSettingsFields = React.useMemo(() => {
142
+ return getSettingsFields(itemArraySchema?.itemSettingsSchema);
143
+ }, [itemArraySchema]);
112
144
  const currentSchema = React.useMemo(() => {
113
145
  if (!blockDef) return null;
114
146
  if (currentItemId == null) return blockDef._internal.contentSchema;
@@ -379,6 +411,62 @@ const PageContentSheet = () => {
379
411
  return null;
380
412
  })]
381
413
  }),
414
+ currentItemId != null && !fieldHasOwnView && itemSettingsFields.length > 0 && /* @__PURE__ */ jsxs("div", {
415
+ className: "border-border space-y-4 border-b px-4 py-4",
416
+ children: [/* @__PURE__ */ jsx(Label, {
417
+ className: "text-muted-foreground",
418
+ children: "Settings"
419
+ }), itemSettingsFields.map((field_0) => {
420
+ const label_0 = field_0.label ?? formatFieldName(field_0.name);
421
+ const itemSettingsValues = currentItem?.settings ?? {};
422
+ const itemSettingsSchemaProps = itemArraySchema?.itemSettingsSchema?.properties;
423
+ if (field_0.fieldType === "Enum") {
424
+ const value_4 = itemSettingsValues[field_0.name] ?? itemSettingsSchemaProps?.[field_0.name]?.default ?? "";
425
+ return /* @__PURE__ */ jsxs("div", {
426
+ className: "space-y-2",
427
+ children: [/* @__PURE__ */ jsx(Label, {
428
+ htmlFor: `item-setting-${field_0.name}`,
429
+ children: label_0
430
+ }), /* @__PURE__ */ jsxs(Select, {
431
+ value: value_4,
432
+ onValueChange: (newValue_1) => {
433
+ updateRepeatableSettings.mutate({
434
+ id: currentItemId,
435
+ settings: { [field_0.name]: newValue_1 }
436
+ });
437
+ },
438
+ children: [/* @__PURE__ */ jsx(SelectTrigger, {
439
+ id: `item-setting-${field_0.name}`,
440
+ children: /* @__PURE__ */ jsx(SelectValue, {})
441
+ }), /* @__PURE__ */ jsx(SelectContent, { children: field_0.enumValues?.map((enumValue_0) => /* @__PURE__ */ jsx(SelectItem, {
442
+ value: enumValue_0,
443
+ children: field_0.enumLabels?.[enumValue_0] ?? enumValue_0
444
+ }, enumValue_0)) })]
445
+ })]
446
+ }, field_0.name);
447
+ }
448
+ if (field_0.fieldType === "Boolean") {
449
+ const checked_0 = itemSettingsValues[field_0.name] ?? itemSettingsSchemaProps?.[field_0.name]?.default ?? false;
450
+ return /* @__PURE__ */ jsxs("div", {
451
+ className: "flex items-center justify-between",
452
+ children: [/* @__PURE__ */ jsx(Label, {
453
+ htmlFor: `item-setting-${field_0.name}`,
454
+ children: label_0
455
+ }), /* @__PURE__ */ jsx(Switch, {
456
+ id: `item-setting-${field_0.name}`,
457
+ checked: checked_0,
458
+ onCheckedChange: (newValue_2) => {
459
+ updateRepeatableSettings.mutate({
460
+ id: currentItemId,
461
+ settings: { [field_0.name]: newValue_2 }
462
+ });
463
+ }
464
+ })]
465
+ }, field_0.name);
466
+ }
467
+ return null;
468
+ })]
469
+ }),
382
470
  isViewingAsset && assetFieldName && isMultipleAsset && /* @__PURE__ */ jsx(MultipleAssetFieldEditor, {
383
471
  fieldName: assetFieldName,
384
472
  assetType,
@@ -401,12 +489,12 @@ const PageContentSheet = () => {
401
489
  href: "",
402
490
  newTab: false
403
491
  },
404
- onSave: (fieldName_1, value_4) => {
405
- activeFieldChangeHandler(fieldName_1, value_4);
492
+ onSave: (fieldName_1, value_5) => {
493
+ activeFieldChangeHandler(fieldName_1, value_5);
406
494
  }
407
495
  })
408
496
  }),
409
- !isViewingAsset && !isViewingLink && /* @__PURE__ */ jsx(ItemFieldsEditor, {
497
+ !isViewingAsset && !isViewingLink && (currentItemId == null || currentItem) && /* @__PURE__ */ jsx(ItemFieldsEditor, {
410
498
  schema: currentSchema,
411
499
  data: currentData,
412
500
  blockId: block.id,
@@ -1,4 +1,5 @@
1
1
  import { previewStore } from "../previewStore.js";
2
+ import { blockQueries } from "../../../lib/queries.js";
2
3
  import { cn } from "../../../lib/utils.js";
3
4
  import { usePageBlocks } from "../../../lib/normalized-data.js";
4
5
  import { useCamoxApp } from "../../provider/components/CamoxAppContext.js";
@@ -7,6 +8,7 @@ import { useUpdateBlockPosition } from "./useUpdateBlockPosition.js";
7
8
  import { BlockActionsPopover } from "./BlockActionsPopover.js";
8
9
  import { usePreviewedPage } from "../CamoxPreview.js";
9
10
  import { c } from "react/compiler-runtime";
11
+ import { useQuery } from "@tanstack/react-query";
10
12
  import { useSelector } from "@xstate/store/react";
11
13
  import * as React from "react";
12
14
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
@@ -137,12 +139,26 @@ const FieldItem = (t0) => {
137
139
  return t9;
138
140
  };
139
141
  const BlockFields = (t0) => {
140
- const $ = c(2);
142
+ const $ = c(4);
141
143
  const { block } = t0;
142
144
  const schemaProperties = useCamoxApp().getBlockById(block.type)?._internal.contentSchema.properties;
143
145
  const selection = useSelector(previewStore, _temp3);
144
146
  const iframeElement = useSelector(previewStore, _temp4);
145
- const selectedFieldName = selection?.type === "block-field" && selection.blockId === block.id ? selection.fieldName : null;
147
+ let t1;
148
+ if ($[0] !== block.id) {
149
+ t1 = blockQueries.get(block.id);
150
+ $[0] = block.id;
151
+ $[1] = t1;
152
+ } else t1 = $[1];
153
+ const { data: blockBundle } = useQuery(t1);
154
+ let selectedFieldName = null;
155
+ if (selection?.type === "block-field" && selection.blockId === block.id) selectedFieldName = selection.fieldName;
156
+ else if ((selection?.type === "item" || selection?.type === "item-field") && selection.blockId === block.id && blockBundle) {
157
+ const itemsById = new Map(blockBundle.repeatableItems.map(_temp5));
158
+ let current = itemsById.get(selection.itemId);
159
+ while (current?.parentItemId != null) current = itemsById.get(current.parentItemId);
160
+ selectedFieldName = current?.fieldName ?? null;
161
+ }
146
162
  const handleFieldClick = (fieldName, fieldType) => {
147
163
  previewStore.send({
148
164
  type: "selectBlockField",
@@ -191,8 +207,8 @@ const BlockFields = (t0) => {
191
207
  iframeElement.contentWindow.postMessage(message_2, "*");
192
208
  }
193
209
  };
194
- const t1 = "my-1 space-y-1 pl-7";
195
- const t2 = Object.keys(schemaProperties ?? {}).map((fieldName_3) => {
210
+ const t2 = "my-1 space-y-1 pl-7";
211
+ const t3 = Object.keys(schemaProperties ?? {}).map((fieldName_3) => {
196
212
  const value = block.content[fieldName_3];
197
213
  const fieldSchema = schemaProperties?.[fieldName_3];
198
214
  if (!fieldSchema) return null;
@@ -211,23 +227,23 @@ const BlockFields = (t0) => {
211
227
  onMouseLeave: () => handleFieldMouseLeave(fieldName_3, isRepeatable_1)
212
228
  }, fieldName_3);
213
229
  });
214
- let t3;
215
- if ($[0] !== t2) {
216
- t3 = /* @__PURE__ */ jsx("ul", {
217
- className: t1,
218
- children: t2
230
+ let t4;
231
+ if ($[2] !== t3) {
232
+ t4 = /* @__PURE__ */ jsx("ul", {
233
+ className: t2,
234
+ children: t3
219
235
  });
220
- $[0] = t2;
221
- $[1] = t3;
222
- } else t3 = $[1];
223
- return t3;
236
+ $[2] = t3;
237
+ $[3] = t4;
238
+ } else t4 = $[3];
239
+ return t4;
224
240
  };
225
241
  function useBlockTreeItem(block, t0) {
226
242
  const $ = c(16);
227
243
  const isDragging = t0 === void 0 ? false : t0;
228
244
  const [ellipsisPopoverOpen, setEllipsisPopoverOpen] = React.useState(false);
229
- const selection = useSelector(previewStore, _temp5);
230
- const iframeElement = useSelector(previewStore, _temp6);
245
+ const selection = useSelector(previewStore, _temp6);
246
+ const iframeElement = useSelector(previewStore, _temp7);
231
247
  const isBlockSelected = selection?.type === "block" && selection.blockId === block.id;
232
248
  const shouldShowHover = !isDragging && !isBlockSelected;
233
249
  const shouldShowActive = isDragging || isBlockSelected;
@@ -296,10 +312,10 @@ function useBlockTreeItem(block, t0) {
296
312
  } else t4 = $[15];
297
313
  return t4;
298
314
  }
299
- function _temp6(state_0) {
315
+ function _temp7(state_0) {
300
316
  return state_0.context.iframeElement;
301
317
  }
302
- function _temp5(state) {
318
+ function _temp6(state) {
303
319
  return state.context.selection;
304
320
  }
305
321
  const BlockTreeItemHeader = (t0) => {
@@ -891,13 +907,13 @@ const PageTree = () => {
891
907
  } else t7 = $[16];
892
908
  let t8;
893
909
  if ($[17] !== pageBlocks) {
894
- t8 = pageBlocks.map(_temp7);
910
+ t8 = pageBlocks.map(_temp8);
895
911
  $[17] = pageBlocks;
896
912
  $[18] = t8;
897
913
  } else t8 = $[18];
898
914
  let t9;
899
915
  if ($[19] !== pageBlocks) {
900
- t9 = pageBlocks.map(_temp8);
916
+ t9 = pageBlocks.map(_temp9);
901
917
  $[19] = pageBlocks;
902
918
  $[20] = t9;
903
919
  } else t9 = $[20];
@@ -993,7 +1009,7 @@ const PageTree = () => {
993
1009
  if ($[43] === Symbol.for("react.memo_cache_sentinel")) {
994
1010
  t16 = /* @__PURE__ */ jsxs(Button, {
995
1011
  variant: "secondary",
996
- onClick: _temp9,
1012
+ onClick: _temp0,
997
1013
  children: [/* @__PURE__ */ jsx(Plus, {}), "Add block"]
998
1014
  });
999
1015
  $[43] = t16;
@@ -1012,13 +1028,16 @@ function _temp3(state) {
1012
1028
  function _temp4(state_0) {
1013
1029
  return state_0.context.iframeElement;
1014
1030
  }
1015
- function _temp7(block_2) {
1031
+ function _temp5(i) {
1032
+ return [i.id, i];
1033
+ }
1034
+ function _temp8(block_2) {
1016
1035
  return String(block_2.id);
1017
1036
  }
1018
- function _temp8(block_3) {
1037
+ function _temp9(block_3) {
1019
1038
  return /* @__PURE__ */ jsx(SortableBlock, { block: block_3 }, String(block_3.id));
1020
1039
  }
1021
- function _temp9() {
1040
+ function _temp0() {
1022
1041
  return previewStore.send({ type: "openAddBlockSheet" });
1023
1042
  }
1024
1043
 
@@ -214,6 +214,7 @@ const RepeatableItemsList = ({ items, blockId, fieldName, minItems, maxItems, sc
214
214
  if (prop.type === "array" && prop.items?.properties) continue;
215
215
  if ("default" in prop) defaultContent[key] = prop.default;
216
216
  }
217
+ const defaultSettings = schema?.defaultItemSettings;
217
218
  const nestedItems = [];
218
219
  if (itemsSchema?.properties) {
219
220
  let seedCounter = 0;
@@ -230,6 +231,7 @@ const RepeatableItemsList = ({ items, blockId, fieldName, minItems, maxItems, sc
230
231
  if (ps.type === "array" && ps.items?.properties) continue;
231
232
  if ("default" in ps) nestedContent[propName] = ps.default;
232
233
  }
234
+ const nestedSettingsDefaults = fs.defaultItemSettings;
233
235
  let prevPos = null;
234
236
  for (let i = 0; i < defaultCount; i++) {
235
237
  const tempId = `nested_${++seedCounter}`;
@@ -240,6 +242,7 @@ const RepeatableItemsList = ({ items, blockId, fieldName, minItems, maxItems, sc
240
242
  parentTempId,
241
243
  fieldName: nestedFieldName,
242
244
  content: { ...nestedContent },
245
+ settings: nestedSettingsDefaults ? { ...nestedSettingsDefaults } : void 0,
243
246
  position
244
247
  });
245
248
  buildNestedSeeds(nestedItemProps, tempId);
@@ -252,8 +255,15 @@ const RepeatableItemsList = ({ items, blockId, fieldName, minItems, maxItems, sc
252
255
  blockId,
253
256
  fieldName,
254
257
  content: defaultContent,
258
+ settings: defaultSettings ? { ...defaultSettings } : void 0,
255
259
  nestedItems: nestedItems.length > 0 ? nestedItems : void 0
256
- });
260
+ }, { onSuccess: (created) => {
261
+ previewStore.send({
262
+ type: "selectItem",
263
+ blockId,
264
+ itemId: created.id
265
+ });
266
+ } });
257
267
  };
258
268
  const handleRemoveItem = (itemId) => {
259
269
  deleteRepeatableItem.mutate({ id: itemId });
@@ -105,6 +105,7 @@ const repeatableItemMutations = {
105
105
  delete: () => getOrpc().repeatableItems.delete.mutationOptions(),
106
106
  duplicate: () => getOrpc().repeatableItems.duplicate.mutationOptions(),
107
107
  updateContent: () => getOrpc().repeatableItems.updateContent.mutationOptions(),
108
+ updateSettings: () => getOrpc().repeatableItems.updateSettings.mutationOptions(),
108
109
  updatePosition: () => getOrpc().repeatableItems.updatePosition.mutationOptions()
109
110
  };
110
111
  const pageMutations = {
@@ -25,17 +25,19 @@
25
25
  --camox-overlay-inset-block-selected: 0px;
26
26
  }
27
27
 
28
- /* Position context for field-level and repeater elements.
28
+ /* Position context for typed-field, repeater, and detached overlays.
29
29
  BlockComponent already has position: relative inline.
30
- Detached wraps user elements (e.g. fixed navbars) so must NOT get position: relative. */
31
- [data-camox-field-id],
30
+ Inline string fields don't use ::after (see box-shadow rules below) so they
31
+ don't need position: relative — adding it to inline elements wouldn't help
32
+ since inline-relative containing blocks collapse to the start fragment. */
33
+ [data-camox-field-id][data-camox-field-type],
32
34
  [data-camox-repeater-item-id] {
33
35
  position: relative;
34
36
  }
35
37
 
36
38
  /* ---- Hovered state ---- */
37
39
 
38
- [data-camox-field-id][data-camox-hovered]::after,
40
+ [data-camox-field-id][data-camox-field-type][data-camox-hovered]::after,
39
41
  [data-camox-block-id][data-camox-hovered]:not([data-camox-detached])::after,
40
42
  [data-camox-repeater-item-id][data-camox-hovered]::after {
41
43
  content: "";
@@ -46,14 +48,9 @@
46
48
  border: var(--camox-overlay-width-hover) solid var(--camox-overlay-color-hover);
47
49
  }
48
50
 
49
- /* Field-level elements use field-specific inset */
50
- [data-camox-field-id][data-camox-hovered]:not([data-camox-field-type])::after {
51
- inset: var(--camox-overlay-inset-field-hover);
52
- }
53
-
54
51
  /* ---- Focused state (overrides hovered via source order) ---- */
55
52
 
56
- [data-camox-field-id][data-camox-focused]::after,
53
+ [data-camox-field-id][data-camox-field-type][data-camox-focused]::after,
57
54
  [data-camox-block-id][data-camox-focused]:not([data-camox-detached])::after {
58
55
  content: "";
59
56
  position: absolute;
@@ -63,14 +60,28 @@
63
60
  border: var(--camox-overlay-width-selected) solid var(--camox-overlay-color-selected);
64
61
  }
65
62
 
66
- /* Field-level elements use field-specific inset */
67
- [data-camox-field-id][data-camox-focused]:not([data-camox-field-type])::after {
68
- inset: var(--camox-overlay-inset-field-selected);
63
+ /* ---- Inline string fields ----
64
+ Use box-shadow + box-decoration-break: clone so the outline repeats per line
65
+ fragment when the inline wraps. Applied directly to the element via attribute
66
+ selectors with !important so user-supplied className/style box-shadows can't
67
+ clobber it. */
68
+ [data-camox-field-id]:not([data-camox-field-type])[data-camox-hovered],
69
+ [data-camox-field-id]:not([data-camox-field-type])[data-camox-focused] {
70
+ -webkit-box-decoration-break: clone;
71
+ box-decoration-break: clone;
72
+ }
73
+
74
+ [data-camox-field-id]:not([data-camox-field-type])[data-camox-hovered] {
75
+ box-shadow: 0 0 0 var(--camox-overlay-width-hover) var(--camox-overlay-color-hover) !important;
76
+ }
77
+
78
+ [data-camox-field-id]:not([data-camox-field-type])[data-camox-focused] {
79
+ box-shadow: 0 0 0 var(--camox-overlay-width-selected) var(--camox-overlay-color-selected) !important;
69
80
  }
70
81
 
71
82
  /* ---- Layout mode color overrides ---- */
72
83
 
73
- [data-camox-field-id][data-camox-overlay-mode="layout"][data-camox-hovered]::after,
84
+ [data-camox-field-id][data-camox-field-type][data-camox-overlay-mode="layout"][data-camox-hovered]::after,
74
85
  [data-camox-block-id][data-camox-overlay-mode="layout"][data-camox-hovered]:not(
75
86
  [data-camox-detached]
76
87
  )::after,
@@ -78,13 +89,27 @@
78
89
  border-color: var(--camox-overlay-layout-color-hover);
79
90
  }
80
91
 
81
- [data-camox-field-id][data-camox-overlay-mode="layout"][data-camox-focused]::after,
92
+ [data-camox-field-id][data-camox-field-type][data-camox-overlay-mode="layout"][data-camox-focused]::after,
82
93
  [data-camox-block-id][data-camox-overlay-mode="layout"][data-camox-focused]:not(
83
94
  [data-camox-detached]
84
95
  )::after {
85
96
  border-color: var(--camox-overlay-layout-color-selected);
86
97
  }
87
98
 
99
+ [data-camox-field-id]:not(
100
+ [data-camox-field-type]
101
+ )[data-camox-overlay-mode="layout"][data-camox-hovered] {
102
+ box-shadow: 0 0 0 var(--camox-overlay-width-hover) var(--camox-overlay-layout-color-hover) !important;
103
+ border-radius: 0 !important;
104
+ }
105
+
106
+ [data-camox-field-id]:not(
107
+ [data-camox-field-type]
108
+ )[data-camox-overlay-mode="layout"][data-camox-focused] {
109
+ box-shadow: 0 0 0 var(--camox-overlay-width-selected) var(--camox-overlay-layout-color-selected) !important;
110
+ border-radius: 0 !important;
111
+ }
112
+
88
113
  /* ---- Embed: z-index above click-interceptor ---- */
89
114
 
90
115
  [data-camox-field-type="embed"][data-camox-hovered]::after,