@grafloria/element 0.4.18 → 0.4.19

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.18",
3
+ "version": "0.4.19",
4
4
  "type": "module",
5
5
  "main": "./src/index.js",
6
6
  "types": "./src/index.d.ts",
@@ -252,6 +252,12 @@ export interface DashboardOptions {
252
252
  * hook). Omit for a titled placeholder frame.
253
253
  */
254
254
  renderWidget?: (widget: DashboardWidgetSpec, host: HTMLElement) => void;
255
+ /**
256
+ * The selection changed on a board: the selected id — a widget or a SECTION
257
+ * (container) — or undefined when cleared, and the view it belongs to. A
258
+ * press on a section's empty band selects the section.
259
+ */
260
+ onSelect?: (id: string | undefined, viewId: string) => void;
255
261
  /** Fires after any committed gesture, with the view whose layout changed. */
256
262
  onLayoutChange?: (viewId: string, widgets: DashboardWidgetSpec[]) => void;
257
263
  /** Extra binder options, merged last (escape hatch to the layer below). */
@@ -1264,7 +1264,7 @@ export function dashboard(options) {
1264
1264
  if (e.type === 'commit')
1265
1265
  reportChanged();
1266
1266
  (_b = (_a = options.binder) === null || _a === void 0 ? void 0 : _a.onGesture) === null || _b === void 0 ? void 0 : _b.call(_a, e);
1267
- } });
1267
+ }, onSelect: (id) => { var _a; return (_a = options.onSelect) === null || _a === void 0 ? void 0 : _a.call(options, id, v.id); } });
1268
1268
  if (viewLayout === 'split') {
1269
1269
  return bindDashboardSplit(a, g, Object.assign(Object.assign(Object.assign({}, common), { columns: (_h = v.columns) !== null && _h !== void 0 ? _h : columns, baseRowHeight: rowHeight, designHeight: viewH(v) }), (v.tree !== undefined ? { tree: v.tree } : {})));
1270
1270
  }
@@ -1282,7 +1282,7 @@ export function dashboard(options) {
1282
1282
  if (e.type === 'commit')
1283
1283
  reportChanged();
1284
1284
  (_b = (_a = options.binder) === null || _a === void 0 ? void 0 : _a.onGesture) === null || _b === void 0 ? void 0 : _b.call(_a, e);
1285
- } });
1285
+ }, onSelect: (id) => { var _a, _b; return (_a = options.onSelect) === null || _a === void 0 ? void 0 : _a.call(options, id, (_b = ctx.viewOfBoard.get(w.id)) !== null && _b !== void 0 ? _b : ctx.active); } });
1286
1286
  if (((_h = ctx.layoutOf.get(w.id)) !== null && _h !== void 0 ? _h : w.layout) === 'split') {
1287
1287
  // A splitter tree covering the pane; the pane's frame is the parent's
1288
1288
  // slab, so no design height of its own.
@@ -196,6 +196,8 @@ export interface DashboardGridOptions {
196
196
  changed: boolean;
197
197
  }) => void;
198
198
  /** Inject the hover-revealed corner resize handle into member hosts (default true). */
199
+ /** The selection on this board changed: the selected member — a widget or a SECTION (container) — or undefined. */
200
+ onSelect?: (id: string | undefined) => void;
199
201
  resizeHandles?: boolean;
200
202
  /**
201
203
  * FLUID board: the group's frame follows the CANVAS CONTAINER — width
@@ -436,6 +438,20 @@ interface BinderPeer {
436
438
  * boards after three clicks).
437
439
  */
438
440
  clearSelection?(): void;
441
+ /** A nested board's live row bound — what its slab must never shrink below. */
442
+ innerRows?(): number;
443
+ /**
444
+ * SECTION PRESSES. A press on a section's empty band lands on the NESTED
445
+ * board's tool (it claims its own frame), which knows nothing to select;
446
+ * the section is a member of the PARENT. The child asks the parent to
447
+ * select it and, on an edge or the corner handle, to run the section
448
+ * resize — and forwards the rest of the pointer sequence.
449
+ */
450
+ selectMember?(id: string): void;
451
+ beginSlabResize?(id: string, edges: ResizeEdges, ev: ToolPointerEvent): boolean;
452
+ slabMove?(ev: ToolPointerEvent): void;
453
+ slabUp?(): void;
454
+ slabCancel?(): void;
439
455
  /** The member's current cell in this board (undefined when absent). */
440
456
  memberCell(id: string): CellRect | undefined;
441
457
  /**
@@ -501,4 +517,10 @@ interface AdoptedLeg {
501
517
  export declare function clearOtherSelections(container: HTMLElement, self: BinderPeer | null): void;
502
518
  export declare function registerBoardPeer(container: HTMLElement, peer: BinderPeer): () => void;
503
519
  export type { BinderPeer };
520
+ interface ResizeEdges {
521
+ n: boolean;
522
+ e: boolean;
523
+ s: boolean;
524
+ w: boolean;
525
+ }
504
526
  export declare function bindDashboardGrid(api: DashboardGridApi, group: GroupModel, options?: DashboardGridOptions): DashboardGridHandle;
@@ -465,6 +465,19 @@ export function bindDashboardGrid(api, group, options = {}) {
465
465
  /** The engine item a member should enter as (cells from GridItemConfig /
466
466
  * group metadata; spans falling back to metadata columnSpan/rowSpan; last
467
467
  * resort: adopt from the current pixel geometry). */
