@elytracms/studio 0.0.68 → 0.0.70

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 (79) hide show
  1. package/dist/components/app-views/studio-config.d.ts +22 -0
  2. package/dist/components/app-views/studio-config.js.map +1 -1
  3. package/dist/components/assets/asset-detail.d.ts +4 -2
  4. package/dist/components/assets/asset-detail.js +4 -2
  5. package/dist/components/assets/asset-detail.js.map +1 -1
  6. package/dist/components/assets/assets-collection-view-source.d.ts +30 -19
  7. package/dist/components/assets/assets-collection-view-source.js +55 -23
  8. package/dist/components/assets/assets-collection-view-source.js.map +1 -1
  9. package/dist/components/builder/blocks-model.d.ts +91 -29
  10. package/dist/components/builder/blocks-model.js +83 -54
  11. package/dist/components/builder/blocks-model.js.map +1 -1
  12. package/dist/components/builder/blocks-panel.d.ts +40 -12
  13. package/dist/components/builder/blocks-panel.js +88 -34
  14. package/dist/components/builder/blocks-panel.js.map +1 -1
  15. package/dist/components/builder/canvas-chrome-css.js +6 -1
  16. package/dist/components/builder/canvas-chrome-css.js.map +1 -1
  17. package/dist/components/builder/fixture-thumbnail.d.ts +13 -23
  18. package/dist/components/builder/fixture-thumbnail.js +27 -99
  19. package/dist/components/builder/fixture-thumbnail.js.map +1 -1
  20. package/dist/components/builder/selection-overlay.d.ts +17 -4
  21. package/dist/components/builder/selection-overlay.js +17 -15
  22. package/dist/components/builder/selection-overlay.js.map +1 -1
  23. package/dist/components/builder/spacing-handles.d.ts +34 -6
  24. package/dist/components/builder/spacing-handles.js +70 -31
  25. package/dist/components/builder/spacing-handles.js.map +1 -1
  26. package/dist/components/cms/bulk-bar.d.ts +11 -1
  27. package/dist/components/cms/bulk-bar.js +10 -3
  28. package/dist/components/cms/bulk-bar.js.map +1 -1
  29. package/dist/components/cms/collection-table.js +19 -2
  30. package/dist/components/cms/collection-table.js.map +1 -1
  31. package/dist/components/cms/field-control.js +5 -1
  32. package/dist/components/cms/field-control.js.map +1 -1
  33. package/dist/components/cms/prop-fields.js +5 -1
  34. package/dist/components/cms/prop-fields.js.map +1 -1
  35. package/dist/components/cms/reference-confirm.js +5 -1
  36. package/dist/components/cms/reference-confirm.js.map +1 -1
  37. package/dist/components/cms/rich-text/editor.d.ts +24 -5
  38. package/dist/components/cms/rich-text/editor.js +23 -8
  39. package/dist/components/cms/rich-text/editor.js.map +1 -1
  40. package/dist/components/rail-view-source.d.ts +50 -1
  41. package/dist/components/rail-view-source.js +56 -2
  42. package/dist/components/rail-view-source.js.map +1 -1
  43. package/dist/components/redirects/redirects-collection-view-source.d.ts +3 -2
  44. package/dist/components/redirects/redirects-collection-view-source.js +4 -3
  45. package/dist/components/redirects/redirects-collection-view-source.js.map +1 -1
  46. package/dist/components/session-expired-overlay.d.ts +16 -0
  47. package/dist/components/session-expired-overlay.js +22 -6
  48. package/dist/components/session-expired-overlay.js.map +1 -1
  49. package/dist/components/sign-in-view.d.ts +10 -1
  50. package/dist/components/sign-in-view.js +14 -3
  51. package/dist/components/sign-in-view.js.map +1 -1
  52. package/dist/components/users/users-collection-view-source.js +1 -1
  53. package/dist/components/users/users-collection-view-source.js.map +1 -1
  54. package/dist/customization.d.ts +9 -1
  55. package/dist/customization.js.map +1 -1
  56. package/dist/lib/cms-views/view-model.d.ts +6 -6
  57. package/dist/lib/collection-view/collection-view-source.d.ts +12 -0
  58. package/dist/lib/collection-view/collection-view-source.js.map +1 -1
  59. package/dist/lib/dnd/gap-drag.d.ts +8 -1
  60. package/dist/lib/dnd/gap-drag.js +4 -0
  61. package/dist/lib/dnd/gap-drag.js.map +1 -1
  62. package/dist/lib/i18n/messages.de.js +8 -0
  63. package/dist/lib/i18n/messages.de.js.map +1 -1
  64. package/dist/lib/i18n/messages.en.d.ts +6 -0
  65. package/dist/lib/i18n/messages.en.js +9 -0
  66. package/dist/lib/i18n/messages.en.js.map +1 -1
  67. package/dist/lib/references/references-store.d.ts +8 -1
  68. package/dist/lib/references/references-store.js +12 -2
  69. package/dist/lib/references/references-store.js.map +1 -1
  70. package/dist/lib/workspace/index.d.ts +10 -0
  71. package/dist/lib/workspace/index.js +11 -0
  72. package/dist/lib/workspace/index.js.map +1 -1
  73. package/dist/ui/dialog.d.ts +9 -1
  74. package/dist/ui/dialog.js +8 -2
  75. package/dist/ui/dialog.js.map +1 -1
  76. package/dist/ui/surface-geometry.d.ts +8 -5
  77. package/dist/ui/surface-geometry.js +8 -5
  78. package/dist/ui/surface-geometry.js.map +1 -1
  79. package/package.json +2 -2
@@ -14,8 +14,9 @@ import { createRectTracker, measureElementRect, scheduleFrame, subscribeCanvasLa
14
14
  * container is selected on the canvas, draggable gap handles appear between
15
15
  * its children, axis-aware via the shared `containerFlowAxis`. Dragging snaps
16
16
  * through the EC-161 spacing-token scale (`lib/dnd/gap-drag` — pure math, no
17
- * pixel value can escape it) with a live plain-language label (the layout
18
- * inspector's vocabulary). The drag previews imperatively on the rendered
17
+ * pixel value can escape it); the static gap tags (EC-435) stay put and read
18
+ * the LIVE token + re-measured px during the drag (EC-438 — no
19
+ * pointer-following label). The drag previews imperatively on the rendered
19
20
  * element; releasing commits exactly ONE builder dispatch — `setProp` with the
20
21
  * token, or `removeProp` for "no gap", the identical values the inspector's
21
22
  * gap select writes — so one undo restores, and the graph stays the single
@@ -25,6 +26,18 @@ import { createRectTracker, measureElementRect, scheduleFrame, subscribeCanvasLa
25
26
  export function gapStepLabel(step) {
26
27
  return step === null ? translate(GAP_NONE_LABEL_KEY) : translate(GAP_LABEL_KEYS[step]);
27
28
  }
29
+ /**
30
+ * The hatched gap REGION for a spot (EC-435 AD-2): the rectangle between child
31
+ * N's trailing edge and child N+1's leading edge along the flow axis, spanning
32
+ * the pair's shared cross extent — the same measured child-rect basis the
33
+ * handle spots come from (`gapHandleSpots`), in the same document coordinates.
34
+ * Pure over the spot; a closed gap yields a zero-extent rect (nothing to paint).
35
+ */
36
+ export function gapRegionRect(spot, axis) {
37
+ return axis === 'horizontal'
38
+ ? { left: spot.gapStart, top: spot.crossStart, width: spot.gapLength, height: spot.crossLength }
39
+ : { left: spot.crossStart, top: spot.gapStart, width: spot.crossLength, height: spot.gapLength };
40
+ }
28
41
  /**
29
42
  * The container's rendered element (the Stack/Grid div carrying the gap style).
30
43
  * Resilient to legitimate intermediate wrappers between the editable
@@ -92,7 +105,9 @@ function spotsEqual(a, b) {
92
105
  x.x !== y.x ||
93
106
  x.y !== y.y ||
94
107
  x.crossStart !== y.crossStart ||
95
- x.crossLength !== y.crossLength) {
108
+ x.crossLength !== y.crossLength ||
109
+ x.gapStart !== y.gapStart ||
110
+ x.gapLength !== y.gapLength) {
96
111
  return false;
97
112
  }
98
113
  }
@@ -150,10 +165,54 @@ const HANDLE_MAX_LENGTH = 48;
150
165
  * contract test can walk it with `renderToStaticMarkup`. In-frame chrome
151
166
  * contract (EC-414 AD-B): `ec-*`/`data-ec-*` plus inline styles only; the root
152
167
  * carries the `.ec-chrome` containment recipe.
168
+ *
169
+ * EC-435 AD-2: every open gap region renders a pointer-transparent hatched
170
+ * rectangle (diagonal accent stripes, Figma-grid style) under the handles,
171
+ * each with a small static token-label size tag beside its handle. During a
172
+ * gap drag the tags STAY PUT and read live (EC-438 — a gap drag previews ONE
173
+ * container-wide gap, so every tag shows the live step's token + its own
174
+ * re-measured px); no label follows the pointer. The gap handles remain the
175
+ * only interactive element.
153
176
  */
154
177
  export function SpacingHandlesView(props) {
155
- const { spots, horizontal, drag } = props;
178
+ const { spots, horizontal, gapLabel, drag } = props;
179
+ const axis = horizontal ? 'horizontal' : 'vertical';
156
180
  return (_jsxs("div", { "data-ec-spacing-handles": "", className: "ec-chrome", children: [spots.map((spot) => {
181
+ const region = gapRegionRect(spot, axis);
182
+ if (spot.gapLength < 1)
183
+ return null;
184
+ return (_jsx("div", { "data-ec-gap-hatch": spot.index, style: {
185
+ position: 'absolute',
186
+ left: region.left,
187
+ top: region.top,
188
+ width: region.width,
189
+ height: region.height,
190
+ zIndex: 10,
191
+ pointerEvents: 'none',
192
+ background: 'repeating-linear-gradient(45deg, rgba(91,140,255,0.28) 0px, rgba(91,140,255,0.28) 3px, rgba(91,140,255,0.06) 3px, rgba(91,140,255,0.06) 9px)',
193
+ boxShadow: 'inset 0 0 0 1px rgba(91,140,255,0.3)',
194
+ } }, spot.index));
195
+ }), spots.map((spot) => {
196
+ if (spot.gapLength < 1)
197
+ return null;
198
+ const length = Math.min(spot.crossLength, HANDLE_MAX_LENGTH);
199
+ return (_jsxs("div", { "data-ec-gap-hatch-label": spot.index, style: {
200
+ position: 'absolute',
201
+ left: horizontal ? spot.x : spot.x + length / 2 + 6,
202
+ top: horizontal ? spot.y + length / 2 + 6 : spot.y,
203
+ transform: horizontal ? 'translateX(-50%)' : 'translateY(-50%)',
204
+ zIndex: 45,
205
+ pointerEvents: 'none',
206
+ padding: '1px 6px',
207
+ borderRadius: 5,
208
+ fontSize: 10,
209
+ fontWeight: 600,
210
+ whiteSpace: 'nowrap',
211
+ color: '#fff',
212
+ backgroundColor: 'rgba(91,140,255,0.92)',
213
+ boxShadow: '0 1px 4px rgba(8,12,24,0.3)',
214
+ }, children: [drag ? drag.label : gapLabel, " \u00B7 ", Math.round(spot.gapLength), "px"] }, spot.index));
215
+ }), spots.map((spot) => {
157
216
  const length = Math.min(spot.crossLength, HANDLE_MAX_LENGTH);
158
217
  const active = drag?.index === spot.index;
159
218
  return (_jsx("div", { "data-ec-gap-handle": spot.index, role: "separator", "aria-label": translate('builder.spacing.gapAfterChild', { index: spot.index + 1 }), "aria-orientation": horizontal ? 'vertical' : 'horizontal', onPointerDown: props.onPointerDown(spot), onPointerMove: props.onPointerMove, onPointerUp: props.onPointerUp, onPointerCancel: props.onPointerCancel, style: {
@@ -175,21 +234,7 @@ export function SpacingHandlesView(props) {
175
234
  backgroundColor: active ? '#5b8cff' : 'rgba(91, 140, 255, 0.65)',
176
235
  boxShadow: '0 0 0 1px rgba(8, 12, 24, 0.55)',
177
236
  } }) }, spot.index));
178
- }), drag ? (_jsx("div", { "data-ec-gap-label": "", style: {
179
- position: 'absolute',
180
- left: drag.labelX + 12,
181
- top: drag.labelY + 12,
182
- zIndex: 50,
183
- pointerEvents: 'none',
184
- padding: '2px 8px',
185
- borderRadius: 6,
186
- fontSize: 11,
187
- fontWeight: 600,
188
- whiteSpace: 'nowrap',
189
- color: '#fff',
190
- backgroundColor: '#5b8cff',
191
- boxShadow: '0 2px 8px rgba(8, 12, 24, 0.4)',
192
- }, children: drag.label })) : null] }));
237
+ })] }));
193
238
  }
