@grafloria/element 0.4.12 → 0.4.13

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.12",
3
+ "version": "0.4.13",
4
4
  "type": "module",
5
5
  "main": "./src/index.js",
6
6
  "types": "./src/index.d.ts",
@@ -282,6 +282,14 @@ export interface DashboardHandle {
282
282
  * and a quiet ring; a void click clears. False when the id is unknown.
283
283
  */
284
284
  focusWidget(id: string): boolean;
285
+ /**
286
+ * SELECT a widget WITHOUT moving keyboard focus — the ring and the grip,
287
+ * nothing else; what a mouse press does. `undefined` clears the selection
288
+ * on every view. False when the id is unknown.
289
+ */
290
+ selectWidget(id: string | undefined): boolean;
291
+ /** The selected widget, if any (across views: only the on-camera one can be). */
292
+ getSelectedWidget(): string | undefined;
285
293
  /** Every widget handle of a view (default: the active one). */
286
294
  widgetsOf(viewId?: string): WidgetHandle[];
287
295
  /**
@@ -520,6 +520,24 @@ export function createDashboardHandle(ctx) {
520
520
  queueMicrotask(() => retry(4));
521
521
  return true;
522
522
  },
523
+ selectWidget(id) {
524
+ var _a;
525
+ if (id === undefined) {
526
+ for (const b of binders.values())
527
+ b.selectWidget(undefined);
528
+ return true;
529
+ }
530
+ const b = binders.get((_a = viewOfWidget.get(id)) !== null && _a !== void 0 ? _a : '');
531
+ return !!b && b.selectWidget(id);
532
+ },
533
+ getSelectedWidget() {
534
+ for (const b of binders.values()) {
535
+ const s = b.getSelectedWidget();
536
+ if (s)
537
+ return s;
538
+ }
539
+ return undefined;
540
+ },
523
541
  widgetsOf(viewId) {
524
542
  var _a;
525
543
  const v = views.find((x) => x.id === (viewId !== null && viewId !== void 0 ? viewId : ctx.active));
@@ -1043,7 +1061,7 @@ export function dashboard(options) {
1043
1061
  // its tree from exactly those, so any stale tree is cleared first;
1044
1062
  // split → grid rebuilds from them, so the column cache goes too.
1045
1063
  ctx.rebindView = (viewId, next) => {
1046
- var _a, _b;
1064
+ var _a, _b, _c;
1047
1065
  const v = views.find((x) => x.id === viewId);
1048
1066
  const g = groups.get(viewId);
1049
1067
  const b = binders.get(viewId);
@@ -1052,6 +1070,7 @@ export function dashboard(options) {
1052
1070
  const cells = b.saveLayout().cells;
1053
1071
  const live = { rtl: b.getRtl(), static: b.getStatic(), dragHandle: b.getDragHandle() };
1054
1072
  const focused = b.getFocusedWidget();
1073
+ const selected = b.getSelectedWidget();
1055
1074
  b.dispose();
1056
1075
  const write = (fn) => (model.runSystemWrite ? model.runSystemWrite(fn) : fn());
1057
1076
  write(() => {
@@ -1072,9 +1091,13 @@ export function dashboard(options) {
1072
1091
  binders.set(viewId, bindView(v, g, next, live));
1073
1092
  (_a = binders.get(viewId)) === null || _a === void 0 ? void 0 : _a.sync();
1074
1093
  // 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.
1094
+ // the DevExpress designer — the host used to have to restate it. A
1095
+ // MOUSE selection is not a focus (the press never focuses the host),
1096
+ // so it is carried on its own: the kit lab's L21 lost it (2026-09-08).
1076
1097
  if (focused)
1077
1098
  (_b = binders.get(viewId)) === null || _b === void 0 ? void 0 : _b.focusWidget(focused);
1099
+ else if (selected)
1100
+ (_c = binders.get(viewId)) === null || _c === void 0 ? void 0 : _c.selectWidget(selected);
1078
1101
  };
1079
1102
  handle.showView(ctx.active);
1080
1103
  ctx.rebindContainer = (id) => {
@@ -50,6 +50,7 @@
50
50
  * second `bindDashboardGrid` on the section for a nested pack grid.
51
51
  */
52
52
  import { Command, type DiagramModel, type GridColumnLayout, type GroupModel, type NodeModel } from '@grafloria/engine';
53
+ import { type ToolPointerEvent } from '@grafloria/renderer';
53
54
  import { type CellRect, type WorldRect } from './grid-mapping.js';
54
55
  /** The slice of a DiagramInstance the binder needs (structural, test-friendly). */
55
56
  export interface DashboardGridApi {
@@ -261,6 +262,23 @@ export declare const gripOf: (v: DragHandleOption) => DragGripOptions | null;
261
262
  */
262
263
  export declare function syncGrip(host: HTMLElement, cfg: DragGripOptions | null, movable: boolean): void;
263
264
  /** The member host a press on a painted grip belongs to (the grip may sit OUTSIDE the host's box). */
265
+ /**
266
+ * Is this press OURS? The renderer's tool registry is PAGE-GLOBAL: every
267
+ * registered tool is asked about every press on every canvas, ties going to
268
+ * the first registered. Two boards on one page sharing widget ids — a designer
269
+ * beside its preview, a page of examples — had the FIRST board's tool claim
270
+ * the second's presses: it selected its own tile and moved nothing (kit lab,
271
+ * 2026-09-08: 16 of 23 boards dead to the mouse). A tool claims only a press
272
+ * whose DOM target sits in its own container and whose hit node is its own
273
+ * diagram's object, not a namesake from another model.
274
+ */
275
+ export declare function ownsPress(container: HTMLElement, diagram: {
276
+ getNode(id: string): unknown;
277
+ }, ev: ToolPointerEvent, hit: {
278
+ node?: {
279
+ id: string;
280
+ };
281
+ }): boolean;
264
282
  export declare function gripHostOf(target: Element | null): HTMLElement | null;
265
283
  /**
266
284
  * Did a press land on the drag handle? A press INSIDE the handle element
@@ -313,6 +331,14 @@ export interface DashboardGridHandle {
313
331
  focusWidget(id: string): boolean;
314
332
  /** The member the roving tabindex currently rests on. */
315
333
  getFocusedWidget(): string | undefined;
334
+ /**
335
+ * SELECT a member without moving keyboard focus — what a mouse press does
336
+ * (the renderer cancels the press's default, so a click never focuses the
337
+ * host). `undefined` clears. False for a non-member.
338
+ */
339
+ selectWidget(id: string | undefined): boolean;
340
+ /** The selected member: the one wearing the ring and, in grip mode, the grip. */
341
+ getSelectedWidget(): string | undefined;
316
342
  /**
317
343
  * The layout to PERSIST — from the engine's LARGEST cached column count, so
318
344
  * saving while the board is narrow still saves the wide layout the user
@@ -92,6 +92,25 @@ export function syncGrip(host, cfg, movable) {
92
92
  host.classList.add(`axdb-gp-${(_d = cfg.placement) !== null && _d !== void 0 ? _d : 'inside'}`, `axdb-gp-${(_e = cfg.position) !== null && _e !== void 0 ? _e : 'left'}`);
93
93
  }
94
94
  /** The member host a press on a painted grip belongs to (the grip may sit OUTSIDE the host's box). */
95
+ /**
96
+ * Is this press OURS? The renderer's tool registry is PAGE-GLOBAL: every
97
+ * registered tool is asked about every press on every canvas, ties going to
98
+ * the first registered. Two boards on one page sharing widget ids — a designer
99
+ * beside its preview, a page of examples — had the FIRST board's tool claim
100
+ * the second's presses: it selected its own tile and moved nothing (kit lab,
101
+ * 2026-09-08: 16 of 23 boards dead to the mouse). A tool claims only a press
102
+ * whose DOM target sits in its own container and whose hit node is its own
103
+ * diagram's object, not a namesake from another model.
104
+ */
105
+ export function ownsPress(container, diagram, ev, hit) {
106
+ var _a;
107
+ const t = (_a = ev.source) === null || _a === void 0 ? void 0 : _a.target;
108
+ if (typeof Node !== 'undefined' && t instanceof Node && !container.contains(t))
109
+ return false;
110
+ if (hit.node && diagram.getNode(hit.node.id) !== hit.node)
111
+ return false;
112
+ return true;
113
+ }
95
114
  export function gripHostOf(target) {
96
115
  var _a, _b;
97
116
  const grip = (_a = target === null || target === void 0 ? void 0 : target.closest) === null || _a === void 0 ? void 0 : _a.call(target, '.' + GRIP_CLASS);
@@ -1673,6 +1692,8 @@ export function bindDashboardGrid(api, group, options = {}) {
1673
1692
  return false;
1674
1693
  if (gesture)
1675
1694
  return true; // own the rest of an in-flight gesture
1695
+ if (!ownsPress(api.container, diagram, ev, hit))
1696
+ return false;
1676
1697
  if (hit.node) {
1677
1698
  if (((_a = group.members) !== null && _a !== void 0 ? _a : new Set()).has(hit.node.id))
1678
1699
  return true;
@@ -1825,8 +1846,10 @@ export function bindDashboardGrid(api, group, options = {}) {
1825
1846
  }
1826
1847
  }
1827
1848
  }
1828
- if (hoverHost && hoverHost !== host)
1849
+ if (hoverHost && hoverHost !== host) {
1829
1850
  hoverHost.style.cursor = '';
1851
+ hoverHost.removeAttribute('data-axdb-edge'); // the affordance follows the pointer off a tile
1852
+ }
1830
1853
  hoverHost = host;
1831
1854
  if (!host)
1832
1855
  return;
@@ -2262,6 +2285,16 @@ export function bindDashboardGrid(api, group, options = {}) {
2262
2285
  return true;
2263
2286
  },
2264
2287
  getFocusedWidget: () => focusedId,
2288
+ selectWidget(id) {
2289
+ var _a;
2290
+ if (disposed)
2291
+ return false;
2292
+ if (id !== undefined && (!((_a = group.members) !== null && _a !== void 0 ? _a : new Set()).has(id) || !diagram.getNode(id)))
2293
+ return false;
2294
+ selectWidget(id);
2295
+ return true;
2296
+ },
2297
+ getSelectedWidget: () => selectedId,
2265
2298
  setStatic(on) {
2266
2299
  if (on === isStatic)
2267
2300
  return;
@@ -25,7 +25,7 @@
25
25
  import { __awaiter } from "tslib";
26
26
  import { Command } from '@grafloria/engine';
27
27
  import { LiveRegionController, registerTool } from '@grafloria/renderer';
28
- import { dragHandleSelector, gripHostOf, gripOf, normalizeDragHandle, pressOnDragHandle, syncGrip, DRAG_HANDLE_CLASS } from './grid-binder.js';
28
+ import { dragHandleSelector, gripHostOf, gripOf, normalizeDragHandle, ownsPress, pressOnDragHandle, syncGrip, DRAG_HANDLE_CLASS } from './grid-binder.js';
29
29
  import { cellFromGridItem } from './grid-mapping.js';
30
30
  import { addSplitLeaf, cellsFromSplit, cloneSplit, dividersOf, groupRectsOf, insertSplitLeaf, moveSplitDivider, normalizeSplit, pathToLeaf, projectSplit, removeSplitLeaf, splitFromCells, splitLeaves, } from './split-layout.js';
31
31
  import { ensureDashboardKitStyles } from './styles.js';
@@ -631,6 +631,8 @@ export function bindDashboardSplit(api, group, options = {}) {
631
631
  return false;
632
632
  if (gesture)
633
633
  return true;
634
+ if (!ownsPress(api.container, diagram, ev, hit))
635
+ return false;
634
636
  if (hit.node)
635
637
  return ((_a = group.members) !== null && _a !== void 0 ? _a : new Set()).has(hit.node.id);
636
638
  return worldInsideBoard(ev.world.x, ev.world.y);
@@ -1025,6 +1027,16 @@ export function bindDashboardSplit(api, group, options = {}) {
1025
1027
  return true;
1026
1028
  },
1027
1029
  getFocusedWidget: () => focusedId,
1030
+ selectWidget(id) {
1031
+ var _a;
1032
+ if (disposed)
1033
+ return false;
1034
+ if (id !== undefined && (!((_a = group.members) !== null && _a !== void 0 ? _a : new Set()).has(id) || !diagram.getNode(id)))
1035
+ return false;
1036
+ selectWidget(id);
1037
+ return true;
1038
+ },
1039
+ getSelectedWidget: () => selectedId,
1028
1040
  saveLayout() {
1029
1041
  return { columns, cells: cellsFromSplit(readTree(), columns, rowsGuess()) };
1030
1042
  },
@@ -244,6 +244,11 @@ const CSS = `
244
244
  .grafloria-node-host.axdb-gp-inside.axdb-gp-right .axdb-widget > .axdb-widget-h { padding-right: 32px; }
245
245
  .grafloria-node-host.axdb-gp-inside.axdb-gp-center .axdb-widget > .axdb-widget-h { padding-top: 12px; }
246
246
  .axdb-widget-b { flex: 1; min-height: 0; position: relative; }
247
+ /* A drag across a STATIC board (nothing prevents the press's default there)
248
+ used to select every label on it; kit cards are not prose. Tables stay
249
+ copyable — a figure in a grid is the one thing a viewer selects. */
250
+ .axdb-widget { user-select: none; -webkit-user-select: none; }
251
+ .axdb-widget .axdb-table { user-select: text; -webkit-user-select: text; }
247
252
  .axdb-widget-b > svg { display: block; width: 100%; height: 100%; }
248
253
  .axdb-widget-b.axdb-scroll { overflow: auto; }
249
254
  /* A chart WITH a legend under it: the plot yields height, the legend keeps its
@@ -332,6 +337,9 @@ const CSS = `
332
337
  tile gets a bigger ring, not dead card), square, capped so its centre figure
333
338
  stays a figure and not a headline. */
334
339
  .axdb-widget-b.axdb-donut { display: flex; align-items: center; gap: 10px; }
340
+ /* A legend taller than a SHORT body (a 2-row donut on a squeezed board) is
341
+ clipped at its own foot, not centred over the title above it. */
342
+ .axdb-widget-b.axdb-donut > .axdb-lg--col { max-height: 100%; min-height: 0; overflow: hidden; }
335
343
  .axdb-widget-b.axdb-donut > svg {
336
344
  flex: 0 0 auto; width: auto; height: 100%; max-height: 260px; max-width: 60%; aspect-ratio: 1 / 1;
337
345
  }