468
+ /**
469
+ * A SECTION's floor: its nested board's live rows (the peer), else the rows
470
+ * it was mounted with — a section resized by hand never squeezes its
471
+ * children below their cells.
472
+ */
473
+ const innerRowsOf = (id) => {
474
+ var _a, _b, _c, _d;
475
+ for (const p of (_a = BOARD_REGISTRY.get(api.container)) !== null && _a !== void 0 ? _a : [])
476
+ if (p.group.id === id && p.innerRows)
477
+ return Math.max(1, p.innerRows());
478
+ const meta = (_c = (_b = diagram.getGroup(id)) === null || _b === void 0 ? void 0 : _b.getMetadata) === null || _c === void 0 ? void 0 : _c.call(_b, 'containerWidget');
479
+ return Math.max(1, (_d = meta === null || meta === void 0 ? void 0 : meta.maxRows) !== null && _d !== void 0 ? _d : 1);
480
+ };
468
481
  const itemFor = (id) => {
469
482
  var _a, _b, _c, _d, _e, _f, _g;
470
483
  const node = diagram.getNode(id);
@@ -708,23 +721,27 @@ export function bindDashboardGrid(api, group, options = {}) {
708
721
  */
709
722
  let selectedId;
710
723
  const selectWidget = (id) => {
724
+ var _a;
711
725
  if (id !== undefined)
712
726
  clearOtherSelections(api.container, selfPeerRef);
713
727
  if (id === selectedId)
714
728
  return;
715
729
  selectedId = id;
716
730
  syncA11y();
731
+ syncSlabs();
732
+ (_a = options.onSelect) === null || _a === void 0 ? void 0 : _a.call(options, id);
717
733
  };
718
734
  /** Set once the peer object exists (below); `selectWidget` runs before that only on boot. */
719
735
  let selfPeerRef = null;
720
736
  const syncA11y = (only) => {
721
- var _a, _b;
737
+ var _a, _b, _c;
722
738
  if (disposed)
723
739
  return;
724
740
  const members = [...((_a = group.members) !== null && _a !== void 0 ? _a : [])].filter((id) => !!diagram.getNode(id));
725
741
  if (focusedId && !members.includes(focusedId))
726
742
  focusedId = undefined;
727
- if (selectedId && !members.includes(selectedId))
743
+ // A selected SECTION is a group member, not a node: keep it as long as it is a member.
744
+ if (selectedId && !members.includes(selectedId) && !(((_b = group.members) === null || _b === void 0 ? void 0 : _b.has(selectedId)) && memberEntity(selectedId)))
728
745
  selectedId = undefined;
729
746
  const stop = focusedId !== null && focusedId !== void 0 ? focusedId : members[0];
730
747
  for (const id of members) {
@@ -738,7 +755,7 @@ export function bindDashboardGrid(api, group, options = {}) {
738
755
  const bits = [nameOf(node)];
739
756
  if (cell)
740
757
  bits.push(describeCell(cell));
741
- if (((_b = node.state) === null || _b === void 0 ? void 0 : _b.locked) === true)
758
+ if (((_c = node.state) === null || _c === void 0 ? void 0 : _c.locked) === true)
742
759
  bits.push('pinned');
743
760
  host.setAttribute('role', 'group');
744
761
  host.setAttribute('aria-roledescription', 'dashboard widget');
@@ -759,6 +776,7 @@ export function bindDashboardGrid(api, group, options = {}) {
759
776
  if (disposed)
760
777
  return;
761
778
  ensureStaticGuard();
779
+ syncSlabs();
762
780
  const grip = gripOf(dragHandle);
763
781
  for (const id of (_a = group.members) !== null && _a !== void 0 ? _a : []) {
764
782
  if (only && !only.has(id))
@@ -975,6 +993,7 @@ export function bindDashboardGrid(api, group, options = {}) {
975
993
  finally {
976
994
  writing = false;
977
995
  }
996
+ syncSlabs();
978
997
  };
979
998
  const applyColumns = (n, layout) => {
980
999
  var _a;
@@ -1044,6 +1063,80 @@ export function bindDashboardGrid(api, group, options = {}) {
1044
1063
  const band = rowHeightFor(geom(), rows()) + gap;
1045
1064
  return x >= f.x && x <= f.x + f.width && y >= f.y && y <= f.y + boardVisualHeight() + band;
1046
1065
  };
1066
+ /** The member SECTION whose frame holds the world point, if any. */
1067
+ const memberGroupAt = (x, y) => {
1068
+ var _a;
1069
+ for (const id of (_a = group.members) !== null && _a !== void 0 ? _a : []) {
1070
+ const grp = diagram.getGroup(id);
1071
+ if (!grp)
1072
+ continue;
1073
+ const p = grp.position;
1074
+ const s = sizeOf(grp);
1075
+ if (x >= p.x && x <= p.x + s.width && y >= p.y && y <= p.y + s.height)
1076
+ return id;
1077
+ }
1078
+ return null;
1079
+ };
1080
+ /** Which of a section frame's edges a world point is within EDGE_GRIP of. */
1081
+ const slabEdgesNear = (grp, x, y) => {
1082
+ const p = grp.position;
1083
+ const s = sizeOf(grp);
1084
+ return { n: y - p.y <= EDGE_GRIP, s: p.y + s.height - y <= EDGE_GRIP, w: x - p.x <= EDGE_GRIP, e: p.x + s.width - x <= EDGE_GRIP };
1085
+ };
1086
+ /**
1087
+ * SECTION CHROME. A section (member group) paints no card of its own, so it
1088
+ * had nothing to press: a click on its empty band cleared the selection and
1089
+ * its frame had no handle and no edge — a section with many children could
1090
+ * not be selected at all, and could only be resized by pulling a child
1091
+ * (Quantia, Groups page). Every section gets a pointer-transparent overlay
1092
+ * in the HTML layer that wears the selection ring and, while selected, the
1093
+ * corner handle; its frame edges answer the resize cursor.
1094
+ */
1095
+ const slabEls = new Map();
1096
+ let slabLayer = null;
1097
+ const syncSlabs = () => {
1098
+ var _a, _b;
1099
+ if (disposed)
1100
+ return;
1101
+ const layer = (slabLayer === null || slabLayer === void 0 ? void 0 : slabLayer.isConnected) ? slabLayer : (slabLayer = htmlLayer());
1102
+ if (!layer)
1103
+ return;
1104
+ const seen = new Set();
1105
+ for (const id of (_a = group.members) !== null && _a !== void 0 ? _a : []) {
1106
+ const grp = diagram.getGroup(id);
1107
+ if (!grp || diagram.getNode(id))
1108
+ continue;
1109
+ seen.add(id);
1110
+ let el = slabEls.get(id);
1111
+ if (!el || el.parentElement !== layer) {
1112
+ el === null || el === void 0 ? void 0 : el.remove();
1113
+ el = document.createElement('div');
1114
+ el.className = 'axdb-slab';
1115
+ el.setAttribute('data-slab-id', id);
1116
+ const rs = document.createElement('div');
1117
+ rs.className = 'axdb-rs';
1118
+ rs.setAttribute('title', 'Resize section');
1119
+ el.appendChild(rs);
1120
+ layer.appendChild(el);
1121
+ slabEls.set(id, el);
1122
+ }
1123
+ const p = grp.position;
1124
+ const sz = sizeOf(grp);
1125
+ el.style.left = `${p.x}px`;
1126
+ el.style.top = `${p.y}px`;
1127
+ el.style.width = `${sz.width}px`;
1128
+ el.style.height = `${sz.height}px`;
1129
+ el.classList.toggle('axdb-slab--selected', selectedId === id);
1130
+ el.classList.toggle('axdb-slab--static', isStatic);
1131
+ (_b = el.querySelector(':scope > .axdb-rs')) === null || _b === void 0 ? void 0 : _b.classList.toggle('axdb-rs--rtl', rtl);
1132
+ }
1133
+ for (const [id, el] of slabEls) {
1134
+ if (!seen.has(id)) {
1135
+ el.remove();
1136
+ slabEls.delete(id);
1137
+ }
1138
+ }
1139
+ };
1047
1140
  const insideMemberGroupFrame = (x, y) => {
1048
1141
  var _a;
1049
1142
  for (const id of (_a = group.members) !== null && _a !== void 0 ? _a : []) {
@@ -1057,6 +1150,131 @@ export function bindDashboardGrid(api, group, options = {}) {
1057
1150
  }
1058
1151
  return false;
1059
1152
  };
1153
+ let slabGesture = null;
1154
+ /** The PARENT running a resize of OUR section from a press this tool claimed. */
1155
+ let forwardSlab = null;
1156
+ const frameOfGroup = (grp) => ({ x: grp.position.x, y: grp.position.y, width: sizeOf(grp).width, height: sizeOf(grp).height });
1157
+ const beginSlabResize = (id, edges, ev) => {
1158
+ const grp = diagram.getGroup(id);
1159
+ const it = engine.getItem(id);
1160
+ if (!grp || !it || gesture || slabGesture)
1161
+ return;
1162
+ engine.beginGesture();
1163
+ const snap = snapshotAll();
1164
+ slabGesture = {
1165
+ id,
1166
+ edges,
1167
+ pointerId: typeof PointerEvent !== 'undefined' && ev.source instanceof PointerEvent ? ev.source.pointerId : null,
1168
+ started: false,
1169
+ downScreen: { x: ev.screen.x, y: ev.screen.y },
1170
+ startCells: snap.cells,
1171
+ startGeom: snap.geoms,
1172
+ cellBefore: { x: it.x, y: it.y, w: it.w, h: it.h },
1173
+ frameBefore: frameOfGroup(grp),
1174
+ grab: {
1175
+ dx: edges.e ? grp.position.x + sizeOf(grp).width - ev.world.x : edges.w ? grp.position.x - ev.world.x : 0,
1176
+ dy: edges.s ? grp.position.y + sizeOf(grp).height - ev.world.y : edges.n ? grp.position.y - ev.world.y : 0,
1177
+ },
1178
+ };
1179
+ capturePointer(slabGesture.pointerId);
1180
+ api.container.style.cursor = cursorFor(edges);
1181
+ };
1182
+ const slabMove = (ev) => {
1183
+ const g = slabGesture;
1184
+ if (!g)
1185
+ return;
1186
+ if (!g.started) {
1187
+ if (Math.abs(ev.screen.x - g.downScreen.x) + Math.abs(ev.screen.y - g.downScreen.y) < DRAG_THRESHOLD)
1188
+ return;
1189
+ g.started = true;
1190
+ armGlide();
1191
+ }
1192
+ const it = engine.getItem(g.id);
1193
+ if (!it)
1194
+ return;
1195
+ const f = frame();
1196
+ const gg = geom();
1197
+ const cu = columnUnitFor(gg, f.width);
1198
+ const rh = rowHeightFor(gg, rows());
1199
+ // The grid line nearest the pointer, in cells (mirrored on RTL).
1200
+ const colAt = (wx) => Math.round((rtl ? f.x + f.width - padding - wx : wx - f.x - padding) / (cu + gap));
1201
+ const rowAt = (wy) => Math.round((wy - f.y - padding) / (rh + gap));
1202
+ let { x, y, w, h } = it;
1203
+ const px = ev.world.x + g.grab.dx;
1204
+ const py = ev.world.y + g.grab.dy;
1205
+ if (g.edges.e)
1206
+ w = Math.max(1, colAt(px) - x);
1207
+ if (g.edges.s)
1208
+ h = Math.max(1, rowAt(py) - y);
1209
+ if (g.edges.w) {
1210
+ const nx = Math.max(0, Math.min(x + w - 1, colAt(px)));
1211
+ w = x + w - nx;
1212
+ x = nx;
1213
+ }
1214
+ if (g.edges.n) {
1215
+ const ny = Math.max(0, Math.min(y + h - 1, rowAt(py)));
1216
+ h = y + h - ny;
1217
+ y = ny;
1218
+ }
1219
+ w = Math.max(1, Math.min(w, columns - x));
1220
+ // Never below the children's rows (the nested board's live bound).
1221
+ const floor = innerRowsOf(g.id);
1222
+ if (h < floor) {
1223
+ if (g.edges.n)
1224
+ y = y + h - floor;
1225
+ h = floor;
1226
+ }
1227
+ let changed = false;
1228
+ if (x !== it.x || y !== it.y)
1229
+ changed = engine.moveCheck(g.id, x, y, { gate: false }).changed || changed;
1230
+ if (w !== it.w || h !== it.h)
1231
+ changed = engine.resizeCheck(g.id, w, h).changed || changed;
1232
+ if (changed)
1233
+ project();
1234
+ };
1235
+ const slabUp = () => {
1236
+ var _a;
1237
+ const g = slabGesture;
1238
+ if (!g)
1239
+ return;
1240
+ slabGesture = null;
1241
+ releasePointer(g.pointerId);
1242
+ api.container.style.cursor = '';
1243
+ if (!g.started) {
1244
+ engine.endGesture();
1245
+ return;
1246
+ }
1247
+ engine.endGesture();
1248
+ project();
1249
+ const it = engine.getItem(g.id);
1250
+ const grp = diagram.getGroup(g.id);
1251
+ const commands = buildCommitCommands(deltasSince(g.startCells, g.startGeom, g.id));
1252
+ const b = g.cellBefore;
1253
+ if (it && grp && (b.x !== it.x || b.y !== it.y || b.w !== it.w || b.h !== it.h)) {
1254
+ commands.push(new SetGroupCellCommand(g.id, b, { x: it.x, y: it.y, w: it.w, h: it.h }, g.frameBefore, frameOfGroup(grp)));
1255
+ }
1256
+ const changed = execute('Resize section', commands);
1257
+ disarmGlideSoon();
1258
+ syncHandles();
1259
+ api.renderNow();
1260
+ (_a = options.onGesture) === null || _a === void 0 ? void 0 : _a.call(options, { type: 'commit', kind: 'resize', nodeId: g.id, changed });
1261
+ };
1262
+ const slabCancel = () => {
1263
+ var _a;
1264
+ const g = slabGesture;
1265
+ if (!g)
1266
+ return;
1267
+ slabGesture = null;
1268
+ releasePointer(g.pointerId);
1269
+ api.container.style.cursor = '';
1270
+ if (g.started)
1271
+ engine.cancelGesture();
1272
+ else
1273
+ engine.endGesture();
1274
+ project();
1275
+ disarmGlideSoon();
1276
+ (_a = options.onGesture) === null || _a === void 0 ? void 0 : _a.call(options, { type: 'cancel', kind: 'resize', nodeId: g.id, changed: false });
1277
+ };
1060
1278
  const capturePointer = (pointerId) => {
1061
1279
  var _a, _b;
1062
1280
  if (pointerId === null)
@@ -1163,7 +1381,13 @@ export function bindDashboardGrid(api, group, options = {}) {
1163
1381
  (_a = options.onGesture) === null || _a === void 0 ? void 0 : _a.call(options, { type: 'commit', kind: g.kind, nodeId: g.id, changed });
1164
1382
  };
1165
1383
  const cancelActiveGesture = (notify = true) => {
1166
- var _a, _b, _c;
1384
+ var _a, _b, _c, _d;
1385
+ if (forwardSlab) {
1386
+ (_a = forwardSlab.slabCancel) === null || _a === void 0 ? void 0 : _a.call(forwardSlab);
1387
+ forwardSlab = null;
1388
+ }
1389
+ if (slabGesture)
1390
+ slabCancel();
1167
1391
  const g = gesture;
1168
1392
  if (!g)
1169
1393
  return;
@@ -1184,7 +1408,7 @@ export function bindDashboardGrid(api, group, options = {}) {
1184
1408
  engine.endGesture();
1185
1409
  const items = [];
1186
1410
  for (const [id, c] of g.startCells) {
1187
- const lockedNode = ((_b = (_a = diagram.getNode(id)) === null || _a === void 0 ? void 0 : _a.state) === null || _b === void 0 ? void 0 : _b.locked) === true;
1411
+ const lockedNode = ((_c = (_b = diagram.getNode(id)) === null || _b === void 0 ? void 0 : _b.state) === null || _c === void 0 ? void 0 : _c.locked) === true;
1188
1412
  items.push(Object.assign(Object.assign({ id }, c), { locked: lockedNode || isGroupMember(id) }));
1189
1413
  }
1190
1414
  engine = engineFrom(items);
@@ -1232,7 +1456,7 @@ export function bindDashboardGrid(api, group, options = {}) {
1232
1456
  enforceBoardHeight();
1233
1457
  api.renderNow();
1234
1458
  if (notify) {
1235
- (_c = options.onGesture) === null || _c === void 0 ? void 0 : _c.call(options, { type: 'cancel', kind: g.kind, nodeId: g.id, changed: false });
1459
+ (_d = options.onGesture) === null || _d === void 0 ? void 0 : _d.call(options, { type: 'cancel', kind: g.kind, nodeId: g.id, changed: false });
1236
1460
  }
1237
1461
  };
1238
1462
  /** Centre a w×h-span tile's top-left under the cursor, in world px. */
@@ -1802,11 +2026,31 @@ export function bindDashboardGrid(api, group, options = {}) {
1802
2026
  const selfPeer = {
1803
2027
  group,
1804
2028
  clearSelection: () => {
2029
+ var _a;
1805
2030
  if (selectedId === undefined)
1806
2031
  return;
1807
2032
  selectedId = undefined;
1808
2033
  syncA11y();
2034
+ syncSlabs();
2035
+ (_a = options.onSelect) === null || _a === void 0 ? void 0 : _a.call(options, undefined);
1809
2036
  },
2037
+ innerRows: () => maxRows !== null && maxRows !== void 0 ? maxRows : rows(),
2038
+ selectMember: (id) => {
2039
+ var _a;
2040
+ if (((_a = group.members) !== null && _a !== void 0 ? _a : new Set()).has(id)) {
2041
+ selectWidget(id);
2042
+ api.render();
2043
+ }
2044
+ },
2045
+ beginSlabResize: (id, edges, ev) => {
2046
+ if (isStatic)
2047
+ return false;
2048
+ beginSlabResize(id, edges, ev);
2049
+ return (slabGesture === null || slabGesture === void 0 ? void 0 : slabGesture.id) === id;
2050
+ },
2051
+ slabMove: (ev) => slabMove(ev),
2052
+ slabUp: () => slabUp(),
2053
+ slabCancel: () => slabCancel(),
1810
2054
  hasItem: (id) => !!engine.getItem(id),
1811
2055
  memberCell: (id) => {
1812
2056
  const it = engine.getItem(id);
@@ -1853,7 +2097,7 @@ export function bindDashboardGrid(api, group, options = {}) {
1853
2097
  var _a, _b;
1854
2098
  if (disposed)
1855
2099
  return false;
1856
- if (gesture)
2100
+ if (gesture || slabGesture || forwardSlab)
1857
2101
  return true; // own the rest of an in-flight gesture
1858
2102
  if (!ownsPress(api.container, diagram, ev, hit))
1859
2103
  return false;
@@ -1882,7 +2126,7 @@ export function bindDashboardGrid(api, group, options = {}) {
1882
2126
  return insideMemberGroupFrame(ev.world.x, ev.world.y) || worldInsideBoard(ev.world.x, ev.world.y);
1883
2127
  },
1884
2128
  onPointerDown(ev, hit) {
1885
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
2129
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s;
1886
2130
  if (gesture)
1887
2131
  return; // mid-palette
1888
2132
  const target = ((_b = (_a = ev.source) === null || _a === void 0 ? void 0 : _a.target) !== null && _b !== void 0 ? _b : null);
@@ -1893,9 +2137,43 @@ export function bindDashboardGrid(api, group, options = {}) {
1893
2137
  const gripId = (_c = gripHost === null || gripHost === void 0 ? void 0 : gripHost.getAttribute('data-node-id')) !== null && _c !== void 0 ? _c : null;
1894
2138
  const onGrip = !!gripId && ((_d = group.members) !== null && _d !== void 0 ? _d : new Set()).has(gripId);
1895
2139
  if (!hit.node && !onGrip) {
2140
+ // A press on a SECTION — its empty band, its corner handle or its
2141
+ // frame edge — selects the section; the handle or an edge resizes it.
2142
+ const slabHandle = (_e = target === null || target === void 0 ? void 0 : target.closest) === null || _e === void 0 ? void 0 : _e.call(target, '.axdb-slab > .axdb-rs');
2143
+ const slabId = (_g = (_f = slabHandle === null || slabHandle === void 0 ? void 0 : slabHandle.parentElement) === null || _f === void 0 ? void 0 : _f.getAttribute('data-slab-id')) !== null && _g !== void 0 ? _g : memberGroupAt(ev.world.x, ev.world.y);
2144
+ const grp = slabId && ((_h = group.members) !== null && _h !== void 0 ? _h : new Set()).has(slabId) ? diagram.getGroup(slabId) : undefined;
2145
+ if (slabId && grp) {
2146
+ selectWidget(slabId);
2147
+ api.render();
2148
+ if (isStatic)
2149
+ return;
2150
+ const edges = slabHandle
2151
+ ? rtl ? { n: false, e: false, s: true, w: true } : { n: false, e: true, s: true, w: false }
2152
+ : slabEdgesNear(grp, ev.world.x, ev.world.y);
2153
+ if (anyEdge(edges))
2154
+ beginSlabResize(slabId, edges, ev);
2155
+ return;
2156
+ }
2157
+ // OUR OWN empty band, and we are a section of a parent board: the
2158
+ // press selects the section there, and its edge or corner handle
2159
+ // starts the section resize, which the parent runs while this tool
2160
+ // forwards the pointer sequence.
2161
+ const parent = parentPeer();
2162
+ if ((parent === null || parent === void 0 ? void 0 : parent.selectMember) && worldInsideBoard(ev.world.x, ev.world.y)) {
2163
+ const ownHandle = ((_j = slabHandle === null || slabHandle === void 0 ? void 0 : slabHandle.parentElement) === null || _j === void 0 ? void 0 : _j.getAttribute('data-slab-id')) === group.id;
2164
+ parent.selectMember(group.id);
2165
+ if (!isStatic && parent.beginSlabResize) {
2166
+ const edges = ownHandle
2167
+ ? rtl ? { n: false, e: false, s: true, w: true } : { n: false, e: true, s: true, w: false }
2168
+ : slabEdgesNear(group, ev.world.x, ev.world.y);
2169
+ if (anyEdge(edges) && parent.beginSlabResize(group.id, edges, ev))
2170
+ forwardSlab = parent;
2171
+ }
2172
+ return;
2173
+ }
1896
2174
  // The board's own empty area: a void click. Nothing to drag, and the
1897
2175
  // selection clears exactly as a click outside any board would.
1898
- (_f = (_e = diagram).clearSelection) === null || _f === void 0 ? void 0 : _f.call(_e);
2176
+ (_l = (_k = diagram).clearSelection) === null || _l === void 0 ? void 0 : _l.call(_k);
1899
2177
  selectWidget(undefined);
1900
2178
  api.render();
1901
2179
  return;
@@ -1904,17 +2182,17 @@ export function bindDashboardGrid(api, group, options = {}) {
1904
2182
  if (!node)
1905
2183
  return;
1906
2184
  selectWidget(node.id); // a press selects, whether or not it starts a gesture
1907
- if (((_g = node.state) === null || _g === void 0 ? void 0 : _g.locked) === true)
2185
+ if (((_m = node.state) === null || _m === void 0 ? void 0 : _m.locked) === true)
1908
2186
  return; // pinned: refuse; click still focuses
1909
2187
  if (isStatic)
1910
2188
  return; // a static board: claimed and deadened, click still focuses
1911
2189
  // Which edges did the press take? The corner handle names its own (s+e,
1912
2190
  // or s+w on RTL); a bare press within EDGE_GRIP of the tile's border
1913
2191
  // takes that border; anywhere else is a move. A grip press is a move.
1914
- const onHandle = !!((_h = target === null || target === void 0 ? void 0 : target.closest) === null || _h === void 0 ? void 0 : _h.call(target, '.axdb-rs'));
2192
+ const onHandle = !!((_o = target === null || target === void 0 ? void 0 : target.closest) === null || _o === void 0 ? void 0 : _o.call(target, '.axdb-rs'));
1915
2193
  const hostEl = hostOf(node.id);
1916
- const resizable = ((_j = node.getMetadata) === null || _j === void 0 ? void 0 : _j.call(node, 'widgetResizable')) !== false;
1917
- const movable = ((_k = node.getMetadata) === null || _k === void 0 ? void 0 : _k.call(node, 'widgetMovable')) !== false;
2194
+ const resizable = ((_p = node.getMetadata) === null || _p === void 0 ? void 0 : _p.call(node, 'widgetResizable')) !== false;
2195
+ const movable = ((_q = node.getMetadata) === null || _q === void 0 ? void 0 : _q.call(node, 'widgetMovable')) !== false;
1918
2196
  // `ev.screen` is ELEMENT-LOCAL px; host rects are in client px. Compare
1919
2197
  // like with like — the source event's clientX/Y when there is one, else
1920
2198
  // the container's origin plus the local offset.
@@ -1962,7 +2240,7 @@ export function bindDashboardGrid(api, group, options = {}) {
1962
2240
  startSize: { width: node.size.width, height: node.size.height },
1963
2241
  startPos: { x: node.position.x, y: node.position.y },
1964
2242
  edges,
1965
- spans: { w: (_l = it === null || it === void 0 ? void 0 : it.w) !== null && _l !== void 0 ? _l : 1, h: (_m = it === null || it === void 0 ? void 0 : it.h) !== null && _m !== void 0 ? _m : 1 },
2243
+ spans: { w: (_r = it === null || it === void 0 ? void 0 : it.w) !== null && _r !== void 0 ? _r : 1, h: (_s = it === null || it === void 0 ? void 0 : it.h) !== null && _s !== void 0 ? _s : 1 },
1966
2244
  removedFromBoard: false,
1967
2245
  leg: null,
1968
2246
  lastWorld: null,
@@ -1973,10 +2251,24 @@ export function bindDashboardGrid(api, group, options = {}) {
1973
2251
  };
1974
2252
  },
1975
2253
  onPointerMove(ev) {
1976
- onToolMove(ev);
2254
+ var _a;
2255
+ if (forwardSlab)
2256
+ (_a = forwardSlab.slabMove) === null || _a === void 0 ? void 0 : _a.call(forwardSlab, ev);
2257
+ else if (slabGesture)
2258
+ slabMove(ev);
2259
+ else
2260
+ onToolMove(ev);
1977
2261
  },
1978
2262
  onPointerUp() {
1979
- onToolUp();
2263
+ var _a;
2264
+ if (forwardSlab) {
2265
+ (_a = forwardSlab.slabUp) === null || _a === void 0 ? void 0 : _a.call(forwardSlab);
2266
+ forwardSlab = null;
2267
+ }
2268
+ else if (slabGesture)
2269
+ slabUp();
2270
+ else
2271
+ onToolUp();
1980
2272
  },
1981
2273
  onCancel() {
1982
2274
  cancelActiveGesture();
@@ -1990,7 +2282,7 @@ export function bindDashboardGrid(api, group, options = {}) {
1990
2282
  */
1991
2283
  let hoverHost = null;
1992
2284
  const onHover = (e) => {
1993
- var _a, _b, _c, _d, _e, _f, _g;
2285
+ var _a, _b, _c, _d, _e, _f, _g, _h;
1994
2286
  if (disposed || gesture)
1995
2287
  return;
1996
2288
  // The event may target the host, its content, or (when a host's content
@@ -2014,13 +2306,23 @@ export function bindDashboardGrid(api, group, options = {}) {
2014
2306
  hoverHost.removeAttribute('data-axdb-edge'); // the affordance follows the pointer off a tile
2015
2307
  }
2016
2308
  hoverHost = host;
2017
- if (!host)
2309
+ if (!host) {
2310
+ // No tile under the pointer: a SECTION's frame edge still says resize.
2311
+ const wpt = ((_d = api.viewport) === null || _d === void 0 ? void 0 : _d.clientToWorld) ? api.viewport.clientToWorld(e.clientX, e.clientY, api.container.getBoundingClientRect()) : null;
2312
+ const sid = wpt ? memberGroupAt(wpt.x, wpt.y) : null;
2313
+ const grp = sid ? diagram.getGroup(sid) : undefined;
2314
+ const c = grp && wpt && !isStatic ? cursorFor(slabEdgesNear(grp, wpt.x, wpt.y)) : '';
2315
+ if (!slabGesture)
2316
+ api.container.style.cursor = c;
2018
2317
  return;
2019
- const id = (_d = host.getAttribute('data-node-id')) !== null && _d !== void 0 ? _d : '';
2020
- if (!((_e = group.members) !== null && _e !== void 0 ? _e : new Set()).has(id))
2318
+ }
2319
+ if (!slabGesture && api.container.style.cursor)
2320
+ api.container.style.cursor = '';
2321
+ const id = (_e = host.getAttribute('data-node-id')) !== null && _e !== void 0 ? _e : '';
2322
+ if (!((_f = group.members) !== null && _f !== void 0 ? _f : new Set()).has(id))
2021
2323
  return;
2022
2324
  const node = diagram.getNode(id);
2023
- 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;
2325
+ const resizable = !!node && !isStatic && ((_g = node.state) === null || _g === void 0 ? void 0 : _g.locked) !== true && ((_h = node.getMetadata) === null || _h === void 0 ? void 0 : _h.call(node, 'widgetResizable')) !== false;
2024
2326
  const cursor = resizable ? cursorFor(edgesNear(host, e.clientX, e.clientY)) : '';
2025
2327
  // The affordance rides on an ATTRIBUTE, not the inline style: a repaint
2026
2328
  // rewrites the host's style and used to clear the cursor mid-hover, and
@@ -2354,6 +2656,17 @@ export function bindDashboardGrid(api, group, options = {}) {
2354
2656
  writing = false;
2355
2657
  }
2356
2658
  const commands = buildCommitCommands(deltasSince(snap.cells, snap.geoms));
2659
+ if (isGroupMember(id)) {
2660
+ // A SECTION: its cell lives in group metadata and its frame is written
2661
+ // by project(); the node commands above skip groups (D5 of the review).
2662
+ const it = engine.getItem(id);
2663
+ const grp = diagram.getGroup(id);
2664
+ const cb = snap.cells.get(id);
2665
+ const gb = snap.geoms.get(id);
2666
+ if (it && grp && cb && gb && (cb.x !== it.x || cb.y !== it.y || cb.w !== it.w || cb.h !== it.h)) {
2667
+ commands.push(new SetGroupCellCommand(id, cb, { x: it.x, y: it.y, w: it.w, h: it.h }, { x: gb.pos.x, y: gb.pos.y, width: gb.size.width, height: gb.size.height }, frameOfGroup(grp)));
2668
+ }
2669
+ }
2357
2670
  engine.endGesture();
2358
2671
  disarmGlideSoon();
2359
2672
  enforceBoardHeight();
@@ -2452,7 +2765,7 @@ export function bindDashboardGrid(api, group, options = {}) {
2452
2765
  var _a;
2453
2766
  if (disposed)
2454
2767
  return false;
2455
- if (id !== undefined && (!((_a = group.members) !== null && _a !== void 0 ? _a : new Set()).has(id) || !diagram.getNode(id)))
2768
+ if (id !== undefined && (!((_a = group.members) !== null && _a !== void 0 ? _a : new Set()).has(id) || !memberEntity(id)))
2456
2769
  return false;
2457
2770
  selectWidget(id);
2458
2771
  return true;
@@ -2581,7 +2894,8 @@ export function bindDashboardGrid(api, group, options = {}) {
2581
2894
  return programmatic('Move widget', id, () => engine.moveCheck(id, x, y).changed);
2582
2895
  },
2583
2896
  resizeTo(id, w, h) {
2584
- return programmatic('Resize widget', id, () => engine.resizeCheck(id, w, h).changed);
2897
+ const hh = isGroupMember(id) ? Math.max(h, innerRowsOf(id)) : h; // a section: never below its children
2898
+ return programmatic('Resize widget', id, () => engine.resizeCheck(id, w, hh).changed);
2585
2899
  },
2586
2900
  beginPaletteDrag,
2587
2901
  dispose() {
@@ -2608,6 +2922,9 @@ export function bindDashboardGrid(api, group, options = {}) {
2608
2922
  placeholder = null;
2609
2923
  if (glideTimer)
2610
2924
  clearTimeout(glideTimer);
2925
+ for (const el of slabEls.values())
2926
+ el.remove();
2927
+ slabEls.clear();
2611
2928
  // A rebind inside the 60 ms window (a layout switch right after an undo)
2612
2929
  // disposed this binder with the timer pending — the host kept its
2613
2930
  // lifted ghost for good (visual gate, nested-containers ⑤).
@@ -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' | 'squeeze' | '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' | 'onSelect'> {
31
31
  /** An authored tree. Default: the persisted one, else derived from the members' cells. */
32
32
  tree?: SplitNode | null;
33
33
  }
@@ -372,12 +372,14 @@ export function bindDashboardSplit(api, group, options = {}) {
372
372
  /** The selected widget (see grid-binder): stamped `axdb-selected`, shows the grip. */
373
373
  let selectedId;
374
374
  const selectWidget = (id) => {
375
+ var _a;
375
376
  if (id !== undefined)
376
377
  clearOtherSelections(api.container, selfPeerRef);
377
378
  if (id === selectedId)
378
379
  return;
379
380
  selectedId = id;
380
381
  syncA11y();
382
+ (_a = options.onSelect) === null || _a === void 0 ? void 0 : _a.call(options, id);
381
383
  };
382
384
  let selfPeerRef = null;
383
385
  // Static boards let content be clicked — see grid-binder's staticGuard.
@@ -1193,10 +1195,12 @@ export function bindDashboardSplit(api, group, options = {}) {
1193
1195
  const selfPeer = {
1194
1196
  group,
1195
1197
  clearSelection: () => {
1198
+ var _a;
1196
1199
  if (selectedId === undefined)
1197
1200
  return;
1198
1201
  selectedId = undefined;
1199
1202
  syncA11y();
1203
+ (_a = options.onSelect) === null || _a === void 0 ? void 0 : _a.call(options, undefined);
1200
1204
  },
1201
1205
  hasItem: (id) => { var _a; return ((_a = group.members) !== null && _a !== void 0 ? _a : new Set()).has(id); },
1202
1206
  memberCell: (id) => handle.cellOf(id),
@@ -68,7 +68,7 @@ const CSS = `
68
68
  }
69
69
 
70
70
  /* ===== corner resize handle (hover-revealed, radius-matched) ===== */
71
- .grafloria-node-host > .axdb-rs {
71
+ :is(.grafloria-node-host, .axdb-slab) > .axdb-rs {
72
72
  position: absolute;
73
73
  right: 0;
74
74
  bottom: 0;
@@ -88,7 +88,7 @@ const CSS = `
88
88
  @media (hover: none) { .grafloria-node-host > .axdb-rs { opacity: .8; } }
89
89
  .grafloria-node-host > .axdb-rs:hover { border-color: #3b52d9; }
90
90
  /* RTL boards grow leftwards, so the grab corner mirrors with them. */
91
- .grafloria-node-host > .axdb-rs.axdb-rs--rtl {
91
+ :is(.grafloria-node-host, .axdb-slab) > .axdb-rs.axdb-rs--rtl {
92
92
  right: auto;
93
93
  left: 0;
94
94
  cursor: nesw-resize;
@@ -355,6 +355,14 @@ const CSS = `
355
355
  .axdb-tier-2 .axdb-xt, .axdb-tier-2 .axdb-vt,
356
356
  .axdb-lg--off { display: none; }
357
357
 
358
+ /* SECTION CHROME: a pointer-transparent overlay on every member group. It
359
+ wears the selection ring and, while selected, the corner handle. */
360
+ .grafloria-html-layer > .axdb-slab { position: absolute; pointer-events: none; border-radius: var(--axdb-rs-radius, 3px); z-index: 4; }
361
+ .grafloria-html-layer > .axdb-slab.axdb-slab--selected { box-shadow: 0 0 0 1.5px var(--axdb-accent-ring, rgba(59, 82, 217, .55)); }
362
+ .grafloria-html-layer > .axdb-slab > .axdb-rs { pointer-events: auto; opacity: 0; }
363
+ .grafloria-html-layer > .axdb-slab.axdb-slab--selected > .axdb-rs { opacity: 1; }
364
+ .grafloria-html-layer > .axdb-slab.axdb-slab--static > .axdb-rs { display: none; }
365
+
358
366
  /* legend chips, shared by line and donut */
359
367
  .axdb-lg { display: flex; flex-wrap: wrap; gap: 4px 12px; margin-top: 9px; }
360
368
  .axdb-lg--col { flex-direction: column; flex-wrap: nowrap; gap: 6px; margin-top: 0; }