camox 0.75.0 → 0.76.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.
Files changed (72) hide show
  1. package/dist/core/editing/createEditableBlock.js +1077 -1681
  2. package/dist/core/editing/fieldSlot.js +251 -0
  3. package/dist/core/editing/useFieldSlot.js +159 -0
  4. package/dist/core/layoutContext.d.ts +2 -0
  5. package/dist/core/layoutContext.js +27 -1
  6. package/dist/features/canvas/CanvasPageFrame.js +2 -2
  7. package/dist/features/canvas/CanvasPreviewPageSlot.js +1 -1
  8. package/dist/features/canvas/canvasCameraController.js +595 -0
  9. package/dist/features/canvas/useCanvasCamera.js +141 -587
  10. package/dist/features/canvas/useCanvasFrame.js +1 -1
  11. package/dist/features/page/DerivedPageContent.js +4 -8
  12. package/dist/features/preview/components/AssetLightbox.js +6 -65
  13. package/dist/features/preview/components/CanvasPreview.js +2 -2
  14. package/dist/features/preview/components/Frame.js +3 -3
  15. package/dist/features/preview/components/ItemFieldsEditor.js +376 -460
  16. package/dist/features/preview/components/Overlays.js +4 -9
  17. package/dist/features/preview/components/PreviewPanel.js +3 -3
  18. package/dist/features/preview/components/RemovedBlockPlaceholder.js +2 -2
  19. package/dist/features/preview/components/SelectionEditor.js +285 -339
  20. package/dist/features/preview/components/ZoomableImage.js +105 -0
  21. package/dist/features/preview/components/contentFieldSchema.js +3 -1
  22. package/dist/features/preview/components/useFieldWriter.js +30 -51
  23. package/dist/features/preview/components/useRecordView.js +3 -3
  24. package/dist/features/preview/overlayMessages.js +20 -58
  25. package/dist/features/preview/previewStore.js +6 -123
  26. package/dist/features/preview/repeatableItemPath.js +47 -0
  27. package/dist/features/provider/AuthenticatedCamoxProvider.js +75 -63
  28. package/dist/features/renderer/RendererApp.js +6 -6
  29. package/dist/features/renderer/RendererFrames.js +2 -2
  30. package/dist/features/renderer/rendererHost.js +1 -1
  31. package/dist/features/routes/pageRuntime.d.ts +8 -12
  32. package/dist/features/routes/pageRuntime.js +26 -106
  33. package/dist/features/runtime/pageApp.js +1 -1
  34. package/dist/features/runtime/pageRead.js +105 -0
  35. package/dist/features/runtime/reader.d.ts +25 -0
  36. package/dist/features/runtime/reader.js +142 -0
  37. package/dist/features/runtime/runtime.d.ts +9 -3
  38. package/dist/features/runtime/runtime.js +608 -776
  39. package/dist/features/sdkBundle/buildSdkBundle.d.ts +5 -0
  40. package/dist/features/sdkBundle/buildSdkBundle.js +9 -3
  41. package/dist/features/studio/components/ChatWidget.js +137 -121
  42. package/dist/features/studio/components/ClearThread.js +121 -0
  43. package/dist/features/studio/components/DeleteDraftDialog.js +67 -65
  44. package/dist/features/studio/components/DraftBar.js +60 -49
  45. package/dist/features/studio/components/DraftNotices.js +149 -0
  46. package/dist/features/studio/components/DraftsControl.js +217 -180
  47. package/dist/features/studio/components/DraftsSheet.js +446 -0
  48. package/dist/features/studio/components/PublishedTab.js +511 -0
  49. package/dist/features/studio/components/ReviewDraftDialog.js +104 -89
  50. package/dist/features/studio/components/SheetTable.js +46 -0
  51. package/dist/features/studio/components/Specialists.js +1 -1
  52. package/dist/features/studio/components/ThreadMessages.js +227 -176
  53. package/dist/features/studio/draftLabel.js +15 -1
  54. package/dist/features/studio/draftNotice.js +1 -0
  55. package/dist/features/studio/showInCanvas.js +9 -19
  56. package/dist/features/studio/studioStore.js +5 -5
  57. package/dist/features/studio/useArchiveDraft.js +60 -0
  58. package/dist/features/studio/useEditorSelection.js +8 -19
  59. package/dist/features/studio/useRestoreDraft.js +70 -0
  60. package/dist/features/studio/viewSwitch.js +19 -11
  61. package/dist/lib/draft-scope.d.ts +1 -1
  62. package/dist/lib/draft-scope.js +6 -3
  63. package/dist/lib/queries.js +12 -1
  64. package/dist/lib/studio-reads.js +22 -0
  65. package/dist/lib/use-project-room.js +19 -13
  66. package/dist/lib/use-thread.js +1 -1
  67. package/dist/package.js +1 -1
  68. package/dist/studio.css +1 -1
  69. package/package.json +4 -4
  70. package/dist/core/hooks/useFieldSelection.js +0 -67
  71. package/dist/core/hooks/useIsEditable.js +0 -21
  72. package/dist/core/hooks/useOverlayMessage.js +0 -70