194
239
  export function SpacingHandles({ doc, node }) {
195
240
  const builder = useBuilder();
@@ -311,8 +356,6 @@ export function SpacingHandles({ doc, node }) {
311
356
  startStep,
312
357
  step: startStep,
313
358
  startCoord: axis === 'horizontal' ? e.clientX : e.clientY,
314
- labelX: spot.x,
315
- labelY: spot.y,
316
359
  };
317
360
  dragRef.current = next;
318
361
  setDrag(next);
@@ -321,22 +364,18 @@ export function SpacingHandles({ doc, node }) {
321
364
  const active = dragRef.current;
322
365
  if (!active)
323
366
  return;
324
- const win = doc.defaultView;
325
367
  const coord = axis === 'horizontal' ? e.clientX : e.clientY;
326
368
  const step = gapStepFromDrag(active.startStep, coord - active.startCoord);
327
- const next = {
328
- ...active,
329
- step,
330
- labelX: e.clientX + (win?.scrollX ?? 0),
331
- labelY: e.clientY + (win?.scrollY ?? 0),
332
- };
369
+ const next = { ...active, step };
333
370
  dragRef.current = next;
334
371
  setDrag(next);
335
372
  applyPreview(step);
336
373
  };
337
374
  const horizontal = axis === 'horizontal';
338
- return createPortal(_jsx(SpacingHandlesView, { spots: spots, horizontal: horizontal, drag: drag
339
- ? { index: drag.index, labelX: drag.labelX, labelY: drag.labelY, label: gapStepLabel(drag.step) }
340
- : null, onPointerDown: onPointerDown, onPointerMove: onPointerMove, onPointerUp: () => finish('commit'), onPointerCancel: () => finish('cancel') }), doc.body);
375
+ return createPortal(_jsx(SpacingHandlesView, { spots: spots, horizontal: horizontal,
376
+ // The static size tags share ONE vocabulary with the inspector's gap
377
+ // select: the token label of the node's committed gap step (EC-435
378
+ // AD-2); during a drag they read the live step instead (EC-438).
379
+ gapLabel: gapStepLabel(containerGapStep(node)), drag: drag ? { index: drag.index, label: gapStepLabel(drag.step) } : null, onPointerDown: onPointerDown, onPointerMove: onPointerMove, onPointerUp: () => finish('commit'), onPointerCancel: () => finish('cancel') }), doc.body);
341
380
  }
