@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 +1 -1
- package/src/index.d.ts +2 -2
- package/src/index.js +1 -1
- package/src/lib/dashboard-kit/dashboard.d.ts +36 -3
- package/src/lib/dashboard-kit/dashboard.js +202 -9
- package/src/lib/dashboard-kit/grid-binder.js +40 -5
- package/src/lib/dashboard-kit/index.d.ts +1 -0
- package/src/lib/dashboard-kit/index.js +1 -0
- package/src/lib/dashboard-kit/styles.js +37 -0
- package/src/lib/dashboard-kit/tabs.d.ts +36 -0
- package/src/lib/dashboard-kit/tabs.js +84 -0
- package/src/lib/load.js +13 -3
package/package.json
CHANGED
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
|
-
|
|
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, (
|
|
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
|
-
(
|
|
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: (
|
|
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
|
-
|
|
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 (
|
|
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
|
|
2278
|
-
const
|
|
2279
|
-
|
|
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
|
-
(
|
|
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,
|