@grafloria/element 0.4.28 → 0.4.29

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.28",
3
+ "version": "0.4.29",
4
4
  "type": "module",
5
5
  "main": "./src/index.js",
6
6
  "types": "./src/index.d.ts",
package/src/index.d.ts CHANGED
@@ -79,7 +79,7 @@ export type { JoinColumn, JoinEnd, MatchTier, JoinGuidanceApi, JoinGuidanceOptio
79
79
  export type { ErColumn, ErEntitySpec, ErRelationshipSpec, ErCardinality, ErSide, ErDiagramOptions, UmlClassSpec, UmlRelationshipSpec, UmlRelationKind, UmlSide, UmlDiagramOptions, ErEntityDelta, UmlClassDelta, CardEditingHandle, } from './lib/diagram-kit/index.js';
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
- export { bindDashboardGrid, paintTabStrip, tabStripReserve, tabStripKey, TAB_STRIP_HEIGHT, normalizeCaption, captionReserve, captionBandHeight, paintCaptionBand, CAPTION_HEIGHT, CAPTION_HEIGHT_SUBTITLE, CAPTION_HEIGHT_TIGHT, CAPTION_PASS_THROUGH, 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';
82
+ export { bindDashboardGrid, paintTabStrip, tabStripReserve, tabStripKey, TAB_STRIP_HEIGHT, TAB_DRAG_THRESHOLD, normalizeCaption, captionReserve, captionBandHeight, paintCaptionBand, CAPTION_HEIGHT, CAPTION_HEIGHT_SUBTITLE, CAPTION_HEIGHT_TIGHT, CAPTION_PASS_THROUGH, 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
83
  export type { DashboardGridApi, DashboardGridOptions, DashboardGridHandle, CellRect, WorldRect, DashboardGridGeometry, TileDelta, DashboardOptions, DashboardSpec, DashboardSnapshot, DragHandleOption, DragGripOptions, DashboardHandle, DashboardViewSpec, DashboardWidgetSpec, SectionCaption, SectionCaptionOptions, TabsOptions, TabPage, SectionCaptionAction, SectionCaptionFont, 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';
package/src/index.js CHANGED
@@ -230,7 +230,7 @@ export { erTable, umlClass, erTables, umlClasses, CardHandle, ErTable, ErField,
230
230
  // GridItemConfig. Element-local code, so the Phase-0 wholesale re-export
231
231
  // below does NOT cover it — this curated block is its only door.
232
232
  // ===========================================================================
233
- export { bindDashboardGrid, paintTabStrip, tabStripReserve, tabStripKey, TAB_STRIP_HEIGHT, normalizeCaption, captionReserve, captionBandHeight, paintCaptionBand, CAPTION_HEIGHT, CAPTION_HEIGHT_SUBTITLE, CAPTION_HEIGHT_TIGHT, CAPTION_PASS_THROUGH, rowHeightFor, boardHeightFor, columnUnitFor, cellToRect, pointToCell, sizeToSpan, gridItemFromCell, cellFromGridItem, buildCommitCommands, ensureDashboardKitStyles, DASHBOARD_KIT_STYLE_ID,
233
+ export { bindDashboardGrid, paintTabStrip, tabStripReserve, tabStripKey, TAB_STRIP_HEIGHT, TAB_DRAG_THRESHOLD, normalizeCaption, captionReserve, captionBandHeight, paintCaptionBand, CAPTION_HEIGHT, CAPTION_HEIGHT_SUBTITLE, CAPTION_HEIGHT_TIGHT, CAPTION_PASS_THROUGH, rowHeightFor, boardHeightFor, columnUnitFor, cellToRect, pointToCell, sizeToSpan, gridItemFromCell, cellFromGridItem, buildCommitCommands, ensureDashboardKitStyles, DASHBOARD_KIT_STYLE_ID,
234
234
  // The DATA-FIRST authoring API — the erDiagram()/umlDiagram() equivalent.
235
235
  dashboard,
236
236
  // …and the built-in renderers behind `kind`, so a dashboard is useful with
@@ -44,7 +44,7 @@
44
44
  */
45
45
  import { __awaiter, __rest } from "tslib";
46
46
  import { BatchCommand, BringNodeToFrontCommand, Command, GroupModel, NodeModel, RemoveFromGroupCommand, RemoveGroupCommand, RemoveNodeCommand, SendNodeToBackCommand, } from '@grafloria/engine';
47
- import { bindDashboardGrid, } from './grid-binder.js';
47
+ import { bindDashboardGrid, parentPeerOf, } from './grid-binder.js';
48
48
  import { bindDashboardSplit, SPLIT_TREE_KEY } from './split-binder.js';
49
49
  import { gridItemFromCell } from './grid-mapping.js';
50
50
  import { ensureDashboardKitStyles } from './styles.js';
@@ -342,12 +342,44 @@ function assignCells(widgets, columns) {
342
342
  const cssEscape = (v) => typeof CSS !== 'undefined' && CSS.escape ? CSS.escape(v) : v.replace(/"/g, '\\"');
343
343
  export function attachTabsRuntime(ctx, model, container, handle) {
344
344
  var _a, _b, _c;
345
+ // Pages come from LIVE membership, in the authored order, with anything that
346
+ // arrived later on the end. Reading the authored spec instead meant a page
347
+ // that LEFT the container kept its tab — a tab pointing at nothing, which is
348
+ // exactly the "the content is taken and the tab remains" report — and a page
349
+ // that arrived never got one.
345
350
  const pagesOf = (id) => {
346
- var _a;
347
- const w = ctx.specById.get(id);
348
- return ((_a = w === null || w === void 0 ? void 0 : w.widgets) !== null && _a !== void 0 ? _a : []).filter((c) => !!c.widgets).map((p) => { var _a; return ({ id: p.id, label: (_a = p.title) !== null && _a !== void 0 ? _a : p.id }); });
351
+ var _a, _b, _c, _d;
352
+ const cg = (_a = ctx.boardGroups.get(id)) !== null && _a !== void 0 ? _a : model.getGroup(id);
353
+ const authored = ((_c = (_b = ctx.specById.get(id)) === null || _b === void 0 ? void 0 : _b.widgets) !== null && _c !== void 0 ? _c : []).map((c) => c.id);
354
+ const live = [...((_d = cg === null || cg === void 0 ? void 0 : cg.members) !== null && _d !== void 0 ? _d : [])].filter((m) => !!model.getGroup(m));
355
+ const rank = (pid) => {
356
+ // A plain child is wrapped as `<id>__page`, so the authored order is
357
+ // keyed on the id BEFORE the wrap as well as after it.
358
+ const i = authored.indexOf(pid);
359
+ const j = i < 0 ? authored.indexOf(pid.replace(/__page$/, '')) : i;
360
+ return j < 0 ? Number.MAX_SAFE_INTEGER : j;
361
+ };
362
+ live.sort((a, b) => rank(a) - rank(b));
363
+ return live.map((pid) => { var _a, _b; return ({ id: pid, label: (_b = (_a = ctx.specById.get(pid)) === null || _a === void 0 ? void 0 : _a.title) !== null && _b !== void 0 ? _b : pid }); });
349
364
  };
350
365
  const isTabs = (id) => { var _a, _b; return ((_a = ctx.layoutOf.get(id)) !== null && _a !== void 0 ? _a : (_b = ctx.specById.get(id)) === null || _b === void 0 ? void 0 : _b.layout) === 'tabs' && pagesOf(id).length > 0; };
366
+ /**
367
+ * A tab dragged off its strip tears its page out onto the board that owns
368
+ * the container — VS Code's "drag a tab out and it becomes a group of its
369
+ * own". Only the board below can place it, so the gesture is handed to that
370
+ * binder; a container with a single page keeps it, because a tab container
371
+ * with no pages has nothing to show.
372
+ */
373
+ const tearOut = (containerId, pageId, ev) => {
374
+ var _a, _b, _c;
375
+ if (!container)
376
+ return false;
377
+ const pages = pagesOf(containerId);
378
+ if (pages.length <= 1)
379
+ return false;
380
+ const label = (_b = (_a = pages.find((p) => p.id === pageId)) === null || _a === void 0 ? void 0 : _a.label) !== null && _b !== void 0 ? _b : pageId;
381
+ return ((_c = parentPeerOf(container, containerId)) === null || _c === void 0 ? void 0 : _c.tearOutMember(pageId, containerId, label, ev)) === true;
382
+ };
351
383
  const paintStrip = (id, f, h, pages, active) => {
352
384
  var _a, _b, _c;
353
385
  const layer = container === null || container === void 0 ? void 0 : container.querySelector('.grafloria-html-layer');
@@ -370,7 +402,7 @@ export function attachTabsRuntime(ctx, model, container, handle) {
370
402
  const key = tabStripKey(pages, active, ctx.tabsOf.get(id), rtl);
371
403
  if (el.getAttribute('data-key') === key)
372
404
  return;
373
- paintTabStrip(el, pages, active, ctx.tabsOf.get(id), rtl, (pid) => handle.activateTab(id, pid), () => handle.selectWidget(id));
405
+ paintTabStrip(el, pages, active, ctx.tabsOf.get(id), rtl, (pid) => handle.activateTab(id, pid), () => handle.selectWidget(id), (pid, ev) => tearOut(id, pid, ev));
374
406
  el.setAttribute('data-key', key);
375
407
  };
376
408
  const sync = (id) => {
@@ -379,7 +411,12 @@ export function attachTabsRuntime(ctx, model, container, handle) {
379
411
  if (!cg || !isTabs(id))
380
412
  return;
381
413
  const pages = pagesOf(id);
382
- const active = (_b = ctx.activeTab.get(id)) !== null && _b !== void 0 ? _b : pages[0].id;
414
+ // The page that was showing may have just been dragged out.
415
+ let active = (_b = ctx.activeTab.get(id)) !== null && _b !== void 0 ? _b : pages[0].id;
416
+ if (!pages.some((p) => p.id === active)) {
417
+ active = pages[0].id;
418
+ ctx.activeTab.set(id, active);
419
+ }
383
420
  const strip = tabStripReserve(ctx.tabsOf.get(id), pages.length);
384
421
  const f = { x: cg.position.x, y: cg.position.y, width: (_d = (_c = cg.size) === null || _c === void 0 ? void 0 : _c.width) !== null && _d !== void 0 ? _d : 0, height: (_f = (_e = cg.size) === null || _e === void 0 ? void 0 : _e.height) !== null && _f !== void 0 ? _f : 0 };
385
422
  const inner = { width: f.width, height: Math.max(0, f.height - strip) };
@@ -430,10 +467,14 @@ export function attachTabsRuntime(ctx, model, container, handle) {
430
467
  if ((meta === null || meta === void 0 ? void 0 : meta.tabs) && !ctx.tabsOf.has(id))
431
468
  ctx.tabsOf.set(id, meta.tabs);
432
469
  }
433
- // Re-lay the pages whenever the parent moves or resizes the container.
434
- const off = cg.on('bounds:changed', (() => sync(id)));
435
- if (typeof off === 'function')
436
- ctx.subscriptions.push(off);
470
+ // Re-lay the pages whenever the parent moves or resizes the container
471
+ // and whenever a page joins or leaves, so the strip always shows the pages
472
+ // that are actually there.
473
+ for (const ev of ['bounds:changed', 'member:added', 'member:removed']) {
474
+ const off = cg.on(ev, (() => sync(id)));
475
+ if (typeof off === 'function')
476
+ ctx.subscriptions.push(off);
477
+ }
437
478
  sync(id);
438
479
  }
439
480
  }
@@ -485,13 +485,21 @@ interface BinderPeer {
485
485
  */
486
486
  containsWorldExtended(x: number, y: number): boolean;
487
487
  frameArea(): number;
488
- adopt(node: NodeModel, world: {
488
+ adopt(node: {
489
+ id: string;
490
+ }, world: {
489
491
  x: number;
490
492
  y: number;
491
493
  }, pxSize: {
492
494
  width: number;
493
495
  height: number;
494
496
  }): AdoptedLeg | null;
497
+ /**
498
+ * Tear a tab page out of its container and onto THIS board, driven by the
499
+ * press that started on its tab. Answers false when the board will not take
500
+ * it, so the strip can leave the press alone.
501
+ */
502
+ tearOutMember(pageId: string, fromGroupId: string, label: string, ev: PointerEvent): boolean;
495
503
  }
496
504
  interface AdoptedLeg {
497
505
  groupId: string;
@@ -448,6 +448,8 @@ export function bindDashboardGrid(api, group, options = {}) {
448
448
  let placeholder = null;
449
449
  /** Foreign tile currently adopted from another binder's gesture. */
450
450
  let adoptedGhostId = null;
451
+ /** The tab page this board is currently tearing out of a container, if any. */
452
+ let tearing = null;
451
453
  let glideTimer = null;
452
454
  let ghostTimer = null;
453
455
  /**
@@ -1091,6 +1093,11 @@ export function bindDashboardGrid(api, group, options = {}) {
1091
1093
  * the plan prototype cannot leave its board at all (cells clamp at the
1092
1094
  * edges), so a 60px slip past the frame must not dim or delete. */
1093
1095
  const EDGE_GRACE = 60;
1096
+ /** A point inside a member group's own frame (its strip and its pages). */
1097
+ const worldInsideGroup = (g, x, y) => {
1098
+ const s = sizeOf(g);
1099
+ return x >= g.position.x && x <= g.position.x + s.width && y >= g.position.y && y <= g.position.y + s.height;
1100
+ };
1094
1101
  const worldInsideBoardGrace = (x, y) => {
1095
1102
  if (worldInsideBoardExtended(x, y))
1096
1103
  return true;
@@ -2014,6 +2021,14 @@ export function bindDashboardGrid(api, group, options = {}) {
2014
2021
  const geomBefore = g.startGeom.get(g.id);
2015
2022
  const crossing = [
2016
2023
  ...sourceDisplaced,
2024
+ // …and the TARGET board's own displaced tiles. Dropping onto an
2025
+ // occupied row pushes that row down, and the ghost draws the pushed
2026
+ // layout — but these commands were computed and then dropped on the
2027
+ // floor, so nothing persisted the push. The next rebuild read the
2028
+ // pre-drop cells, found the arriving tile overlapping them, and pushed
2029
+ // IT to the bottom instead: the drop landed a whole row away from the
2030
+ // ghost that promised it ("it goes in as an extra row").
2031
+ ...fin.commands,
2017
2032
  new RemoveFromGroupCommand(group.id, g.id),
2018
2033
  new AddToGroupCommand(targetGroupId, g.id),
2019
2034
  ...buildCommitCommands([
@@ -2125,20 +2140,56 @@ export function bindDashboardGrid(api, group, options = {}) {
2125
2140
  * cannot take the cell under the pointer still lands beside it.
2126
2141
  */
2127
2142
  const placeNear = (id, x, y, w) => {
2128
- if (engine.moveCheck(id, x, y, { gate: false }).changed)
2129
- return true;
2143
+ // `changed:false` means BOTH "refused" and "it is already there", and
2144
+ // reading the second as the first made the search walk straight past the
2145
+ // right answer: parked on the nearest legal cell, the next pointer move
2146
+ // re-tried the wanted cell (refused), re-tried the cell it was ON (no
2147
+ // change, read as refused) and then took a FARTHER one — so the tile
2148
+ // oscillated for the whole drag and settled wherever the last swing left
2149
+ // it. Ask where the tile IS, not whether the call moved it.
2150
+ const at = (cx, cy) => {
2151
+ const i = engine.getItem(id);
2152
+ return !!i && i.x === cx && i.y === cy;
2153
+ };
2130
2154
  const maxX = Math.max(0, columns - w);
2131
- for (let d = 1; d <= columns; d++) {
2132
- for (const cx of [x - d, x + d]) {
2133
- if (cx < 0 || cx > maxX)
2155
+ const scanRow = (cy) => {
2156
+ if (at(x, cy))
2157
+ return true;
2158
+ if (engine.moveCheck(id, x, cy, { gate: false }).changed)
2159
+ return true;
2160
+ for (let d = 1; d <= columns; d++) {
2161
+ for (const cx of [x - d, x + d]) {
2162
+ if (cx < 0 || cx > maxX)
2163
+ continue;
2164
+ if (at(cx, cy))
2165
+ return true;
2166
+ if (engine.moveCheck(id, cx, cy, { gate: false }).changed)
2167
+ return true;
2168
+ }
2169
+ }
2170
+ return false;
2171
+ };
2172
+ if (scanRow(y))
2173
+ return true;
2174
+ // EVERY column on that row refused — which is what a locked section
2175
+ // spanning the full width does, and there are plenty of those. Sliding
2176
+ // sideways can never clear it, so try the rows either side, nearest first.
2177
+ // Without this the drop either fell to the bottom of the board or, when
2178
+ // the board had no room down there either, did nothing at all.
2179
+ const reach = Math.max(1, rows()) + 2;
2180
+ for (let d = 1; d <= reach; d++) {
2181
+ for (const cy of [y - d, y + d]) {
2182
+ if (cy < 0)
2134
2183
  continue;
2135
- if (engine.moveCheck(id, cx, y, { gate: false }).changed)
2184
+ if (scanRow(cy))
2136
2185
  return true;
2137
2186
  }
2138
2187
  }
2139
2188
  return false;
2140
2189
  };
2141
- const adopt = (node, world, pxSize) => {
2190
+ const adopt = (
2191
+ // Only the id is used: a tab page arriving here is a GROUP, not a node.
2192
+ node, world, pxSize) => {
2142
2193
  if (disposed)
2143
2194
  return null;
2144
2195
  const f = frame();
@@ -2220,6 +2271,7 @@ export function bindDashboardGrid(api, group, options = {}) {
2220
2271
  };
2221
2272
  const selfPeer = {
2222
2273
  group,
2274
+ tearOutMember: (pageId, fromGroupId, label, ev) => beginTearOut(pageId, fromGroupId, label, ev),
2223
2275
  clearSelection: () => {
2224
2276
  var _a;
2225
2277
  if (selectedId === undefined)
@@ -2757,6 +2809,124 @@ export function bindDashboardGrid(api, group, options = {}) {
2757
2809
  };
2758
2810
  api.container.addEventListener('focusin', onFocusIn);
2759
2811
  api.container.addEventListener('keydown', onKey);
2812
+ // -- tab tear-out -----------------------------------------------------------
2813
+ /**
2814
+ * A press that TRAVELLED on a tab drags the whole page onto this board, the
2815
+ * way dragging a VS Code tab out of its group makes a group of its own. The
2816
+ * page is a GROUP, so it rides the same `adopt` leg a widget uses — entry at
2817
+ * the bottom, `placeNear` under the pointer, the dashed placeholder drawn
2818
+ * from the live cell — and the commit is one batch: the cell and frame it
2819
+ * landed on, out of the container, into this board, plus every tile this
2820
+ * board had to push aside.
2821
+ *
2822
+ * Like VS Code, the page itself does not follow the pointer; a chip carrying
2823
+ * the tab's label does, and the placeholder shows where the drop will land.
2824
+ */
2825
+ const beginTearOut = (pageId, fromGroupId, label, ev) => {
2826
+ var _a, _b, _c;
2827
+ if (disposed || gesture || slabGesture || isStatic || tearing)
2828
+ return false;
2829
+ const pg = diagram.getGroup(pageId);
2830
+ const from = diagram.getGroup(fromGroupId);
2831
+ if (!pg || !from || !engine.getItem(fromGroupId))
2832
+ return false;
2833
+ const size = sizeOf(pg);
2834
+ const frameBefore = frameOfGroup(pg);
2835
+ const cellBefore = (_b = cellFromGridItem((_a = pg.getMetadata) === null || _a === void 0 ? void 0 : _a.call(pg, 'gridItem'))) !== null && _b !== void 0 ? _b : {
2836
+ x: 0,
2837
+ y: 0,
2838
+ w: 1,
2839
+ h: 1,
2840
+ };
2841
+ const toWorld = (cx, cy) => {
2842
+ var _a;
2843
+ const rect = api.container.getBoundingClientRect();
2844
+ return ((_a = api.viewport) === null || _a === void 0 ? void 0 : _a.clientToWorld) ? api.viewport.clientToWorld(cx, cy, rect) : { x: cx - rect.left, y: cy - rect.top };
2845
+ };
2846
+ const first = toWorld(ev.clientX, ev.clientY);
2847
+ const leg = adopt({ id: pageId }, first, { width: size.width, height: size.height });
2848
+ if (!leg)
2849
+ return false;
2850
+ tearing = pageId;
2851
+ const doc = (_c = api.container.ownerDocument) !== null && _c !== void 0 ? _c : document;
2852
+ const chip = doc.createElement('div');
2853
+ chip.className = 'axdb-drag-chip axdb-tab-chip';
2854
+ chip.textContent = label;
2855
+ doc.body.appendChild(chip);
2856
+ const moveChip = (cx, cy) => {
2857
+ chip.style.left = `${cx + 6}px`;
2858
+ chip.style.top = `${cy + 6}px`;
2859
+ };
2860
+ moveChip(ev.clientX, ev.clientY);
2861
+ armGlide();
2862
+ api.render();
2863
+ let last = { x: ev.clientX, y: ev.clientY };
2864
+ const detach = () => {
2865
+ window.removeEventListener('pointermove', onMove, true);
2866
+ window.removeEventListener('pointerup', onUp, true);
2867
+ window.removeEventListener('pointercancel', onCancel, true);
2868
+ window.removeEventListener('keydown', onKey, true);
2869
+ };
2870
+ const onMove = (e) => {
2871
+ if (disposed)
2872
+ return detach();
2873
+ last = { x: e.clientX, y: e.clientY };
2874
+ moveChip(e.clientX, e.clientY);
2875
+ const world = toWorld(e.clientX, e.clientY);
2876
+ const home = worldInsideGroup(from, world.x, world.y);
2877
+ // Over its own container it goes home: a tab dragged around its own
2878
+ // strip must not tear itself out.
2879
+ chip.classList.toggle('axdb-out', home || !worldInsideBoardGrace(world.x, world.y));
2880
+ if (!home)
2881
+ leg.move(world);
2882
+ api.render();
2883
+ };
2884
+ const finish = (commit) => {
2885
+ var _a, _b;
2886
+ detach();
2887
+ chip.remove();
2888
+ tearing = null;
2889
+ if (disposed)
2890
+ return;
2891
+ const world = toWorld(last.x, last.y);
2892
+ const home = worldInsideGroup(from, world.x, world.y);
2893
+ if (!commit || home || !worldInsideBoardGrace(world.x, world.y)) {
2894
+ leg.abort();
2895
+ disarmGlideSoon();
2896
+ api.renderNow();
2897
+ (_a = options.onGesture) === null || _a === void 0 ? void 0 : _a.call(options, { type: 'cancel', kind: 'move', nodeId: pageId, changed: false });
2898
+ return;
2899
+ }
2900
+ const fin = leg.finalize();
2901
+ if (!fin) {
2902
+ disarmGlideSoon();
2903
+ api.renderNow();
2904
+ return;
2905
+ }
2906
+ const changed = execute('Move tab out', [
2907
+ ...fin.commands,
2908
+ new SetGroupCellCommand(pageId, cellBefore, fin.cell, frameBefore, fin.rect),
2909
+ new RemoveFromGroupCommand(fromGroupId, pageId),
2910
+ new AddToGroupCommand(group.id, pageId),
2911
+ ]);
2912
+ disarmGlideSoon();
2913
+ enforceBoardHeight();
2914
+ persistLayouts();
2915
+ api.renderNow();
2916
+ (_b = options.onGesture) === null || _b === void 0 ? void 0 : _b.call(options, { type: 'commit', kind: 'move', nodeId: pageId, changed });
2917
+ };
2918
+ const onUp = () => finish(true);
2919
+ const onCancel = () => finish(false);
2920
+ const onKey = (e) => {
2921
+ if (e.key === 'Escape')
2922
+ finish(false);
2923
+ };
2924
+ window.addEventListener('pointermove', onMove, true);
2925
+ window.addEventListener('pointerup', onUp, true);
2926
+ window.addEventListener('pointercancel', onCancel, true);
2927
+ window.addEventListener('keydown', onKey, true);
2928
+ return true;
2929
+ };
2760
2930
  // -- palette drag-in --------------------------------------------------------
2761
2931
  const beginPaletteDrag = (node, spec, event) => {
2762
2932
  var _a, _b;
@@ -3175,6 +3345,7 @@ export function bindDashboardGrid(api, group, options = {}) {
3175
3345
  api.container.removeEventListener('keydown', onKey);
3176
3346
  hostObserver.disconnect();
3177
3347
  containerObserver === null || containerObserver === void 0 ? void 0 : containerObserver.disconnect();
3348
+ tearing = null;
3178
3349
  for (const off of subs)
3179
3350
  off();
3180
3351
  // The corner handles are THIS binder's affordance: a board re-bound as a
@@ -3,7 +3,7 @@ export { bindDashboardSplit, SetSplitTreeCommand, SPLIT_TREE_KEY, type Dashboard
3
3
  export { projectSplit, dividersOf, addSplitLeaf, removeSplitLeaf, insertSplitLeaf, moveSplitDivider, splitFromCells, cellsFromSplit, splitLeaves, type SplitNode, type SplitGroup, type SplitLeaf, type SplitDir, type SplitSide, type SplitDivider, } from './split-layout.js';
4
4
  export { rowHeightFor, boardHeightFor, columnUnitFor, cellToRect, pointToCell, sizeToSpan, spanWidthPx, gridItemFromCell, cellFromGridItem, buildCommitCommands, type CellRect, type WorldRect, type DashboardGridGeometry, type TileDelta, } from './grid-mapping.js';
5
5
  export { ensureDashboardKitStyles, DASHBOARD_KIT_STYLE_ID } from './styles.js';
6
- export { paintTabStrip, tabStripReserve, tabStripKey, TAB_STRIP_HEIGHT, type TabsOptions, type TabPage } from './tabs.js';
6
+ export { paintTabStrip, tabStripReserve, tabStripKey, TAB_STRIP_HEIGHT, TAB_DRAG_THRESHOLD, type TabsOptions, type TabPage } from './tabs.js';
7
7
  export { normalizeCaption, captionReserve, captionBandHeight, paintCaptionBand, CAPTION_HEIGHT, CAPTION_HEIGHT_SUBTITLE, CAPTION_HEIGHT_TIGHT, CAPTION_PASS_THROUGH, type SectionCaption, type SectionCaptionOptions, type SectionCaptionAction, type SectionCaptionFont, } from './caption.js';
8
8
  export { dashboard, type DashboardOptions, type DashboardSpec, type DashboardSnapshot, type DashboardHandle, type DashboardViewSpec, type DashboardWidgetSpec, type WidgetHandle, } from './dashboard.js';
9
9
  export { defaultWidgetRenderer, renderKpiWidget, renderLineWidget, renderBarWidget, renderDonutWidget, renderFunnelWidget, renderTableWidget, BUILT_IN_WIDGET_KINDS, type WidgetRenderer, type KpiWidgetData, type LineWidgetData, type LineSeries, type BarWidgetData, type DonutWidgetData, type FunnelWidgetData, type TableWidgetData, } from './widgets.js';
@@ -3,7 +3,7 @@ export { bindDashboardSplit, SetSplitTreeCommand, SPLIT_TREE_KEY, } from './spli
3
3
  export { projectSplit, dividersOf, addSplitLeaf, removeSplitLeaf, insertSplitLeaf, moveSplitDivider, splitFromCells, cellsFromSplit, splitLeaves, } from './split-layout.js';
4
4
  export { rowHeightFor, boardHeightFor, columnUnitFor, cellToRect, pointToCell, sizeToSpan, spanWidthPx, gridItemFromCell, cellFromGridItem, buildCommitCommands, } from './grid-mapping.js';
5
5
  export { ensureDashboardKitStyles, DASHBOARD_KIT_STYLE_ID } from './styles.js';
6
- export { paintTabStrip, tabStripReserve, tabStripKey, TAB_STRIP_HEIGHT } from './tabs.js';
6
+ export { paintTabStrip, tabStripReserve, tabStripKey, TAB_STRIP_HEIGHT, TAB_DRAG_THRESHOLD } from './tabs.js';
7
7
  export { normalizeCaption, captionReserve, captionBandHeight, paintCaptionBand, CAPTION_HEIGHT, CAPTION_HEIGHT_SUBTITLE, CAPTION_HEIGHT_TIGHT, CAPTION_PASS_THROUGH, } from './caption.js';
8
8
  // The DATA-FIRST authoring API (the erDiagram/umlDiagram equivalent).
9
9
  export { dashboard, } from './dashboard.js';
@@ -1251,6 +1251,9 @@ export function bindDashboardSplit(api, group, options = {}) {
1251
1251
  hasItem: (id) => { var _a; return ((_a = group.members) !== null && _a !== void 0 ? _a : new Set()).has(id); },
1252
1252
  memberCell: (id) => handle.cellOf(id),
1253
1253
  resizeMemberBy: () => ({ changed: false }),
1254
+ // A split board has no cells to drop a torn-out page into: it refuses,
1255
+ // and the press stays a plain tab click.
1256
+ tearOutMember: () => false,
1254
1257
  containsWorld: (x, y) => worldInsideBoard(x, y),
1255
1258
  containsWorldExtended: (x, y) => worldInsideBoard(x, y),
1256
1259
  frameArea: () => {
@@ -111,6 +111,27 @@ const CSS = `
111
111
  (the same signal a tile ghost gives outside the board). Without this rule
112
112
  the class was set and nothing showed — a refusal a user could not see. */
113
113
  .axdb-drag-chip.axdb-out { opacity: .35; filter: grayscale(.7) drop-shadow(0 8px 14px rgba(16, 24, 40, .2)); }
114
+ /* A tab dragged off its strip: VS Code carries the TAB, not the editor, so
115
+ what follows the pointer is a chip wearing the tab's own label. */
116
+ .axdb-tab-chip {
117
+ padding: 5px 12px;
118
+ max-width: 220px;
119
+ overflow: hidden;
120
+ text-overflow: ellipsis;
121
+ white-space: nowrap;
122
+ font: 500 12px/1.4 var(--axdb-font, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif);
123
+ color: var(--axdb-tabs-on-fg, #1f2430);
124
+ background: var(--axdb-tabs-on-bg, #fff);
125
+ border-radius: var(--axdb-rs-radius, 3px);
126
+ box-shadow: 0 0 0 1px rgba(31, 36, 48, .12);
127
+ }
128
+ @media (prefers-color-scheme: dark) {
129
+ .axdb-tab-chip {
130
+ color: var(--axdb-tabs-on-fg, #eceef4);
131
+ background: var(--axdb-tabs-on-bg, #1a1d25);
132
+ box-shadow: 0 0 0 1px rgba(236, 238, 244, .16);
133
+ }
134
+ }
114
135
 
115
136
  /* ===========================================================================
116
137
  BUILT-IN WIDGET CARDS — what widgets.ts paints when a page writes no
@@ -25,6 +25,12 @@ export interface TabPage {
25
25
  label: string;
26
26
  }
27
27
  export declare const TAB_STRIP_HEIGHT = 30;
28
+ /**
29
+ * Travel, in px, that turns a press on a tab from a click into a drag. VS Code
30
+ * uses the platform drag threshold; 4 px is the same order and keeps a shaky
31
+ * click a click.
32
+ */
33
+ export declare const TAB_DRAG_THRESHOLD = 4;
28
34
  /** Pixels the pages give up at the top of the container. */
29
35
  export declare function tabStripReserve(o: TabsOptions | undefined, pageCount: number): number;
30
36
  /**
@@ -33,4 +39,11 @@ export declare function tabStripReserve(o: TabsOptions | undefined, pageCount: n
33
39
  * rebuild — and so a `click` listener survives between switches.
34
40
  */
35
41
  export declare function tabStripKey(pages: TabPage[], activeId: string, o: TabsOptions | undefined, rtl: boolean): string;
36
- export declare function paintTabStrip(strip: HTMLElement, pages: TabPage[], activeId: string, o: TabsOptions | undefined, rtl: boolean, onPick: (id: string) => void, onSelectContainer?: () => void): void;
42
+ export declare function paintTabStrip(strip: HTMLElement, pages: TabPage[], activeId: string, o: TabsOptions | undefined, rtl: boolean, onPick: (id: string) => void, onSelectContainer?: () => void,
43
+ /**
44
+ * The tab is the PAGE's drag handle, as it is in VS Code: press one and
45
+ * travel, and the whole page leaves — not the widget under the pointer,
46
+ * which used to be the only way to drag anything out and left the tab
47
+ * behind pointing at an empty page.
48
+ */
49
+ onDrag?: (pageId: string, ev: PointerEvent) => boolean): void;
@@ -9,6 +9,12 @@
9
9
  * binder's.
10
10
  */
11
11
  export const TAB_STRIP_HEIGHT = 30;
12
+ /**
13
+ * Travel, in px, that turns a press on a tab from a click into a drag. VS Code
14
+ * uses the platform drag threshold; 4 px is the same order and keeps a shaky
15
+ * click a click.
16
+ */
17
+ export const TAB_DRAG_THRESHOLD = 4;
12
18
  /** Pixels the pages give up at the top of the container. */
13
19
  export function tabStripReserve(o, pageCount) {
14
20
  var _a;
@@ -24,8 +30,19 @@ export function tabStripReserve(o, pageCount) {
24
30
  export function tabStripKey(pages, activeId, o, rtl) {
25
31
  return JSON.stringify([pages, activeId, o !== null && o !== void 0 ? o : null, rtl]);
26
32
  }
27
- export function paintTabStrip(strip, pages, activeId, o, rtl, onPick, onSelectContainer) {
33
+ export function paintTabStrip(strip, pages, activeId, o, rtl, onPick, onSelectContainer,
34
+ /**
35
+ * The tab is the PAGE's drag handle, as it is in VS Code: press one and
36
+ * travel, and the whole page leaves — not the widget under the pointer,
37
+ * which used to be the only way to drag anything out and left the tab
38
+ * behind pointing at an empty page.
39
+ */
40
+ onDrag) {
28
41
  const doc = strip.ownerDocument;
42
+ // Set by a tab drag, read by the click that trails it. Strip-level so it
43
+ // survives between the two listeners, cleared on the next press so an
44
+ // aborted drag never eats a later click.
45
+ let suppressClick = false;
29
46
  strip.className = 'axdb-tabs';
30
47
  if (o === null || o === void 0 ? void 0 : o.className)
31
48
  for (const c of o.className.split(/\s+/).filter(Boolean))
@@ -60,8 +77,41 @@ export function paintTabStrip(strip, pages, activeId, o, rtl, onPick, onSelectCo
60
77
  b.tabIndex = p.id === activeId ? 0 : -1;
61
78
  b.textContent = p.label;
62
79
  b.title = p.label;
80
+ b.addEventListener('pointerdown', (e) => {
81
+ const pe = e;
82
+ if (pe.button !== undefined && pe.button > 0)
83
+ return;
84
+ suppressClick = false;
85
+ if (!onDrag)
86
+ return;
87
+ const from = { x: pe.clientX, y: pe.clientY };
88
+ const stop = () => {
89
+ doc.removeEventListener('pointermove', move, true);
90
+ doc.removeEventListener('pointerup', stop, true);
91
+ doc.removeEventListener('pointercancel', stop, true);
92
+ };
93
+ const move = (m) => {
94
+ const pm = m;
95
+ if (Math.abs(pm.clientX - from.x) < TAB_DRAG_THRESHOLD &&
96
+ Math.abs(pm.clientY - from.y) < TAB_DRAG_THRESHOLD)
97
+ return;
98
+ stop();
99
+ // A press that TRAVELLED is a drag: the click that may follow it must
100
+ // not also switch the page — but only if the drag was actually TAKEN.
101
+ // A board that cannot place the page (a split pane, a container down
102
+ // to its last page) refuses, and then the press is still a click.
103
+ suppressClick = onDrag(p.id, pm) === true;
104
+ };
105
+ doc.addEventListener('pointermove', move, true);
106
+ doc.addEventListener('pointerup', stop, true);
107
+ doc.addEventListener('pointercancel', stop, true);
108
+ });
63
109
  b.addEventListener('click', (e) => {
64
110
  e.stopPropagation();
111
+ if (suppressClick) {
112
+ suppressClick = false;
113
+ return;
114
+ }
65
115
  onPick(p.id);
66
116
  });
67
117
  b.addEventListener('keydown', (e) => {