342
381
  //# sourceMappingURL=spacing-handles.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"spacing-handles.js","sourceRoot":"","sources":["../../../src/components/builder/spacing-handles.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'\nimport { createPortal } from 'react-dom'\nimport { DEFAULT_SLOT, isLayoutContainerNode, staticProp } from '@elytracms/core/project-graph'\nimport type { ComponentNode } from '@elytracms/core/project-graph'\nimport { containerFlowAxis } from '@elytracms/studio/lib/dnd/drop-rules'\nimport type { FlowAxis } from '@elytracms/studio/lib/dnd/drop-rules'\nimport {\n containerGapStep,\n gapHandleSpots,\n gapStepCss,\n gapStepFromDrag,\n} from '@elytracms/studio/lib/dnd/gap-drag'\nimport type { GapStep } from '@elytracms/studio/lib/dnd/gap-drag'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { useBuilder } from './builder-store'\nimport { GAP_LABEL_KEYS, GAP_NONE_LABEL_KEY } from './layout-inspector'\nimport { activeDrag, onDragSessionEnd, onDragSessionStart } from './drag-session'\nimport {\n createRectTracker,\n measureElementRect,\n scheduleFrame,\n subscribeCanvasLayout,\n} from './canvas-rect-tracking'\nimport type { FrameScheduler, RectTracker } from './canvas-rect-tracking'\n\n/**\n * Tokenized spacing handles (EC-163, vision §3.1 / AD-10): when a Stack/Grid\n * container is selected on the canvas, draggable gap handles appear between\n * its children, axis-aware via the shared `containerFlowAxis`. Dragging snaps\n * through the EC-161 spacing-token scale (`lib/dnd/gap-drag` — pure math, no\n * pixel value can escape it) with a live plain-language label (the layout\n * inspector's vocabulary). The drag previews imperatively on the rendered\n * element; releasing commits exactly ONE builder dispatch — `setProp` with the\n * token, or `removeProp` for \"no gap\", the identical values the inspector's\n * gap select writes — so one undo restores, and the graph stays the single\n * source of truth. Esc mid-drag cancels: preview reverts, nothing dispatches.\n */\n\n/** Plain-language label for a gap step — the inspector's vocabulary. */\nexport function gapStepLabel(step: GapStep): string {\n return step === null ? translate(GAP_NONE_LABEL_KEY) : translate(GAP_LABEL_KEYS[step])\n}\n\nexport interface HandleSpotView {\n index: number\n /** Document-coordinate position of the handle's center. */\n x: number\n y: number\n crossStart: number\n crossLength: number\n}\n\ninterface ActiveDrag {\n index: number\n startStep: GapStep\n step: GapStep\n /** Pointer start along the flow axis (client coords). */\n startCoord: number\n /** Live pointer position (document coords) for the floating label. */\n labelX: number\n labelY: number\n}\n\n/**\n * The container's rendered element (the Stack/Grid div carrying the gap style).\n * Resilient to legitimate intermediate wrappers between the editable\n * `[data-ec-node-id]` wrapper and the primitive (an error-boundary/provider DOM\n * node, a future decorator layer): the FIRST `[data-ec-primitive]` inside the\n * wrapper whose NEAREST node wrapper is THIS node — so it tolerates a wrapper\n * above the primitive but never descends into a CHILD node's subtree and grabs\n * that nested container's own `[data-ec-primitive]`. Mirrors `measurePropRect`'s\n * nearest-node scoping (EC-408). Today no such wrapper exists — `decorate` wraps\n * `<RenderErrorBoundary>` which renders its children with no DOM element, so the\n * primitive is a direct child — but the scoping makes 2(b) unregressable.\n */\nexport function containerElementFor(doc: Document, nodeId: string): HTMLElement | null {\n const wrapper = doc.querySelector(`[data-ec-node-id=\"${nodeId}\"]`)\n if (!wrapper) return null\n for (const primitive of wrapper.querySelectorAll('[data-ec-primitive]')) {\n if (primitive.closest('[data-ec-node-id]') === wrapper) return primitive as HTMLElement\n }\n return null\n}\n\n/** The container's rendered child node-wrappers — the DOM child basis. */\nfunction childWrappers(container: Element): HTMLElement[] {\n return Array.from(container.querySelectorAll(':scope > [data-ec-node-id]')) as HTMLElement[]\n}\n\ninterface Measured {\n spots: HandleSpotView[]\n /** The axis the children are *rendered* along (drag direction follows it). */\n axis: FlowAxis\n}\n\nconst EMPTY_MEASURED = (axis: FlowAxis): Measured => ({ spots: [], axis })\n\n/**\n * Measure the handle spots between the container's rendered children — the\n * SINGLE source of truth for whether handles show (spots) and where. Eligibility\n * and measurement agree on the SAME (DOM) child basis by construction: a\n * null-rendering graph child (condition-gated → no `[data-ec-node-id]` wrapper)\n * simply isn't counted, so it can neither block handles when ≥2 children render\n * nor phantom them when <2 do; kept fresh by the live tracker (EC-404) so a child\n * add/remove or resize re-runs this. The graph-level flow axis\n * (`containerFlowAxis`) is tried first; when the rendered layout disagrees — a\n * stacking Grid below the mobile breakpoint lays its \"columns\" out vertically —\n * the cross axis is measured instead, so handles always sit in the gaps the\n * editor actually sees.\n */\nexport function measureSpots(doc: Document, nodeId: string, flowAxis: FlowAxis): Measured {\n const container = containerElementFor(doc, nodeId)\n const win = doc.defaultView\n if (!container || !win) return EMPTY_MEASURED(flowAxis)\n const children = childWrappers(container)\n if (children.length < 2) return EMPTY_MEASURED(flowAxis)\n const rects = children.map((el) => measureElementRect(el, win))\n const along = gapHandleSpots(rects, flowAxis)\n if (along.length > 0) return { spots: along, axis: flowAxis }\n const cross: FlowAxis = flowAxis === 'horizontal' ? 'vertical' : 'horizontal'\n const across = gapHandleSpots(rects, cross)\n return across.length > 0 ? { spots: across, axis: cross } : EMPTY_MEASURED(flowAxis)\n}\n\n/** Structural equality of two spot lists — suppresses redundant tracker emits. */\nfunction spotsEqual(a: readonly HandleSpotView[], b: readonly HandleSpotView[]): boolean {\n if (a.length !== b.length) return false\n for (let i = 0; i < a.length; i += 1) {\n const x = a[i]!\n const y = b[i]!\n if (\n x.index !== y.index ||\n x.x !== y.x ||\n x.y !== y.y ||\n x.crossStart !== y.crossStart ||\n x.crossLength !== y.crossLength\n ) {\n return false\n }\n }\n return true\n}\n\n/** True when two measurements are equivalent (same axis, same spots). */\nexport function measuredEqual(a: Measured, b: Measured): boolean {\n return a.axis === b.axis && spotsEqual(a.spots, b.spots)\n}\n\n/** The elements the tracker's `ResizeObserver` watches: the container + its children. */\nfunction spacingTargets(doc: Document, nodeId: string): Element[] {\n const container = containerElementFor(doc, nodeId)\n if (!container) return []\n return [container, ...childWrappers(container)]\n}\n\n/**\n * Wire a live rect tracker for the spacing handles onto the shared EC-404 core:\n * `subscribeCanvasLayout` re-measures under child resize (image/font decode),\n * child add/remove (async population, conditional render — `MutationObserver`\n * `childList`), container resize, and scroll/viewport change; `scheduleFrame`\n * coalesces on the PARENT window's frame loop with the mandatory 100 ms timeout\n * backstop (the iframe's rAF is paused offscreen / never fires under automation).\n * `dispose` tears down every observer + listener — no leaks on unmount. The\n * once-only `useLayoutEffect` measure this replaces went stale the instant any of\n * those settled after first paint (EC-406 evidence).\n */\nexport function createSpacingTracker(\n doc: Document,\n nodeId: string,\n flowAxis: FlowAxis,\n host: FrameScheduler,\n onChange: (measured: Measured) => void,\n): RectTracker {\n return createRectTracker<Measured>({\n measure: () => measureSpots(doc, nodeId, flowAxis),\n equals: measuredEqual,\n onChange,\n schedule: (flush) => scheduleFrame(host, flush),\n subscribe: (invalidate) => subscribeCanvasLayout(doc, () => spacingTargets(doc, nodeId), invalidate),\n })\n}\n\n/**\n * True when the node is a layout container the handles can attach to — a cheap,\n * NECESSARY graph precondition for the mount (the authoritative \"are there ≥2\n * gaps to space\" answer is the live DOM {@link measureSpots}). Because each graph\n * child renders at most one `[data-ec-node-id]` wrapper, rendered DOM children ⊆\n * graph children, so requiring ≥2 graph children can never wrongly hide handles\n * that a fresh DOM measure would show.\n */\nexport function hasSpacingHandles(node: ComponentNode | null): node is ComponentNode {\n return (\n node !== null &&\n isLayoutContainerNode(node) &&\n (node.slots?.[DEFAULT_SLOT]?.length ?? 0) >= 2\n )\n}\n\nconst HANDLE_THICKNESS = 14\nconst HANDLE_BAR = 4\nconst HANDLE_MAX_LENGTH = 48\n\n/** Everything the portal-free handle markup needs (chrome contract surface). */\nexport interface SpacingHandlesViewProps {\n spots: HandleSpotView[]\n horizontal: boolean\n /** The live drag, for the active-handle tint and the floating label. */\n drag: { index: number; labelX: number; labelY: number; label: string } | null\n onPointerDown: (spot: HandleSpotView) => (e: React.PointerEvent<HTMLDivElement>) => void\n onPointerMove: (e: React.PointerEvent<HTMLDivElement>) => void\n onPointerUp: () => void\n onPointerCancel: () => void\n}\n\n/**\n * The spacing handles' rendered markup, portal-free so the AD-D chrome\n * contract test can walk it with `renderToStaticMarkup`. In-frame chrome\n * contract (EC-414 AD-B): `ec-*`/`data-ec-*` plus inline styles only; the root\n * carries the `.ec-chrome` containment recipe.\n */\nexport function SpacingHandlesView(props: SpacingHandlesViewProps) {\n const { spots, horizontal, drag } = props\n return (\n <div data-ec-spacing-handles=\"\" className=\"ec-chrome\">\n {spots.map((spot) => {\n const length = Math.min(spot.crossLength, HANDLE_MAX_LENGTH)\n const active = drag?.index === spot.index\n return (\n <div\n key={spot.index}\n data-ec-gap-handle={spot.index}\n role=\"separator\"\n aria-label={translate('builder.spacing.gapAfterChild', { index: spot.index + 1 })}\n aria-orientation={horizontal ? 'vertical' : 'horizontal'}\n onPointerDown={props.onPointerDown(spot)}\n onPointerMove={props.onPointerMove}\n onPointerUp={props.onPointerUp}\n onPointerCancel={props.onPointerCancel}\n style={{\n position: 'absolute',\n left: spot.x - (horizontal ? HANDLE_THICKNESS / 2 : length / 2),\n top: spot.y - (horizontal ? length / 2 : HANDLE_THICKNESS / 2),\n width: horizontal ? HANDLE_THICKNESS : length,\n height: horizontal ? length : HANDLE_THICKNESS,\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n cursor: horizontal ? 'col-resize' : 'row-resize',\n zIndex: 40,\n touchAction: 'none',\n }}\n >\n <div\n style={{\n width: horizontal ? HANDLE_BAR : '100%',\n height: horizontal ? '100%' : HANDLE_BAR,\n borderRadius: 999,\n backgroundColor: active ? '#5b8cff' : 'rgba(91, 140, 255, 0.65)',\n boxShadow: '0 0 0 1px rgba(8, 12, 24, 0.55)',\n }}\n />\n </div>\n )\n })}\n {drag ? (\n <div\n data-ec-gap-label=\"\"\n style={{\n position: 'absolute',\n left: drag.labelX + 12,\n top: drag.labelY + 12,\n zIndex: 50,\n pointerEvents: 'none',\n padding: '2px 8px',\n borderRadius: 6,\n fontSize: 11,\n fontWeight: 600,\n whiteSpace: 'nowrap',\n color: '#fff',\n backgroundColor: '#5b8cff',\n boxShadow: '0 2px 8px rgba(8, 12, 24, 0.4)',\n }}\n >\n {drag.label}\n </div>\n ) : null}\n </div>\n )\n}\n\nexport function SpacingHandles({ doc, node }: { doc: Document; node: ComponentNode }) {\n const builder = useBuilder()\n const { dispatch } = builder\n const flowAxis = containerFlowAxis(node)\n const [measured, setMeasured] = useState<Measured>(() => ({ spots: [], axis: flowAxis }))\n const [drag, setDrag] = useState<ActiveDrag | null>(null)\n const dragRef = useRef<ActiveDrag | null>(null)\n const trackerRef = useRef<RectTracker | null>(null)\n // The container's pre-drag inline gap, restored on cancel/commit (the graph\n // re-render owns the committed value; the preview is imperative-only).\n const inlineGapRef = useRef<string>('')\n\n // Measurement lifetime (EC-406): ride the shared rect-tracking core instead of\n // the once-only useLayoutEffect + lone resize listener that went stale. The\n // tracker re-measures under child resize (image/font decode), child add/remove\n // (async population, conditional render), container resize, and scroll/viewport\n // change — every settle the old code missed. Re-subscribed on a graph/axis/\n // viewport change so the ResizeObserver re-points at the current children.\n useLayoutEffect(() => {\n const win = doc.defaultView\n if (!win) return\n const host = (win.parent ?? win) as unknown as FrameScheduler\n const tracker = createSpacingTracker(doc, node.id, flowAxis, host, setMeasured)\n trackerRef.current = tracker\n return () => {\n tracker.dispose()\n trackerRef.current = null\n }\n }, [doc, node.id, flowAxis, builder.present, builder.viewport])\n\n // The gap-drag previews imperatively on `container.style.gap` (the tracker's\n // MutationObserver is childList-only and ignores attribute writes, by design),\n // so nudge a coalesced re-measure per step to keep every handle glued to the\n // previewed gap as the children shift. Harmless off-drag (an unchanged measure\n // emits nothing).\n useEffect(() => {\n trackerRef.current?.invalidate()\n }, [drag?.step])\n\n // Scope 4: yield the handles during an active CONTENT drag (node move/insert) —\n // the DnD before/after/into indicators own the \"where it lands\" story then. The\n // handles' OWN gap drag uses pointer capture, never `beginDrag`, so it does not\n // trip this. Inline-editing suppression is handled at the canvas mount (the\n // handles unmount while a text/rich-text session owns the node).\n const [contentDragging, setContentDragging] = useState(() => activeDrag() !== null)\n useEffect(() => {\n const offStart = onDragSessionStart(() => setContentDragging(true))\n const offEnd = onDragSessionEnd(() => setContentDragging(false))\n return () => {\n offStart()\n offEnd()\n }\n }, [])\n\n const { spots: measuredSpots, axis } = measured\n const spots = contentDragging ? [] : measuredSpots\n\n const applyPreview = useCallback(\n (step: GapStep) => {\n const container = containerElementFor(doc, node.id)\n if (container) container.style.gap = gapStepCss(step)\n },\n [doc, node.id],\n )\n\n const finish = useCallback(\n (mode: 'commit' | 'cancel') => {\n const active = dragRef.current\n if (!active) return\n dragRef.current = null\n setDrag(null)\n // Drop the imperative preview; the rendered graph value takes over.\n const container = containerElementFor(doc, node.id)\n if (container) container.style.gap = inlineGapRef.current\n if (mode === 'commit' && active.step !== active.startStep) {\n // Exactly one dispatch — one operations command, one undo step — and\n // the identical prop value the inspector's gap select produces.\n if (active.step === null) {\n dispatch({ type: 'removeProp', id: node.id, name: 'gap' })\n } else {\n dispatch({ type: 'setProp', id: node.id, name: 'gap', value: staticProp(active.step) })\n }\n }\n },\n [doc, node.id, dispatch],\n )\n\n // Esc cancels mid-drag — listen on both documents (focus may sit either side\n // of the iframe boundary while the pointer is captured).\n useEffect(() => {\n if (!drag) return\n const onKeyDown = (e: KeyboardEvent) => {\n if (e.key === 'Escape') {\n e.preventDefault()\n e.stopPropagation()\n finish('cancel')\n }\n }\n doc.addEventListener('keydown', onKeyDown, true)\n document.addEventListener('keydown', onKeyDown, true)\n return () => {\n doc.removeEventListener('keydown', onKeyDown, true)\n document.removeEventListener('keydown', onKeyDown, true)\n }\n }, [drag, doc, finish])\n\n // Cleanup on unmount/selection change mid-drag: revert, never dispatch.\n useEffect(() => () => finish('cancel'), [finish])\n\n const onPointerDown = (spot: HandleSpotView) => (e: React.PointerEvent<HTMLDivElement>) => {\n if (e.button !== 0) return\n e.preventDefault()\n e.stopPropagation()\n const startStep = containerGapStep(node)\n const container = containerElementFor(doc, node.id)\n inlineGapRef.current = container?.style.gap ?? ''\n try {\n e.currentTarget.setPointerCapture(e.pointerId)\n } catch {\n // Synthetic pointer events (tests, automation) have no active pointer\n // to capture; move/up still arrive via the handle's own listeners.\n }\n const next: ActiveDrag = {\n index: spot.index,\n startStep,\n step: startStep,\n startCoord: axis === 'horizontal' ? e.clientX : e.clientY,\n labelX: spot.x,\n labelY: spot.y,\n }\n dragRef.current = next\n setDrag(next)\n }\n\n const onPointerMove = (e: React.PointerEvent<HTMLDivElement>) => {\n const active = dragRef.current\n if (!active) return\n const win = doc.defaultView\n const coord = axis === 'horizontal' ? e.clientX : e.clientY\n const step = gapStepFromDrag(active.startStep, coord - active.startCoord)\n const next: ActiveDrag = {\n ...active,\n step,\n labelX: e.clientX + (win?.scrollX ?? 0),\n labelY: e.clientY + (win?.scrollY ?? 0),\n }\n dragRef.current = next\n setDrag(next)\n applyPreview(step)\n }\n\n const horizontal = axis === 'horizontal'\n\n return createPortal(\n <SpacingHandlesView\n spots={spots}\n horizontal={horizontal}\n drag={\n drag\n ? { index: drag.index, labelX: drag.labelX, labelY: drag.labelY, label: gapStepLabel(drag.step) }\n : null\n }\n onPointerDown={onPointerDown}\n onPointerMove={onPointerMove}\n onPointerUp={() => finish('commit')}\n onPointerCancel={() => finish('cancel')}\n />,\n doc.body,\n )\n}\n"]}
