@grafloria/element 0.4.11 → 0.4.12

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@grafloria/element",
3
- "version": "0.4.11",
3
+ "version": "0.4.12",
4
4
  "type": "module",
5
5
  "main": "./src/index.js",
6
6
  "types": "./src/index.d.ts",
@@ -199,6 +199,14 @@ export interface DashboardOptions {
199
199
  * interactive. Live: `handle.setDragHandle()`.
200
200
  */
201
201
  dragHandle?: DragHandleOption;
202
+ /**
203
+ * FIT AND THE ROW FLOOR. `true` (default): a bounded fit board squeezes its
204
+ * rows toward `minRowHeight` before refusing growth. `false`: rows freeze at
205
+ * the height they have now — a gesture that needs a row the frame does not
206
+ * hold is refused outright and no other tile shrinks. See the grid binder's
207
+ * `squeeze` for the exact rule.
208
+ */
209
+ squeeze?: boolean;
202
210
  /**
203
211
  * RIGHT-TO-LEFT boards: column x=0 renders at the RIGHT edge and columns run
204
212
  * leftwards. Cells are untouched — the same `widgets` array describes the
@@ -505,9 +505,20 @@ export function createDashboardHandle(ctx) {
505
505
  return makeWidgetHandle(id);
506
506
  },
507
507
  focusWidget(id) {
508
- var _a, _b;
508
+ var _a;
509
509
  const b = binders.get((_a = viewOfWidget.get(id)) !== null && _a !== void 0 ? _a : '');
510
- return (_b = b === null || b === void 0 ? void 0 : b.focusWidget(id)) !== null && _b !== void 0 ? _b : false;
510
+ if (!b)
511
+ return false;
512
+ if (b.focusWidget(id))
513
+ return true;
514
+ // A widget added in this same tick is not a member yet (adds commit
515
+ // asynchronously): the spec knows it, so select it once the commit lands.
516
+ if (!specById.has(id))
517
+ return false;
518
+ const retry = (n) => { if (!b.focusWidget(id) && n > 0)
519
+ setTimeout(() => retry(n - 1), 16); };
520
+ queueMicrotask(() => retry(4));
521
+ return true;
511
522
  },
512
523
  widgetsOf(viewId) {
513
524
  var _a;
@@ -1032,7 +1043,7 @@ export function dashboard(options) {
1032
1043
  // its tree from exactly those, so any stale tree is cleared first;
1033
1044
  // split → grid rebuilds from them, so the column cache goes too.
1034
1045
  ctx.rebindView = (viewId, next) => {
1035
- var _a;
1046
+ var _a, _b;
1036
1047
  const v = views.find((x) => x.id === viewId);
1037
1048
  const g = groups.get(viewId);
1038
1049
  const b = binders.get(viewId);
@@ -1040,6 +1051,7 @@ export function dashboard(options) {
1040
1051
  return;
1041
1052
  const cells = b.saveLayout().cells;
1042
1053
  const live = { rtl: b.getRtl(), static: b.getStatic(), dragHandle: b.getDragHandle() };
1054
+ const focused = b.getFocusedWidget();
1043
1055
  b.dispose();
1044
1056
  const write = (fn) => (model.runSystemWrite ? model.runSystemWrite(fn) : fn());
1045
1057
  write(() => {
@@ -1059,6 +1071,10 @@ export function dashboard(options) {
1059
1071
  ctx.layoutOf.set(viewId, next);
1060
1072
  binders.set(viewId, bindView(v, g, next, live));
1061
1073
  (_a = binders.get(viewId)) === null || _a === void 0 ? void 0 : _a.sync();
1074
+ // The selected widget (and its grip) survives the switch, as it does in
1075
+ // the DevExpress designer — the host used to have to restate it.
1076
+ if (focused)
1077
+ (_b = binders.get(viewId)) === null || _b === void 0 ? void 0 : _b.focusWidget(focused);
1062
1078
  };
1063
1079
  handle.showView(ctx.active);
1064
1080
  ctx.rebindContainer = (id) => {
@@ -1153,7 +1169,7 @@ export function dashboard(options) {
1153
1169
  */
1154
1170
  function bindView(v, g, viewLayout, live) {
1155
1171
  var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
1156
- const common = Object.assign(Object.assign({ gap, padding: gap, rtl: (_b = (_a = live === null || live === void 0 ? void 0 : live.rtl) !== null && _a !== void 0 ? _a : options.rtl) !== null && _b !== void 0 ? _b : false, fluid: mode === 'fluid', static: (_d = (_c = live === null || live === void 0 ? void 0 : live.static) !== null && _c !== void 0 ? _c : options.static) !== null && _d !== void 0 ? _d : false, dragHandle: (_f = (_e = live === null || live === void 0 ? void 0 : live.dragHandle) !== null && _e !== void 0 ? _e : options.dragHandle) !== null && _f !== void 0 ? _f : false }, ((_g = options.binder) !== null && _g !== void 0 ? _g : {})), { onGesture: (e) => {
1172
+ const common = Object.assign(Object.assign(Object.assign({ gap, padding: gap, rtl: (_b = (_a = live === null || live === void 0 ? void 0 : live.rtl) !== null && _a !== void 0 ? _a : options.rtl) !== null && _b !== void 0 ? _b : false, fluid: mode === 'fluid', static: (_d = (_c = live === null || live === void 0 ? void 0 : live.static) !== null && _c !== void 0 ? _c : options.static) !== null && _d !== void 0 ? _d : false, dragHandle: (_f = (_e = live === null || live === void 0 ? void 0 : live.dragHandle) !== null && _e !== void 0 ? _e : options.dragHandle) !== null && _f !== void 0 ? _f : false }, (options.squeeze !== undefined ? { squeeze: options.squeeze } : {})), ((_g = options.binder) !== null && _g !== void 0 ? _g : {})), { onGesture: (e) => {
1157
1173
  var _a, _b;
1158
1174
  if (e.type === 'commit')
1159
1175
  reportChanged();
@@ -1167,25 +1183,12 @@ export function dashboard(options) {
1167
1183
  /** Bind (or re-bind) a container's inner grid on its group. */
1168
1184
  function bindContainer(cg, w, viewId) {
1169
1185
  var _a, _b, _c, _d, _e;
1170
- binders.set(w.id, bindDashboardGrid(a, cg, {
1171
- columns: innerColumnsOf(w),
1172
- gap,
1173
- padding: 0,
1174
- sizing: 'fit',
1175
- baseRowHeight: rowHeight,
1176
- designHeight: 0,
1177
- maxRows: (_a = w.maxRows) !== null && _a !== void 0 ? _a : rowExtentOf((_b = w.widgets) !== null && _b !== void 0 ? _b : []),
1178
- float: false,
1179
- rtl: (_c = options.rtl) !== null && _c !== void 0 ? _c : false,
1180
- static: (_d = options.static) !== null && _d !== void 0 ? _d : false,
1181
- dragHandle: (_e = options.dragHandle) !== null && _e !== void 0 ? _e : false,
1182
- onGesture: (e) => {
1186
+ binders.set(w.id, bindDashboardGrid(a, cg, Object.assign(Object.assign({ columns: innerColumnsOf(w), gap, padding: 0, sizing: 'fit', baseRowHeight: rowHeight, designHeight: 0, maxRows: (_a = w.maxRows) !== null && _a !== void 0 ? _a : rowExtentOf((_b = w.widgets) !== null && _b !== void 0 ? _b : []), float: false, rtl: (_c = options.rtl) !== null && _c !== void 0 ? _c : false, static: (_d = options.static) !== null && _d !== void 0 ? _d : false, dragHandle: (_e = options.dragHandle) !== null && _e !== void 0 ? _e : false }, (options.squeeze !== undefined ? { squeeze: options.squeeze } : {})), { onGesture: (e) => {
1183
1187
  var _a, _b;
1184
1188
  if (e.type === 'commit')
1185
1189
  reportChanged();
1186
1190
  (_b = (_a = options.binder) === null || _a === void 0 ? void 0 : _a.onGesture) === null || _b === void 0 ? void 0 : _b.call(_a, e);
1187
- },
1188
- }));
1191
+ } })));
1189
1192
  }
1190
1193
  /**
1191
1194
  * One reporter for every binder on a view — the view's own and each
@@ -223,6 +223,18 @@ export interface DashboardGridOptions {
223
223
  * interactive (scroll a table, click a legend). Live: `setDragHandle`.
224
224
  */
225
225
  dragHandle?: DragHandleOption;
226
+ /**
227
+ * FIT AND THE ROW FLOOR. `true` (default): a bounded fit board holds as many
228
+ * rows as fit at `minRowHeight` — a gesture that needs another row SQUEEZES
229
+ * every row toward the floor, and is refused only when even that is not
230
+ * enough. `false`: rows FREEZE at the height they have now — the capacity is
231
+ * what the frame holds at the current row height, so a resize, move or add
232
+ * that needs a row the frame does not have is refused outright (placeholder
233
+ * stays, `onGesture` reports `changed: false`) and no other tile shrinks. A
234
+ * board loaded with more rows than fit still squeezes to the frame (fit never
235
+ * scrolls); it just cannot be pushed further by a gesture.
236
+ */
237
+ squeeze?: boolean;
226
238
  }
227
239
  /** A painted grip: the only drag zone, placed along the card's top edge. */
228
240
  export interface DragGripOptions {
@@ -257,6 +269,8 @@ export declare function gripHostOf(target: Element | null): HTMLElement | null;
257
269
  * included — so the pointer need not hit the header's text to grab the tile.
258
270
  */
259
271
  export declare function pressOnDragHandle(sel: string, target: Element | null, hostEl: HTMLElement | null, clientX: number, clientY: number): boolean;
272
+ /** The caption strip of a host with no kit header, px from the card's top. */
273
+ export declare const CAPTION_BAND = 28;
260
274
  export interface DashboardGridHandle {
261
275
  /** Rebuild the engine from the group's members + their cells, re-project pixels. */
262
276
  sync(): void;
@@ -110,13 +110,15 @@ export function pressOnDragHandle(sel, target, hostEl, clientX, clientY) {
110
110
  return true;
111
111
  if (sel !== '.axdb-widget-h' || !hostEl)
112
112
  return false;
113
- const header = hostEl.querySelector('.axdb-widget-h');
114
- if (!header)
115
- return false;
116
113
  const hr = hostEl.getBoundingClientRect();
117
- const r = header.getBoundingClientRect();
118
- return clientX >= hr.left && clientX <= hr.right && clientY >= hr.top && clientY <= r.bottom;
114
+ const header = hostEl.querySelector('.axdb-widget-h');
115
+ // A host painting its own content has no kit header: its caption is the top
116
+ // band of the card, so `dragHandle: true` still means something there.
117
+ const bottom = header ? header.getBoundingClientRect().bottom : hr.top + CAPTION_BAND;
118
+ return clientX >= hr.left && clientX <= hr.right && clientY >= hr.top && clientY <= bottom;
119
119
  }
120
+ /** The caption strip of a host with no kit header, px from the card's top. */
121
+ export const CAPTION_BAND = 28;
120
122
  /**
121
123
  * Binders on the same canvas find each other here. A WeakMap: a canvas that
122
124
  * is gone takes its (empty) peer set with it — the strong Map it replaced kept
@@ -237,6 +239,7 @@ export function bindDashboardGrid(api, group, options = {}) {
237
239
  const padding = (_c = options.padding) !== null && _c !== void 0 ? _c : gap;
238
240
  const baseRowHeight = (_d = options.baseRowHeight) !== null && _d !== void 0 ? _d : 110;
239
241
  const minRowHeight = (_e = options.minRowHeight) !== null && _e !== void 0 ? _e : 28;
242
+ const squeeze = options.squeeze !== false;
240
243
  let float = (_f = options.float) !== null && _f !== void 0 ? _f : false;
241
244
  /** The AUTHORED bound — a nested strip's design (row-first push, escalation). */
242
245
  const maxRows = options.maxRows;
@@ -285,7 +288,10 @@ export function bindDashboardGrid(api, group, options = {}) {
285
288
  const fitCapacity = () => {
286
289
  if (maxRows !== undefined || sizing !== 'fit' || overflow === 'scroll' || designH <= 0)
287
290
  return undefined;
288
- const rowsThatFit = Math.floor((designH - 2 * padding + gap) / (minRowHeight + gap));
291
+ // Elastic (default): rows at the floor. Frozen (`squeeze: false`): rows at
292
+ // the height they have NOW, so a gesture never shrinks a neighbour.
293
+ const floor = squeeze ? minRowHeight : Math.max(minRowHeight, rowHeightFor(geom(), rows()));
294
+ const rowsThatFit = Math.floor((designH - 2 * padding + gap) / (floor + gap));
289
295
  return Math.max(1, rowsThatFit, engine.rows());
290
296
  };
291
297
  /** Re-derive the capacity; true when the engine must be rebuilt to carry it. */
@@ -664,6 +670,7 @@ export function bindDashboardGrid(api, group, options = {}) {
664
670
  syncA11y(only);
665
671
  if (disposed)
666
672
  return;
673
+ ensureStaticGuard();
667
674
  const grip = gripOf(dragHandle);
668
675
  for (const id of (_a = group.members) !== null && _a !== void 0 ? _a : []) {
669
676
  if (only && !only.has(id))
@@ -1828,9 +1835,50 @@ export function bindDashboardGrid(api, group, options = {}) {
1828
1835
  return;
1829
1836
  const node = diagram.getNode(id);
1830
1837
  const resizable = !!node && !isStatic && ((_f = node.state) === null || _f === void 0 ? void 0 : _f.locked) !== true && ((_g = node.getMetadata) === null || _g === void 0 ? void 0 : _g.call(node, 'widgetResizable')) !== false;
1831
- host.style.cursor = resizable ? cursorFor(edgesNear(host, e.clientX, e.clientY)) : '';
1838
+ const cursor = resizable ? cursorFor(edgesNear(host, e.clientX, e.clientY)) : '';
1839
+ // The affordance rides on an ATTRIBUTE, not the inline style: a repaint
1840
+ // rewrites the host's style and used to clear the cursor mid-hover, and
1841
+ // content that sets its own cursor (a chart canvas) hid it — the
1842
+ // stylesheet applies the attribute's cursor to the host and everything in it.
1843
+ if (cursor)
1844
+ host.setAttribute('data-axdb-edge', cursor);
1845
+ else
1846
+ host.removeAttribute('data-axdb-edge');
1832
1847
  };
1833
1848
  api.container.addEventListener('pointermove', onHover, { passive: true });
1849
+ /**
1850
+ * STATIC BOARDS LET CONTENT BE CLICKED. The renderer prevents the default of
1851
+ * every press a tool claims, which cancels the compatibility mouse events —
1852
+ * a chart inside a read-only board could not be clicked. So under `static`
1853
+ * a press inside a member's CONTENT (not on kit chrome) is stopped on the
1854
+ * HTML layer, in the bubble phase: the content has already received it, the
1855
+ * renderer never does, nothing is prevented.
1856
+ */
1857
+ const staticGuard = (e) => {
1858
+ var _a, _b, _c, _d;
1859
+ if (!isStatic || disposed)
1860
+ return;
1861
+ const t = e.target;
1862
+ const host = (_a = t === null || t === void 0 ? void 0 : t.closest) === null || _a === void 0 ? void 0 : _a.call(t, '.grafloria-node-host');
1863
+ if (!host || !((_b = group.members) !== null && _b !== void 0 ? _b : new Set()).has((_c = host.getAttribute('data-node-id')) !== null && _c !== void 0 ? _c : ''))
1864
+ return;
1865
+ if ((_d = t === null || t === void 0 ? void 0 : t.closest) === null || _d === void 0 ? void 0 : _d.call(t, '.axdb-rs, .axdb-grip, .axdb-div'))
1866
+ return;
1867
+ e.stopPropagation();
1868
+ };
1869
+ let guardedLayer = null;
1870
+ const ensureStaticGuard = () => {
1871
+ // One lookup, ever: the host observer budgets container lookups per
1872
+ // repaint, and the layer element lives as long as the instance.
1873
+ if (guardedLayer === null || guardedLayer === void 0 ? void 0 : guardedLayer.isConnected)
1874
+ return;
1875
+ const layer = htmlLayer();
1876
+ if (!layer)
1877
+ return;
1878
+ guardedLayer === null || guardedLayer === void 0 ? void 0 : guardedLayer.removeEventListener('pointerdown', staticGuard);
1879
+ guardedLayer = layer;
1880
+ layer.addEventListener('pointerdown', staticGuard);
1881
+ };
1834
1882
  /**
1835
1883
  * KEYBOARD OPERATION (WCAG 2.1.1, and the non-drag alternative 2.5.7 asks
1836
1884
  * for): on a focused member, arrows move it one cell, Shift+arrows resize it
@@ -2349,6 +2397,7 @@ export function bindDashboardGrid(api, group, options = {}) {
2349
2397
  peersOnCanvas().delete(selfPeer);
2350
2398
  unregisterTool();
2351
2399
  api.container.removeEventListener('pointermove', onHover);
2400
+ guardedLayer === null || guardedLayer === void 0 ? void 0 : guardedLayer.removeEventListener('pointerdown', staticGuard);
2352
2401
  api.container.removeEventListener('focusin', onFocusIn);
2353
2402
  api.container.removeEventListener('keydown', onKey);
2354
2403
  hostObserver.disconnect();
@@ -27,7 +27,7 @@ import type { DashboardGridApi, DashboardGridHandle, DashboardGridOptions } from
27
27
  import { type SplitNode } from './split-layout.js';
28
28
  /** Group metadata key the tree persists under. */
29
29
  export declare const SPLIT_TREE_KEY = "dashboardTree";
30
- export interface DashboardSplitOptions extends Pick<DashboardGridOptions, 'columns' | 'gap' | 'padding' | 'rtl' | 'fluid' | 'static' | 'dragHandle' | 'designHeight' | 'baseRowHeight' | 'dragOut' | 'removeZone' | 'onRemoveRequest' | 'onDropIn' | 'onGesture'> {
30
+ export interface DashboardSplitOptions extends Pick<DashboardGridOptions, 'columns' | 'gap' | 'padding' | 'rtl' | 'fluid' | 'static' | 'dragHandle' | 'squeeze' | 'designHeight' | 'baseRowHeight' | 'dragOut' | 'removeZone' | 'onRemoveRequest' | 'onDropIn' | 'onGesture'> {
31
31
  /** An authored tree. Default: the persisted one, else derived from the members' cells. */
32
32
  tree?: SplitNode | null;
33
33
  }
@@ -377,23 +377,48 @@ export function bindDashboardSplit(api, group, options = {}) {
377
377
  selectedId = id;
378
378
  syncA11y();
379
379
  };
380
- const syncA11y = () => {
380
+ // Static boards let content be clicked — see grid-binder's staticGuard.
381
+ const staticGuard = (e) => {
381
382
  var _a, _b, _c, _d;
383
+ if (!isStatic || disposed)
384
+ return;
385
+ const t = e.target;
386
+ const host = (_a = t === null || t === void 0 ? void 0 : t.closest) === null || _a === void 0 ? void 0 : _a.call(t, '.grafloria-node-host');
387
+ if (!host || !((_b = group.members) !== null && _b !== void 0 ? _b : new Set()).has((_c = host.getAttribute('data-node-id')) !== null && _c !== void 0 ? _c : ''))
388
+ return;
389
+ if ((_d = t === null || t === void 0 ? void 0 : t.closest) === null || _d === void 0 ? void 0 : _d.call(t, '.axdb-rs, .axdb-grip, .axdb-div'))
390
+ return;
391
+ e.stopPropagation();
392
+ };
393
+ let guardedLayer = null;
394
+ const ensureStaticGuard = () => {
395
+ if (guardedLayer === null || guardedLayer === void 0 ? void 0 : guardedLayer.isConnected)
396
+ return;
397
+ const layer = api.container.querySelector('.grafloria-html-layer');
398
+ if (!layer)
399
+ return;
400
+ guardedLayer === null || guardedLayer === void 0 ? void 0 : guardedLayer.removeEventListener('pointerdown', staticGuard);
401
+ guardedLayer = layer;
402
+ layer.addEventListener('pointerdown', staticGuard);
403
+ };
404
+ const syncA11y = () => {
405
+ var _a, _b, _c, _d, _e;
382
406
  if (disposed)
383
407
  return;
408
+ ensureStaticGuard();
384
409
  const order = splitLeaves(paintedTree()).filter((id) => !!diagram.getNode(id));
385
410
  if (selectedId && !order.includes(selectedId))
386
411
  selectedId = undefined;
387
412
  // No corner handles on a split board: size comes from the dividers. A host
388
413
  // that carried the grid's handle (a board switched live) sheds it here.
389
- for (const id of order)
390
- (_b = (_a = hostOf(id)) === null || _a === void 0 ? void 0 : _a.querySelector(':scope > .axdb-rs')) === null || _b === void 0 ? void 0 : _b.remove();
414
+ for (const id of (_a = group.members) !== null && _a !== void 0 ? _a : [])
415
+ (_c = (_b = hostOf(id)) === null || _b === void 0 ? void 0 : _b.querySelector(':scope > .axdb-rs')) === null || _c === void 0 ? void 0 : _c.remove();
391
416
  // The painted grip (or none), on every leaf host.
392
417
  for (const id of order) {
393
418
  const host = hostOf(id);
394
419
  const node = diagram.getNode(id);
395
420
  if (host && node)
396
- syncGrip(host, gripOf(dragHandle), ((_c = node.state) === null || _c === void 0 ? void 0 : _c.locked) !== true && !isStatic && ((_d = node.getMetadata) === null || _d === void 0 ? void 0 : _d.call(node, 'widgetMovable')) !== false);
421
+ syncGrip(host, gripOf(dragHandle), ((_d = node.state) === null || _d === void 0 ? void 0 : _d.locked) !== true && !isStatic && ((_e = node.getMetadata) === null || _e === void 0 ? void 0 : _e.call(node, 'widgetMovable')) !== false);
397
422
  }
398
423
  if (focusedId && !order.includes(focusedId))
399
424
  focusedId = undefined;
@@ -1103,6 +1128,7 @@ export function bindDashboardSplit(api, group, options = {}) {
1103
1128
  });
1104
1129
  },
1105
1130
  dispose() {
1131
+ guardedLayer === null || guardedLayer === void 0 ? void 0 : guardedLayer.removeEventListener('pointerdown', staticGuard);
1106
1132
  if (disposed)
1107
1133
  return;
1108
1134
  cancelActiveGesture();
@@ -35,7 +35,7 @@ const CSS = `
35
35
 
36
36
  /* ===== keyboard focus: the roving tab stop shows where it is (WCAG 2.4.7) ===== */
37
37
  .grafloria-html-layer > .grafloria-node-host:focus-visible {
38
- outline: 2px solid #3b52d9;
38
+ outline: 2px solid var(--axdb-accent, #3b52d9);
39
39
  outline-offset: 2px;
40
40
  border-radius: var(--axdb-rs-radius, 3px);
41
41
  }
@@ -203,9 +203,16 @@ const CSS = `
203
203
  content: ''; position: absolute; left: 5px; top: 2px; width: 12px; height: 6px;
204
204
  background: radial-gradient(circle, currentColor 1px, transparent 1.4px) 0 0 / 4px 3px;
205
205
  }
206
- .grafloria-node-host > .axdb-grip:hover { border-color: #3b52d9; color: #3b52d9; }
207
- /* The selected card says so, quietly. */
208
- .grafloria-node-host.axdb-selected > .axdb-widget { box-shadow: 0 0 0 1.5px rgba(59, 82, 217, .55), 0 1px 2px rgba(16, 24, 40, .05), 0 1px 3px rgba(16, 24, 40, .05); }
206
+ .grafloria-node-host > .axdb-grip:hover { border-color: var(--axdb-accent, #3b52d9); color: var(--axdb-accent, #3b52d9); }
207
+ /* The selected card says so, quietly. --axdb-accent themes the grip's hover, the
208
+ ring and the focus outline together; --axdb-accent-ring the ring alone. */
209
+ .grafloria-node-host.axdb-selected > .axdb-widget { box-shadow: 0 0 0 1.5px var(--axdb-accent-ring, rgba(59, 82, 217, .55)), 0 1px 2px rgba(16, 24, 40, .05), 0 1px 3px rgba(16, 24, 40, .05); }
210
+ /* While a resize edge is near, the host and everything in it (a chart canvas
211
+ with its own cursor included) show the edge's cursor. */
212
+ .grafloria-node-host[data-axdb-edge="ns-resize"], .grafloria-node-host[data-axdb-edge="ns-resize"] * { cursor: ns-resize !important; }
213
+ .grafloria-node-host[data-axdb-edge="ew-resize"], .grafloria-node-host[data-axdb-edge="ew-resize"] * { cursor: ew-resize !important; }
214
+ .grafloria-node-host[data-axdb-edge="nwse-resize"], .grafloria-node-host[data-axdb-edge="nwse-resize"] * { cursor: nwse-resize !important; }
215
+ .grafloria-node-host[data-axdb-edge="nesw-resize"], .grafloria-node-host[data-axdb-edge="nesw-resize"] * { cursor: nesw-resize !important; }
209
216
  /* INSIDE: centred on the header's text line and flush with the card padding,
210
217
  per size tier (padding 13/15, then 8/14, 4/12, 2/12). */
211
218
  .grafloria-node-host > .axdb-grip--inside { top: 14px; }