@@ -0,0 +1,105 @@
1
+ import { c } from "react/compiler-runtime";
2
+ import { useEffect, useRef, useState } from "react";
3
+ import { jsx } from "react/jsx-runtime";
4
+
5
+ //#region src/features/preview/components/ZoomableImage.tsx
6
+ /**
7
+ * An image shown whole in the area it fills, which a click zooms to twice its size and another
8
+ * click zooms back out. Zoomed in, the area scrolls to every edge of the image, centered on where
9
+ * it was clicked. The checkerboard shows only through the image's transparent pixels.
10
+ */
11
+ function ZoomableImage(t0) {
12
+ const $ = c(14);
13
+ const { src, alt } = t0;
14
+ const containerRef = useRef(null);
15
+ const imgRef = useRef(null);
16
+ const [zoomedWidth, setZoomedWidth] = useState(null);
17
+ const clickFractionRef = useRef(null);
18
+ let t1;
19
+ let t2;
20
+ if ($[0] !== zoomedWidth) {
21
+ t1 = () => {
22
+ const container = containerRef.current;
23
+ const img = imgRef.current;
24
+ const frac = clickFractionRef.current;
25
+ if (zoomedWidth == null || !container || !img || !frac) return;
26
+ const frame = requestAnimationFrame(() => {
27
+ container.scrollTo(img.offsetLeft + img.offsetWidth * frac.x - container.clientWidth / 2, img.offsetTop + img.offsetHeight * frac.y - container.clientHeight / 2);
28
+ clickFractionRef.current = null;
29
+ });
30
+ return () => cancelAnimationFrame(frame);
31
+ };
32
+ t2 = [zoomedWidth];
33
+ $[0] = zoomedWidth;
34
+ $[1] = t1;
35
+ $[2] = t2;
36
+ } else {
37
+ t1 = $[1];
38
+ t2 = $[2];
39
+ }
40
+ useEffect(t1, t2);
41
+ const zoomed = zoomedWidth != null;
42
+ const t3 = `absolute inset-0 ${zoomed ? "overflow-auto" : "flex items-center justify-center overflow-hidden p-6"}`;
43
+ let t4;
44
+ if ($[3] !== zoomed) {
45
+ t4 = (e) => {
46
+ const img_0 = imgRef.current;
47
+ if (!img_0) return;
48
+ if (zoomed) {
49
+ setZoomedWidth(null);
50
+ containerRef.current?.scrollTo(0, 0);
51
+ return;
52
+ }
53
+ const rect = img_0.getBoundingClientRect();
54
+ clickFractionRef.current = {
55
+ x: (e.clientX - rect.left) / rect.width,
56
+ y: (e.clientY - rect.top) / rect.height
57
+ };
58
+ setZoomedWidth(img_0.clientWidth * 2);
59
+ };
60
+ $[3] = zoomed;
61
+ $[4] = t4;
62
+ } else t4 = $[4];
63
+ let t5;
64
+ if ($[5] !== alt || $[6] !== src || $[7] !== zoomed || $[8] !== zoomedWidth) {
65
+ t5 = zoomed ? /* @__PURE__ */ jsx("div", {
66
+ className: "flex min-h-full w-max min-w-full p-6",
67
+ children: /* @__PURE__ */ jsx("img", {
68
+ ref: imgRef,
69
+ src,
70
+ alt,
71
+ className: "checkered m-auto max-w-none cursor-zoom-out shadow-lg",
72
+ style: { width: zoomedWidth },
73
+ draggable: false
74
+ })
75
+ }) : /* @__PURE__ */ jsx("img", {
76
+ ref: imgRef,
77
+ src,
78
+ alt,
79
+ className: "checkered max-h-full max-w-full cursor-zoom-in object-contain shadow-lg",
80
+ draggable: false
81
+ });
82
+ $[5] = alt;
83
+ $[6] = src;
84
+ $[7] = zoomed;
85
+ $[8] = zoomedWidth;
86
+ $[9] = t5;
87
+ } else t5 = $[9];
88
+ let t6;
89
+ if ($[10] !== t3 || $[11] !== t4 || $[12] !== t5) {
90
+ t6 = /* @__PURE__ */ jsx("div", {
91
+ ref: containerRef,
92
+ className: t3,
93
+ onClick: t4,
94
+ children: t5
95
+ });
96
+ $[10] = t3;
97
+ $[11] = t4;
98
+ $[12] = t5;
99
+ $[13] = t6;
100
+ } else t6 = $[13];
101
+ return t6;
102
+ }
103
+
104
+ //#endregion
105
+ export { ZoomableImage };
@@ -7,6 +7,8 @@ function contentFieldSchema(schema, fieldName) {
7
7
  function contentFieldNames(schema) {
8
8
  return Object.keys(schema?.properties ?? {});
9
9
  }
10
+ /** A field's name as a label, for fields without a title: `ctaLabel` reads "Cta Label". */
11
+ const formatFieldName = (fieldName) => fieldName.replace(/([A-Z])/g, " $1").replace(/^./, (str) => str.toUpperCase()).trim();
10
12
 
11
13
  //#endregion
12
- export { contentFieldNames, contentFieldSchema };
14
+ export { contentFieldNames, contentFieldSchema, formatFieldName };
@@ -3,13 +3,13 @@ import { referenceList } from "../../../lib/normalized-data.js";
3
3
  import { reads, useStudioCache } from "../../../lib/studio-reads.js";
4
4
  import { referenceListIds } from "../../../core/lib/reference.js";
5
5
  import { recordText } from "../../../core/recordItem.js";
6
- import { blockMutations, collectionMutations, repeatableItemMutations } from "../../../lib/queries.js";
7
- import { invalidateCollectionRecordViews } from "../../../lib/collection-cache.js";
6
+ import { blockMutations, repeatableItemMutations } from "../../../lib/queries.js";
8
7
  import { serializeAssetField } from "../../content/collection-form.js";
9
- import { referenceWritesFor } from "../../../core/editing/referenceWrites.js";
8
+ import { fieldEdit } from "../../../core/editing/fieldSlot.js";
9
+ import { useApplyContentEdit } from "../../../core/editing/contentEdits.js";
10
10
  import { contentFieldSchema } from "./contentFieldSchema.js";
11
11
  import { c } from "react/compiler-runtime";
12
- import { useMutation, useQueryClient } from "@tanstack/react-query";
12
+ import { useMutation } from "@tanstack/react-query";
13
13
  import { toast } from "@camox/ui/toaster";
14
14
 
15
15
  //#region src/features/preview/components/useFieldWriter.ts
@@ -73,8 +73,7 @@ function applyReferenceListOptimistically(studioCache, blockId, owner, fieldName
73
73
  * collection asset snapshots rather than block file markers.
74
74
  */
75
75
  function useFieldWriter(target, schema) {
76
- const $ = c(12);
77
- const queryClient = useQueryClient();
76
+ const $ = c(10);
78
77
  const projectUuid = useProjectUuid();
79
78
  let t0;
80
79
  if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
@@ -88,35 +87,17 @@ function useFieldWriter(target, schema) {
88
87
  $[1] = t1;
89
88
  } else t1 = $[1];
90
89
  const updateItemContent = useMutation(t1);
91
- let t2;
92
- if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
93
- t2 = collectionMutations.edit();
94
- $[2] = t2;
95
- } else t2 = $[2];
96
- const editRecord = useMutation(t2);
90
+ const applyEdit = useApplyContentEdit();
97
91
  const studioCache = useStudioCache();
98
- let t3;
99
- if ($[3] !== editRecord || $[4] !== projectUuid || $[5] !== queryClient || $[6] !== schema || $[7] !== studioCache || $[8] !== target || $[9] !== updateBlockContent || $[10] !== updateItemContent) {
100
- t3 = (fieldName, value) => {
92
+ let t2;
93
+ if ($[2] !== applyEdit || $[3] !== projectUuid || $[4] !== schema || $[5] !== studioCache || $[6] !== target || $[7] !== updateBlockContent || $[8] !== updateItemContent) {
94
+ t2 = (fieldName, value) => {
101
95
  if (target?.kind === "record") {
102
96
  const fieldType = fieldTypeOf(schema, fieldName);
103
97
  const awaited = fieldType === "Reference" || fieldType === "ReferenceList";
104
- const { record } = target;
105
- const saving = referenceWritesFor(queryClient).save(record, fieldName, serializeAssetField(fieldType, value), async (input) => {
106
- const saved = await editRecord.mutateAsync({
107
- ...input,
108
- projectUuid
109
- });
110
- studioCache.set(reads.record({
111
- projectUuid,
112
- collectionId: record.collectionId,
113
- id: record.id
114
- }), saved);
115
- invalidateCollectionRecordViews(studioCache, projectUuid, record.collectionId);
116
- return saved;
117
- }).then(_temp);
98
+ const saving = applyEdit(fieldEdit(target, fieldName, serializeAssetField(fieldType, value)));
118
99
  if (awaited) return saving;
119
- saving.catch(_temp2);
100
+ saving.catch(_temp);
120
101
  return;
121
102
  }
122
103
  const content = { [fieldName]: value };
@@ -124,10 +105,10 @@ function useFieldWriter(target, schema) {
124
105
  const id = target?.kind === "item" ? target.itemId : target?.blockId;
125
106
  const field = contentFieldSchema(schema, fieldName);
126
107
  if (field?.fieldType === "ReferenceList" && target) {
127
- const { collectionId: t4 } = field;
128
- const collectionId = t4 === void 0 ? "" : t4;
108
+ const { collectionId: t3 } = field;
109
+ const collectionId = t3 === void 0 ? "" : t3;
129
110
  const linkRecord = async (recordId) => {
130
- const [record_0, collection] = await Promise.all([studioCache.ensure(reads.record({
111
+ const [record, collection] = await Promise.all([studioCache.ensure(reads.record({
131
112
  projectUuid,
132
113
  collectionId,
133
114
  id: recordId
@@ -136,10 +117,10 @@ function useFieldWriter(target, schema) {
136
117
  collectionId
137
118
  }))]);
138
119
  return {
139
- id: record_0.id,
120
+ id: record.id,
140
121
  collectionId,
141
- content: record_0.content,
142
- ...recordText(collection, record_0.content)
122
+ content: record.content,
123
+ ...recordText(collection, record.content)
143
124
  };
144
125
  };
145
126
  return (async () => {
@@ -156,8 +137,8 @@ function useFieldWriter(target, schema) {
156
137
  id: target.blockId,
157
138
  content
158
139
  });
159
- } catch (t5) {
160
- const cause_0 = t5;
140
+ } catch (t4) {
141
+ const cause_0 = t4;
161
142
  restore();
162
143
  throw cause_0;
163
144
  }
@@ -177,22 +158,20 @@ function useFieldWriter(target, schema) {
177
158
  content
178
159
  });
179
160
  };
180
- $[3] = editRecord;
181
- $[4] = projectUuid;
182
- $[5] = queryClient;
183
- $[6] = schema;
184
- $[7] = studioCache;
185
- $[8] = target;
186
- $[9] = updateBlockContent;
187
- $[10] = updateItemContent;
188
- $[11] = t3;
189
- } else t3 = $[11];
190
- return t3;
161
+ $[2] = applyEdit;
162
+ $[3] = projectUuid;
163
+ $[4] = schema;
164
+ $[5] = studioCache;
165
+ $[6] = target;
166
+ $[7] = updateBlockContent;
167
+ $[8] = updateItemContent;
168
+ $[9] = t2;
169
+ } else t2 = $[9];
170
+ return t2;
191
171
  }
192
- function _temp2(cause) {
172
+ function _temp(cause) {
193
173
  toast.error(cause instanceof Error ? cause.message : "Could not save item");
194
174
  }
195
- function _temp() {}
196
175
 
197
176
  //#endregion
198
177
  export { useFieldWriter };
@@ -1,9 +1,9 @@
1
1
  import { placedRecords, referenceList } from "../../../lib/normalized-data.js";
2
2
  import { referenceListIds } from "../../../core/lib/reference.js";
3
3
  import { useCollectionDefinition } from "../../../lib/studio-definitions.js";
4
- import { isBlockPlacement, previewStore, recordPlacement } from "../previewStore.js";
5
- import { contentFieldSchema } from "./contentFieldSchema.js";
6
- import { formatFieldName } from "./ItemFieldsEditor.js";
4
+ import { isBlockPlacement, recordPlacement } from "../../../core/editing/fieldSlot.js";
5
+ import { previewStore } from "../previewStore.js";
6
+ import { contentFieldSchema, formatFieldName } from "./contentFieldSchema.js";
7
7
  import { c } from "react/compiler-runtime";
8
8
  import * as React from "react";
9
9
  import { isQueryBacked } from "@camox/api-contract";
@@ -1,78 +1,40 @@
1
- import { isBlockPlacement } from "./previewStore.js";
1
+ import { placementId, slotId, slotOf } from "../../core/editing/fieldSlot.js";
2
2
 
3
3
  //#region src/features/preview/overlayMessages.ts
4
4
  /**
5
- * The field ID shared by the preview's editable fields and the sidebar's hover/focus messages:
6
- * `block__field` for block fields, `block__item__field` for repeater item fields, and
7
- * the placement's ID followed by `__recordField` for the fields of a placed collection record.
8
- * Record field IDs carry the record ID so that each record of a reference list is its own target.
5
+ * The message that starts or ends the sidebar's hover of what `selection` selects, as the page's
6
+ * editable fields and records follow it.
9
7
  */
10
- function overlayFieldId(blockId, fieldName, scope = {}) {
11
- if (scope.placement) return recordFieldId(scope.placement, fieldName);
12
- if (scope.itemId != null) return `${blockId}__${scope.itemId}__${fieldName}`;
13
- return `${blockId}__${fieldName}`;
14
- }
15
- /** The field ID of one field of a placed collection record. */
16
- function recordFieldId(placement, recordFieldName) {
17
- return `${recordPlacementId(placement)}__${recordFieldName}`;
18
- }
19
- /**
20
- * The overlay ID of one placement of a record: `block__referenceField__record`, or
21
- * `block__item__referenceField__record` when a repeatable item places it, or
22
- * `layout-layoutId__record` when a layout renders it on a record page. Followed by
23
- * `__recordField__linkedRecord` for a record that record links. Unique per record
24
- * within a reference list, and distinct from the same record placed elsewhere.
25
- */
26
- function recordPlacementId(placement) {
27
- const id = isBlockPlacement(placement) ? `${blockPlacementOwnerId(placement)}__${placement.fieldName}__${placement.recordId}` : `layout-${placement.layoutId}__${placement.recordId}`;
28
- if (!placement.nested) return id;
29
- return `${id}__${placement.nested.fieldName}__${placement.nested.recordId}`;
30
- }
31
- function blockPlacementOwnerId(placement) {
32
- if (placement.itemId == null) return String(placement.blockId);
33
- return `${placement.blockId}__${placement.itemId}`;
34
- }
35
- /** Use the same preview targets for selection-path hover as the sidebar editors. */
36
8
  function selectionHoverMessage(selection, hovered) {
37
9
  if (!selection) return { type: hovered ? "CAMOX_HOVER_PAGE" : "CAMOX_HOVER_PAGE_END" };
38
- if (selection.type === "record") {
39
- const fieldId = recordPlacementId(selection);
40
- return {
41
- type: hovered ? "CAMOX_HOVER_FIELD" : "CAMOX_HOVER_FIELD_END",
42
- fieldId
43
- };
44
- }
45
- if (selection.type === "record-field") {
46
- const fieldId = recordFieldId(selection, selection.recordFieldName);
10
+ const field = (fieldId) => ({
11
+ type: hovered ? "CAMOX_HOVER_FIELD" : "CAMOX_HOVER_FIELD_END",
12
+ fieldId
13
+ });
14
+ if (selection.type === "record") return field(placementId(selection));
15
+ if (selection.type === "block") {
16
+ const { blockId } = selection;
47
17
  return {
48
- type: hovered ? "CAMOX_HOVER_FIELD" : "CAMOX_HOVER_FIELD_END",
49
- fieldId
18
+ type: hovered ? "CAMOX_HOVER_BLOCK" : "CAMOX_HOVER_BLOCK_END",
19
+ blockId
50
20
  };
51
21
  }
52
- const blockId = String(selection.blockId);
53
- if (selection.type === "block") return {
54
- type: hovered ? "CAMOX_HOVER_BLOCK" : "CAMOX_HOVER_BLOCK_END",
55
- blockId
56
- };
57
22
  if (selection.type === "item") return {
58
23
  type: hovered ? "CAMOX_HOVER_REPEATABLE_ITEM" : "CAMOX_HOVER_REPEATABLE_ITEM_END",
59
- blockId,
60
- itemId: String(selection.itemId)
24
+ blockId: selection.blockId,
25
+ itemId: selection.itemId
61
26
  };
62
- if (selection.fieldType === "Repeater") return {
27
+ const slot = slotOf(selection);
28
+ if (slot.type === "Repeater" && slot.owner.kind !== "record") return {
63
29
  type: hovered ? "CAMOX_HOVER_REPEATER" : "CAMOX_HOVER_REPEATER_END",
64
- blockId,
65
- fieldName: selection.fieldName
66
- };
67
- const fieldId = overlayFieldId(blockId, selection.fieldName, { itemId: selection.type === "item-field" ? selection.itemId : null });
68
- return {
69
- type: hovered ? "CAMOX_HOVER_FIELD" : "CAMOX_HOVER_FIELD_END",
70
- fieldId
30
+ blockId: slot.owner.blockId,
31
+ fieldName: slot.name
71
32
  };
33
+ return field(slotId(slot));
72
34
  }
73
35
  function isOverlayMessage(data) {
74
36
  return typeof data === "object" && data !== null && "type" in data && typeof data.type === "string" && data.type.startsWith("CAMOX_");
75
37
  }
76
38
 
77
39
  //#endregion
78
- export { isOverlayMessage, overlayFieldId, recordFieldId, recordPlacementId, selectionHoverMessage };
40
+ export { isOverlayMessage, selectionHoverMessage };
@@ -1,43 +1,7 @@
1
+ import { isBlockPlacement, parentOf, recordPlacement, samePlacement } from "../../core/editing/fieldSlot.js";
1
2
  import { createStore } from "@xstate/store-react";
2
3
 
3
4
  //#region src/features/preview/previewStore.ts
4
- /**
5
- * Whether a block places the record. Checks `blockId` rather than `layoutId`, because editing
6
- * events spread a layout editing owner's `layoutId` next to block-owned placements.
7
- */
8
- function isBlockPlacement(placement) {
9
- return "blockId" in placement && placement.blockId != null;
10
- }
11
- function copyNested(placement) {
12
- if (!placement.nested) return {};
13
- return { nested: {
14
- fieldName: placement.nested.fieldName,
15
- recordId: placement.nested.recordId
16
- } };
17
- }
18
- function recordPlacement(placement) {
19
- if (!isBlockPlacement(placement)) return {
20
- layoutId: placement.layoutId,
21
- recordId: placement.recordId,
22
- ...copyNested(placement)
23
- };
24
- return {
25
- blockId: placement.blockId,
26
- ...placement.itemId == null ? {} : { itemId: placement.itemId },
27
- fieldName: placement.fieldName,
28
- recordId: placement.recordId,
29
- ...copyNested(placement)
30
- };
31
- }
32
- /** Whether two placements are the same occurrence: same owner, field and record. */
33
- function samePlacement(a, b) {
34
- if (a.recordId !== b.recordId) return false;
35
- if (a.nested?.fieldName !== b.nested?.fieldName) return false;
36
- if (a.nested?.recordId !== b.nested?.recordId) return false;
37
- if (!isBlockPlacement(a)) return !isBlockPlacement(b) && a.layoutId === b.layoutId;
38
- if (!isBlockPlacement(b)) return false;
39
- return a.blockId === b.blockId && (a.itemId ?? null) === (b.itemId ?? null) && a.fieldName === b.fieldName;
40
- }
41
5
  function sameEditingOwner(a, b) {
42
6
  if (!a || !b) return a === b;
43
7
  if (a.kind === "page") return b.kind === "page" && a.pageId === b.pageId;
@@ -84,15 +48,6 @@ function selectionItemId(sel) {
84
48
  if (sel.type === "record" || sel.type === "record-field") return sel.itemId ?? null;
85
49
  return null;
86
50
  }
87
- /** Check if the selection is viewing a terminal field (link, image, file, etc.). */
88
- function selectionField(sel) {
89
- if (!sel) return null;
90
- if (sel.type === "block-field" || sel.type === "item-field") return {
91
- fieldName: sel.fieldName,
92
- fieldType: sel.fieldType
93
- };
94
- return null;
95
- }
96
51
  const selectIsCanvasShown = (state) => state.context.view === "canvas";
97
52
  const initialPreviewContext = {
98
53
  view: "preview",
@@ -221,87 +176,15 @@ const previewStore = createStore({
221
176
  selectParent: (context) => {
222
177
  const editingContext = context.editingContext;
223
178
  const sel = editingContext?.selection;
224
- if (!sel) return context;
225
- if (sel.type === "record-field") return {
226
- ...context,
227
- editingContext: {
228
- ...editingContext,
229
- selection: {
230
- type: "record",
231
- ...recordPlacement(sel)
232
- }
233
- }
234
- };
235
- if (sel.type === "record" && sel.nested) {
236
- const { nested: _nested, ...outer } = recordPlacement(sel);
237
- return {
238
- ...context,
239
- editingContext: {
240
- ...editingContext,
241
- selection: {
242
- type: "record-field",
243
- ...outer,
244
- recordFieldName: sel.nested.fieldName,
245
- recordFieldType: "Reference"
246
- }
247
- }
248
- };
249
- }
250
- if (sel.type === "record" && !isBlockPlacement(sel)) return context;
251
- if (sel.type === "record") {
252
- const field = {
253
- fieldName: sel.fieldName,
254
- fieldType: "Reference"
255
- };
256
- return {
257
- ...context,
258
- editingContext: {
259
- ...editingContext,
260
- selection: sel.itemId == null ? {
261
- type: "block-field",
262
- blockId: sel.blockId,
263
- ...field
264
- } : {
265
- type: "item-field",
266
- blockId: sel.blockId,
267
- itemId: sel.itemId,
268
- ...field
269
- }
270
- }
271
- };
272
- }
273
- if (sel.type === "block-field") return {
274
- ...context,
275
- editingContext: {
276
- ...editingContext,
277
- selection: {
278
- type: "block",
279
- blockId: sel.blockId
280
- }
281
- }
282
- };
283
- if (sel.type === "item-field") return {
284
- ...context,
285
- editingContext: {
286
- ...editingContext,
287
- selection: {
288
- type: "item",
289
- blockId: sel.blockId,
290
- itemId: sel.itemId
291
- }
292
- }
293
- };
294
- if (sel.type === "item") return {
179
+ const parent = sel ? parentOf(sel) : null;
180
+ if (!parent || !editingContext) return context;
181
+ return {
295
182
  ...context,
296
183
  editingContext: {
297
184
  ...editingContext,
298
- selection: {
299
- type: "block",
300
- blockId: sel.blockId
301
- }
185
+ selection: parent
302
186
  }
303
187
  };
304
- return context;
305
188
  },
306
189
  clearSelection: (context) => ({
307
190
  ...context,
@@ -333,4 +216,4 @@ const previewStore = createStore({
333
216
  });
334
217
 
335
218
  //#endregion
336
- export { isBlockPlacement, isBlockSelection, previewStore, recordPlacement, sameEditingOwner, samePlacement, selectIsCanvasShown, selectionBlockId, selectionField, selectionForOwner, selectionForPage, selectionItemId };
219
+ export { isBlockPlacement, isBlockSelection, previewStore, recordPlacement, sameEditingOwner, selectIsCanvasShown, selectionBlockId, selectionForOwner, selectionForPage, selectionItemId };
@@ -0,0 +1,47 @@
1
+ //#region src/features/preview/repeatableItemPath.ts
2
+ /** The repeatable items from the block down to `itemId`, each item's parent before it. */
3
+ function itemPath(itemId, itemsMap) {
4
+ const path = [];
5
+ for (let item = itemId == null ? void 0 : itemsMap.get(itemId); item;) {
6
+ path.unshift(item);
7
+ item = item.parentItemId ? itemsMap.get(item.parentItemId) : void 0;
8
+ }
9
+ return path;
10
+ }
11
+ /**
12
+ * The repeater field schemas along a path, from the block's content schema: the one holding each
13
+ * item of the path, in order. Stops where the schema no longer declares the item's repeater.
14
+ */
15
+ function repeaterSchemas(contentSchema, path) {
16
+ const repeaters = [];
17
+ let schema = contentSchema;
18
+ for (const item of path) {
19
+ const repeater = schema?.properties?.[item.fieldName];
20
+ if (!repeater?.items) break;
21
+ repeaters.push(repeater);
22
+ schema = repeater.items;
23
+ }
24
+ return repeaters;
25
+ }
26
+ /**
27
+ * The content schema of the path's last item: the block's own with no item, or null when the
28
+ * block's schema no longer declares it.
29
+ */
30
+ function itemContentSchema(contentSchema, path) {
31
+ const repeaters = repeaterSchemas(contentSchema, path);
32
+ if (repeaters.length < path.length) return null;
33
+ const last = repeaters.at(-1);
34
+ return last ? last.items : contentSchema;
35
+ }
36
+ /**
37
+ * The repeater field schema holding the path's last item, where its item settings are declared,
38
+ * or null when the block's schema no longer declares it.
39
+ */
40
+ function itemRepeaterSchema(contentSchema, path) {
41
+ if (path.length === 0) return null;
42
+ const repeaters = repeaterSchemas(contentSchema, path);
43
+ return repeaters.length < path.length ? null : repeaters.at(-1);
44
+ }
45
+
46
+ //#endregion
47
+ export { itemContentSchema, itemPath, itemRepeaterSchema };