1
+ {"version":3,"file":"spacing-handles.js","sourceRoot":"","sources":["../../../src/components/builder/spacing-handles.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'\nimport { createPortal } from 'react-dom'\nimport { DEFAULT_SLOT, isLayoutContainerNode, staticProp } from '@elytracms/core/project-graph'\nimport type { ComponentNode } from '@elytracms/core/project-graph'\nimport { containerFlowAxis } from '@elytracms/studio/lib/dnd/drop-rules'\nimport type { FlowAxis } from '@elytracms/studio/lib/dnd/drop-rules'\nimport {\n containerGapStep,\n gapHandleSpots,\n gapStepCss,\n gapStepFromDrag,\n} from '@elytracms/studio/lib/dnd/gap-drag'\nimport type { GapRect, GapStep } from '@elytracms/studio/lib/dnd/gap-drag'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { useBuilder } from './builder-store'\nimport { GAP_LABEL_KEYS, GAP_NONE_LABEL_KEY } from './layout-inspector'\nimport { activeDrag, onDragSessionEnd, onDragSessionStart } from './drag-session'\nimport {\n createRectTracker,\n measureElementRect,\n scheduleFrame,\n subscribeCanvasLayout,\n} from './canvas-rect-tracking'\nimport type { FrameScheduler, RectTracker } from './canvas-rect-tracking'\n\n/**\n * Tokenized spacing handles (EC-163, vision §3.1 / AD-10): when a Stack/Grid\n * container is selected on the canvas, draggable gap handles appear between\n * its children, axis-aware via the shared `containerFlowAxis`. Dragging snaps\n * through the EC-161 spacing-token scale (`lib/dnd/gap-drag` — pure math, no\n * pixel value can escape it); the static gap tags (EC-435) stay put and read\n * the LIVE token + re-measured px during the drag (EC-438 — no\n * pointer-following label). The drag previews imperatively on the rendered\n * element; releasing commits exactly ONE builder dispatch — `setProp` with the\n * token, or `removeProp` for \"no gap\", the identical values the inspector's\n * gap select writes — so one undo restores, and the graph stays the single\n * source of truth. Esc mid-drag cancels: preview reverts, nothing dispatches.\n */\n\n/** Plain-language label for a gap step — the inspector's vocabulary. */\nexport function gapStepLabel(step: GapStep): string {\n return step === null ? translate(GAP_NONE_LABEL_KEY) : translate(GAP_LABEL_KEYS[step])\n}\n\nexport interface HandleSpotView {\n index: number\n /** Document-coordinate position of the handle's center. */\n x: number\n y: number\n crossStart: number\n crossLength: number\n /** Leading edge of the gap region along the flow axis (EC-435 hatch). */\n gapStart: number\n /** Rendered extent of the gap along the flow axis (0 for a closed gap). */\n gapLength: number\n}\n\n/**\n * The hatched gap REGION for a spot (EC-435 AD-2): the rectangle between child\n * N's trailing edge and child N+1's leading edge along the flow axis, spanning\n * the pair's shared cross extent — the same measured child-rect basis the\n * handle spots come from (`gapHandleSpots`), in the same document coordinates.\n * Pure over the spot; a closed gap yields a zero-extent rect (nothing to paint).\n */\nexport function gapRegionRect(spot: HandleSpotView, axis: FlowAxis): GapRect {\n return axis === 'horizontal'\n ? { left: spot.gapStart, top: spot.crossStart, width: spot.gapLength, height: spot.crossLength }\n : { left: spot.crossStart, top: spot.gapStart, width: spot.crossLength, height: spot.gapLength }\n}\n\ninterface ActiveDrag {\n index: number\n startStep: GapStep\n step: GapStep\n /** Pointer start along the flow axis (client coords). */\n startCoord: number\n}\n\n/**\n * The container's rendered element (the Stack/Grid div carrying the gap style).\n * Resilient to legitimate intermediate wrappers between the editable\n * `[data-ec-node-id]` wrapper and the primitive (an error-boundary/provider DOM\n * node, a future decorator layer): the FIRST `[data-ec-primitive]` inside the\n * wrapper whose NEAREST node wrapper is THIS node — so it tolerates a wrapper\n * above the primitive but never descends into a CHILD node's subtree and grabs\n * that nested container's own `[data-ec-primitive]`. Mirrors `measurePropRect`'s\n * nearest-node scoping (EC-408). Today no such wrapper exists — `decorate` wraps\n * `<RenderErrorBoundary>` which renders its children with no DOM element, so the\n * primitive is a direct child — but the scoping makes 2(b) unregressable.\n */\nexport function containerElementFor(doc: Document, nodeId: string): HTMLElement | null {\n const wrapper = doc.querySelector(`[data-ec-node-id=\"${nodeId}\"]`)\n if (!wrapper) return null\n for (const primitive of wrapper.querySelectorAll('[data-ec-primitive]')) {\n if (primitive.closest('[data-ec-node-id]') === wrapper) return primitive as HTMLElement\n }\n return null\n}\n\n/** The container's rendered child node-wrappers — the DOM child basis. */\nfunction childWrappers(container: Element): HTMLElement[] {\n return Array.from(container.querySelectorAll(':scope > [data-ec-node-id]')) as HTMLElement[]\n}\n\ninterface Measured {\n spots: HandleSpotView[]\n /** The axis the children are *rendered* along (drag direction follows it). */\n axis: FlowAxis\n}\n\nconst EMPTY_MEASURED = (axis: FlowAxis): Measured => ({ spots: [], axis })\n\n/**\n * Measure the handle spots between the container's rendered children — the\n * SINGLE source of truth for whether handles show (spots) and where. Eligibility\n * and measurement agree on the SAME (DOM) child basis by construction: a\n * null-rendering graph child (condition-gated → no `[data-ec-node-id]` wrapper)\n * simply isn't counted, so it can neither block handles when ≥2 children render\n * nor phantom them when <2 do; kept fresh by the live tracker (EC-404) so a child\n * add/remove or resize re-runs this. The graph-level flow axis\n * (`containerFlowAxis`) is tried first; when the rendered layout disagrees — a\n * stacking Grid below the mobile breakpoint lays its \"columns\" out vertically —\n * the cross axis is measured instead, so handles always sit in the gaps the\n * editor actually sees.\n */\nexport function measureSpots(doc: Document, nodeId: string, flowAxis: FlowAxis): Measured {\n const container = containerElementFor(doc, nodeId)\n const win = doc.defaultView\n if (!container || !win) return EMPTY_MEASURED(flowAxis)\n const children = childWrappers(container)\n if (children.length < 2) return EMPTY_MEASURED(flowAxis)\n const rects = children.map((el) => measureElementRect(el, win))\n const along = gapHandleSpots(rects, flowAxis)\n if (along.length > 0) return { spots: along, axis: flowAxis }\n const cross: FlowAxis = flowAxis === 'horizontal' ? 'vertical' : 'horizontal'\n const across = gapHandleSpots(rects, cross)\n return across.length > 0 ? { spots: across, axis: cross } : EMPTY_MEASURED(flowAxis)\n}\n\n/** Structural equality of two spot lists — suppresses redundant tracker emits. */\nfunction spotsEqual(a: readonly HandleSpotView[], b: readonly HandleSpotView[]): boolean {\n if (a.length !== b.length) return false\n for (let i = 0; i < a.length; i += 1) {\n const x = a[i]!\n const y = b[i]!\n if (\n x.index !== y.index ||\n x.x !== y.x ||\n x.y !== y.y ||\n x.crossStart !== y.crossStart ||\n x.crossLength !== y.crossLength ||\n x.gapStart !== y.gapStart ||\n x.gapLength !== y.gapLength\n ) {\n return false\n }\n }\n return true\n}\n\n/** True when two measurements are equivalent (same axis, same spots). */\nexport function measuredEqual(a: Measured, b: Measured): boolean {\n return a.axis === b.axis && spotsEqual(a.spots, b.spots)\n}\n\n/** The elements the tracker's `ResizeObserver` watches: the container + its children. */\nfunction spacingTargets(doc: Document, nodeId: string): Element[] {\n const container = containerElementFor(doc, nodeId)\n if (!container) return []\n return [container, ...childWrappers(container)]\n}\n\n/**\n * Wire a live rect tracker for the spacing handles onto the shared EC-404 core:\n * `subscribeCanvasLayout` re-measures under child resize (image/font decode),\n * child add/remove (async population, conditional render — `MutationObserver`\n * `childList`), container resize, and scroll/viewport change; `scheduleFrame`\n * coalesces on the PARENT window's frame loop with the mandatory 100 ms timeout\n * backstop (the iframe's rAF is paused offscreen / never fires under automation).\n * `dispose` tears down every observer + listener — no leaks on unmount. The\n * once-only `useLayoutEffect` measure this replaces went stale the instant any of\n * those settled after first paint (EC-406 evidence).\n */\nexport function createSpacingTracker(\n doc: Document,\n nodeId: string,\n flowAxis: FlowAxis,\n host: FrameScheduler,\n onChange: (measured: Measured) => void,\n): RectTracker {\n return createRectTracker<Measured>({\n measure: () => measureSpots(doc, nodeId, flowAxis),\n equals: measuredEqual,\n onChange,\n schedule: (flush) => scheduleFrame(host, flush),\n subscribe: (invalidate) => subscribeCanvasLayout(doc, () => spacingTargets(doc, nodeId), invalidate),\n })\n}\n\n/**\n * True when the node is a layout container the handles can attach to — a cheap,\n * NECESSARY graph precondition for the mount (the authoritative \"are there ≥2\n * gaps to space\" answer is the live DOM {@link measureSpots}). Because each graph\n * child renders at most one `[data-ec-node-id]` wrapper, rendered DOM children ⊆\n * graph children, so requiring ≥2 graph children can never wrongly hide handles\n * that a fresh DOM measure would show.\n */\nexport function hasSpacingHandles(node: ComponentNode | null): node is ComponentNode {\n return (\n node !== null &&\n isLayoutContainerNode(node) &&\n (node.slots?.[DEFAULT_SLOT]?.length ?? 0) >= 2\n )\n}\n\nconst HANDLE_THICKNESS = 14\nconst HANDLE_BAR = 4\nconst HANDLE_MAX_LENGTH = 48\n\n/** Everything the portal-free handle markup needs (chrome contract surface). */\nexport interface SpacingHandlesViewProps {\n spots: HandleSpotView[]\n horizontal: boolean\n /**\n * Token label for the container's COMMITTED gap (EC-435 AD-2) — the SAME\n * plain-language vocabulary the layout inspector resolves (`gapStepLabel`),\n * stamped on every hatched gap region as a static size tag while idle.\n */\n gapLabel: string\n /**\n * The live drag: the active handle index (tint) and the live step's token\n * label — the static tags read it in place while dragging (EC-438).\n */\n drag: { index: number; label: string } | null\n onPointerDown: (spot: HandleSpotView) => (e: React.PointerEvent<HTMLDivElement>) => void\n onPointerMove: (e: React.PointerEvent<HTMLDivElement>) => void\n onPointerUp: () => void\n onPointerCancel: () => void\n}\n\n/**\n * The spacing handles' rendered markup, portal-free so the AD-D chrome\n * contract test can walk it with `renderToStaticMarkup`. In-frame chrome\n * contract (EC-414 AD-B): `ec-*`/`data-ec-*` plus inline styles only; the root\n * carries the `.ec-chrome` containment recipe.\n *\n * EC-435 AD-2: every open gap region renders a pointer-transparent hatched\n * rectangle (diagonal accent stripes, Figma-grid style) under the handles,\n * each with a small static token-label size tag beside its handle. During a\n * gap drag the tags STAY PUT and read live (EC-438 — a gap drag previews ONE\n * container-wide gap, so every tag shows the live step's token + its own\n * re-measured px); no label follows the pointer. The gap handles remain the\n * only interactive element.\n */\nexport function SpacingHandlesView(props: SpacingHandlesViewProps) {\n const { spots, horizontal, gapLabel, drag } = props\n const axis: FlowAxis = horizontal ? 'horizontal' : 'vertical'\n return (\n <div data-ec-spacing-handles=\"\" className=\"ec-chrome\">\n {spots.map((spot) => {\n const region = gapRegionRect(spot, axis)\n if (spot.gapLength < 1) return null\n return (\n <div\n key={spot.index}\n data-ec-gap-hatch={spot.index}\n style={{\n position: 'absolute',\n left: region.left,\n top: region.top,\n width: region.width,\n height: region.height,\n zIndex: 10,\n pointerEvents: 'none',\n background:\n 'repeating-linear-gradient(45deg, rgba(91,140,255,0.28) 0px, rgba(91,140,255,0.28) 3px, rgba(91,140,255,0.06) 3px, rgba(91,140,255,0.06) 9px)',\n boxShadow: 'inset 0 0 0 1px rgba(91,140,255,0.3)',\n }}\n />\n )\n })}\n {spots.map((spot) => {\n if (spot.gapLength < 1) return null\n const length = Math.min(spot.crossLength, HANDLE_MAX_LENGTH)\n return (\n <div\n key={spot.index}\n data-ec-gap-hatch-label={spot.index}\n style={{\n position: 'absolute',\n left: horizontal ? spot.x : spot.x + length / 2 + 6,\n top: horizontal ? spot.y + length / 2 + 6 : spot.y,\n transform: horizontal ? 'translateX(-50%)' : 'translateY(-50%)',\n zIndex: 45,\n pointerEvents: 'none',\n padding: '1px 6px',\n borderRadius: 5,\n fontSize: 10,\n fontWeight: 600,\n whiteSpace: 'nowrap',\n color: '#fff',\n backgroundColor: 'rgba(91,140,255,0.92)',\n boxShadow: '0 1px 4px rgba(8,12,24,0.3)',\n }}\n >\n {drag ? drag.label : gapLabel} · {Math.round(spot.gapLength)}px\n </div>\n )\n })}\n {spots.map((spot) => {\n const length = Math.min(spot.crossLength, HANDLE_MAX_LENGTH)\n const active = drag?.index === spot.index\n return (\n <div\n key={spot.index}\n data-ec-gap-handle={spot.index}\n role=\"separator\"\n aria-label={translate('builder.spacing.gapAfterChild', { index: spot.index + 1 })}\n aria-orientation={horizontal ? 'vertical' : 'horizontal'}\n onPointerDown={props.onPointerDown(spot)}\n onPointerMove={props.onPointerMove}\n onPointerUp={props.onPointerUp}\n onPointerCancel={props.onPointerCancel}\n style={{\n position: 'absolute',\n left: spot.x - (horizontal ? HANDLE_THICKNESS / 2 : length / 2),\n top: spot.y - (horizontal ? length / 2 : HANDLE_THICKNESS / 2),\n width: horizontal ? HANDLE_THICKNESS : length,\n height: horizontal ? length : HANDLE_THICKNESS,\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n cursor: horizontal ? 'col-resize' : 'row-resize',\n zIndex: 40,\n touchAction: 'none',\n }}\n >\n <div\n style={{\n width: horizontal ? HANDLE_BAR : '100%',\n height: horizontal ? '100%' : HANDLE_BAR,\n borderRadius: 999,\n backgroundColor: active ? '#5b8cff' : 'rgba(91, 140, 255, 0.65)',\n boxShadow: '0 0 0 1px rgba(8, 12, 24, 0.55)',\n }}\n />\n </div>\n )\n })}\n </div>\n )\n}\n\nexport function SpacingHandles({ doc, node }: { doc: Document; node: ComponentNode }) {\n const builder = useBuilder()\n const { dispatch } = builder\n const flowAxis = containerFlowAxis(node)\n const [measured, setMeasured] = useState<Measured>(() => ({ spots: [], axis: flowAxis }))\n const [drag, setDrag] = useState<ActiveDrag | null>(null)\n const dragRef = useRef<ActiveDrag | null>(null)\n const trackerRef = useRef<RectTracker | null>(null)\n // The container's pre-drag inline gap, restored on cancel/commit (the graph\n // re-render owns the committed value; the preview is imperative-only).\n const inlineGapRef = useRef<string>('')\n\n // Measurement lifetime (EC-406): ride the shared rect-tracking core instead of\n // the once-only useLayoutEffect + lone resize listener that went stale. The\n // tracker re-measures under child resize (image/font decode), child add/remove\n // (async population, conditional render), container resize, and scroll/viewport\n // change — every settle the old code missed. Re-subscribed on a graph/axis/\n // viewport change so the ResizeObserver re-points at the current children.\n useLayoutEffect(() => {\n const win = doc.defaultView\n if (!win) return\n const host = (win.parent ?? win) as unknown as FrameScheduler\n const tracker = createSpacingTracker(doc, node.id, flowAxis, host, setMeasured)\n trackerRef.current = tracker\n return () => {\n tracker.dispose()\n trackerRef.current = null\n }\n }, [doc, node.id, flowAxis, builder.present, builder.viewport])\n\n // The gap-drag previews imperatively on `container.style.gap` (the tracker's\n // MutationObserver is childList-only and ignores attribute writes, by design),\n // so nudge a coalesced re-measure per step to keep every handle glued to the\n // previewed gap as the children shift. Harmless off-drag (an unchanged measure\n // emits nothing).\n useEffect(() => {\n trackerRef.current?.invalidate()\n }, [drag?.step])\n\n // Scope 4: yield the handles during an active CONTENT drag (node move/insert) —\n // the DnD before/after/into indicators own the \"where it lands\" story then. The\n // handles' OWN gap drag uses pointer capture, never `beginDrag`, so it does not\n // trip this. Inline-editing suppression is handled at the canvas mount (the\n // handles unmount while a text/rich-text session owns the node).\n const [contentDragging, setContentDragging] = useState(() => activeDrag() !== null)\n useEffect(() => {\n const offStart = onDragSessionStart(() => setContentDragging(true))\n const offEnd = onDragSessionEnd(() => setContentDragging(false))\n return () => {\n offStart()\n offEnd()\n }\n }, [])\n\n const { spots: measuredSpots, axis } = measured\n const spots = contentDragging ? [] : measuredSpots\n\n const applyPreview = useCallback(\n (step: GapStep) => {\n const container = containerElementFor(doc, node.id)\n if (container) container.style.gap = gapStepCss(step)\n },\n [doc, node.id],\n )\n\n const finish = useCallback(\n (mode: 'commit' | 'cancel') => {\n const active = dragRef.current\n if (!active) return\n dragRef.current = null\n setDrag(null)\n // Drop the imperative preview; the rendered graph value takes over.\n const container = containerElementFor(doc, node.id)\n if (container) container.style.gap = inlineGapRef.current\n if (mode === 'commit' && active.step !== active.startStep) {\n // Exactly one dispatch — one operations command, one undo step — and\n // the identical prop value the inspector's gap select produces.\n if (active.step === null) {\n dispatch({ type: 'removeProp', id: node.id, name: 'gap' })\n } else {\n dispatch({ type: 'setProp', id: node.id, name: 'gap', value: staticProp(active.step) })\n }\n }\n },\n [doc, node.id, dispatch],\n )\n\n // Esc cancels mid-drag — listen on both documents (focus may sit either side\n // of the iframe boundary while the pointer is captured).\n useEffect(() => {\n if (!drag) return\n const onKeyDown = (e: KeyboardEvent) => {\n if (e.key === 'Escape') {\n e.preventDefault()\n e.stopPropagation()\n finish('cancel')\n }\n }\n doc.addEventListener('keydown', onKeyDown, true)\n document.addEventListener('keydown', onKeyDown, true)\n return () => {\n doc.removeEventListener('keydown', onKeyDown, true)\n document.removeEventListener('keydown', onKeyDown, true)\n }\n }, [drag, doc, finish])\n\n // Cleanup on unmount/selection change mid-drag: revert, never dispatch.\n useEffect(() => () => finish('cancel'), [finish])\n\n const onPointerDown = (spot: HandleSpotView) => (e: React.PointerEvent<HTMLDivElement>) => {\n if (e.button !== 0) return\n e.preventDefault()\n e.stopPropagation()\n const startStep = containerGapStep(node)\n const container = containerElementFor(doc, node.id)\n inlineGapRef.current = container?.style.gap ?? ''\n try {\n e.currentTarget.setPointerCapture(e.pointerId)\n } catch {\n // Synthetic pointer events (tests, automation) have no active pointer\n // to capture; move/up still arrive via the handle's own listeners.\n }\n const next: ActiveDrag = {\n index: spot.index,\n startStep,\n step: startStep,\n startCoord: axis === 'horizontal' ? e.clientX : e.clientY,\n }\n dragRef.current = next\n setDrag(next)\n }\n\n const onPointerMove = (e: React.PointerEvent<HTMLDivElement>) => {\n const active = dragRef.current\n if (!active) return\n const coord = axis === 'horizontal' ? e.clientX : e.clientY\n const step = gapStepFromDrag(active.startStep, coord - active.startCoord)\n const next: ActiveDrag = { ...active, step }\n dragRef.current = next\n setDrag(next)\n applyPreview(step)\n }\n\n const horizontal = axis === 'horizontal'\n\n return createPortal(\n <SpacingHandlesView\n spots={spots}\n horizontal={horizontal}\n // The static size tags share ONE vocabulary with the inspector's gap\n // select: the token label of the node's committed gap step (EC-435\n // AD-2); during a drag they read the live step instead (EC-438).\n gapLabel={gapStepLabel(containerGapStep(node))}\n drag={drag ? { index: drag.index, label: gapStepLabel(drag.step) } : null}\n onPointerDown={onPointerDown}\n onPointerMove={onPointerMove}\n onPointerUp={() => finish('commit')}\n onPointerCancel={() => finish('cancel')}\n />,\n doc.body,\n )\n}\n"]}
@@ -1,6 +1,7 @@
1
1
  import type { CollectionDef } from '@elytracms/core/cms-core';
