@grafloria/element 0.4.7 → 0.4.9

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.7",
3
+ "version": "0.4.9",
4
4
  "type": "module",
5
5
  "main": "./src/index.js",
6
6
  "types": "./src/index.d.ts",
package/src/index.d.ts CHANGED
@@ -80,7 +80,7 @@ export type { ErColumn, ErEntitySpec, ErRelationshipSpec, ErCardinality, ErSide,
80
80
  export { erTable, umlClass, erTables, umlClasses, CardHandle, ErTable, ErField, ErColumnList, UmlClass, UmlMemberList, } from './lib/diagram-kit/index.js';
81
81
  export type { HandleApi } from './lib/diagram-kit/index.js';
82
82
  export { bindDashboardGrid, rowHeightFor, boardHeightFor, columnUnitFor, cellToRect, pointToCell, sizeToSpan, gridItemFromCell, cellFromGridItem, buildCommitCommands, ensureDashboardKitStyles, DASHBOARD_KIT_STYLE_ID, dashboard, defaultWidgetRenderer, renderKpiWidget, renderLineWidget, renderBarWidget, renderDonutWidget, renderFunnelWidget, renderTableWidget, BUILT_IN_WIDGET_KINDS, } from './lib/dashboard-kit/index.js';
83
- export type { DashboardGridApi, DashboardGridOptions, DashboardGridHandle, CellRect, WorldRect, DashboardGridGeometry, TileDelta, DashboardOptions, DashboardSpec, DashboardSnapshot, DashboardHandle, DashboardViewSpec, DashboardWidgetSpec, WidgetHandle, WidgetRenderer, KpiWidgetData, LineWidgetData, LineSeries, BarWidgetData, DonutWidgetData, FunnelWidgetData, TableWidgetData, } from './lib/dashboard-kit/index.js';
83
+ export type { DashboardGridApi, DashboardGridOptions, DashboardGridHandle, CellRect, WorldRect, DashboardGridGeometry, TileDelta, DashboardOptions, DashboardSpec, DashboardSnapshot, DragHandleOption, DragGripOptions, DashboardHandle, DashboardViewSpec, DashboardWidgetSpec, WidgetHandle, WidgetRenderer, KpiWidgetData, LineWidgetData, LineSeries, BarWidgetData, DonutWidgetData, FunnelWidgetData, TableWidgetData, } from './lib/dashboard-kit/index.js';
84
84
  export { bindStencilPalette, bindShapeDataPanel, ensureStencilKitStyles } from './lib/stencil-kit/index.js';
85
85
  export type { StencilPaletteApi, StencilPaletteOptions, StencilPaletteHandle, ShapeDataPanelApi, ShapeDataPanelOptions, ShapeDataPanelHandle, } from './lib/stencil-kit/index.js';
86
86
  export * from '@grafloria/engine';
@@ -268,6 +268,12 @@ export interface DashboardHandle {
268
268
  readonly activeView: string;
269
269
  /** A widget handle by id (undefined when unknown). */
270
270
  widget(id: string): WidgetHandle | undefined;
271
+ /**
272
+ * SELECT a widget and move keyboard focus to it — what a press on the widget
273
+ * does. The selected widget shows its painted grip (`dragHandle: { grip }`)
274
+ * and a quiet ring; a void click clears. False when the id is unknown.
275
+ */
276
+ focusWidget(id: string): boolean;
271
277
  /** Every widget handle of a view (default: the active one). */
272
278
  widgetsOf(viewId?: string): WidgetHandle[];
273
279
  /**
@@ -504,6 +504,11 @@ export function createDashboardHandle(ctx) {
504
504
  widget(id) {
505
505
  return makeWidgetHandle(id);
506
506
  },
507
+ focusWidget(id) {
508
+ var _a, _b;
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;
511
+ },
507
512
  widgetsOf(viewId) {
508
513
  var _a;
509
514
  const v = views.find((x) => x.id === (viewId !== null && viewId !== void 0 ? viewId : ctx.active));
@@ -609,6 +609,20 @@ export function bindDashboardGrid(api, group, options = {}) {
609
609
  * and the ROVING TABINDEX — exactly one member per board is a tab stop.
610
610
  * Runs with the handles, so a repainted host gets it back too.
611
611
  */
612
+ /**
613
+ * THE SELECTED WIDGET — the one a press or keyboard focus last landed on.
614
+ * Stamped on its host as `axdb-selected`; that is what shows the painted
615
+ * grip (the DevExpress designer shows an item's bar on the selected item
616
+ * only). A press on a member selects it even when it starts no gesture; a
617
+ * void click clears it. Distinct from the roving tab stop, which must stay.
618
+ */
619
+ let selectedId;
620
+ const selectWidget = (id) => {
621
+ if (id === selectedId)
622
+ return;
623
+ selectedId = id;
624
+ syncA11y();
625
+ };
612
626
  const syncA11y = (only) => {
613
627
  var _a, _b;
614
628
  if (disposed)
@@ -616,6 +630,8 @@ export function bindDashboardGrid(api, group, options = {}) {
616
630
  const members = [...((_a = group.members) !== null && _a !== void 0 ? _a : [])].filter((id) => !!diagram.getNode(id));
617
631
  if (focusedId && !members.includes(focusedId))
618
632
  focusedId = undefined;
633
+ if (selectedId && !members.includes(selectedId))
634
+ selectedId = undefined;
619
635
  const stop = focusedId !== null && focusedId !== void 0 ? focusedId : members[0];
620
636
  for (const id of members) {
621
637
  if (only && !only.has(id))
@@ -634,6 +650,7 @@ export function bindDashboardGrid(api, group, options = {}) {
634
650
  host.setAttribute('aria-roledescription', 'dashboard widget');
635
651
  host.setAttribute('aria-label', bits.join(', '));
636
652
  host.setAttribute('tabindex', id === stop ? '0' : '-1');
653
+ host.classList.toggle('axdb-selected', id === selectedId);
637
654
  }
638
655
  };
639
656
  /**
@@ -1688,11 +1705,15 @@ export function bindDashboardGrid(api, group, options = {}) {
1688
1705
  // The board's own empty area: a void click. Nothing to drag, and the
1689
1706
  // selection clears exactly as a click outside any board would.
1690
1707
  (_f = (_e = diagram).clearSelection) === null || _f === void 0 ? void 0 : _f.call(_e);
1708
+ selectWidget(undefined);
1691
1709
  api.render();
1692
1710
  return;
1693
1711
  }
1694
1712
  const node = diagram.getNode(onGrip ? gripId : hit.node.id);
1695
- if (!node || ((_g = node.state) === null || _g === void 0 ? void 0 : _g.locked) === true)
1713
+ if (!node)
1714
+ return;
1715
+ selectWidget(node.id); // a press selects, whether or not it starts a gesture
1716
+ if (((_g = node.state) === null || _g === void 0 ? void 0 : _g.locked) === true)
1696
1717
  return; // pinned: refuse; click still focuses
1697
1718
  if (isStatic)
1698
1719
  return; // a static board: claimed and deadened, click still focuses
@@ -1833,8 +1854,9 @@ export function bindDashboardGrid(api, group, options = {}) {
1833
1854
  const hit = memberHostAt(e.target);
1834
1855
  if (!hit || disposed)
1835
1856
  return;
1836
- if (focusedId !== hit.id) {
1857
+ if (focusedId !== hit.id || selectedId !== hit.id) {
1837
1858
  focusedId = hit.id;
1859
+ selectedId = hit.id;
1838
1860
  syncA11y();
1839
1861
  }
1840
1862
  };
@@ -2186,6 +2208,7 @@ export function bindDashboardGrid(api, group, options = {}) {
2186
2208
  if (disposed || !((_a = group.members) !== null && _a !== void 0 ? _a : new Set()).has(id) || !diagram.getNode(id))
2187
2209
  return false;
2188
2210
  focusedId = id;
2211
+ selectedId = id;
2189
2212
  syncA11y();
2190
2213
  (_c = (_b = hostOf(id)) === null || _b === void 0 ? void 0 : _b.focus) === null || _c === void 0 ? void 0 : _c.call(_b, { preventScroll: true });
2191
2214
  return true;
@@ -369,11 +369,21 @@ export function bindDashboardSplit(api, group, options = {}) {
369
369
  return '';
370
370
  return `${Math.round((r.width / f.width) * 100)} percent wide, ${Math.round((r.height / f.height) * 100)} percent tall`;
371
371
  };
372
+ /** The selected widget (see grid-binder): stamped `axdb-selected`, shows the grip. */
373
+ let selectedId;
374
+ const selectWidget = (id) => {
375
+ if (id === selectedId)
376
+ return;
377
+ selectedId = id;
378
+ syncA11y();
379
+ };
372
380
  const syncA11y = () => {
373
381
  var _a, _b, _c, _d;
374
382
  if (disposed)
375
383
  return;
376
384
  const order = splitLeaves(paintedTree()).filter((id) => !!diagram.getNode(id));
385
+ if (selectedId && !order.includes(selectedId))
386
+ selectedId = undefined;
377
387
  // No corner handles on a split board: size comes from the dividers. A host
378
388
  // that carried the grid's handle (a board switched live) sheds it here.
379
389
  for (const id of order)
@@ -401,6 +411,7 @@ export function bindDashboardSplit(api, group, options = {}) {
401
411
  host.setAttribute('aria-roledescription', 'dashboard widget');
402
412
  host.setAttribute('aria-label', bits.filter(Boolean).join(', '));
403
413
  host.setAttribute('tabindex', id === stop ? '0' : '-1');
414
+ host.classList.toggle('axdb-selected', id === selectedId);
404
415
  });
405
416
  };
406
417
  // -- commit -----------------------------------------------------------------
@@ -637,11 +648,15 @@ export function bindDashboardSplit(api, group, options = {}) {
637
648
  const onGrip = !!gripId && ((_f = group.members) !== null && _f !== void 0 ? _f : new Set()).has(gripId);
638
649
  if (!hit.node && !onGrip) {
639
650
  (_h = (_g = diagram).clearSelection) === null || _h === void 0 ? void 0 : _h.call(_g);
651
+ selectWidget(undefined);
640
652
  api.render();
641
653
  return;
642
654
  }
643
655
  const node = diagram.getNode(onGrip ? gripId : hit.node.id);
644
- if (!node || ((_j = node.state) === null || _j === void 0 ? void 0 : _j.locked) === true || isStatic)
656
+ if (!node)
657
+ return;
658
+ selectWidget(node.id); // a press selects, whether or not it starts a gesture
659
+ if (((_j = node.state) === null || _j === void 0 ? void 0 : _j.locked) === true || isStatic)
645
660
  return;
646
661
  if (((_k = node.getMetadata) === null || _k === void 0 ? void 0 : _k.call(node, 'widgetMovable')) === false)
647
662
  return;
@@ -779,8 +794,9 @@ export function bindDashboardSplit(api, group, options = {}) {
779
794
  const hit = memberHostAt(e.target);
780
795
  if (!hit || disposed)
781
796
  return;
782
- if (focusedId !== hit.id) {
797
+ if (focusedId !== hit.id || selectedId !== hit.id) {
783
798
  focusedId = hit.id;
799
+ selectedId = hit.id;
784
800
  syncA11y();
785
801
  }
786
802
  };
@@ -978,6 +994,7 @@ export function bindDashboardSplit(api, group, options = {}) {
978
994
  if (!((_a = group.members) !== null && _a !== void 0 ? _a : new Set()).has(id) || !diagram.getNode(id))
979
995
  return false;
980
996
  focusedId = id;
997
+ selectedId = id;
981
998
  syncA11y();
982
999
  (_c = (_b = hostOf(id)) === null || _b === void 0 ? void 0 : _b.focus) === null || _c === void 0 ? void 0 : _c.call(_b, { preventScroll: true });
983
1000
  return true;
@@ -192,21 +192,50 @@ const CSS = `
192
192
  .grafloria-node-host > .axdb-grip {
193
193
  position: absolute; z-index: 4; box-sizing: border-box; width: 24px; height: 12px;
194
194
  border: 1px solid var(--axdb-line, #e7eaf1); border-radius: 3px; background: var(--axdb-card, #fff);
195
- color: var(--axdb-muted, #5a6478); cursor: grab; opacity: .8;
195
+ color: var(--axdb-muted, #5a6478); cursor: grab;
196
+ /* Shown on the SELECTED widget only (the DevExpress designer shows an item's
197
+ bar on the selected item): a press or keyboard focus selects, a void
198
+ click clears. Hidden, it takes no pointer either. */
199
+ opacity: 0; pointer-events: none; transition: opacity .12s ease;
196
200
  }
201
+ .grafloria-node-host.axdb-selected > .axdb-grip, .grafloria-node-host:focus-within > .axdb-grip { opacity: 1; pointer-events: auto; }
197
202
  .grafloria-node-host > .axdb-grip::before {
198
203
  content: ''; position: absolute; left: 5px; top: 2px; width: 12px; height: 6px;
199
204
  background: radial-gradient(circle, currentColor 1px, transparent 1.4px) 0 0 / 4px 3px;
200
205
  }
201
- .grafloria-node-host:hover > .axdb-grip, .grafloria-node-host > .axdb-grip:hover { opacity: 1; border-color: #3b52d9; color: #3b52d9; }
202
- .grafloria-node-host > .axdb-grip--inside { top: 5px; }
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); }
209
+ /* INSIDE: centred on the header's text line and flush with the card padding,
210
+ per size tier (padding 13/15, then 8/14, 4/12, 2/12). */
211
+ .grafloria-node-host > .axdb-grip--inside { top: 14px; }
212
+ .grafloria-node-host > .axdb-grip--inside.axdb-grip--left { left: 15px; }
213
+ .grafloria-node-host > .axdb-grip--inside.axdb-grip--right { right: 15px; }
214
+ .grafloria-node-host > .axdb-grip--inside.axdb-grip--center { top: 3px; }
215
+ @container axdb-tile (max-height: 90px) {
216
+ .grafloria-node-host > .axdb-grip--inside { top: 9px; }
217
+ .grafloria-node-host > .axdb-grip--inside.axdb-grip--left { left: 14px; }
218
+ .grafloria-node-host > .axdb-grip--inside.axdb-grip--right { right: 14px; }
219
+ .grafloria-node-host > .axdb-grip--inside.axdb-grip--center { top: 2px; }
220
+ }
221
+ @container axdb-tile (max-height: 46px) {
222
+ .grafloria-node-host > .axdb-grip--inside { top: 5px; }
223
+ .grafloria-node-host > .axdb-grip--inside.axdb-grip--left { left: 12px; }
224
+ .grafloria-node-host > .axdb-grip--inside.axdb-grip--right { right: 12px; }
225
+ .grafloria-node-host > .axdb-grip--inside.axdb-grip--center { top: 1px; }
226
+ }
227
+ @container axdb-tile (max-height: 26px) {
228
+ .grafloria-node-host > .axdb-grip--inside { top: 2px; }
229
+ }
230
+ /* OUTSIDE: a tab on the card's top edge, its corners in line with the card's. */
203
231
  .grafloria-node-host > .axdb-grip--outside { top: -11px; height: 11px; border-bottom-left-radius: 0; border-bottom-right-radius: 0; border-bottom: 0; box-shadow: 0 -1px 2px rgba(16, 24, 40, .08); }
204
- .grafloria-node-host > .axdb-grip--left { left: 8px; }
205
- .grafloria-node-host > .axdb-grip--right { right: 8px; }
232
+ .grafloria-node-host > .axdb-grip--outside.axdb-grip--left { left: 10px; }
233
+ .grafloria-node-host > .axdb-grip--outside.axdb-grip--right { right: 10px; }
206
234
  .grafloria-node-host > .axdb-grip--center { left: 50%; transform: translateX(-50%); }
207
- .grafloria-node-host.axdb-gp-inside.axdb-gp-left .axdb-widget > .axdb-widget-h { padding-left: 24px; }
208
- .grafloria-node-host.axdb-gp-inside.axdb-gp-right .axdb-widget > .axdb-widget-h { padding-right: 24px; }
209
- .grafloria-node-host.axdb-gp-inside.axdb-gp-center .axdb-widget > .axdb-widget-h { padding-top: 10px; }
235
+ /* The header makes room for an inside grip on its side; a centred one sits above it. */
236
+ .grafloria-node-host.axdb-gp-inside.axdb-gp-left .axdb-widget > .axdb-widget-h { padding-left: 32px; }
237
+ .grafloria-node-host.axdb-gp-inside.axdb-gp-right .axdb-widget > .axdb-widget-h { padding-right: 32px; }
238
+ .grafloria-node-host.axdb-gp-inside.axdb-gp-center .axdb-widget > .axdb-widget-h { padding-top: 12px; }
210
239
  .axdb-widget-b { flex: 1; min-height: 0; position: relative; }
211
240
  .axdb-widget-b > svg { display: block; width: 100%; height: 100%; }
212
241
  .axdb-widget-b.axdb-scroll { overflow: auto; }