@grafloria/element 0.4.26 → 0.4.28

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.26",
3
+ "version": "0.4.28",
4
4
  "type": "module",
5
5
  "main": "./src/index.js",
6
6
  "types": "./src/index.d.ts",
package/src/index.d.ts CHANGED
@@ -79,8 +79,8 @@ 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, 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
- export type { DashboardGridApi, DashboardGridOptions, DashboardGridHandle, CellRect, WorldRect, DashboardGridGeometry, TileDelta, DashboardOptions, DashboardSpec, DashboardSnapshot, DragHandleOption, DragGripOptions, DashboardHandle, DashboardViewSpec, DashboardWidgetSpec, SectionCaption, SectionCaptionOptions, SectionCaptionAction, SectionCaptionFont, WidgetHandle, WidgetRenderer, KpiWidgetData, LineWidgetData, LineSeries, BarWidgetData, DonutWidgetData, FunnelWidgetData, TableWidgetData, } from './lib/dashboard-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';
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';
86
86
  export * from '@grafloria/engine';
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, 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, 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
@@ -47,6 +47,7 @@ import { type DashboardGridHandle, type DashboardGridOptions, type DashboardResp
47
47
  import type { DragHandleOption } from './grid-binder.js';
48
48
  import type { SplitNode } from './split-layout.js';
49
49
  import type { SectionCaption } from './caption.js';
50
+ import { type TabsOptions } from './tabs.js';
50
51
  /** A widget, declared as data. */
51
52
  export interface DashboardWidgetSpec {
52
53
  id: string;
@@ -113,7 +114,16 @@ export interface DashboardWidgetSpec {
113
114
  * always covers the pane. Switch live with `setLayout(mode, containerId)`;
114
115
  * `toJSON()` writes it per container.
115
116
  */
116
- layout?: 'grid' | 'split';
117
+ layout?: 'grid' | 'split' | 'tabs';
118
+ /**
119
+ * TAB CONTAINER (`layout: 'tabs'`). Every child that carries `widgets` is a
120
+ * PAGE: one is visible at a time, and a strip of tabs across the top
121
+ * switches between them (DevExpress' Tab Container; VS Code's editor area
122
+ * is a split of these). `active` is the page showing, persisted by
123
+ * `toJSON()`; `tabs` styles the strip.
124
+ */
125
+ active?: string;
126
+ tabs?: TabsOptions;
117
127
  /**
118
128
  * Container only: a CAPTION painted by the kit on the section's slab —
119
129
  * `true` for the title, a string, or the full options (subtitle,
@@ -276,6 +286,8 @@ export interface DashboardOptions {
276
286
  renderCaption?: (widget: DashboardWidgetSpec, host: HTMLElement) => void;
277
287
  /** A press on a caption action (`caption.actions`): the section, the action id, the view. */
278
288
  onCaptionAction?: (sectionId: string, actionId: string, viewId: string) => void;
289
+ /** A tab container switched pages: the container, the page now showing, the view. */
290
+ onTabChange?: (containerId: string, pageId: string, viewId: string) => void;
279
291
  /** Fires after any committed gesture, with the view whose layout changed. */
280
292
  onLayoutChange?: (viewId: string, widgets: DashboardWidgetSpec[]) => void;
281
293
  /** Extra binder options, merged last (escape hatch to the layer below). */
@@ -342,7 +354,8 @@ export interface DashboardHandle {
342
354
  * saved document reopens in the layout it was left in.
343
355
  */
344
356
  setLayout(layout: 'grid' | 'split', viewId?: string): void;
345
- getLayout(viewId?: string): 'grid' | 'split';
357
+ /** A container built with `layout: 'tabs'` reports 'tabs'; views never do. */
358
+ getLayout(viewId?: string): 'grid' | 'split' | 'tabs';
346
359
  /**
347
360
  * Set a SECTION's caption live — `false` removes it, `true` is the title, a
348
361
  * string or the options. Repaints the band, gives the reserve back or takes
@@ -352,6 +365,14 @@ export interface DashboardHandle {
352
365
  setCaption(id: string, caption: SectionCaption): boolean;
353
366
  /** The caption as authored or last set; undefined when the section has none. */
354
367
  getCaption(id: string): SectionCaption | undefined;
368
+ /**
369
+ * Show a PAGE of a tab container (`layout: 'tabs'`). False when the
370
+ * container or the page is not one. Persisted, so a saved board reopens on
371
+ * the page it was left on.
372
+ */
373
+ activateTab(containerId: string, pageId: string): boolean;
374
+ /** The page showing in a tab container. */
375
+ getActiveTab(containerId: string): string | undefined;
355
376
  /** Live sizing/float switches — the two prototype toggles. */
356
377
  setSizing(mode: 'fit' | 'grow'): void;
357
378
  getSizing(): 'fit' | 'grow';
@@ -527,6 +548,10 @@ export interface DashboardApiRef {
527
548
  onChange?(listener: (state: unknown) => void): () => void;
528
549
  };
529
550
  }
551
+ export declare function attachTabsRuntime(ctx: DashboardHandleContext, model: {
552
+ getGroup(id: string): GroupModel | undefined;
553
+ runSystemWrite?(fn: () => void): void;
554
+ }, container: HTMLElement | null, handle: DashboardHandle): void;
530
555
  export interface DashboardHandleContext {
531
556
  /** The views — MUTATED in place by addWidget (push) and remove (filter). */
532
557
  views: DashboardViewSpec[];
@@ -555,7 +580,15 @@ export interface DashboardHandleContext {
555
580
  mode: 'fluid' | 'fixed';
556
581
  overflow: 'bounded' | 'scroll';
557
582
  /** See DashboardOptions.layout — per view. */
558
- layoutOf: Map<string, 'grid' | 'split'>;
583
+ layoutOf: Map<string, 'grid' | 'split' | 'tabs'>;
584
+ /** Tab containers: the page showing, the strip options, the strip element. */
585
+ activeTab: Map<string, string>;
586
+ tabsOf: Map<string, TabsOptions>;
587
+ tabStrips: Map<string, HTMLElement>;
588
+ /** Set by finalize: re-lay a tab container's pages and repaint its strip. */
589
+ syncTabs?: (containerId: string) => void;
590
+ /** Set by finalize: the user's `onTabChange`, so the handle can fire it. */
591
+ onTabChange?: (containerId: string, pageId: string, viewId: string) => void;
559
592
  /** Set by finalize: re-bind a VIEW's board under the given layout (setLayout). */
560
593
  rebindView?: (viewId: string, layout: 'grid' | 'split') => void;
561
594
  /**
@@ -48,6 +48,7 @@ import { bindDashboardGrid, } 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';
51
+ import { paintTabStrip, tabStripKey, tabStripReserve } from './tabs.js';
51
52
  import { defaultWidgetRenderer } from './widgets.js';
52
53
  /**
53
54
  * Add a widget node AND its board membership as ONE undoable step.
@@ -229,6 +230,29 @@ function buildWidgetNode(w, rowHeight) {
229
230
  node.removePort(p.id);
230
231
  return node;
231
232
  }
233
+ /**
234
+ * EVERY CHILD OF A TAB CONTAINER IS A PAGE. A page is a container, so a plain
235
+ * widget written among them is wrapped in one carrying its title. Without this
236
+ * the tab system simply ignored it: it was never positioned and painted at the
237
+ * board's origin at its 100×60 placeholder size, silently losing part of the
238
+ * author's layout (measured on a container mixing a widget with two pages).
239
+ */
240
+ function wrapTabPages(ws) {
241
+ for (const w of ws) {
242
+ if (!w.widgets)
243
+ continue;
244
+ if (w.layout === 'tabs') {
245
+ w.widgets = w.widgets.map((c) => {
246
+ var _a;
247
+ return c.widgets
248
+ ? c
249
+ : Object.assign(Object.assign({ id: `${c.id}__page`, title: (_a = c.title) !== null && _a !== void 0 ? _a : c.id }, (w.columns !== undefined ? { columns: w.columns } : {})), { widgets: [Object.assign(Object.assign({}, c), { x: 0, y: 0 })] });
250
+ });
251
+ }
252
+ wrapTabPages(w.widgets);
253
+ }
254
+ return ws;
255
+ }
232
256
  function cloneWidgets(ws) {
233
257
  return ws.map((w) => (Object.assign(Object.assign({}, w), (w.widgets ? { widgets: cloneWidgets(w.widgets) } : {}))));
234
258
  }
@@ -302,6 +326,117 @@ function assignCells(widgets, columns) {
302
326
  * reads and writes is the whole reason a second handle implementation, which
303
327
  * would silently drift, is not needed.
304
328
  */
329
+ /**
330
+ * TAB CONTAINER RUNTIME, shared by `dashboard()` and `fromDocument()`.
331
+ *
332
+ * A tab container binds no grid of its own: it positions its PAGES itself —
333
+ * one into its frame under the strip, the rest parked off-canvas, exactly as
334
+ * `showView` parks a view one level up — and paints the strip. Each page is an
335
+ * ordinary container with an ordinary binder, so a page can be a grid or a
336
+ * split, and the parent board still sees ONE member group to place and resize.
337
+ *
338
+ * Both entry points call this, so a board reloaded from a document behaves
339
+ * like one built from a literal (the first version lived inside `dashboard()`
340
+ * and `fromDocument` silently produced a tab container that never switched).
341
+ */
342
+ const cssEscape = (v) => typeof CSS !== 'undefined' && CSS.escape ? CSS.escape(v) : v.replace(/"/g, '\\"');
343
+ export function attachTabsRuntime(ctx, model, container, handle) {
344
+ var _a, _b, _c;
345
+ 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 }); });
349
+ };
350
+ 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; };
351
+ const paintStrip = (id, f, h, pages, active) => {
352
+ var _a, _b, _c;
353
+ const layer = container === null || container === void 0 ? void 0 : container.querySelector('.grafloria-html-layer');
354
+ if (!layer)
355
+ return;
356
+ let el = ctx.tabStrips.get(id);
357
+ if (!el || el.parentElement !== layer) {
358
+ el === null || el === void 0 ? void 0 : el.remove();
359
+ el = document.createElement('div');
360
+ el.setAttribute('data-tabs-id', id);
361
+ layer.appendChild(el);
362
+ ctx.tabStrips.set(id, el);
363
+ }
364
+ el.style.position = 'absolute';
365
+ el.style.left = `${f.x}px`;
366
+ el.style.top = `${f.y}px`;
367
+ el.style.width = `${f.width}px`;
368
+ el.style.height = `${h}px`;
369
+ const rtl = (_c = (_b = ctx.binders.get((_a = ctx.viewOfBoard.get(id)) !== null && _a !== void 0 ? _a : ctx.active)) === null || _b === void 0 ? void 0 : _b.getRtl()) !== null && _c !== void 0 ? _c : false;
370
+ const key = tabStripKey(pages, active, ctx.tabsOf.get(id), rtl);
371
+ if (el.getAttribute('data-key') === key)
372
+ return;
373
+ paintTabStrip(el, pages, active, ctx.tabsOf.get(id), rtl, (pid) => handle.activateTab(id, pid), () => handle.selectWidget(id));
374
+ el.setAttribute('data-key', key);
375
+ };
376
+ const sync = (id) => {
377
+ var _a, _b, _c, _d, _e, _f, _g, _h;
378
+ const cg = (_a = ctx.boardGroups.get(id)) !== null && _a !== void 0 ? _a : model.getGroup(id);
379
+ if (!cg || !isTabs(id))
380
+ return;
381
+ const pages = pagesOf(id);
382
+ const active = (_b = ctx.activeTab.get(id)) !== null && _b !== void 0 ? _b : pages[0].id;
383
+ const strip = tabStripReserve(ctx.tabsOf.get(id), pages.length);
384
+ 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
+ const inner = { width: f.width, height: Math.max(0, f.height - strip) };
386
+ const write = (fn) => (model.runSystemWrite ? model.runSystemWrite(fn) : fn());
387
+ write(() => {
388
+ for (const p of pages) {
389
+ const pg = model.getGroup(p.id);
390
+ if (!pg)
391
+ continue;
392
+ // A PARKED page keeps its size, so its own board keeps its layout and
393
+ // comes back exactly as it was; only its x leaves the canvas.
394
+ pg.setFrame({ x: p.id === active ? f.x : OFFSCREEN_X, y: f.y + strip, width: inner.width, height: inner.height });
395
+ }
396
+ });
397
+ for (const p of pages)
398
+ (_g = ctx.binders.get(p.id)) === null || _g === void 0 ? void 0 : _g.sync();
399
+ // A PARKED page is off-canvas but still in the DOM: take its widgets out
400
+ // of the tab order and hide them from assistive tech, or a keyboard user
401
+ // tabs through pages nobody can see (the accessibility scenario counted
402
+ // five tab stops on a board with three of them hidden).
403
+ for (const p of pages) {
404
+ const pg = model.getGroup(p.id);
405
+ const parked = p.id !== active;
406
+ for (const m of (_h = pg === null || pg === void 0 ? void 0 : pg.members) !== null && _h !== void 0 ? _h : []) {
407
+ const host = container === null || container === void 0 ? void 0 : container.querySelector(`.grafloria-node-host[data-node-id="${cssEscape(m)}"]`);
408
+ if (!host)
409
+ continue;
410
+ if (parked) {
411
+ host.setAttribute('aria-hidden', 'true');
412
+ host.tabIndex = -1;
413
+ }
414
+ else
415
+ host.removeAttribute('aria-hidden');
416
+ }
417
+ }
418
+ paintStrip(id, f, strip, pages, active);
419
+ };
420
+ ctx.syncTabs = sync;
421
+ ctx.subscriptions = (_a = ctx.subscriptions) !== null && _a !== void 0 ? _a : [];
422
+ for (const [id, cg] of ctx.boardGroups) {
423
+ if (!isTabs(id))
424
+ continue;
425
+ const pages = pagesOf(id);
426
+ if (!ctx.activeTab.has(id)) {
427
+ const meta = cg.getMetadata('containerWidget');
428
+ const want = (_c = (_b = ctx.specById.get(id)) === null || _b === void 0 ? void 0 : _b.active) !== null && _c !== void 0 ? _c : meta === null || meta === void 0 ? void 0 : meta.active;
429
+ ctx.activeTab.set(id, want && pages.some((p) => p.id === want) ? want : pages[0].id);
430
+ if ((meta === null || meta === void 0 ? void 0 : meta.tabs) && !ctx.tabsOf.has(id))
431
+ ctx.tabsOf.set(id, meta.tabs);
432
+ }
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);
437
+ sync(id);
438
+ }
439
+ }
305
440
  /** `setCaption` as one history step: execute re-applies the value, undo the previous one. */