2
2
  import type { FilterScalar } from '@elytracms/core/content';
3
3
  import type { FilterableFieldOption } from '@elytracms/studio/lib/cms-views';
4
+ import type { CollectionViewCapabilities } from '@elytracms/studio/lib/collection-view/collection-view-source';
4
5
  import type { BulkAction, BulkReport, BulkSetField } from './bulk-actions.js';
5
6
  /**
6
7
  * The scalar filterable fields a bulk set-field may target (EC-247): the
@@ -19,9 +20,18 @@ export declare function initialSetValue(option: FilterableFieldOption | undefine
19
20
  * reference-aware EC-173 dialog (`ReferenceConfirmDialog`), which enumerates
20
21
  * every inbound reference that will break before anything dispatches — this bar
21
22
  * only raises the action. Set-field is a draft edit, so it applies directly.
23
+ *
24
+ * Every action is CAPABILITY-gated (EC-433), mirroring the row `⋮` menu's
25
+ * `rowActionsFor` (EC-316): a rail without publish never shows
26
+ * publish/unpublish, one without remove never shows Delete, and set-field
27
+ * follows `inlineEdit` (it's a bulk cell write). Before this gate the bar
28
+ * offered every action to every rail — the assets rail's bulk Delete then
29
+ * confirmed into a source without a bulk runner and silently no-oped.
22
30
  */