306
441
  class SetCaptionCommand extends Command {
307
442
  constructor(sectionId, before, after, apply) {
@@ -484,16 +619,27 @@ export function createDashboardHandle(ctx) {
484
619
  for (const v of handle.toJSON().views)
485
620
  lastReported.set(v.id, JSON.stringify(v.widgets));
486
621
  const onHistory = () => {
487
- var _a;
622
+ var _a, _b;
488
623
  if (!ctx.apiRef)
489
624
  return;
490
625
  const model = ctx.apiRef.getModel();
491
626
  for (const id of [...ctx.boardGroups.keys()]) {
627
+ // A TAB CONTAINER deliberately has no binder — it places its pages
628
+ // itself. "No binder" must not read as "needs rebinding", or every
629
+ // history event bound a GRID on it and laid the three pages out as
630
+ // cells side by side (measured: 2×2 of 216×200 after one resize).
631
+ if (ctx.layoutOf.get(id) === 'tabs')
632
+ continue;
492
633
  if (!binders.has(id) && model.getGroup(id))
493
634
  (_a = ctx.rebindContainer) === null || _a === void 0 ? void 0 : _a.call(ctx, id);
494
635
  }
495
636
  for (const b of binders.values())
496
637
  b.sync();
638
+ // …and undo/redo of a section resize must re-place the pages under the
639
+ // strip, which only the tabs runtime knows how to do.
640
+ for (const id of ctx.layoutOf.keys())
641
+ if (ctx.layoutOf.get(id) === 'tabs')
642
+ (_b = ctx.syncTabs) === null || _b === void 0 ? void 0 : _b.call(ctx, id);
497
643
  clampCamera();
498
644
  ctx.apiRef.renderNow();
499
645
  reportChanged();
@@ -630,6 +776,27 @@ export function createDashboardHandle(ctx) {
630
776
  return true;
631
777
  },
632
778
  getCaption: (id) => { var _a; return (_a = specById.get(id)) === null || _a === void 0 ? void 0 : _a.caption; },
779
+ activateTab(containerId, pageId) {
780
+ var _a, _b, _c, _d, _e, _f, _g;
781
+ const cg = ctx.boardGroups.get(containerId);
782
+ const w = specById.get(containerId);
783
+ if (!cg || !w || ((_a = ctx.layoutOf.get(containerId)) !== null && _a !== void 0 ? _a : w.layout) !== 'tabs')
784
+ return false;
785
+ if (!((_b = w.widgets) !== null && _b !== void 0 ? _b : []).some((p) => p.id === pageId && p.widgets))
786
+ return false;
787
+ if (ctx.activeTab.get(containerId) === pageId)
788
+ return true;
789
+ ctx.activeTab.set(containerId, pageId);
790
+ w.active = pageId;
791
+ const cw = (_c = cg.getMetadata('containerWidget')) !== null && _c !== void 0 ? _c : {};
792
+ cg.setMetadata('containerWidget', Object.assign(Object.assign({}, cw), { active: pageId }));
793
+ (_d = ctx.syncTabs) === null || _d === void 0 ? void 0 : _d.call(ctx, containerId);
794
+ (_e = ctx.apiRef) === null || _e === void 0 ? void 0 : _e.renderNow();
795
+ (_f = ctx.onTabChange) === null || _f === void 0 ? void 0 : _f.call(ctx, containerId, pageId, (_g = ctx.viewOfBoard.get(containerId)) !== null && _g !== void 0 ? _g : ctx.active);
796
+ reportChanged();
797
+ return true;
798
+ },
799
+ getActiveTab: (containerId) => ctx.activeTab.get(containerId),
633
800
  setSizing(mode) {
634
801
  var _a;
635
802
  for (const b of binders.values())
@@ -989,8 +1156,8 @@ export function dashboard(options) {
989
1156
  const sizing = (_h = options.sizing) !== null && _h !== void 0 ? _h : (mode === 'fluid' ? 'grow' : 'fit');
990
1157
  const layout = (_j = options.layout) !== null && _j !== void 0 ? _j : 'grid';
991
1158
  const views = options.views
992
- ? options.views.map((v) => (Object.assign(Object.assign({}, v), { widgets: cloneWidgets(v.widgets) })))
993
- : [{ id: 'main', widgets: cloneWidgets((_k = options.widgets) !== null && _k !== void 0 ? _k : []) }];
1159
+ ? options.views.map((v) => (Object.assign(Object.assign({}, v), { widgets: wrapTabPages(cloneWidgets(v.widgets)) })))
1160
+ : [{ id: 'main', widgets: wrapTabPages(cloneWidgets((_k = options.widgets) !== null && _k !== void 0 ? _k : [])) }];
994
1161
  for (const v of views)
995
1162
  assignCellsDeep(v.widgets, (_l = v.columns) !== null && _l !== void 0 ? _l : columns);
996
1163
  // -- the render spec: one custom-HTML node per widget ----------------------
@@ -1061,6 +1228,9 @@ export function dashboard(options) {
1061
1228
  boardH,
1062
1229
  mode,
1063
1230
  overflow,
1231
+ activeTab: new Map(),
1232
+ tabsOf: new Map(),
1233
+ tabStrips: new Map(),
1064
1234
  layoutOf: new Map(views.map((v) => { var _a; return [v.id, (_a = v.layout) !== null && _a !== void 0 ? _a : layout]; })),
1065
1235
  optionsBase: options,
1066
1236
  active: (_p = (_o = views[0]) === null || _o === void 0 ? void 0 : _o.id) !== null && _p !== void 0 ? _p : 'main',
@@ -1079,7 +1249,7 @@ export function dashboard(options) {
1079
1249
  }, get handle() {
1080
1250
  return handle;
1081
1251
  }, finalize: (api) => {
1082
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
1252
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
1083
1253
  const a = api;
1084
1254
  if (!a)
1085
1255
  return;
@@ -1179,13 +1349,15 @@ export function dashboard(options) {
1179
1349
  };
1180
1350
  handle.showView(ctx.active);
1181
1351
  ctx.rebindContainer = (id) => {
1182
- var _a;
1352
+ var _a, _b;
1183
1353
  const g = model.getGroup(id);
1184
1354
  const w = specById.get(id);
1185
1355
  if (!g || !w || !w.widgets)
1186
1356
  return;
1357
+ if (((_a = ctx.layoutOf.get(id)) !== null && _a !== void 0 ? _a : w.layout) === 'tabs')
1358
+ return; // places its own pages
1187
1359
  ctx.boardGroups.set(id, g);
1188
- bindContainer(g, w, (_a = ctx.viewOfBoard.get(id)) !== null && _a !== void 0 ? _a : ctx.active);
1360
+ bindContainer(g, w, (_b = ctx.viewOfBoard.get(id)) !== null && _b !== void 0 ? _b : ctx.active);
1189
1361
  };
1190
1362
  /**
1191
1363
  * LIVE LAYOUT SWITCH ON A CONTAINER (item 7) — the view's contract one
@@ -1236,7 +1408,13 @@ export function dashboard(options) {
1236
1408
  else if (selected)
1237
1409
  (_f = binders.get(id)) === null || _f === void 0 ? void 0 : _f.selectWidget(selected);
1238
1410
  }
1239
- (_m = ctx.attachHistory) === null || _m === void 0 ? void 0 : _m.call(ctx);
1411
+ // (Statements must live BEFORE this return everything below it is a
1412
+ // hoisted function declaration, so an assignment placed there never
1413
+ // runs. That cost me a debugging round: `ctx.syncTabs` stayed undefined
1414
+ // and every tab switch silently did nothing.)
1415
+ ctx.onTabChange = options.onTabChange;
1416
+ attachTabsRuntime(ctx, model, (_m = a.container) !== null && _m !== void 0 ? _m : null, handle);
1417
+ (_o = ctx.attachHistory) === null || _o === void 0 ? void 0 : _o.call(ctx);
1240
1418
  return;
1241
1419
  /**
1242
1420
  * Mount one board's widgets into its group — and recurse for CONTAINERS.
@@ -1248,7 +1426,7 @@ export function dashboard(options) {
1248
1426
  * hit-testing and the height-escalation ratchet all apply unchanged.
1249
1427
  */
1250
1428
  function mountBoard(boardId, viewId, widgets, boardGroup) {
1251
- var _a, _b, _c, _d, _e, _f, _g;
1429
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
1252
1430
  for (const w of widgets) {
1253
1431
  if (w.widgets) {
1254
1432
  const innerColumns = innerColumnsOf(w);
@@ -1285,6 +1463,21 @@ export function dashboard(options) {
1285
1463
  boardGroup.addMember(w.id);
1286
1464
  ctx.boardGroups.set(w.id, cg);
1287
1465
  mountBoard(w.id, viewId, w.widgets, cg);
1466
+ if (w.layout === 'tabs') {
1467
+ // A TAB CONTAINER lays its pages out itself — one visible, the
1468
+ // rest parked — so it binds no grid of its own. Its pages are
1469
+ // ordinary containers with ordinary binders. The runtime that
1470
+ // positions them is attached once, after every board is mounted.
1471
+ const pages = ((_f = w.widgets) !== null && _f !== void 0 ? _f : []).filter((c) => !!c.widgets);
1472
+ const active = w.active && pages.some((p) => p.id === w.active) ? w.active : (_g = pages[0]) === null || _g === void 0 ? void 0 : _g.id;
1473
+ if (active) {
1474
+ ctx.activeTab.set(w.id, active);
1475
+ w.active = active;
1476
+ }
1477
+ cg.setMetadata('containerWidget', Object.assign(Object.assign(Object.assign(Object.assign({}, cg.getMetadata('containerWidget')), { layout: 'tabs' }), (active ? { active } : {})), (w.tabs ? { tabs: w.tabs } : {})));
1478
+ ctx.tabsOf.set(w.id, (_h = w.tabs) !== null && _h !== void 0 ? _h : {});
1479
+ continue;
1480
+ }
1288
1481
  bindContainer(cg, w, viewId);
1289
1482
  continue;
1290
1483
  }
@@ -1299,7 +1492,7 @@ export function dashboard(options) {
1299
1492
  // and made toJSON() → dashboard() NOT round-trip (a saved layout
1300
1493
  // rebuilt back into its declaration order rather than its cells).
1301
1494
  if (w.x !== undefined && w.y !== undefined) {
1302
- n.setGridItem(gridItemFromCell({ x: w.x, y: w.y, w: (_f = w.span) !== null && _f !== void 0 ? _f : 3, h: (_g = w.rows) !== null && _g !== void 0 ? _g : 1 }));
1495
+ n.setGridItem(gridItemFromCell({ x: w.x, y: w.y, w: (_j = w.span) !== null && _j !== void 0 ? _j : 3, h: (_k = w.rows) !== null && _k !== void 0 ? _k : 1 }));
1303
1496
  }
1304
1497
  if (w.pinned)
1305
1498
  n.setState({ locked: true });
@@ -114,6 +114,9 @@ export function ownsPress(container, diagram, ev, hit) {
114
114
  // anything the caption's `passThrough` names) is the content's, not any
115
115
  // tool's: no selection, no drag, no resize — the DOM handles it.
116
116
  if (typeof Element !== 'undefined' && t instanceof Element) {
117
+ // A TAB STRIP is content, never a board press: its tabs are real buttons.
118
+ if (t.closest('.axdb-tabs'))
119
+ return false;
117
120
  const band = t.closest('.axdb-slab > .axdb-slab-h');
118
121
  const sid = (_b = band === null || band === void 0 ? void 0 : band.parentElement) === null || _b === void 0 ? void 0 : _b.getAttribute('data-slab-id');
119
122
  if (band && sid) {
@@ -2113,6 +2116,28 @@ export function bindDashboardGrid(api, group, options = {}) {
2113
2116
  return best;
2114
2117
  };
2115
2118
  /** This binder's side of an adoption: enter gateless, then live-push. */
2119
+ /**
2120
+ * Put an ARRIVING tile as close to the pointer as the board allows. The
2121
+ * exact cell is often refused because a SECTION is a locked tile and the
2122
+ * incoming tile overlaps it (E4b), and a refused placement left the tile
2123
+ * wherever it entered — the bottom row. That read as "it did not drop where
2124
+ * I put it". Slide along the row instead, nearest first, so a tile that
2125
+ * cannot take the cell under the pointer still lands beside it.
2126
+ */
2127
+ const placeNear = (id, x, y, w) => {
2128
+ if (engine.moveCheck(id, x, y, { gate: false }).changed)
2129
+ return true;
2130
+ 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)
2134
+ continue;
2135
+ if (engine.moveCheck(id, cx, y, { gate: false }).changed)
2136
+ return true;
2137
+ }
2138
+ }
2139
+ return false;
2140
+ };
2116
2141
  const adopt = (node, world, pxSize) => {
2117
2142
  if (disposed)
2118
2143
  return null;
@@ -2150,7 +2175,7 @@ export function bindDashboardGrid(api, group, options = {}) {
2150
2175
  adoptedGhostId = node.id;
2151
2176
  const tl = centredTopLeft(world.x, world.y, span);
2152
2177
  const cell0 = pointToCell(tl.x, tl.y, f, gg, rows(), span.w);
2153
- engine.moveCheck(node.id, cell0.x, cell0.y, { gate: false });
2178
+ placeNear(node.id, cell0.x, cell0.y, span.w);
2154
2179
  armGlide();
2155
2180
  project();
2156
2181
  syncPlaceholder();
@@ -2162,7 +2187,7 @@ export function bindDashboardGrid(api, group, options = {}) {
2162
2187
  return;
2163
2188
  const tlm = centredTopLeft(w.x, w.y, { w: item.w, h: item.h });
2164
2189
  const cell = pointToCell(tlm.x, tlm.y, frame(), geom(), rows(), item.w);
2165
- if (engine.moveCheck(node.id, cell.x, cell.y).changed)
2190
+ if (placeNear(node.id, cell.x, cell.y, item.w))
2166
2191
  project();
2167
2192
  syncPlaceholder();
2168
2193
  },
@@ -2274,10 +2299,20 @@ export function bindDashboardGrid(api, group, options = {}) {
2274
2299
  // A press on one of OUR sections' caption bands is ours by the DOM: a
2275
2300
  // 'tab' band sits above the frame, over the gap or the tile above,
2276
2301
  // where the geometry says otherwise.
2277
- const bandTarget = (_c = (_b = (_a = ev.source) === null || _a === void 0 ? void 0 : _a.target) === null || _b === void 0 ? void 0 : _b.closest) === null || _c === void 0 ? void 0 : _c.call(_b, '.axdb-slab > .axdb-slab-h');
2278
- const bandId = (_d = bandTarget === null || bandTarget === void 0 ? void 0 : bandTarget.parentElement) === null || _d === void 0 ? void 0 : _d.getAttribute('data-slab-id');
2279
- if (bandId && ((_e = group.members) !== null && _e !== void 0 ? _e : new Set()).has(bandId))
2302
+ const chrome = (_c = (_b = (_a = ev.source) === null || _a === void 0 ? void 0 : _a.target) === null || _b === void 0 ? void 0 : _b.closest) === null || _c === void 0 ? void 0 : _c.call(_b, '.axdb-slab > .axdb-slab-h, .axdb-slab > .axdb-rs');
2303
+ const chromeId = (_d = chrome === null || chrome === void 0 ? void 0 : chrome.parentElement) === null || _d === void 0 ? void 0 : _d.getAttribute('data-slab-id');
2304
+ const mine = !!chromeId && ((_e = group.members) !== null && _e !== void 0 ? _e : new Set()).has(chromeId);
2305
+ if (mine)
2280
2306
  return true;
2307
+ // Someone else's SECTION HANDLE is never ours to claim. A tab
2308
+ // container's pages register before the parent board, so a page's tool
2309
+ // won the tie for the CONTAINER's corner handle and, finding no peer to
2310
+ // hand it to, just cleared the selection — the container could not be
2311
+ // resized by hand at all while the API resized it fine. (Only the
2312
+ // handle: declining every foreign band broke the press that reaches the
2313
+ // content under a hidden `show: 'hover'` caption.)
2314
+ if (chromeId && (chrome === null || chrome === void 0 ? void 0 : chrome.classList.contains('axdb-rs')))
2315
+ return false;
2281
2316
  if (hit.node) {
2282
2317
  if (((_f = group.members) !== null && _f !== void 0 ? _f : new Set()).has(hit.node.id))
2283
2318
  return true;
@@ -3,6 +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
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';
7
8
  export { dashboard, type DashboardOptions, type DashboardSpec, type DashboardSnapshot, type DashboardHandle, type DashboardViewSpec, type DashboardWidgetSpec, type WidgetHandle, } from './dashboard.js';
8
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,6 +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
7
  export { normalizeCaption, captionReserve, captionBandHeight, paintCaptionBand, CAPTION_HEIGHT, CAPTION_HEIGHT_SUBTITLE, CAPTION_HEIGHT_TIGHT, CAPTION_PASS_THROUGH, } from './caption.js';
7
8
  // The DATA-FIRST authoring API (the erDiagram/umlDiagram equivalent).
8
9
  export { dashboard, } from './dashboard.js';
@@ -442,6 +442,43 @@ const CSS = `
442
442
  .axdb-slab-h-action:hover { background: rgba(236, 238, 244, .1); }
443
443
  }
444
444
 
445
+ /* TAB CONTAINER (0.4.27): a strip of pages across the container's top. The
446
+ strip takes the pointer (the tabs are real buttons); the pages below it are
447
+ ordinary boards. */
448
+ .grafloria-html-layer > .axdb-tabs {
449
+ position: absolute; box-sizing: border-box; pointer-events: auto; z-index: 5;
450
+ display: flex; align-items: flex-end; gap: 2px; padding: 0 6px; overflow-x: auto; overflow-y: hidden;
451
+ background: var(--axdb-tabs-bg, rgba(31, 36, 48, .05));
452
+ border-bottom: 1px solid var(--axdb-tabs-line, rgba(31, 36, 48, .12));
453
+ border-radius: var(--axdb-rs-radius, 3px) var(--axdb-rs-radius, 3px) 0 0;
454
+ scrollbar-width: thin;
455
+ }
456
+ .axdb-tabs--center { justify-content: center; }
457
+ .axdb-tabs--end { justify-content: flex-end; }
458
+ .axdb-tabs--stretch > .axdb-tab { flex: 1 1 0; }
459
+ .axdb-tab {
460
+ all: unset; box-sizing: border-box; flex: 0 0 auto; max-width: 200px;
461
+ padding: 0 12px; height: calc(100% - 4px); display: inline-flex; align-items: center;
462
+ font: 600 12px/1 system-ui, -apple-system, "Segoe UI", sans-serif;
463
+ color: var(--axdb-tabs-fg, #5a6478); cursor: pointer;
464
+ border-radius: var(--axdb-rs-radius, 3px) var(--axdb-rs-radius, 3px) 0 0;
465
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
466
+ }
467
+ .axdb-tab:hover { background: rgba(31, 36, 48, .06); }
468
+ .axdb-tab:focus-visible { outline: 2px solid var(--axdb-accent-ring, rgba(59, 82, 217, .55)); outline-offset: -2px; }
469
+ /* The active tab reads as the front page: the card's own ground, lifted. */
470
+ .axdb-tab.axdb-tab--on {
471
+ background: var(--axdb-tabs-on-bg, #fff);
472
+ color: var(--axdb-tabs-on-fg, #1f2430);
473
+ box-shadow: 0 -1px 0 var(--axdb-accent, #3b52d9) inset, 0 0 0 1px rgba(31, 36, 48, .1);
474
+ }
475
+ @media (prefers-color-scheme: dark) {
476
+ .grafloria-html-layer > .axdb-tabs { background: var(--axdb-tabs-bg, rgba(236, 238, 244, .06)); border-bottom-color: var(--axdb-tabs-line, rgba(236, 238, 244, .14)); }
477
+ .axdb-tab { color: var(--axdb-tabs-fg, #98a1b4); }
478
+ .axdb-tab:hover { background: rgba(236, 238, 244, .08); }
479
+ .axdb-tab.axdb-tab--on { background: var(--axdb-tabs-on-bg, #1a1d25); color: var(--axdb-tabs-on-fg, #eceef4); box-shadow: 0 -1px 0 var(--axdb-accent, #7d8ff0) inset, 0 0 0 1px rgba(236, 238, 244, .14); }
480
+ }
481
+
445
482
  /* legend chips, shared by line and donut */
446
483
  .axdb-lg { display: flex; flex-wrap: wrap; gap: 4px 12px; margin-top: 9px; }
447
484
  .axdb-lg--col { flex-direction: column; flex-wrap: nowrap; gap: 6px; margin-top: 0; }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * TAB CONTAINERS — a container whose children are PAGES, one visible at a
3
+ * time, with a strip of tabs across its top. DevExpress ships this as its Tab
4
+ * Container; VS Code's editor area is a split of them.
5
+ *
6
+ * This module owns the strip: its height, its geometry and its DOM. Who is
7
+ * active, where the pages sit and what a press means are the container's
8
+ * business (dashboard.ts), exactly as the caption band's press routing is the
9
+ * binder's.
10
+ */
11
+ export interface TabsOptions {
12
+ /** Where the strip sits. Only 'top' today; 'bottom' | 'start' | 'end' follow. */
13
+ position?: 'top';
14
+ /** Along the strip (default 'start', mirrored on RTL). */
15
+ align?: 'start' | 'center' | 'end';
16
+ /** Tabs share the strip's width equally. */
17
+ stretch?: boolean;
18
+ /** Strip height, px (default 30). */
19
+ height?: number;
20
+ /** Your class on the strip, for per-container theming. */
21
+ className?: string;
22
+ }
23
+ export interface TabPage {
24
+ id: string;
25
+ label: string;
26
+ }
27
+ export declare const TAB_STRIP_HEIGHT = 30;
28
+ /** Pixels the pages give up at the top of the container. */
29
+ export declare function tabStripReserve(o: TabsOptions | undefined, pageCount: number): number;
30
+ /**
31
+ * Paint the strip. Repainted only when its identity changes (the pages, the
32
+ * active one, the options, RTL) so a page switch is a class toggle, not a
33
+ * rebuild — and so a `click` listener survives between switches.
34
+ */
35
+ 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;
@@ -0,0 +1,84 @@
1
+ /**
2
+ * TAB CONTAINERS — a container whose children are PAGES, one visible at a
3
+ * time, with a strip of tabs across its top. DevExpress ships this as its Tab
4
+ * Container; VS Code's editor area is a split of them.
5
+ *
6
+ * This module owns the strip: its height, its geometry and its DOM. Who is
7
+ * active, where the pages sit and what a press means are the container's
8
+ * business (dashboard.ts), exactly as the caption band's press routing is the
9
+ * binder's.
10
+ */
11
+ export const TAB_STRIP_HEIGHT = 30;
12
+ /** Pixels the pages give up at the top of the container. */
13
+ export function tabStripReserve(o, pageCount) {
14
+ var _a;
15
+ if (pageCount <= 0)
16
+ return 0;
17
+ return Math.max(18, (_a = o === null || o === void 0 ? void 0 : o.height) !== null && _a !== void 0 ? _a : TAB_STRIP_HEIGHT);
18
+ }
19
+ /**
20
+ * Paint the strip. Repainted only when its identity changes (the pages, the
21
+ * active one, the options, RTL) so a page switch is a class toggle, not a
22
+ * rebuild — and so a `click` listener survives between switches.
23
+ */
24
+ export function tabStripKey(pages, activeId, o, rtl) {
25
+ return JSON.stringify([pages, activeId, o !== null && o !== void 0 ? o : null, rtl]);
26
+ }
27
+ export function paintTabStrip(strip, pages, activeId, o, rtl, onPick, onSelectContainer) {
28
+ const doc = strip.ownerDocument;
29
+ strip.className = 'axdb-tabs';
30
+ if (o === null || o === void 0 ? void 0 : o.className)
31
+ for (const c of o.className.split(/\s+/).filter(Boolean))
32
+ strip.classList.add(c);
33
+ strip.classList.toggle('axdb-tabs--center', (o === null || o === void 0 ? void 0 : o.align) === 'center');
34
+ strip.classList.toggle('axdb-tabs--end', (o === null || o === void 0 ? void 0 : o.align) === 'end');
35
+ strip.classList.toggle('axdb-tabs--stretch', (o === null || o === void 0 ? void 0 : o.stretch) === true);
36
+ strip.setAttribute('dir', rtl ? 'rtl' : 'ltr');
37
+ strip.setAttribute('role', 'tablist');
38
+ strip.textContent = '';
39
+ // The strip's EMPTY space selects the container. A tab container's pages
40
+ // cover it completely, so without this there is nowhere to press to select
41
+ // it — and an unselected section shows no corner handle, which made the
42
+ // container impossible to resize by hand.
43
+ strip.onpointerdown = (e) => {
44
+ var _a;
45
+ if ((_a = e.target) === null || _a === void 0 ? void 0 : _a.closest('.axdb-tab'))
46
+ return;
47
+ onSelectContainer === null || onSelectContainer === void 0 ? void 0 : onSelectContainer();
48
+ };
49
+ for (const p of pages) {
50
+ const b = doc.createElement('button');
51
+ b.type = 'button';
52
+ b.className = 'axdb-tab';
53
+ b.setAttribute('role', 'tab');
54
+ b.setAttribute('data-tab-id', p.id);
55
+ b.setAttribute('aria-selected', String(p.id === activeId));
56
+ b.classList.toggle('axdb-tab--on', p.id === activeId);
57
+ // ONE tab stop for the whole strip, arrows move between tabs — the
58
+ // pattern every tablist uses, and it keeps the board's single tab stop
59
+ // rule intact.
60
+ b.tabIndex = p.id === activeId ? 0 : -1;
61
+ b.textContent = p.label;
62
+ b.title = p.label;
63
+ b.addEventListener('click', (e) => {
64
+ e.stopPropagation();
65
+ onPick(p.id);
66
+ });
67
+ b.addEventListener('keydown', (e) => {
68
+ var _a;
69
+ const k = e.key;
70
+ const step = k === 'ArrowRight' ? 1 : k === 'ArrowLeft' ? -1 : k === 'Home' ? -pages.length : k === 'End' ? pages.length : 0;
71
+ if (!step)
72
+ return;
73
+ e.preventDefault();
74
+ e.stopPropagation();
75
+ const i = pages.findIndex((x) => x.id === p.id);
76
+ const dir = rtl && (k === 'ArrowRight' || k === 'ArrowLeft') ? -step : step;
77
+ const next = Math.max(0, Math.min(pages.length - 1, i + dir));
78
+ onPick(pages[next].id);
79
+ (_a = strip.querySelector(`[data-tab-id="${pages[next].id}"]`)) === null || _a === void 0 ? void 0 : _a.focus();
80
+ });
81
+ strip.appendChild(b);
82
+ }
83
+ }
84
+ //# sourceMappingURL=tabs.js.map
package/src/lib/load.js CHANGED
@@ -72,7 +72,7 @@ import { gridItemFromCell } from './dashboard-kit/grid-mapping.js';
72
72
  import { cellFromGridItem } from './dashboard-kit/grid-mapping.js';
73
73
  import { ensureDashboardKitStyles } from './dashboard-kit/styles.js';
74
74
  import { defaultWidgetRenderer } from './dashboard-kit/widgets.js';
75
- import { createDashboardHandle, } from './dashboard-kit/dashboard.js';
75
+ import { createDashboardHandle, attachTabsRuntime } from './dashboard-kit/dashboard.js';
76
76
  /** True when the node is an ER entity card or a UML class card. */
77
77
  function isDiagramKitCard(node) {
78
78
  return node.getMetadata('kitEntity') !== undefined || node.getMetadata('kitClass') !== undefined;
@@ -206,6 +206,9 @@ export function fromDocument(document, options = {}) {
206
206
  // a fixed world — it stays one. Fluid is only what was saved fluid.
207
207
  mode: (firstBoard === null || firstBoard === void 0 ? void 0 : firstBoard.fluid) === true ? 'fluid' : 'fixed',
208
208
  overflow: (_p = firstBoard === null || firstBoard === void 0 ? void 0 : firstBoard.overflow) !== null && _p !== void 0 ? _p : 'bounded',
209
+ activeTab: new Map(),
210
+ tabsOf: new Map(),
211
+ tabStrips: new Map(),
209
212
  layoutOf: new Map(dashGroups.map((g) => { var _a, _b; return [g.id, ((_b = (_a = g.getMetadata('dashboardBoard')) === null || _a === void 0 ? void 0 : _a.layout) !== null && _b !== void 0 ? _b : 'grid')]; })),
210
213
  // responsive is NOT in the document (a runtime seam), so it is deliberately
211
214
  // absent from the round-trip; width/height/columns/gap/sizing/float/rtl are.
@@ -244,7 +247,7 @@ export function fromDocument(document, options = {}) {
244
247
  (_b = getNodeType(node.type)) === null || _b === void 0 ? void 0 : _b(node, host);
245
248
  };
246
249
  const finalize = (api) => {
247
- var _a, _b, _c, _d, _e;
250
+ var _a, _b, _c, _d, _e, _f;
248
251
  const a = api;
249
252
  if (!a)
250
253
  return;
@@ -289,8 +292,15 @@ export function fromDocument(document, options = {}) {
289
292
  const board = group.getMetadata('dashboardBoard');
290
293
  if (!board)
291
294
  continue;
295
+ // A TAB CONTAINER binds no board of its own — the runtime below places
296
+ // its pages and paints its strip.
297
+ if (board.layout === 'tabs')
298
+ continue;
292
299
  boards.set(group.id, bindBoard(group, board));
293
300
  }
301
+ // Tab containers, with the same runtime dashboard() uses — a reloaded
302
+ // document must switch pages exactly like a board built from a literal.
303
+ attachTabsRuntime(ctx, model, (_e = a.container) !== null && _e !== void 0 ? _e : null, handle);
294
304
  // LIVE LAYOUT SWITCH on a loaded document — the same contract dashboard()
295
305
  // finalize offers: cells persisted where the grid reads them, any tree and
296
306
  // column cache cleared, the board's `layout` flag flipped, a fresh binder.
@@ -337,7 +347,7 @@ export function fromDocument(document, options = {}) {
337
347
  }
338
348
  // A loaded board follows the history exactly as an authored one does: an
339
349
  // undo re-syncs every binder without the consumer calling refresh().
340
- (_e = ctx.attachHistory) === null || _e === void 0 ? void 0 : _e.call(ctx);
350
+ (_f = ctx.attachHistory) === null || _f === void 0 ? void 0 : _f.call(ctx);
341
351
  };
342
352
  return Object.assign({ nodes, edges: model.getLinks(), renderCustomNode,
343
353
  finalize,