23
- export declare function BulkBar({ collection, count, busy, onAction, onSetField, onClear, }: {
31
+ export declare function BulkBar({ collection, capabilities, count, busy, onAction, onSetField, onClear, }: {
24
32
  collection: CollectionDef;
33
+ /** The rail's capability matrix (AD-19) — the SAME gate the row menu uses. */
34
+ capabilities: Pick<CollectionViewCapabilities, 'publish' | 'remove' | 'inlineEdit'>;
25
35
  /** Selected rows in the current view. */
26
36
  count: number;
27
37
  busy: boolean;
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useState } from 'react';
3
3
  import { Check, CheckCircle2, CircleSlash, PencilLine, Trash2, X } from 'lucide-react';
4
4
  import { Badge, Button, Code, Row, Select, Spacer, Stack, Text } from '@elytracms/studio/ui';
@@ -28,14 +28,21 @@ export function initialSetValue(option) {
28
28
  * reference-aware EC-173 dialog (`ReferenceConfirmDialog`), which enumerates
29
29
  * every inbound reference that will break before anything dispatches — this bar
30
30
  * only raises the action. Set-field is a draft edit, so it applies directly.
31
+ *
32
+ * Every action is CAPABILITY-gated (EC-433), mirroring the row `⋮` menu's
33
+ * `rowActionsFor` (EC-316): a rail without publish never shows
34
+ * publish/unpublish, one without remove never shows Delete, and set-field
35
+ * follows `inlineEdit` (it's a bulk cell write). Before this gate the bar
36
+ * offered every action to every rail — the assets rail's bulk Delete then
37
+ * confirmed into a source without a bulk runner and silently no-oped.
31
38
  */
32
- export function BulkBar({ collection, count, busy, onAction, onSetField, onClear, }) {
39
+ export function BulkBar({ collection, capabilities, count, busy, onAction, onSetField, onClear, }) {
33
40
  const [setMode, setSetMode] = useState(false);
34
41
  if (count === 0)
35
42
  return null;
36
43
  const options = setFieldOptions(collection);
37
44
  const settable = options.length > 0;
38
- return (_jsxs(Stack, { gap: "none", className: "shrink-0", "data-testid": "bulk-bar", children: [_jsxs(Row, { gap: "2", className: "border-b border-border bg-muted/40 px-4 py-2 sm:px-6", children: [_jsx(Text, { size: "sm", weight: "medium", "data-testid": "bulk-count", children: translate('cms.list.bulk.count', { count }) }), _jsxs(Button, { variant: "outline", size: "sm", disabled: busy, "data-testid": "bulk-publish", onClick: () => onAction('publish'), children: [_jsx(CheckCircle2, {}), " ", translate('cms.list.bulk.publish')] }), _jsxs(Button, { variant: "outline", size: "sm", disabled: busy, "data-testid": "bulk-unpublish", onClick: () => onAction('unpublish'), children: [_jsx(CircleSlash, {}), " ", translate('cms.list.bulk.unpublish')] }), _jsxs(Button, { variant: "outline", size: "sm", disabled: busy || !settable, "aria-pressed": setMode, title: settable ? undefined : translate('cms.list.bulk.noSettableFields'), "data-testid": "bulk-set-field", onClick: () => setSetMode((prev) => !prev), children: [_jsx(PencilLine, {}), " ", translate('cms.list.bulk.setField')] }), _jsxs(Button, { variant: "destructive", size: "sm", disabled: busy, "data-testid": "bulk-delete", onClick: () => onAction('delete'), children: [_jsx(Trash2, {}), " ", translate('common.delete')] }), _jsx(Spacer, {}), _jsxs(Button, { variant: "ghost", size: "sm", disabled: busy, "data-testid": "bulk-clear", onClick: onClear, children: [_jsx(X, {}), " ", translate('cms.list.bulk.clearSelection')] })] }), setMode && settable ? (_jsx(SetFieldPanel, { options: options, busy: busy, onApply: (set) => {
45
+ return (_jsxs(Stack, { gap: "none", className: "shrink-0", "data-testid": "bulk-bar", children: [_jsxs(Row, { gap: "2", className: "border-b border-border bg-muted/40 px-4 py-2 sm:px-6", children: [_jsx(Text, { size: "sm", weight: "medium", "data-testid": "bulk-count", children: translate('cms.list.bulk.count', { count }) }), capabilities.publish ? (_jsxs(_Fragment, { children: [_jsxs(Button, { variant: "outline", size: "sm", disabled: busy, "data-testid": "bulk-publish", onClick: () => onAction('publish'), children: [_jsx(CheckCircle2, {}), " ", translate('cms.list.bulk.publish')] }), _jsxs(Button, { variant: "outline", size: "sm", disabled: busy, "data-testid": "bulk-unpublish", onClick: () => onAction('unpublish'), children: [_jsx(CircleSlash, {}), " ", translate('cms.list.bulk.unpublish')] })] })) : null, capabilities.inlineEdit ? (_jsxs(Button, { variant: "outline", size: "sm", disabled: busy || !settable, "aria-pressed": setMode, title: settable ? undefined : translate('cms.list.bulk.noSettableFields'), "data-testid": "bulk-set-field", onClick: () => setSetMode((prev) => !prev), children: [_jsx(PencilLine, {}), " ", translate('cms.list.bulk.setField')] })) : null, capabilities.remove ? (_jsxs(Button, { variant: "destructive", size: "sm", disabled: busy, "data-testid": "bulk-delete", onClick: () => onAction('delete'), children: [_jsx(Trash2, {}), " ", translate('common.delete')] })) : null, _jsx(Spacer, {}), _jsxs(Button, { variant: "ghost", size: "sm", disabled: busy, "data-testid": "bulk-clear", onClick: onClear, children: [_jsx(X, {}), " ", translate('cms.list.bulk.clearSelection')] })] }), setMode && settable && capabilities.inlineEdit ? (_jsx(SetFieldPanel, { options: options, busy: busy, onApply: (set) => {
39
46
  onSetField(set);
40
47
  setSetMode(false);
41
48
  }, onCancel: () => setSetMode(false) })) : null] }));
@@ -1 +1 @@
1
- {"version":3,"file":"bulk-bar.js","sourceRoot":"","sources":["../../../src/components/cms/bulk-bar.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useState } from 'react'\nimport { Check, CheckCircle2, CircleSlash, PencilLine, Trash2, X } from 'lucide-react'\nimport type { CollectionDef } from '@elytracms/core/cms-core'\nimport type { FilterScalar } from '@elytracms/core/content'\nimport { Badge, Button, Code, Row, Select, Spacer, Stack, Text } from '@elytracms/studio/ui'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { filterableFieldOptions } from '@elytracms/studio/lib/cms-views'\nimport type { FilterableFieldOption } from '@elytracms/studio/lib/cms-views'\nimport { bulkSummary } from './bulk-actions'\nimport type { BulkAction, BulkReport, BulkSetField } from './bulk-actions'\nimport { cellEditKind } from './cell-edit'\nimport { ScalarFieldValueEditor } from './scalar-value-editor'\n\n/**\n * The scalar filterable fields a bulk set-field may target (EC-247): the\n * schema's `filterable` fields whose type edits inline (text/number/boolean/\n * select/date — `cellEditKind` 'inline'). Relation/asset (picker) and the\n * publication-state pseudo field are excluded — publish/unpublish own state,\n * and a raw relation id is not a safe bulk control.\n */\nexport function setFieldOptions(collection: CollectionDef): FilterableFieldOption[] {\n return filterableFieldOptions(collection).filter(\n (option) => option.def !== null && cellEditKind(option.def, false) === 'inline',\n )\n}\n\n/** A sensible starting value for a freshly chosen set-field target. */\nexport function initialSetValue(option: FilterableFieldOption | undefined): FilterScalar {\n return option?.def?.type === 'boolean' ? false : ''\n}\n\n/**\n * The bulk-action bar of the collection table (EC-169): appears with a row\n * selection, offers publish/unpublish/delete and a set-field assignment\n * (EC-247) over it. Unpublish and delete are confirmed by the owner through the\n * reference-aware EC-173 dialog (`ReferenceConfirmDialog`), which enumerates\n * every inbound reference that will break before anything dispatches — this bar\n * only raises the action. Set-field is a draft edit, so it applies directly.\n */\nexport function BulkBar({\n collection,\n count,\n busy,\n onAction,\n onSetField,\n onClear,\n}: {\n collection: CollectionDef\n /** Selected rows in the current view. */\n count: number\n busy: boolean\n onAction: (action: BulkAction) => void\n onSetField: (set: BulkSetField) => void\n onClear: () => void\n}) {\n const [setMode, setSetMode] = useState(false)\n if (count === 0) return null\n\n const options = setFieldOptions(collection)\n const settable = options.length > 0\n\n return (\n <Stack gap=\"none\" className=\"shrink-0\" data-testid=\"bulk-bar\">\n <Row gap=\"2\" className=\"border-b border-border bg-muted/40 px-4 py-2 sm:px-6\">\n <Text size=\"sm\" weight=\"medium\" data-testid=\"bulk-count\">\n {translate('cms.list.bulk.count', { count })}\n </Text>\n <Button\n variant=\"outline\"\n size=\"sm\"\n disabled={busy}\n data-testid=\"bulk-publish\"\n onClick={() => onAction('publish')}\n >\n <CheckCircle2 /> {translate('cms.list.bulk.publish')}\n </Button>\n <Button\n variant=\"outline\"\n size=\"sm\"\n disabled={busy}\n data-testid=\"bulk-unpublish\"\n onClick={() => onAction('unpublish')}\n >\n <CircleSlash /> {translate('cms.list.bulk.unpublish')}\n </Button>\n <Button\n variant=\"outline\"\n size=\"sm\"\n disabled={busy || !settable}\n aria-pressed={setMode}\n title={settable ? undefined : translate('cms.list.bulk.noSettableFields')}\n data-testid=\"bulk-set-field\"\n onClick={() => setSetMode((prev) => !prev)}\n >\n <PencilLine /> {translate('cms.list.bulk.setField')}\n </Button>\n <Button\n variant=\"destructive\"\n size=\"sm\"\n disabled={busy}\n data-testid=\"bulk-delete\"\n onClick={() => onAction('delete')}\n >\n <Trash2 /> {translate('common.delete')}\n </Button>\n <Spacer />\n <Button variant=\"ghost\" size=\"sm\" disabled={busy} data-testid=\"bulk-clear\" onClick={onClear}>\n <X /> {translate('cms.list.bulk.clearSelection')}\n </Button>\n </Row>\n {setMode && settable ? (\n <SetFieldPanel\n options={options}\n busy={busy}\n onApply={(set) => {\n onSetField(set)\n setSetMode(false)\n }}\n onCancel={() => setSetMode(false)}\n />\n ) : null}\n </Stack>\n )\n}\n\n/** The inline \"set <field> to <value>\" composer over the current selection. */\nfunction SetFieldPanel({\n options,\n busy,\n onApply,\n onCancel,\n}: {\n options: FilterableFieldOption[]\n busy: boolean\n onApply: (set: BulkSetField) => void\n onCancel: () => void\n}) {\n const [field, setField] = useState(options[0]!.field)\n const option = options.find((candidate) => candidate.field === field) ?? options[0]!\n const [value, setValue] = useState<FilterScalar>(() => initialSetValue(option))\n\n function changeField(next: string) {\n setField(next)\n setValue(initialSetValue(options.find((candidate) => candidate.field === next)))\n }\n\n return (\n <Row\n gap=\"2\"\n align=\"center\"\n className=\"border-b border-border bg-muted/20 px-4 py-2 sm:px-6\"\n data-testid=\"bulk-set-panel\"\n >\n <Text size=\"sm\" tone=\"muted\">\n {translate('cms.list.bulk.set.set')}\n </Text>\n <Select\n aria-label={translate('cms.list.bulk.setField')}\n value={field}\n onChange={(event) => changeField(event.target.value)}\n className=\"h-7 w-auto py-0 text-xs\"\n data-testid=\"bulk-set-field-select\"\n >\n {options.map((candidate) => (\n <option key={candidate.field} value={candidate.field}>\n {candidate.label}\n </option>\n ))}\n </Select>\n <Text size=\"sm\" tone=\"muted\">\n {translate('cms.list.bulk.set.to')}\n </Text>\n <ScalarFieldValueEditor\n key={field}\n option={option}\n value={value}\n ariaLabel={translate('cms.list.bulk.set.valueAria')}\n testId=\"bulk-set-value\"\n onChange={setValue}\n />\n <Spacer />\n <Button\n variant=\"outline\"\n size=\"sm\"\n disabled={busy}\n data-testid=\"bulk-set-apply\"\n onClick={() => onApply({ field, value })}\n >\n <Check /> {translate('cms.list.bulk.set.apply')}\n </Button>\n <Button variant=\"ghost\" size=\"sm\" disabled={busy} data-testid=\"bulk-set-cancel\" onClick={onCancel}>\n {translate('common.cancel')}\n </Button>\n </Row>\n )\n}\n\n/**\n * Per-item bulk results (EC-169): the completion summary plus every failure,\n * named and with its reason — a partial failure is enumerated, never silent.\n */\nexport function BulkReportPanel({\n report,\n onDismiss,\n}: {\n report: BulkReport\n onDismiss: () => void\n}) {\n const { succeeded, failed } = bulkSummary(report)\n const failures = report.results.filter((result) => !result.ok)\n const actionLabel =\n report.action === 'set-field' && report.setField\n ? translate('cms.list.bulk.report.verb.setField', { field: report.setField.field })\n : translate(`cms.list.bulk.report.verb.${report.action}`)\n\n return (\n <Stack gap=\"1\" className=\"shrink-0 border-b border-border px-4 py-2 sm:px-6\" data-testid=\"bulk-report\">\n <Row gap=\"2\">\n <Text size=\"sm\" weight=\"medium\" data-testid=\"bulk-report-summary\">\n {translate('cms.list.bulk.report.summary', { action: actionLabel, succeeded })}\n {failed > 0 ? translate('cms.list.bulk.report.failedSuffix', { count: failed }) : ''}\n </Text>\n {failed > 0 ? (\n <Badge variant=\"error\">{translate('cms.list.bulk.report.failedBadge', { count: failed })}</Badge>\n ) : (\n <Badge variant=\"success\">{translate('cms.list.bulk.report.done')}</Badge>\n )}\n <Spacer />\n <Button variant=\"ghost\" size=\"sm\" data-testid=\"bulk-report-dismiss\" onClick={onDismiss}>\n {translate('common.dismiss')}\n </Button>\n </Row>\n {failures.map((failure) => (\n <Row gap=\"2\" key={failure.ref.id} data-testid={`bulk-failure-${failure.ref.id}`}>\n <Text size=\"xs\" className=\"text-destructive\">\n {failure.title}\n </Text>\n <Code tone=\"muted\">{failure.ref.id}</Code>\n <Text size=\"xs\" tone=\"muted\">\n {failure.reason}\n </Text>\n </Row>\n ))}\n </Stack>\n )\n}\n"]}
1
+ {"version":3,"file":"bulk-bar.js","sourceRoot":"","sources":["../../../src/components/cms/bulk-bar.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useState } from 'react'\nimport { Check, CheckCircle2, CircleSlash, PencilLine, Trash2, X } from 'lucide-react'\nimport type { CollectionDef } from '@elytracms/core/cms-core'\nimport type { FilterScalar } from '@elytracms/core/content'\nimport { Badge, Button, Code, Row, Select, Spacer, Stack, Text } from '@elytracms/studio/ui'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { filterableFieldOptions } from '@elytracms/studio/lib/cms-views'\nimport type { FilterableFieldOption } from '@elytracms/studio/lib/cms-views'\nimport type { CollectionViewCapabilities } from '@elytracms/studio/lib/collection-view/collection-view-source'\nimport { bulkSummary } from './bulk-actions'\nimport type { BulkAction, BulkReport, BulkSetField } from './bulk-actions'\nimport { cellEditKind } from './cell-edit'\nimport { ScalarFieldValueEditor } from './scalar-value-editor'\n\n/**\n * The scalar filterable fields a bulk set-field may target (EC-247): the\n * schema's `filterable` fields whose type edits inline (text/number/boolean/\n * select/date — `cellEditKind` 'inline'). Relation/asset (picker) and the\n * publication-state pseudo field are excluded — publish/unpublish own state,\n * and a raw relation id is not a safe bulk control.\n */\nexport function setFieldOptions(collection: CollectionDef): FilterableFieldOption[] {\n return filterableFieldOptions(collection).filter(\n (option) => option.def !== null && cellEditKind(option.def, false) === 'inline',\n )\n}\n\n/** A sensible starting value for a freshly chosen set-field target. */\nexport function initialSetValue(option: FilterableFieldOption | undefined): FilterScalar {\n return option?.def?.type === 'boolean' ? false : ''\n}\n\n/**\n * The bulk-action bar of the collection table (EC-169): appears with a row\n * selection, offers publish/unpublish/delete and a set-field assignment\n * (EC-247) over it. Unpublish and delete are confirmed by the owner through the\n * reference-aware EC-173 dialog (`ReferenceConfirmDialog`), which enumerates\n * every inbound reference that will break before anything dispatches — this bar\n * only raises the action. Set-field is a draft edit, so it applies directly.\n *\n * Every action is CAPABILITY-gated (EC-433), mirroring the row `⋮` menu's\n * `rowActionsFor` (EC-316): a rail without publish never shows\n * publish/unpublish, one without remove never shows Delete, and set-field\n * follows `inlineEdit` (it's a bulk cell write). Before this gate the bar\n * offered every action to every rail — the assets rail's bulk Delete then\n * confirmed into a source without a bulk runner and silently no-oped.\n */\nexport function BulkBar({\n collection,\n capabilities,\n count,\n busy,\n onAction,\n onSetField,\n onClear,\n}: {\n collection: CollectionDef\n /** The rail's capability matrix (AD-19) — the SAME gate the row menu uses. */\n capabilities: Pick<CollectionViewCapabilities, 'publish' | 'remove' | 'inlineEdit'>\n /** Selected rows in the current view. */\n count: number\n busy: boolean\n onAction: (action: BulkAction) => void\n onSetField: (set: BulkSetField) => void\n onClear: () => void\n}) {\n const [setMode, setSetMode] = useState(false)\n if (count === 0) return null\n\n const options = setFieldOptions(collection)\n const settable = options.length > 0\n\n return (\n <Stack gap=\"none\" className=\"shrink-0\" data-testid=\"bulk-bar\">\n <Row gap=\"2\" className=\"border-b border-border bg-muted/40 px-4 py-2 sm:px-6\">\n <Text size=\"sm\" weight=\"medium\" data-testid=\"bulk-count\">\n {translate('cms.list.bulk.count', { count })}\n </Text>\n {capabilities.publish ? (\n <>\n <Button\n variant=\"outline\"\n size=\"sm\"\n disabled={busy}\n data-testid=\"bulk-publish\"\n onClick={() => onAction('publish')}\n >\n <CheckCircle2 /> {translate('cms.list.bulk.publish')}\n </Button>\n <Button\n variant=\"outline\"\n size=\"sm\"\n disabled={busy}\n data-testid=\"bulk-unpublish\"\n onClick={() => onAction('unpublish')}\n >\n <CircleSlash /> {translate('cms.list.bulk.unpublish')}\n </Button>\n </>\n ) : null}\n {capabilities.inlineEdit ? (\n <Button\n variant=\"outline\"\n size=\"sm\"\n disabled={busy || !settable}\n aria-pressed={setMode}\n title={settable ? undefined : translate('cms.list.bulk.noSettableFields')}\n data-testid=\"bulk-set-field\"\n onClick={() => setSetMode((prev) => !prev)}\n >\n <PencilLine /> {translate('cms.list.bulk.setField')}\n </Button>\n ) : null}\n {capabilities.remove ? (\n <Button\n variant=\"destructive\"\n size=\"sm\"\n disabled={busy}\n data-testid=\"bulk-delete\"\n onClick={() => onAction('delete')}\n >\n <Trash2 /> {translate('common.delete')}\n </Button>\n ) : null}\n <Spacer />\n <Button variant=\"ghost\" size=\"sm\" disabled={busy} data-testid=\"bulk-clear\" onClick={onClear}>\n <X /> {translate('cms.list.bulk.clearSelection')}\n </Button>\n </Row>\n {setMode && settable && capabilities.inlineEdit ? (\n <SetFieldPanel\n options={options}\n busy={busy}\n onApply={(set) => {\n onSetField(set)\n setSetMode(false)\n }}\n onCancel={() => setSetMode(false)}\n />\n ) : null}\n </Stack>\n )\n}\n\n/** The inline \"set <field> to <value>\" composer over the current selection. */\nfunction SetFieldPanel({\n options,\n busy,\n onApply,\n onCancel,\n}: {\n options: FilterableFieldOption[]\n busy: boolean\n onApply: (set: BulkSetField) => void\n onCancel: () => void\n}) {\n const [field, setField] = useState(options[0]!.field)\n const option = options.find((candidate) => candidate.field === field) ?? options[0]!\n const [value, setValue] = useState<FilterScalar>(() => initialSetValue(option))\n\n function changeField(next: string) {\n setField(next)\n setValue(initialSetValue(options.find((candidate) => candidate.field === next)))\n }\n\n return (\n <Row\n gap=\"2\"\n align=\"center\"\n className=\"border-b border-border bg-muted/20 px-4 py-2 sm:px-6\"\n data-testid=\"bulk-set-panel\"\n >\n <Text size=\"sm\" tone=\"muted\">\n {translate('cms.list.bulk.set.set')}\n </Text>\n <Select\n aria-label={translate('cms.list.bulk.setField')}\n value={field}\n onChange={(event) => changeField(event.target.value)}\n className=\"h-7 w-auto py-0 text-xs\"\n data-testid=\"bulk-set-field-select\"\n >\n {options.map((candidate) => (\n <option key={candidate.field} value={candidate.field}>\n {candidate.label}\n </option>\n ))}\n </Select>\n <Text size=\"sm\" tone=\"muted\">\n {translate('cms.list.bulk.set.to')}\n </Text>\n <ScalarFieldValueEditor\n key={field}\n option={option}\n value={value}\n ariaLabel={translate('cms.list.bulk.set.valueAria')}\n testId=\"bulk-set-value\"\n onChange={setValue}\n />\n <Spacer />\n <Button\n variant=\"outline\"\n size=\"sm\"\n disabled={busy}\n data-testid=\"bulk-set-apply\"\n onClick={() => onApply({ field, value })}\n >\n <Check /> {translate('cms.list.bulk.set.apply')}\n </Button>\n <Button variant=\"ghost\" size=\"sm\" disabled={busy} data-testid=\"bulk-set-cancel\" onClick={onCancel}>\n {translate('common.cancel')}\n </Button>\n </Row>\n )\n}\n\n/**\n * Per-item bulk results (EC-169): the completion summary plus every failure,\n * named and with its reason — a partial failure is enumerated, never silent.\n */\nexport function BulkReportPanel({\n report,\n onDismiss,\n}: {\n report: BulkReport\n onDismiss: () => void\n}) {\n const { succeeded, failed } = bulkSummary(report)\n const failures = report.results.filter((result) => !result.ok)\n const actionLabel =\n report.action === 'set-field' && report.setField\n ? translate('cms.list.bulk.report.verb.setField', { field: report.setField.field })\n : translate(`cms.list.bulk.report.verb.${report.action}`)\n\n return (\n <Stack gap=\"1\" className=\"shrink-0 border-b border-border px-4 py-2 sm:px-6\" data-testid=\"bulk-report\">\n <Row gap=\"2\">\n <Text size=\"sm\" weight=\"medium\" data-testid=\"bulk-report-summary\">\n {translate('cms.list.bulk.report.summary', { action: actionLabel, succeeded })}\n {failed > 0 ? translate('cms.list.bulk.report.failedSuffix', { count: failed }) : ''}\n </Text>\n {failed > 0 ? (\n <Badge variant=\"error\">{translate('cms.list.bulk.report.failedBadge', { count: failed })}</Badge>\n ) : (\n <Badge variant=\"success\">{translate('cms.list.bulk.report.done')}</Badge>\n )}\n <Spacer />\n <Button variant=\"ghost\" size=\"sm\" data-testid=\"bulk-report-dismiss\" onClick={onDismiss}>\n {translate('common.dismiss')}\n </Button>\n </Row>\n {failures.map((failure) => (\n <Row gap=\"2\" key={failure.ref.id} data-testid={`bulk-failure-${failure.ref.id}`}>\n <Text size=\"xs\" className=\"text-destructive\">\n {failure.title}\n </Text>\n <Code tone=\"muted\">{failure.ref.id}</Code>\n <Text size=\"xs\" tone=\"muted\">\n {failure.reason}\n </Text>\n </Row>\n ))}\n </Stack>\n )\n}\n"]}
@@ -176,8 +176,25 @@ export function LoadedCollectionTable({ projectId, toolbarExtra, }) {
176
176
  setPendingBulk({ action, targets: [{ collection: collectionId, id: doc.id }] });
177
177
  }
178
178
  async function handleBulk(action, acted, setField) {
179
- if (!source.bulk)
179
+ // Defensive: with the capability gates below (`source.bulk` presence gates
180
+ // the bar, per-action capabilities gate its buttons) this is unreachable —
181
+ // but a confirmed action must NEVER silently no-op (EC-433: the assets
182
+ // rail's bulk delete used to die exactly here), so a missing runner still
183
+ // reports one honest failure per item.
184
+ if (!source.bulk) {
185
+ setBulkReport({
186
+ action,
187
+ ...(setField ? { setField } : {}),
188
+ results: acted.map((id) => ({
189
+ ref: { collection: collectionId, id },
190
+ title: source.titleOf({ collection: collectionId, id }),
191
+ ok: false,
192
+ reason: 'Bulk actions are not available in this view.',
193
+ error: { code: 'unknown', message: 'Bulk actions are not available in this view.' },
194
+ })),
195
+ });
180
196
  return;
197
+ }
181
198
  setBulkBusy(true);
182
199
  try {
183
200
  const report = await source.bulk([...acted], action, setField);
@@ -204,7 +221,7 @@ export function LoadedCollectionTable({ projectId, toolbarExtra, }) {
204
221
  a.click();
205
222
  URL.revokeObjectURL(url);
206
223
  }
207
- return (_jsxs("div", { className: "flex h-full min-h-0 flex-col", children: [_jsx("div", { className: "shrink-0 border-b border-border px-4 py-3 sm:px-6", children: _jsxs(Row, { gap: "2", wrap: true, children: [_jsx(ViewTabs, {}), _jsx(FilterControl, { collection: collection }), _jsx(Separator, { orientation: "vertical", className: "h-5 self-center" }), _jsx(Text, { as: "span", size: "xs", tone: "muted", className: "whitespace-nowrap tabular-nums", "data-testid": "collection-count", children: countLabel }), _jsx(Spacer, {}), source.capabilities.locales ? _jsx(LocaleSwitcher, {}) : null, _jsx(DisplayMenu, { collection: collection, hierarchical: hierarchical, treeMode: treeMode, onToggleTree: setTreeMode }), canCreate ? (_jsxs(Button, { size: "sm", onClick: handleNew, "data-testid": "new-document", children: [_jsx(Plus, {}), " ", isPageCollection ? translate('cms.list.newPage') : translate('cms.list.new')] })) : null, source.capabilities.importExport ? (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { variant: "outline", size: "icon-sm", "aria-label": translate('cms.list.moreActions'), "data-testid": "collection-actions", children: _jsx(MoreHorizontal, {}) }) }), _jsxs(DropdownMenuContent, { align: "end", children: [_jsxs(DropdownMenuItem, { onClick: () => setImportOpen(true), "data-testid": "action-import", children: [_jsx(Upload, {}), " ", translate('cms.list.import')] }), _jsxs(DropdownMenuItem, { onClick: exportJson, disabled: docs.length === 0, "data-testid": "action-export", children: [_jsx(Download, {}), " ", translate('cms.list.export')] })] })] })) : null, toolbarExtra] }) }), _jsx(BulkBar, { collection: collection, count: selectedIds.length, busy: bulkBusy, onAction: raiseBulk, onSetField: (setField) => void handleBulk('set-field', [...selectedIds], setField), onClear: () => setSelection(clearSelection()) }), bulkReport ? (_jsx(BulkReportPanel, { report: bulkReport, onDismiss: () => setBulkReport(null) })) : null, pendingBulk !== null ? (_jsx(ReferenceConfirmDialog, { projectId: projectId, action: pendingBulk.action, targets: pendingBulk.targets, open: true, busy: bulkBusy, onOpenChange: (open) => {
224
+ return (_jsxs("div", { className: "flex h-full min-h-0 flex-col", children: [_jsx("div", { className: "shrink-0 border-b border-border px-4 py-3 sm:px-6", children: _jsxs(Row, { gap: "2", wrap: true, children: [_jsx(ViewTabs, {}), _jsx(FilterControl, { collection: collection }), _jsx(Separator, { orientation: "vertical", className: "h-5 self-center" }), _jsx(Text, { as: "span", size: "xs", tone: "muted", className: "whitespace-nowrap tabular-nums", "data-testid": "collection-count", children: countLabel }), _jsx(Spacer, {}), source.capabilities.locales ? _jsx(LocaleSwitcher, {}) : null, _jsx(DisplayMenu, { collection: collection, hierarchical: hierarchical, treeMode: treeMode, onToggleTree: setTreeMode }), canCreate ? (_jsxs(Button, { size: "sm", onClick: handleNew, "data-testid": "new-document", children: [_jsx(Plus, {}), " ", isPageCollection ? translate('cms.list.newPage') : translate('cms.list.new')] })) : null, source.capabilities.importExport ? (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { variant: "outline", size: "icon-sm", "aria-label": translate('cms.list.moreActions'), "data-testid": "collection-actions", children: _jsx(MoreHorizontal, {}) }) }), _jsxs(DropdownMenuContent, { align: "end", children: [_jsxs(DropdownMenuItem, { onClick: () => setImportOpen(true), "data-testid": "action-import", children: [_jsx(Upload, {}), " ", translate('cms.list.import')] }), _jsxs(DropdownMenuItem, { onClick: exportJson, disabled: docs.length === 0, "data-testid": "action-export", children: [_jsx(Download, {}), " ", translate('cms.list.export')] })] })] })) : null, toolbarExtra] }) }), source.bulk ? (_jsx(BulkBar, { collection: collection, capabilities: source.capabilities, count: selectedIds.length, busy: bulkBusy, onAction: raiseBulk, onSetField: (setField) => void handleBulk('set-field', [...selectedIds], setField), onClear: () => setSelection(clearSelection()) })) : null, bulkReport ? (_jsx(BulkReportPanel, { report: bulkReport, onDismiss: () => setBulkReport(null) })) : null, pendingBulk !== null ? (_jsx(ReferenceConfirmDialog, { projectId: projectId, action: pendingBulk.action, targets: pendingBulk.targets, open: true, busy: bulkBusy, onOpenChange: (open) => {
208
225
  if (!open)
209
226
  setPendingBulk(null);
210
227
  }, onConfirm: () => {