@grafloria/element 0.4.20 → 0.4.22

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.20",
3
+ "version": "0.4.22",
4
4
  "type": "module",
5
5
  "main": "./src/index.js",
6
6
  "types": "./src/index.d.ts",
package/src/index.d.ts CHANGED
@@ -80,7 +80,7 @@ export type { ErColumn, ErEntitySpec, ErRelationshipSpec, ErCardinality, ErSide,
80
80
  export { erTable, umlClass, erTables, umlClasses, CardHandle, ErTable, ErField, ErColumnList, UmlClass, UmlMemberList, } from './lib/diagram-kit/index.js';
81
81
  export type { HandleApi } from './lib/diagram-kit/index.js';
82
82
  export { bindDashboardGrid, rowHeightFor, boardHeightFor, columnUnitFor, cellToRect, pointToCell, sizeToSpan, gridItemFromCell, cellFromGridItem, buildCommitCommands, ensureDashboardKitStyles, DASHBOARD_KIT_STYLE_ID, dashboard, defaultWidgetRenderer, renderKpiWidget, renderLineWidget, renderBarWidget, renderDonutWidget, renderFunnelWidget, renderTableWidget, BUILT_IN_WIDGET_KINDS, } from './lib/dashboard-kit/index.js';
83
- export type { DashboardGridApi, DashboardGridOptions, DashboardGridHandle, CellRect, WorldRect, DashboardGridGeometry, TileDelta, DashboardOptions, DashboardSpec, DashboardSnapshot, DragHandleOption, DragGripOptions, DashboardHandle, DashboardViewSpec, DashboardWidgetSpec, WidgetHandle, WidgetRenderer, KpiWidgetData, LineWidgetData, LineSeries, BarWidgetData, DonutWidgetData, FunnelWidgetData, TableWidgetData, } from './lib/dashboard-kit/index.js';
83
+ export type { DashboardGridApi, DashboardGridOptions, DashboardGridHandle, CellRect, WorldRect, DashboardGridGeometry, TileDelta, DashboardOptions, DashboardSpec, DashboardSnapshot, DragHandleOption, DragGripOptions, DashboardHandle, DashboardViewSpec, DashboardWidgetSpec, SectionCaption, SectionCaptionOptions, 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';
@@ -0,0 +1,123 @@
1
+ /**
2
+ * SECTION CAPTIONS — the header a section (container widget) may carry,
3
+ * painted by the kit on the slab overlay it already owns.
4
+ *
5
+ * Three things only the kit can do for a section header, and this module is
6
+ * where they are decided: RESERVE the pixels (`captionReserve` — the nested
7
+ * board's frame starts below the band, so no child may take it), PAINT it
8
+ * (`paintCaptionBand` — one DOM shape, one set of CSS variables, one theme),
9
+ * and ROUTE its presses (`captionPassThrough` — a button or an input in the
10
+ * band is content, everything else is the band, which selects the section).
11
+ * Persistence is the container's business: the caption rides on the group's
12
+ * `containerWidget` metadata beside `layout` and `sizing`, and `toJSON()`
13
+ * writes it back.
14
+ *
15
+ * Plan and defaults: documentation/api-architecture/section-caption-plan.html.
16
+ */
17
+ export interface SectionCaptionAction {
18
+ id: string;
19
+ /** Accessible name; painted as the glyph when there is no `icon`. */
20
+ label: string;
21
+ /** A glyph (emoji or short text). */
22
+ icon?: string;
23
+ /** Tooltip; default: the label. */
24
+ title?: string;
25
+ disabled?: boolean;
26
+ }
27
+ export interface SectionCaptionFont {
28
+ size?: number;
29
+ weight?: number | string;
30
+ family?: string;
31
+ color?: string;
32
+ transform?: 'none' | 'uppercase';
33
+ }
34
+ export interface SectionCaptionOptions {
35
+ /** Default: the container's `title`. */
36
+ text?: string;
37
+ /** A muted second line; the band grows to 44 px. */
38
+ subtitle?: string;
39
+ /** An ⓘ after the text: tooltip and the section's accessible description. */
40
+ description?: string;
41
+ /** A glyph before the text (emoji or short text). */
42
+ icon?: string;
43
+ /** 'inside' (default): a band inside the frame, reserved. 'tab': above the frame, nothing reserved. */
44
+ position?: 'inside' | 'tab';
45
+ /** Horizontal alignment (default 'start', mirrored on RTL). */
46
+ align?: 'start' | 'center' | 'end';
47
+ /** Vertical alignment within the band (default 'center'). */
48
+ valign?: 'top' | 'center' | 'bottom';
49
+ /** Band height, px (default 28; 44 with a subtitle). */
50
+ height?: number;
51
+ font?: SectionCaptionFont;
52
+ /** Inside the band: px, or [vertical, horizontal] (default [0, 10]). */
53
+ padding?: number | [number, number];
54
+ /** Between the band and the frame: px, or [vertical, horizontal] (default 0). Adds to the reserve. */
55
+ margin?: number | [number, number];
56
+ /** Band fill (default: the theme's). */
57
+ background?: string;
58
+ /** The band's bottom rule, a CSS border value (default none). */
59
+ border?: string;
60
+ /** 'always' (default); 'design' is not painted (nor reserved) under static; 'hover' overlays the content, nothing reserved. */
61
+ show?: 'always' | 'design' | 'hover';
62
+ /** Buttons at the end of the band; a press fires `onCaptionAction` and never selects. */
63
+ actions?: SectionCaptionAction[];
64
+ /** Presses inside descendants matching this selector are content, never the band's. */
65
+ passThrough?: string;
66
+ /** Your class on the band. */
67
+ className?: string;
68
+ }
69
+ /** `false`/absent: no band. `true`: the title. A string: that text. */
70
+ export type SectionCaption = false | true | string | SectionCaptionOptions;
71
+ export declare const CAPTION_HEIGHT = 28;
72
+ export declare const CAPTION_HEIGHT_SUBTITLE = 44;
73
+ export declare const CAPTION_HEIGHT_TIGHT = 22;
74
+ /** A section shorter than this steps its band down to the tight tier. */
75
+ export declare const CAPTION_TIGHT_BELOW = 90;
76
+ export declare const CAPTION_PASS_THROUGH = "button, a, input, select, textarea, [data-axdb-pass]";
77
+ /** The caption as options, or null when there is none. `title` fills the text. */
78
+ export declare function normalizeCaption(c: SectionCaption | undefined, title?: string): SectionCaptionOptions | null;
79
+ /** The caption a section GROUP carries, from its persisted `containerWidget` metadata. */
80
+ export declare function captionOfGroup(grp: {
81
+ getMetadata(key: string): unknown;
82
+ name?: string;
83
+ } | undefined): SectionCaptionOptions | null;
84
+ export declare function pairOf(v: number | [number, number] | undefined, dflt: [number, number]): [number, number];
85
+ /** Painted at all? `show: 'design'` disappears under static. */
86
+ export declare function captionPainted(c: SectionCaptionOptions | null, isStatic: boolean): boolean;
87
+ /** The band's height for a section of `sectionH` px. */
88
+ export declare function captionBandHeight(c: SectionCaptionOptions, sectionH: number): number;
89
+ /** Pixels the nested board's frame gives up at the top: the band plus its vertical margins. */
90
+ export declare function captionReserve(c: SectionCaptionOptions | null, ctx: {
91
+ static: boolean;
92
+ sectionH: number;
93
+ }): number;
94
+ /** Identity of a painted band: repaint only when this changes (the tier is a class toggle, not a repaint). */
95
+ export declare function captionKey(c: SectionCaptionOptions, ctx: {
96
+ rtl: boolean;
97
+ static: boolean;
98
+ }): string;
99
+ /** Is a section of `sectionH` px in the tight tier? */
100
+ export declare const captionTight: (sectionH: number) => boolean;
101
+ /** The per-sync geometry of a painted band: height and tier follow the section's live size. */
102
+ export declare function sizeCaptionBand(band: HTMLElement, c: SectionCaptionOptions, sectionH: number): void;
103
+ /**
104
+ * Is a press on `target`, inside `band`, content rather than the band? An
105
+ * action button is always content: no tool claims it, so the browser's own
106
+ * `click` reaches it — from a mouse, a touch, or Enter/Space on the keyboard
107
+ * (a pointerdown-only action never fired from the keyboard, and the
108
+ * interaction gate's DEAD-BUTTON check, which clicks, called it dead).
109
+ */
110
+ export declare function captionPassThrough(target: Element | null, band: Element, c: SectionCaptionOptions | null): boolean;
111
+ /**
112
+ * Paint the band: geometry (inline, so the reserve and the pixels agree),
113
+ * classes for alignment and mode, CSS variables for typography and box, and
114
+ * the default content — icon, text, subtitle, ⓘ, actions. With `render` the
115
+ * content is yours: the band is handed over empty and keeps its press rules.
116
+ */
117
+ export declare function paintCaptionBand(band: HTMLElement, c: SectionCaptionOptions, ctx: {
118
+ rtl: boolean;
119
+ static: boolean;
120
+ sectionH: number;
121
+ render?: (host: HTMLElement) => void;
122
+ onAction?: (actionId: string) => void;
123
+ }): void;
@@ -0,0 +1,200 @@
1
+ /**
2
+ * SECTION CAPTIONS — the header a section (container widget) may carry,
3
+ * painted by the kit on the slab overlay it already owns.
4
+ *
5
+ * Three things only the kit can do for a section header, and this module is
6
+ * where they are decided: RESERVE the pixels (`captionReserve` — the nested
7
+ * board's frame starts below the band, so no child may take it), PAINT it
8
+ * (`paintCaptionBand` — one DOM shape, one set of CSS variables, one theme),
9
+ * and ROUTE its presses (`captionPassThrough` — a button or an input in the
10
+ * band is content, everything else is the band, which selects the section).
11
+ * Persistence is the container's business: the caption rides on the group's
12
+ * `containerWidget` metadata beside `layout` and `sizing`, and `toJSON()`
13
+ * writes it back.
14
+ *
15
+ * Plan and defaults: documentation/api-architecture/section-caption-plan.html.
16
+ */
17
+ export const CAPTION_HEIGHT = 28;
18
+ export const CAPTION_HEIGHT_SUBTITLE = 44;
19
+ export const CAPTION_HEIGHT_TIGHT = 22;
20
+ /** A section shorter than this steps its band down to the tight tier. */
21
+ export const CAPTION_TIGHT_BELOW = 90;
22
+ export const CAPTION_PASS_THROUGH = 'button, a, input, select, textarea, [data-axdb-pass]';
23
+ /** The caption as options, or null when there is none. `title` fills the text. */
24
+ export function normalizeCaption(c, title) {
25
+ if (c === undefined || c === false)
26
+ return null;
27
+ const o = c === true ? {} : typeof c === 'string' ? { text: c } : Object.assign({}, c);
28
+ if (o.text === undefined && title !== undefined)
29
+ o.text = title;
30
+ return o;
31
+ }
32
+ /** The caption a section GROUP carries, from its persisted `containerWidget` metadata. */
33
+ export function captionOfGroup(grp) {
34
+ var _a;
35
+ if (!grp)
36
+ return null;
37
+ const meta = grp.getMetadata('containerWidget');
38
+ if (!meta || meta.caption === undefined)
39
+ return null;
40
+ return normalizeCaption(meta.caption, (_a = meta.title) !== null && _a !== void 0 ? _a : grp.name);
41
+ }
42
+ export function pairOf(v, dflt) {
43
+ if (v === undefined)
44
+ return dflt;
45
+ return typeof v === 'number' ? [v, v] : [v[0], v[1]];
46
+ }
47
+ /** Painted at all? `show: 'design'` disappears under static. */
48
+ export function captionPainted(c, isStatic) {
49
+ if (!c)
50
+ return false;
51
+ return !(c.show === 'design' && isStatic);
52
+ }
53
+ /** The band's height for a section of `sectionH` px. */
54
+ export function captionBandHeight(c, sectionH) {
55
+ var _a;
56
+ if (sectionH > 0 && sectionH < CAPTION_TIGHT_BELOW)
57
+ return CAPTION_HEIGHT_TIGHT;
58
+ return (_a = c.height) !== null && _a !== void 0 ? _a : (c.subtitle ? CAPTION_HEIGHT_SUBTITLE : CAPTION_HEIGHT);
59
+ }
60
+ /** Pixels the nested board's frame gives up at the top: the band plus its vertical margins. */
61
+ export function captionReserve(c, ctx) {
62
+ if (!c || !captionPainted(c, ctx.static))
63
+ return 0;
64
+ if (c.position === 'tab' || c.show === 'hover')
65
+ return 0;
66
+ const [mv] = pairOf(c.margin, [0, 0]);
67
+ return captionBandHeight(c, ctx.sectionH) + 2 * mv;
68
+ }
69
+ /** Identity of a painted band: repaint only when this changes (the tier is a class toggle, not a repaint). */
70
+ export function captionKey(c, ctx) {
71
+ return JSON.stringify([c, ctx.rtl, ctx.static]);
72
+ }
73
+ /** Is a section of `sectionH` px in the tight tier? */
74
+ export const captionTight = (sectionH) => sectionH > 0 && sectionH < CAPTION_TIGHT_BELOW;
75
+ /** The per-sync geometry of a painted band: height and tier follow the section's live size. */
76
+ export function sizeCaptionBand(band, c, sectionH) {
77
+ const h = captionBandHeight(c, sectionH);
78
+ band.classList.toggle('axdb-slab-h--tight', captionTight(sectionH));
79
+ band.style.height = `${h}px`;
80
+ if (c.position === 'tab')
81
+ band.style.top = `${-h}px`;
82
+ }
83
+ /**
84
+ * Is a press on `target`, inside `band`, content rather than the band? An
85
+ * action button is always content: no tool claims it, so the browser's own
86
+ * `click` reaches it — from a mouse, a touch, or Enter/Space on the keyboard
87
+ * (a pointerdown-only action never fired from the keyboard, and the
88
+ * interaction gate's DEAD-BUTTON check, which clicks, called it dead).
89
+ */
90
+ export function captionPassThrough(target, band, c) {
91
+ var _a;
92
+ if (!target || !band.contains(target))
93
+ return false;
94
+ const sel = `${(_a = c === null || c === void 0 ? void 0 : c.passThrough) !== null && _a !== void 0 ? _a : CAPTION_PASS_THROUGH}, .axdb-slab-h-action`;
95
+ const hit = target.closest(sel);
96
+ return !!hit && band.contains(hit) && hit !== band;
97
+ }
98
+ const el = (doc, cls, text) => {
99
+ const e = doc.createElement('div');
100
+ e.className = cls;
101
+ if (text !== undefined)
102
+ e.textContent = text;
103
+ return e;
104
+ };
105
+ /**
106
+ * Paint the band: geometry (inline, so the reserve and the pixels agree),
107
+ * classes for alignment and mode, CSS variables for typography and box, and
108
+ * the default content — icon, text, subtitle, ⓘ, actions. With `render` the
109
+ * content is yours: the band is handed over empty and keeps its press rules.
110
+ */
111
+ export function paintCaptionBand(band, c, ctx) {
112
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
113
+ const doc = band.ownerDocument;
114
+ const h = captionBandHeight(c, ctx.sectionH);
115
+ const [mv, mh] = pairOf(c.margin, [0, 0]);
116
+ const [pv, ph] = pairOf(c.padding, [0, 10]);
117
+ band.className = 'axdb-slab-h';
118
+ band.textContent = '';
119
+ const cls = (name, on) => band.classList.toggle(name, on);
120
+ cls('axdb-slab-h--tab', c.position === 'tab');
121
+ cls('axdb-slab-h--hover', c.show === 'hover');
122
+ cls('axdb-slab-h--tight', captionTight(ctx.sectionH));
123
+ cls('axdb-slab-h--center', c.align === 'center');
124
+ cls('axdb-slab-h--end', c.align === 'end');
125
+ cls('axdb-slab-h--vtop', c.valign === 'top');
126
+ cls('axdb-slab-h--vbottom', c.valign === 'bottom');
127
+ if (c.className)
128
+ for (const k of c.className.split(/\s+/).filter(Boolean))
129
+ band.classList.add(k);
130
+ band.setAttribute('dir', ctx.rtl ? 'rtl' : 'ltr');
131
+ band.style.height = `${h}px`;
132
+ band.style.top = c.position === 'tab' ? `${-h}px` : `${mv}px`;
133
+ band.style.left = `${mh}px`;
134
+ band.style.right = c.position === 'tab' ? 'auto' : `${mh}px`; // a tab is sized to its text
135
+ const v = (name, value) => {
136
+ if (value === undefined)
137
+ band.style.removeProperty(name);
138
+ else
139
+ band.style.setProperty(name, value);
140
+ };
141
+ v('--axdb-caption-pad', `${pv}px ${ph}px`);
142
+ // Clamped to the CONFIGURED band (the tight tier caps it again in CSS), so
143
+ // the text never exceeds the band whatever size the section is painted at.
144
+ const bandH = (_a = c.height) !== null && _a !== void 0 ? _a : (c.subtitle ? CAPTION_HEIGHT_SUBTITLE : CAPTION_HEIGHT);
145
+ v('--axdb-caption-font-size', ((_b = c.font) === null || _b === void 0 ? void 0 : _b.size) !== undefined ? `${Math.min(c.font.size, Math.max(8, bandH - 2 * pv - 4))}px` : undefined);
146
+ v('--axdb-caption-font-weight', ((_c = c.font) === null || _c === void 0 ? void 0 : _c.weight) !== undefined ? String(c.font.weight) : undefined);
147
+ v('--axdb-caption-font-family', (_d = c.font) === null || _d === void 0 ? void 0 : _d.family);
148
+ v('--axdb-caption-fg', (_e = c.font) === null || _e === void 0 ? void 0 : _e.color);
149
+ v('--axdb-caption-transform', (_f = c.font) === null || _f === void 0 ? void 0 : _f.transform);
150
+ v('--axdb-caption-bg', c.background);
151
+ v('--axdb-caption-border', c.border);
152
+ if (ctx.render) {
153
+ ctx.render(band);
154
+ return;
155
+ }
156
+ if (c.icon)
157
+ band.appendChild(el(doc, 'axdb-slab-h-icon', c.icon));
158
+ const body = el(doc, 'axdb-slab-h-body');
159
+ const text = el(doc, 'axdb-slab-h-text', (_g = c.text) !== null && _g !== void 0 ? _g : '');
160
+ text.setAttribute('title', (_h = c.text) !== null && _h !== void 0 ? _h : '');
161
+ body.appendChild(text);
162
+ if (c.subtitle)
163
+ body.appendChild(el(doc, 'axdb-slab-h-sub', c.subtitle));
164
+ band.appendChild(body);
165
+ if (c.description) {
166
+ const info = el(doc, 'axdb-slab-h-info', 'ⓘ');
167
+ info.setAttribute('title', c.description);
168
+ info.setAttribute('aria-label', c.description);
169
+ band.appendChild(info);
170
+ }
171
+ if ((_j = c.actions) === null || _j === void 0 ? void 0 : _j.length) {
172
+ const row = el(doc, 'axdb-slab-h-actions');
173
+ for (const a of c.actions) {
174
+ const b = doc.createElement('button');
175
+ b.type = 'button';
176
+ b.className = 'axdb-slab-h-action';
177
+ b.setAttribute('data-action', a.id);
178
+ b.setAttribute('aria-label', a.label);
179
+ b.setAttribute('title', (_k = a.title) !== null && _k !== void 0 ? _k : a.label);
180
+ b.textContent = (_l = a.icon) !== null && _l !== void 0 ? _l : a.label;
181
+ if (a.disabled)
182
+ b.disabled = true;
183
+ // Out of the tab order: a board keeps exactly ONE tab stop (the roving
184
+ // tabindex over its widgets — the accessibility scenario asserts it).
185
+ // The band becomes the section's tab stop, with its actions reachable
186
+ // from it, in the section-keyboard round; a click (mouse, touch, or a
187
+ // script) fires the action today.
188
+ b.tabIndex = -1;
189
+ b.addEventListener('click', (e) => {
190
+ var _a;
191
+ e.stopPropagation();
192
+ if (!b.disabled)
193
+ (_a = ctx.onAction) === null || _a === void 0 ? void 0 : _a.call(ctx, a.id);
194
+ });
195
+ row.appendChild(b);
196
+ }
197
+ band.appendChild(row);
198
+ }
199
+ }
200
+ //# sourceMappingURL=caption.js.map
@@ -46,6 +46,7 @@ import { GroupModel, NodeModel, type GridColumnLayout } from '@grafloria/engine'
46
46
  import { type DashboardGridHandle, type DashboardGridOptions, type DashboardResponsiveOptions } from './grid-binder.js';
47
47
  import type { DragHandleOption } from './grid-binder.js';
48
48
  import type { SplitNode } from './split-layout.js';
49
+ import type { SectionCaption } from './caption.js';
49
50
  /** A widget, declared as data. */
50
51
  export interface DashboardWidgetSpec {
51
52
  id: string;
@@ -113,6 +114,14 @@ export interface DashboardWidgetSpec {
113
114
  * `toJSON()` writes it per container.
114
115
  */
115
116
  layout?: 'grid' | 'split';
117
+ /**
118
+ * Container only: a CAPTION painted by the kit on the section's slab —
119
+ * `true` for the title, a string, or the full options (subtitle,
120
+ * description, icon, position, alignment, typography, box, show, actions,
121
+ * pass-through, className). Reserved inside the frame, selectable, themed,
122
+ * persisted by `toJSON()`; live through `setCaption()`. Default: none.
123
+ */
124
+ caption?: SectionCaption;
116
125
  /**
117
126
  * Container only, split layout: the authored splitter tree. Omit it and the
118
127
  * tree is derived from the children's cells. `toJSON()` writes it back.
@@ -258,6 +267,15 @@ export interface DashboardOptions {
258
267
  * press on a section's empty band selects the section.
259
268
  */
260
269
  onSelect?: (id: string | undefined, viewId: string) => void;
270
+ /**
271
+ * Paint a section's caption band yourself (the escape hatch `renderWidget`
272
+ * is for cards): the band arrives empty, sized and themed, and keeps its
273
+ * press rules — a press on it selects the section, a pass-through element
274
+ * (a button, an input, `[data-axdb-pass]`) reaches your content.
275
+ */
276
+ renderCaption?: (widget: DashboardWidgetSpec, host: HTMLElement) => void;
277
+ /** A press on a caption action (`caption.actions`): the section, the action id, the view. */
278
+ onCaptionAction?: (sectionId: string, actionId: string, viewId: string) => void;
261
279
  /** Fires after any committed gesture, with the view whose layout changed. */
262
280
  onLayoutChange?: (viewId: string, widgets: DashboardWidgetSpec[]) => void;
263
281
  /** Extra binder options, merged last (escape hatch to the layer below). */
@@ -325,6 +343,15 @@ export interface DashboardHandle {
325
343
  */
326
344
  setLayout(layout: 'grid' | 'split', viewId?: string): void;
327
345
  getLayout(viewId?: string): 'grid' | 'split';
346
+ /**
347
+ * Set a SECTION's caption live — `false` removes it, `true` is the title, a
348
+ * string or the options. Repaints the band, gives the reserve back or takes
349
+ * it, persists on the section, one undo step. False for anything that is
350
+ * not a container.
351
+ */
352
+ setCaption(id: string, caption: SectionCaption): boolean;
353
+ /** The caption as authored or last set; undefined when the section has none. */
354
+ getCaption(id: string): SectionCaption | undefined;
328
355
  /** Live sizing/float switches — the two prototype toggles. */
329
356
  setSizing(mode: 'fit' | 'grow'): void;
330
357
  getSizing(): 'fit' | 'grow';
@@ -500,18 +527,6 @@ export interface DashboardApiRef {
500
527
  onChange?(listener: (state: unknown) => void): () => void;
501
528
  };
502
529
  }
503
- /**
504
- * Everything one `DashboardHandle` closes over, gathered into ONE object so a
505
- * single builder can serve both `dashboard()` (context built from the authored
506
- * literal) and `fromDocument()` (context reconstructed from the loaded model).
507
- *
508
- * `active` and `apiRef` are the two MUTABLE cells: the handle READS them on
509
- * every call and the builder WRITES them (showView reassigns `active`, the
510
- * caller's finalize sets `apiRef`). They live here rather than as free `let`s
511
- * precisely because there are now two call sites — a boxed cell one builder
512
- * reads and writes is the whole reason a second handle implementation, which
513
- * would silently drift, is not needed.
514
- */
515
530
  export interface DashboardHandleContext {
516
531
  /** The views — MUTATED in place by addWidget (push) and remove (filter). */
517
532
  views: DashboardViewSpec[];
@@ -290,6 +290,37 @@ function assignCells(widgets, columns) {
290
290
  w.rows = rows;
291
291
  }
292
292
  }
293
+ /**
294
+ * Everything one `DashboardHandle` closes over, gathered into ONE object so a
295
+ * single builder can serve both `dashboard()` (context built from the authored
296
+ * literal) and `fromDocument()` (context reconstructed from the loaded model).
297
+ *
298
+ * `active` and `apiRef` are the two MUTABLE cells: the handle READS them on
299
+ * every call and the builder WRITES them (showView reassigns `active`, the
300
+ * caller's finalize sets `apiRef`). They live here rather than as free `let`s
301
+ * precisely because there are now two call sites — a boxed cell one builder
302
+ * reads and writes is the whole reason a second handle implementation, which
303
+ * would silently drift, is not needed.
304
+ */
305
+ /** `setCaption` as one history step: execute re-applies the value, undo the previous one. */
306
+ class SetCaptionCommand extends Command {
307
+ constructor(sectionId, before, after, apply) {
308
+ super('Set section caption');
309
+ this.sectionId = sectionId;
310
+ this.before = before;
311
+ this.after = after;
312
+ this.apply = apply;
313
+ }
314
+ execute() {
315
+ this.apply(this.after);
316
+ }
317
+ undo() {
318
+ this.apply(this.before);
319
+ }
320
+ serialize() {
321
+ return { id: this.id, name: this.name, timestamp: this.timestamp, data: { sectionId: this.sectionId, before: this.before, after: this.after } };
322
+ }
323
+ }
293
324
  /**
294
325
  * Build THE `DashboardHandle` — the one and only implementation, shared by
295
326
  * `dashboard()` and `fromDocument()`. Reads/writes the mutable `ctx.active` /
@@ -564,6 +595,41 @@ export function createDashboardHandle(ctx) {
564
595
  reportChanged();
565
596
  },
566
597
  getLayout: (viewId) => { var _a; return (_a = ctx.layoutOf.get(viewId !== null && viewId !== void 0 ? viewId : ctx.active)) !== null && _a !== void 0 ? _a : 'grid'; },
598
+ setCaption(id, caption) {
599
+ const cg = ctx.boardGroups.get(id);
600
+ const w = specById.get(id);
601
+ if (!cg || !w || !w.widgets || views.some((v) => v.id === id))
602
+ return false;
603
+ const before = w.caption;
604
+ if (JSON.stringify(before !== null && before !== void 0 ? before : null) === JSON.stringify(caption !== null && caption !== void 0 ? caption : null))
605
+ return true;
606
+ const apply = (c) => {
607
+ var _a, _b, _c, _d, _e;
608
+ if (c === undefined)
609
+ delete w.caption;
610
+ else
611
+ w.caption = c;
612
+ const cw = (_a = cg.getMetadata('containerWidget')) !== null && _a !== void 0 ? _a : {};
613
+ const next = Object.assign({}, cw);
614
+ if (c === undefined)
615
+ delete next['caption'];
616
+ else
617
+ next['caption'] = c;
618
+ cg.setMetadata('containerWidget', next);
619
+ // The section's own board re-projects under its new frame; the parent
620
+ // repaints the slab (the band lives there).
621
+ (_b = binders.get(id)) === null || _b === void 0 ? void 0 : _b.sync();
622
+ (_d = binders.get((_c = viewOfWidget.get(id)) !== null && _c !== void 0 ? _c : '')) === null || _d === void 0 ? void 0 : _d.sync();
623
+ (_e = ctx.apiRef) === null || _e === void 0 ? void 0 : _e.renderNow();
624
+ };
625
+ // Applied NOW (a caller reads the band right after), then recorded as
626
+ // one history step whose execute re-applies the same value.
627
+ apply(caption);
628
+ execCommand(new SetCaptionCommand(id, before, caption, apply));
629
+ reportChanged();
630
+ return true;
631
+ },
632
+ getCaption: (id) => { var _a; return (_a = specById.get(id)) === null || _a === void 0 ? void 0 : _a.caption; },
567
633
  setSizing(mode) {
568
634
  var _a;
569
635
  for (const b of binders.values())
@@ -1194,7 +1260,7 @@ export function dashboard(options) {
1194
1260
  cg.setMetadata('gridItem', gridItemFromCell({ x: w.x, y: w.y, w: w.span, h: w.rows }));
1195
1261
  // The container's own spec fields, persisted ON the group — a
1196
1262
  // reloaded document has no authored literal to read them from.
1197
- cg.setMetadata('containerWidget', Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, (w.kind !== undefined ? { kind: w.kind } : {})), (w.title !== undefined ? { title: w.title } : {})), { columns: innerColumns, maxRows: innerRows }), (w.data !== undefined ? { data: w.data } : {})), (w.layout !== undefined ? { layout: w.layout } : {})), (w.sizing !== undefined ? { sizing: w.sizing } : {})));
1263
+ cg.setMetadata('containerWidget', Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, (w.kind !== undefined ? { kind: w.kind } : {})), (w.title !== undefined ? { title: w.title } : {})), { columns: innerColumns, maxRows: innerRows }), (w.data !== undefined ? { data: w.data } : {})), (w.layout !== undefined ? { layout: w.layout } : {})), (w.sizing !== undefined ? { sizing: w.sizing } : {})), (w.caption !== undefined ? { caption: w.caption } : {})));
1198
1264
  // Item 7: the container's own layout and bound, persisted like a view's.
1199
1265
  ctx.layoutOf.set(w.id, (_c = w.layout) !== null && _c !== void 0 ? _c : 'grid');
1200
1266
  if (w.layout === 'split' && w.tree !== undefined)
@@ -1257,14 +1323,27 @@ export function dashboard(options) {
1257
1323
  * switched to static, RTL or drag-by-header LIVE snapped back to its
1258
1324
  * authored options the moment its layout changed (s34 caught it).
1259
1325
  */
1326
+ /** The caption escape hatches, with the spec and the view filled in. */
1327
+ function captionHooks(viewId) {
1328
+ const render = options.renderCaption;
1329
+ return Object.assign(Object.assign({}, (render
1330
+ ? {
1331
+ renderCaption: (sectionId, host) => {
1332
+ const spec = specById.get(sectionId);
1333
+ if (spec)
1334
+ render(spec, host);
1335
+ },
1336
+ }
1337
+ : {})), { onCaptionAction: (sectionId, actionId) => { var _a; return (_a = options.onCaptionAction) === null || _a === void 0 ? void 0 : _a.call(options, sectionId, actionId, viewId); } });
1338
+ }
1260
1339
  function bindView(v, g, viewLayout, live) {
1261
1340
  var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
1262
- const common = Object.assign(Object.assign(Object.assign({ gap, padding: gap, rtl: (_b = (_a = live === null || live === void 0 ? void 0 : live.rtl) !== null && _a !== void 0 ? _a : options.rtl) !== null && _b !== void 0 ? _b : false, fluid: mode === 'fluid', static: (_d = (_c = live === null || live === void 0 ? void 0 : live.static) !== null && _c !== void 0 ? _c : options.static) !== null && _d !== void 0 ? _d : false, dragHandle: (_f = (_e = live === null || live === void 0 ? void 0 : live.dragHandle) !== null && _e !== void 0 ? _e : options.dragHandle) !== null && _f !== void 0 ? _f : false }, (options.squeeze !== undefined ? { squeeze: options.squeeze } : {})), ((_g = options.binder) !== null && _g !== void 0 ? _g : {})), { onGesture: (e) => {
1341
+ const common = Object.assign(Object.assign(Object.assign(Object.assign({ gap, padding: gap, rtl: (_b = (_a = live === null || live === void 0 ? void 0 : live.rtl) !== null && _a !== void 0 ? _a : options.rtl) !== null && _b !== void 0 ? _b : false, fluid: mode === 'fluid', static: (_d = (_c = live === null || live === void 0 ? void 0 : live.static) !== null && _c !== void 0 ? _c : options.static) !== null && _d !== void 0 ? _d : false, dragHandle: (_f = (_e = live === null || live === void 0 ? void 0 : live.dragHandle) !== null && _e !== void 0 ? _e : options.dragHandle) !== null && _f !== void 0 ? _f : false }, (options.squeeze !== undefined ? { squeeze: options.squeeze } : {})), ((_g = options.binder) !== null && _g !== void 0 ? _g : {})), { onGesture: (e) => {
1263
1342
  var _a, _b;
1264
1343
  if (e.type === 'commit')
1265
1344
  reportChanged();
1266
1345
  (_b = (_a = options.binder) === null || _a === void 0 ? void 0 : _a.onGesture) === null || _b === void 0 ? void 0 : _b.call(_a, e);
1267
- }, onSelect: (id) => { var _a; return (_a = options.onSelect) === null || _a === void 0 ? void 0 : _a.call(options, id, v.id); } });
1346
+ }, onSelect: (id) => { var _a; return (_a = options.onSelect) === null || _a === void 0 ? void 0 : _a.call(options, id, v.id); } }), captionHooks(v.id));
1268
1347
  if (viewLayout === 'split') {
1269
1348
  return bindDashboardSplit(a, g, Object.assign(Object.assign(Object.assign({}, common), { columns: (_h = v.columns) !== null && _h !== void 0 ? _h : columns, baseRowHeight: rowHeight, designHeight: viewH(v) }), (v.tree !== undefined ? { tree: v.tree } : {})));
1270
1349
  }
@@ -1272,18 +1351,18 @@ export function dashboard(options) {
1272
1351
  }
1273
1352
  /** Bind (or re-bind) a container's inner grid on its group. */
1274
1353
  function bindContainer(cg, w, viewId, innerRows) {
1275
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
1354
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
1276
1355
  void viewId;
1277
1356
  // The LIVE switches ride on the view's binder when it exists (a
1278
1357
  // container bound after a setStatic/setRtl/setDragHandle must match).
1279
1358
  const vb = binders.get((_a = ctx.viewOfBoard.get(w.id)) !== null && _a !== void 0 ? _a : ctx.active);
1280
- const inner = Object.assign(Object.assign({ columns: innerColumnsOf(w), gap, padding: 0, baseRowHeight: rowHeight, rtl: (_c = (_b = vb === null || vb === void 0 ? void 0 : vb.getRtl()) !== null && _b !== void 0 ? _b : options.rtl) !== null && _c !== void 0 ? _c : false, static: (_e = (_d = vb === null || vb === void 0 ? void 0 : vb.getStatic()) !== null && _d !== void 0 ? _d : options.static) !== null && _e !== void 0 ? _e : false, dragHandle: (_g = (_f = vb === null || vb === void 0 ? void 0 : vb.getDragHandle()) !== null && _f !== void 0 ? _f : options.dragHandle) !== null && _g !== void 0 ? _g : false }, (options.squeeze !== undefined ? { squeeze: options.squeeze } : {})), { onGesture: (e) => {
1359
+ const inner = Object.assign(Object.assign(Object.assign({ columns: innerColumnsOf(w), gap, padding: 0, baseRowHeight: rowHeight, rtl: (_c = (_b = vb === null || vb === void 0 ? void 0 : vb.getRtl()) !== null && _b !== void 0 ? _b : options.rtl) !== null && _c !== void 0 ? _c : false, static: (_e = (_d = vb === null || vb === void 0 ? void 0 : vb.getStatic()) !== null && _d !== void 0 ? _d : options.static) !== null && _e !== void 0 ? _e : false, dragHandle: (_g = (_f = vb === null || vb === void 0 ? void 0 : vb.getDragHandle()) !== null && _f !== void 0 ? _f : options.dragHandle) !== null && _g !== void 0 ? _g : false }, (options.squeeze !== undefined ? { squeeze: options.squeeze } : {})), { onGesture: (e) => {
1281
1360
  var _a, _b;
1282
1361
  if (e.type === 'commit')
1283
1362
  reportChanged();
1284
1363
  (_b = (_a = options.binder) === null || _a === void 0 ? void 0 : _a.onGesture) === null || _b === void 0 ? void 0 : _b.call(_a, e);
1285
- }, onSelect: (id) => { var _a, _b; return (_a = options.onSelect) === null || _a === void 0 ? void 0 : _a.call(options, id, (_b = ctx.viewOfBoard.get(w.id)) !== null && _b !== void 0 ? _b : ctx.active); } });
1286
- if (((_h = ctx.layoutOf.get(w.id)) !== null && _h !== void 0 ? _h : w.layout) === 'split') {
1364
+ }, onSelect: (id) => { var _a, _b; return (_a = options.onSelect) === null || _a === void 0 ? void 0 : _a.call(options, id, (_b = ctx.viewOfBoard.get(w.id)) !== null && _b !== void 0 ? _b : ctx.active); } }), captionHooks((_h = ctx.viewOfBoard.get(w.id)) !== null && _h !== void 0 ? _h : ctx.active));
1365
+ if (((_j = ctx.layoutOf.get(w.id)) !== null && _j !== void 0 ? _j : w.layout) === 'split') {
1287
1366
  // A splitter tree covering the pane; the pane's frame is the parent's
1288
1367
  // slab, so no design height of its own.
1289
1368
  binders.set(w.id, bindDashboardSplit(a, cg, Object.assign(Object.assign({}, inner), (w.tree !== undefined ? { tree: w.tree } : {}))));
@@ -1292,7 +1371,7 @@ export function dashboard(options) {
1292
1371
  binders.set(w.id, bindDashboardGrid(a, cg, Object.assign(Object.assign({}, inner), { sizing: 'fit', designHeight: 0,
1293
1372
  // The DESIGN: authored, else what the group was mounted with, else
1294
1373
  // the children's extent — the binder's live bound follows the cells.
1295
- maxRows: (_l = (_j = innerRows !== null && innerRows !== void 0 ? innerRows : w.maxRows) !== null && _j !== void 0 ? _j : (_k = cg.getMetadata('containerWidget')) === null || _k === void 0 ? void 0 : _k.maxRows) !== null && _l !== void 0 ? _l : rowExtentOf((_m = w.widgets) !== null && _m !== void 0 ? _m : []), float: false, escalate: w.sizing !== 'fit' })));
1374
+ maxRows: (_m = (_k = innerRows !== null && innerRows !== void 0 ? innerRows : w.maxRows) !== null && _k !== void 0 ? _k : (_l = cg.getMetadata('containerWidget')) === null || _l === void 0 ? void 0 : _l.maxRows) !== null && _m !== void 0 ? _m : rowExtentOf((_o = w.widgets) !== null && _o !== void 0 ? _o : []), float: false, escalate: w.sizing !== 'fit' })));
1296
1375
  }
1297
1376
  /**
1298
1377
  * One reporter for every binder on a view — the view's own and each
@@ -198,6 +198,14 @@ export interface DashboardGridOptions {
198
198
  /** Inject the hover-revealed corner resize handle into member hosts (default true). */
199
199
  /** The selection on this board changed: the selected member — a widget or a SECTION (container) — or undefined. */
200
200
  onSelect?: (id: string | undefined) => void;
201
+ /**
202
+ * SECTION CAPTIONS (0.4.22). Paint a member section's caption band yourself:
203
+ * the band is handed over empty, sized and themed, and keeps its press rules.
204
+ * Default: the kit's icon · text · subtitle · ⓘ · actions.
205
+ */
206
+ renderCaption?: (sectionId: string, host: HTMLElement) => void;
207
+ /** A press on a caption action button (see `SectionCaptionOptions.actions`). */
208
+ onCaptionAction?: (sectionId: string, actionId: string) => void;
201
209
  resizeHandles?: boolean;
202
210
  /**
203
211
  * FLUID board: the group's frame follows the CANVAS CONTAINER — width
@@ -513,14 +521,19 @@ interface AdoptedLeg {
513
521
  * unregister. A split board adopts nothing and grows no slab: its `adopt`
514
522
  * answers null and `resizeMemberBy` answers unchanged.
515
523
  */
524
+ /** The board on the canvas that holds `groupId` as a member — a nested board's parent. */
525
+ export declare function parentPeerOf(container: HTMLElement, groupId: string): BinderPeer | null;
516
526
  /** Clear the selection on every OTHER board of the canvas — one selection per canvas. */
517
527
  export declare function clearOtherSelections(container: HTMLElement, self: BinderPeer | null): void;
518
528
  export declare function registerBoardPeer(container: HTMLElement, peer: BinderPeer): () => void;
519
529
  export type { BinderPeer };
520
- interface ResizeEdges {
530
+ /** A press this close (CSS px) to a tile's border takes that edge for a resize. */
531
+ export declare const EDGE_GRIP = 7;
532
+ export interface ResizeEdges {
521
533
  n: boolean;
522
534
  e: boolean;
523
535
  s: boolean;
524
536
  w: boolean;
525
537
  }
538
+ export declare const anyEdge: (E: ResizeEdges) => boolean;
526
539
  export declare function bindDashboardGrid(api: DashboardGridApi, group: GroupModel, options?: DashboardGridOptions): DashboardGridHandle;
@@ -54,6 +54,7 @@ import { AddToGroupCommand, BatchCommand, Command, GridPackEngine, RemoveFromGro
54
54
  import { LiveRegionController, registerTool } from '@grafloria/renderer';
55
55
  import { buildCommitCommands, cellFromGridItem, cellToRect, columnUnitFor, gridItemFromCell, pointToCell, rowHeightFor, sizeToSpan, } from './grid-mapping.js';
56
56
  import { ensureDashboardKitStyles } from './styles.js';
57
+ import { captionOfGroup, captionPainted, captionPassThrough, captionKey, captionReserve, paintCaptionBand, sizeCaptionBand } from './caption.js';
57
58
  /** The class of the painted grip element (a child of the node host). */
58
59
  export const GRIP_CLASS = 'axdb-grip';
59
60
  /** The container class that turns the caption strip's grip dots on. */
@@ -103,12 +104,24 @@ export function syncGrip(host, cfg, movable) {
103
104
  * diagram's object, not a namesake from another model.
104
105
  */
105
106
  export function ownsPress(container, diagram, ev, hit) {
106
- var _a;
107
+ var _a, _b, _c, _d;
107
108
  const t = (_a = ev.source) === null || _a === void 0 ? void 0 : _a.target;
108
109
  if (typeof Node !== 'undefined' && t instanceof Node && !container.contains(t))
109
110
  return false;
110
111
  if (hit.node && diagram.getNode(hit.node.id) !== hit.node)
111
112
  return false;
113
+ // A press on a PASS-THROUGH element of a caption band (a button, an input,
114
+ // anything the caption's `passThrough` names) is the content's, not any
115
+ // tool's: no selection, no drag, no resize — the DOM handles it.
116
+ if (typeof Element !== 'undefined' && t instanceof Element) {
117
+ const band = t.closest('.axdb-slab > .axdb-slab-h');
118
+ const sid = (_b = band === null || band === void 0 ? void 0 : band.parentElement) === null || _b === void 0 ? void 0 : _b.getAttribute('data-slab-id');
119
+ if (band && sid) {
120
+ const grp = (_d = (_c = diagram).getGroup) === null || _d === void 0 ? void 0 : _d.call(_c, sid);
121
+ if (captionPassThrough(t, band, captionOfGroup(grp)))
122
+ return false;
123
+ }
124
+ }
112
125
  return true;
113
126
  }
114
127
  export function gripHostOf(target) {
@@ -153,6 +166,14 @@ const BOARD_REGISTRY = new WeakMap();
153
166
  * unregister. A split board adopts nothing and grows no slab: its `adopt`
154
167
  * answers null and `resizeMemberBy` answers unchanged.
155
168
  */
169
+ /** The board on the canvas that holds `groupId` as a member — a nested board's parent. */
170
+ export function parentPeerOf(container, groupId) {
171
+ var _a;
172
+ for (const p of (_a = BOARD_REGISTRY.get(container)) !== null && _a !== void 0 ? _a : [])
173
+ if (p.group.id !== groupId && p.hasItem(groupId))
174
+ return p;
175
+ return null;
176
+ }
156
177
  /** Clear the selection on every OTHER board of the canvas — one selection per canvas. */
157
178
  export function clearOtherSelections(container, self) {
158
179
  var _a, _b;
@@ -241,7 +262,7 @@ class SetGroupCellCommand extends Command {
241
262
  const DRAG_THRESHOLD = 4;
242
263
  const GLIDE_OFF_DELAY = 400;
243
264
  /** A press this close (CSS px) to a tile's border takes that edge for a resize. */
244
- const EDGE_GRIP = 7;
265
+ export const EDGE_GRIP = 7;
245
266
  const NO_EDGES = { n: false, e: false, s: false, w: false };
246
267
  /** Which of a host's edges a client point is within EDGE_GRIP of (none when outside). */
247
268
  function edgesNear(host, cx, cy) {
@@ -255,7 +276,7 @@ function edgesNear(host, cx, cy) {
255
276
  e: r.right - cx <= EDGE_GRIP,
256
277
  };
257
278
  }
258
- const anyEdge = (E) => E.n || E.e || E.s || E.w;
279
+ export const anyEdge = (E) => E.n || E.e || E.s || E.w;
259
280
  /** The resize cursor for a set of edges ('' when none). */
260
281
  function cursorFor(E) {
261
282
  const v = E.n || E.s;
@@ -426,14 +447,22 @@ export function bindDashboardGrid(api, group, options = {}) {
426
447
  let adoptedGhostId = null;
427
448
  let glideTimer = null;
428
449
  let ghostTimer = null;
450
+ /**
451
+ * OUR OWN CAPTION RESERVE: a section carrying a caption gives the band's
452
+ * pixels up at the top of its frame, so no child may take them — and the
453
+ * band is outside `containsWorld`, so a press on it is the PARENT's (it
454
+ * selects the section) rather than an empty press of this board.
455
+ */
456
+ const ownReserve = () => { var _a, _b; return captionReserve(captionOfGroup(group), { static: isStatic, sectionH: (_b = (_a = group.size) === null || _a === void 0 ? void 0 : _a.height) !== null && _b !== void 0 ? _b : 0 }); };
429
457
  const frame = () => {
430
458
  var _a, _b, _c, _d;
431
- return ({
459
+ const r = ownReserve();
460
+ return {
432
461
  x: group.position.x,
433
- y: group.position.y,
462
+ y: group.position.y + r,
434
463
  width: (_b = (_a = group.size) === null || _a === void 0 ? void 0 : _a.width) !== null && _b !== void 0 ? _b : 0,
435
- height: (_d = (_c = group.size) === null || _c === void 0 ? void 0 : _c.height) !== null && _d !== void 0 ? _d : 0,
436
- });
464
+ height: Math.max(0, ((_d = (_c = group.size) === null || _c === void 0 ? void 0 : _c.height) !== null && _d !== void 0 ? _d : 0) - r),
465
+ };
437
466
  };
438
467
  /** Entity size with GroupModel's optionality flattened away. */
439
468
  const sizeOf = (e) => { var _a; return (_a = e.size) !== null && _a !== void 0 ? _a : { width: 0, height: 0 }; };
@@ -613,6 +642,12 @@ export function bindDashboardGrid(api, group, options = {}) {
613
642
  writing = false;
614
643
  }
615
644
  syncPlaceholder();
645
+ // The section overlays are projected chrome like the placeholder: they
646
+ // follow every frame write, not only a rebuild. Painted at bind time only,
647
+ // a section two levels down kept the geometry of its parent's placeholder
648
+ // frame (100 × 34) after the view board had laid the parent out (the kit
649
+ // lab's L47, 2026-09-08).
650
+ syncSlabs();
616
651
  };
617
652
  // -- placeholder / ghost chrome --------------------------------------------
618
653
  /** The placeholder exists ONLY while a gesture is live — so at any moment
@@ -1137,6 +1172,7 @@ export function bindDashboardGrid(api, group, options = {}) {
1137
1172
  el.classList.toggle('axdb-slab--selected', selectedId === id);
1138
1173
  el.classList.toggle('axdb-slab--static', isStatic);
1139
1174
  (_b = el.querySelector(':scope > .axdb-rs')) === null || _b === void 0 ? void 0 : _b.classList.toggle('axdb-rs--rtl', rtl);
1175
+ syncCaption(el, id, grp, sz.height);
1140
1176
  }
1141
1177
  for (const [id, el] of slabEls) {
1142
1178
  if (!seen.has(id)) {
@@ -1145,6 +1181,38 @@ export function bindDashboardGrid(api, group, options = {}) {
1145
1181
  }
1146
1182
  }
1147
1183
  };
1184
+ /**
1185
+ * THE CAPTION BAND of a section, on its slab overlay. Painted from the
1186
+ * group's persisted caption; repainted only when its identity changes (the
1187
+ * options, RTL, static, the tier) so a custom `renderCaption` is not run
1188
+ * per frame. The band takes the pointer (the slab itself does not): a press
1189
+ * on it selects the section, an action fires, pass-through reaches content.
1190
+ */
1191
+ const syncCaption = (el, id, grp, sectionH) => {
1192
+ const cap = captionOfGroup(grp);
1193
+ let band = el.querySelector(':scope > .axdb-slab-h');
1194
+ if (!cap || !captionPainted(cap, isStatic)) {
1195
+ band === null || band === void 0 ? void 0 : band.remove();
1196
+ el.removeAttribute('aria-label');
1197
+ return;
1198
+ }
1199
+ const ctx = { rtl, static: isStatic, sectionH };
1200
+ const key = captionKey(cap, ctx);
1201
+ if (band && band.getAttribute('data-key') === key) {
1202
+ sizeCaptionBand(band, cap, sectionH); // the tier follows the live size
1203
+ return;
1204
+ }
1205
+ band === null || band === void 0 ? void 0 : band.remove();
1206
+ band = document.createElement('div');
1207
+ el.prepend(band);
1208
+ const render = options.renderCaption;
1209
+ paintCaptionBand(band, cap, Object.assign(Object.assign(Object.assign({}, ctx), (render ? { render: (host) => render(id, host) } : {})), { onAction: (actionId) => { var _a; return (_a = options.onCaptionAction) === null || _a === void 0 ? void 0 : _a.call(options, id, actionId); } }));
1210
+ band.setAttribute('data-key', key);
1211
+ if (cap.text)
1212
+ el.setAttribute('aria-label', cap.text);
1213
+ else
1214
+ el.removeAttribute('aria-label');
1215
+ };
1148
1216
  const insideMemberGroupFrame = (x, y) => {
1149
1217
  var _a;
1150
1218
  for (const id of (_a = group.members) !== null && _a !== void 0 ? _a : []) {
@@ -2127,15 +2195,22 @@ export function bindDashboardGrid(api, group, options = {}) {
2127
2195
  id: `dashboard-grid:${group.id}:${++binderSeq}`,
2128
2196
  priority: 2, // point-specific claim — outranks mode-style tools (see ext/tools.ts)
2129
2197
  hitTest(ev, hit) {
2130
- var _a, _b;
2198
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j;
2131
2199
  if (disposed)
2132
2200
  return false;
2133
2201
  if (gesture || slabGesture || forwardSlab)
2134
2202
  return true; // own the rest of an in-flight gesture
2135
2203
  if (!ownsPress(api.container, diagram, ev, hit))
2136
2204
  return false;
2205
+ // A press on one of OUR sections' caption bands is ours by the DOM: a
2206
+ // 'tab' band sits above the frame, over the gap or the tile above,
2207
+ // where the geometry says otherwise.
2208
+ 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');
2209
+ const bandId = (_d = bandTarget === null || bandTarget === void 0 ? void 0 : bandTarget.parentElement) === null || _d === void 0 ? void 0 : _d.getAttribute('data-slab-id');
2210
+ if (bandId && ((_e = group.members) !== null && _e !== void 0 ? _e : new Set()).has(bandId))
2211
+ return true;
2137
2212
  if (hit.node) {
2138
- if (((_a = group.members) !== null && _a !== void 0 ? _a : new Set()).has(hit.node.id))
2213
+ if (((_f = group.members) !== null && _f !== void 0 ? _f : new Set()).has(hit.node.id))
2139
2214
  return true;
2140
2215
  // A press on a tile that belongs to a NESTED board must reach that
2141
2216
  // board's tool. The dead-zone claim below deadens the slab's EMPTY
@@ -2144,12 +2219,23 @@ export function bindDashboardGrid(api, group, options = {}) {
2144
2219
  // parent's tool won the registration-order tie and the child's resize
2145
2220
  // never armed; grid-options binds parent-first and worked by
2146
2221
  // accident).
2147
- for (const p of (_b = BOARD_REGISTRY.get(api.container)) !== null && _b !== void 0 ? _b : []) {
2222
+ for (const p of (_g = BOARD_REGISTRY.get(api.container)) !== null && _g !== void 0 ? _g : []) {
2148
2223
  if (p !== selfPeer && p.hasItem(hit.node.id))
2149
2224
  return false;
2150
2225
  }
2151
2226
  return insideMemberGroupFrame(ev.world.x, ev.world.y);
2152
2227
  }
2228
+ // An EMPTY press inside a NESTED board's frame is that board's: its
2229
+ // dividers, its band, its own section press (which it hands back up).
2230
+ // Ties went to the first registered tool, and a section re-bound by a
2231
+ // layout switch registers AFTER its parent — so the parent took every
2232
+ // divider press in a split section (dead dividers) and, near the
2233
+ // section's edge, turned it into a section resize (the width changed
2234
+ // while a control's height was being dragged — Quantia, Groups page).
2235
+ for (const p of (_h = BOARD_REGISTRY.get(api.container)) !== null && _h !== void 0 ? _h : []) {
2236
+ if (p !== selfPeer && ((_j = group.members) !== null && _j !== void 0 ? _j : new Set()).has(p.group.id) && p.containsWorld(ev.world.x, ev.world.y))
2237
+ return false;
2238
+ }
2153
2239
  // Claim (and deaden) empty presses inside a member group's frame so the
2154
2240
  // built-in group-drag cannot fight the pack layout for the KPI slab —
2155
2241
  // and empty presses on the BOARD itself: its group is a layout
@@ -2159,7 +2245,7 @@ export function bindDashboardGrid(api, group, options = {}) {
2159
2245
  return insideMemberGroupFrame(ev.world.x, ev.world.y) || worldInsideBoard(ev.world.x, ev.world.y);
2160
2246
  },
2161
2247
  onPointerDown(ev, hit) {
2162
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t;
2248
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y;
2163
2249
  if (gesture)
2164
2250
  return; // mid-palette
2165
2251
  const target = ((_b = (_a = ev.source) === null || _a === void 0 ? void 0 : _a.target) !== null && _b !== void 0 ? _b : null);
@@ -2172,12 +2258,21 @@ export function bindDashboardGrid(api, group, options = {}) {
2172
2258
  // A SECTION's corner handle sits on top of whatever tile shares that
2173
2259
  // corner; the DOM target names the section, the hit test the tile.
2174
2260
  const sectionHandle = (_e = target === null || target === void 0 ? void 0 : target.closest) === null || _e === void 0 ? void 0 : _e.call(target, '.axdb-slab > .axdb-rs');
2175
- if ((!hit.node && !onGrip) || sectionHandle) {
2176
- // A press on a SECTION its empty band, its corner handle or its
2177
- // frame edge selects the section; the handle or an edge resizes it.
2178
- const slabHandle = (_f = target === null || target === void 0 ? void 0 : target.closest) === null || _f === void 0 ? void 0 : _f.call(target, '.axdb-slab > .axdb-rs');
2179
- const slabId = (_h = (_g = slabHandle === null || slabHandle === void 0 ? void 0 : slabHandle.parentElement) === null || _g === void 0 ? void 0 : _g.getAttribute('data-slab-id')) !== null && _h !== void 0 ? _h : memberGroupAt(ev.world.x, ev.world.y);
2180
- const grp = slabId && ((_j = group.members) !== null && _j !== void 0 ? _j : new Set()).has(slabId) ? diagram.getGroup(slabId) : undefined;
2261
+ // A SECTION's CAPTION BAND names its section the same way — a 'tab'
2262
+ // band sits above the frame, over the gap or the tile above. An action
2263
+ // button in it fires and does nothing else.
2264
+ const captionBand = (_f = target === null || target === void 0 ? void 0 : target.closest) === null || _f === void 0 ? void 0 : _f.call(target, '.axdb-slab > .axdb-slab-h');
2265
+ const captionId = (_h = (_g = captionBand === null || captionBand === void 0 ? void 0 : captionBand.parentElement) === null || _g === void 0 ? void 0 : _g.getAttribute('data-slab-id')) !== null && _h !== void 0 ? _h : null;
2266
+ const ownCaption = !!captionId && ((_j = group.members) !== null && _j !== void 0 ? _j : new Set()).has(captionId);
2267
+ // (An action button never reaches here: it is pass-through, and its own
2268
+ // `click` fires onCaptionAction — see captionPassThrough.)
2269
+ if ((!hit.node && !onGrip) || sectionHandle || ownCaption) {
2270
+ // A press on a SECTION — its empty band, its caption, its corner
2271
+ // handle or its frame edge — selects the section; the handle or an
2272
+ // edge resizes it.
2273
+ const slabHandle = (_k = target === null || target === void 0 ? void 0 : target.closest) === null || _k === void 0 ? void 0 : _k.call(target, '.axdb-slab > .axdb-rs');
2274
+ const slabId = (_o = (_m = (_l = slabHandle === null || slabHandle === void 0 ? void 0 : slabHandle.parentElement) === null || _l === void 0 ? void 0 : _l.getAttribute('data-slab-id')) !== null && _m !== void 0 ? _m : (ownCaption ? captionId : null)) !== null && _o !== void 0 ? _o : memberGroupAt(ev.world.x, ev.world.y);
2275
+ const grp = slabId && ((_p = group.members) !== null && _p !== void 0 ? _p : new Set()).has(slabId) ? diagram.getGroup(slabId) : undefined;
2181
2276
  if (slabId && grp) {
2182
2277
  selectWidget(slabId);
2183
2278
  api.render();
@@ -2196,7 +2291,7 @@ export function bindDashboardGrid(api, group, options = {}) {
2196
2291
  // forwards the pointer sequence.
2197
2292
  const parent = parentPeer();
2198
2293
  if ((parent === null || parent === void 0 ? void 0 : parent.selectMember) && worldInsideBoard(ev.world.x, ev.world.y)) {
2199
- const ownHandle = ((_k = slabHandle === null || slabHandle === void 0 ? void 0 : slabHandle.parentElement) === null || _k === void 0 ? void 0 : _k.getAttribute('data-slab-id')) === group.id;
2294
+ const ownHandle = ((_q = slabHandle === null || slabHandle === void 0 ? void 0 : slabHandle.parentElement) === null || _q === void 0 ? void 0 : _q.getAttribute('data-slab-id')) === group.id;
2200
2295
  parent.selectMember(group.id);
2201
2296
  if (!isStatic && parent.beginSlabResize) {
2202
2297
  const edges = ownHandle
@@ -2209,7 +2304,7 @@ export function bindDashboardGrid(api, group, options = {}) {
2209
2304
  }
2210
2305
  // The board's own empty area: a void click. Nothing to drag, and the
2211
2306
  // selection clears exactly as a click outside any board would.
2212
- (_m = (_l = diagram).clearSelection) === null || _m === void 0 ? void 0 : _m.call(_l);
2307
+ (_s = (_r = diagram).clearSelection) === null || _s === void 0 ? void 0 : _s.call(_r);
2213
2308
  selectWidget(undefined);
2214
2309
  api.render();
2215
2310
  return;
@@ -2218,17 +2313,17 @@ export function bindDashboardGrid(api, group, options = {}) {
2218
2313
  if (!node)
2219
2314
  return;
2220
2315
  selectWidget(node.id); // a press selects, whether or not it starts a gesture
2221
- if (((_o = node.state) === null || _o === void 0 ? void 0 : _o.locked) === true)
2316
+ if (((_t = node.state) === null || _t === void 0 ? void 0 : _t.locked) === true)
2222
2317
  return; // pinned: refuse; click still focuses
2223
2318
  if (isStatic)
2224
2319
  return; // a static board: claimed and deadened, click still focuses
2225
2320
  // Which edges did the press take? The corner handle names its own (s+e,
2226
2321
  // or s+w on RTL); a bare press within EDGE_GRIP of the tile's border
2227
2322
  // takes that border; anywhere else is a move. A grip press is a move.
2228
- const onHandle = !!((_p = target === null || target === void 0 ? void 0 : target.closest) === null || _p === void 0 ? void 0 : _p.call(target, '.axdb-rs'));
2323
+ const onHandle = !!((_u = target === null || target === void 0 ? void 0 : target.closest) === null || _u === void 0 ? void 0 : _u.call(target, '.axdb-rs'));
2229
2324
  const hostEl = hostOf(node.id);
2230
- const resizable = ((_q = node.getMetadata) === null || _q === void 0 ? void 0 : _q.call(node, 'widgetResizable')) !== false;
2231
- const movable = ((_r = node.getMetadata) === null || _r === void 0 ? void 0 : _r.call(node, 'widgetMovable')) !== false;
2325
+ const resizable = ((_v = node.getMetadata) === null || _v === void 0 ? void 0 : _v.call(node, 'widgetResizable')) !== false;
2326
+ const movable = ((_w = node.getMetadata) === null || _w === void 0 ? void 0 : _w.call(node, 'widgetMovable')) !== false;
2232
2327
  // `ev.screen` is ELEMENT-LOCAL px; host rects are in client px. Compare
2233
2328
  // like with like — the source event's clientX/Y when there is one, else
2234
2329
  // the container's origin plus the local offset.
@@ -2276,7 +2371,7 @@ export function bindDashboardGrid(api, group, options = {}) {
2276
2371
  startSize: { width: node.size.width, height: node.size.height },
2277
2372
  startPos: { x: node.position.x, y: node.position.y },
2278
2373
  edges,
2279
- spans: { w: (_s = it === null || it === void 0 ? void 0 : it.w) !== null && _s !== void 0 ? _s : 1, h: (_t = it === null || it === void 0 ? void 0 : it.h) !== null && _t !== void 0 ? _t : 1 },
2374
+ spans: { w: (_x = it === null || it === void 0 ? void 0 : it.w) !== null && _x !== void 0 ? _x : 1, h: (_y = it === null || it === void 0 ? void 0 : it.h) !== null && _y !== void 0 ? _y : 1 },
2280
2375
  removedFromBoard: false,
2281
2376
  leg: null,
2282
2377
  lastWorld: null,
@@ -2813,6 +2908,9 @@ export function bindDashboardGrid(api, group, options = {}) {
2813
2908
  isStatic = on;
2814
2909
  if (gesture)
2815
2910
  cancelActiveGesture(false);
2911
+ // A `show: 'design'` caption leaves under static and its reserve with
2912
+ // it (or comes back): the frame moved, re-project the tiles.
2913
+ project();
2816
2914
  syncHandles();
2817
2915
  api.renderNow();
2818
2916
  },
@@ -3,5 +3,6 @@ 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 { 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';
6
7
  export { dashboard, type DashboardOptions, type DashboardSpec, type DashboardSnapshot, type DashboardHandle, type DashboardViewSpec, type DashboardWidgetSpec, type WidgetHandle, } from './dashboard.js';
7
8
  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 { normalizeCaption, captionReserve, captionBandHeight, paintCaptionBand, CAPTION_HEIGHT, CAPTION_HEIGHT_SUBTITLE, CAPTION_HEIGHT_TIGHT, CAPTION_PASS_THROUGH, } from './caption.js';
6
7
  // The DATA-FIRST authoring API (the erDiagram/umlDiagram equivalent).
7
8
  export { dashboard, } from './dashboard.js';
8
9
  // The built-in renderers behind `kind` — dashboard()'s default renderWidget.
@@ -25,10 +25,11 @@
25
25
  import { __awaiter } from "tslib";
26
26
  import { Command } from '@grafloria/engine';
27
27
  import { LiveRegionController, registerTool } from '@grafloria/renderer';
28
- import { clearOtherSelections, dragHandleSelector, gripHostOf, gripOf, normalizeDragHandle, ownsPress, pressOnDragHandle, registerBoardPeer, syncGrip, DRAG_HANDLE_CLASS } from './grid-binder.js';
28
+ import { anyEdge, clearOtherSelections, dragHandleSelector, gripHostOf, gripOf, normalizeDragHandle, ownsPress, parentPeerOf, pressOnDragHandle, registerBoardPeer, syncGrip, DRAG_HANDLE_CLASS, EDGE_GRIP } from './grid-binder.js';
29
29
  import { cellFromGridItem } from './grid-mapping.js';
30
30
  import { addSplitLeaf, cellsFromSplit, cloneSplit, dividersOf, groupRectsOf, insertSplitLeaf, moveSplitDivider, normalizeSplit, pathToLeaf, projectSplit, removeSplitLeaf, splitFromCells, splitLeaves, } from './split-layout.js';
31
31
  import { ensureDashboardKitStyles } from './styles.js';
32
+ import { captionOfGroup, captionReserve } from './caption.js';
32
33
  /** Group metadata key the tree persists under. */
33
34
  export const SPLIT_TREE_KEY = 'dashboardTree';
34
35
  /**
@@ -99,17 +100,22 @@ export function bindDashboardSplit(api, group, options = {}) {
99
100
  let designW = (_k = (_j = group.size) === null || _j === void 0 ? void 0 : _j.width) !== null && _k !== void 0 ? _k : 0;
100
101
  let disposed = false;
101
102
  let gesture = null;
103
+ /** The PARENT running a resize of OUR section from a press this tool claimed. */
104
+ let forwardSlab = null;
102
105
  let focusedId;
103
106
  const live = liveRegionFor(api.container);
104
107
  // -- geometry ---------------------------------------------------------------
108
+ /** Our own caption band's pixels, given up at the top of the pane (see grid-binder). */
109
+ const ownReserve = () => { var _a, _b; return captionReserve(captionOfGroup(group), { static: isStatic, sectionH: (_b = (_a = group.size) === null || _a === void 0 ? void 0 : _a.height) !== null && _b !== void 0 ? _b : 0 }); };
105
110
  const frame = () => {
106
111
  var _a, _b, _c, _d;
107
- return ({
112
+ const r = ownReserve();
113
+ return {
108
114
  x: group.position.x,
109
- y: group.position.y,
115
+ y: group.position.y + r,
110
116
  width: (_b = (_a = group.size) === null || _a === void 0 ? void 0 : _a.width) !== null && _b !== void 0 ? _b : designW,
111
- height: (_d = (_c = group.size) === null || _c === void 0 ? void 0 : _c.height) !== null && _d !== void 0 ? _d : designH,
112
- });
117
+ height: Math.max(0, ((_d = (_c = group.size) === null || _c === void 0 ? void 0 : _c.height) !== null && _d !== void 0 ? _d : designH) - r),
118
+ };
113
119
  };
114
120
  const containerBox = () => ({
115
121
  w: api.container.clientWidth || 0,
@@ -634,7 +640,7 @@ export function bindDashboardSplit(api, group, options = {}) {
634
640
  var _a;
635
641
  if (disposed)
636
642
  return false;
637
- if (gesture)
643
+ if (gesture || forwardSlab)
638
644
  return true;
639
645
  if (!ownsPress(api.container, diagram, ev, hit))
640
646
  return false;
@@ -643,7 +649,7 @@ export function bindDashboardSplit(api, group, options = {}) {
643
649
  return worldInsideBoard(ev.world.x, ev.world.y);
644
650
  },
645
651
  onPointerDown(ev, hit) {
646
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
652
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
647
653
  if (gesture)
648
654
  return;
649
655
  const target = ((_b = (_a = ev.source) === null || _a === void 0 ? void 0 : _a.target) !== null && _b !== void 0 ? _b : null);
@@ -678,8 +684,27 @@ export function bindDashboardSplit(api, group, options = {}) {
678
684
  // sits above the card's box) — see grid-binder.
679
685
  const gripId = (_e = (_d = gripHostOf(target)) === null || _d === void 0 ? void 0 : _d.getAttribute('data-node-id')) !== null && _e !== void 0 ? _e : null;
680
686
  const onGrip = !!gripId && ((_f = group.members) !== null && _f !== void 0 ? _f : new Set()).has(gripId);
681
- if (!hit.node && !onGrip) {
682
- (_h = (_g = diagram).clearSelection) === null || _h === void 0 ? void 0 : _h.call(_g);
687
+ const sectionHandle = (_g = target === null || target === void 0 ? void 0 : target.closest) === null || _g === void 0 ? void 0 : _g.call(target, '.axdb-slab > .axdb-rs');
688
+ if ((!hit.node && !onGrip) || sectionHandle) {
689
+ // OUR OWN empty band or corner handle, and we are a section of a
690
+ // parent board: select the section there; an edge or the handle
691
+ // starts the section resize, which the parent runs while this tool
692
+ // forwards the pointer sequence (the grid binder does the same).
693
+ const parent = parentPeerOf(api.container, group.id);
694
+ if ((parent === null || parent === void 0 ? void 0 : parent.selectMember) && (sectionHandle || worldInsideBoard(ev.world.x, ev.world.y))) {
695
+ const own = ((_h = sectionHandle === null || sectionHandle === void 0 ? void 0 : sectionHandle.parentElement) === null || _h === void 0 ? void 0 : _h.getAttribute('data-slab-id')) === group.id;
696
+ parent.selectMember(group.id);
697
+ if (!isStatic && parent.beginSlabResize) {
698
+ const f = frame();
699
+ const edges = own
700
+ ? rtl ? { n: false, e: false, s: true, w: true } : { n: false, e: true, s: true, w: false }
701
+ : { n: ev.world.y - f.y <= EDGE_GRIP, s: f.y + f.height - ev.world.y <= EDGE_GRIP, w: ev.world.x - f.x <= EDGE_GRIP, e: f.x + f.width - ev.world.x <= EDGE_GRIP };
702
+ if (anyEdge(edges) && parent.beginSlabResize(group.id, edges, ev))
703
+ forwardSlab = parent;
704
+ }
705
+ return;
706
+ }
707
+ (_k = (_j = diagram).clearSelection) === null || _k === void 0 ? void 0 : _k.call(_j);
683
708
  selectWidget(undefined);
684
709
  api.render();
685
710
  return;
@@ -688,9 +713,9 @@ export function bindDashboardSplit(api, group, options = {}) {
688
713
  if (!node)
689
714
  return;
690
715
  selectWidget(node.id); // a press selects, whether or not it starts a gesture
691
- if (((_j = node.state) === null || _j === void 0 ? void 0 : _j.locked) === true || isStatic)
716
+ if (((_l = node.state) === null || _l === void 0 ? void 0 : _l.locked) === true || isStatic)
692
717
  return;
693
- if (((_k = node.getMetadata) === null || _k === void 0 ? void 0 : _k.call(node, 'widgetMovable')) === false)
718
+ if (((_m = node.getMetadata) === null || _m === void 0 ? void 0 : _m.call(node, 'widgetMovable')) === false)
694
719
  return;
695
720
  // Drag-handle mode: only the handle (the caption strip, a custom element
696
721
  // or the painted grip) lifts a widget out — see pressOnDragHandle.
@@ -723,12 +748,27 @@ export function bindDashboardSplit(api, group, options = {}) {
723
748
  };
724
749
  },
725
750
  onPointerMove(ev) {
726
- onToolMove(ev);
751
+ var _a;
752
+ if (forwardSlab)
753
+ (_a = forwardSlab.slabMove) === null || _a === void 0 ? void 0 : _a.call(forwardSlab, ev);
754
+ else
755
+ onToolMove(ev);
727
756
  },
728
757
  onPointerUp() {
729
- onToolUp();
758
+ var _a;
759
+ if (forwardSlab) {
760
+ (_a = forwardSlab.slabUp) === null || _a === void 0 ? void 0 : _a.call(forwardSlab);
761
+ forwardSlab = null;
762
+ }
763
+ else
764
+ onToolUp();
730
765
  },
731
766
  onCancel() {
767
+ var _a;
768
+ if (forwardSlab) {
769
+ (_a = forwardSlab.slabCancel) === null || _a === void 0 ? void 0 : _a.call(forwardSlab);
770
+ forwardSlab = null;
771
+ }
732
772
  cancelActiveGesture();
733
773
  },
734
774
  };
@@ -365,6 +365,61 @@ const CSS = `
365
365
  .grafloria-html-layer > .axdb-slab.axdb-slab--selected > .axdb-rs { pointer-events: auto; opacity: 1; }
366
366
  .grafloria-html-layer > .axdb-slab.axdb-slab--static > .axdb-rs { display: none; }
367
367
 
368
+ /* SECTION CAPTION (0.4.22): the band on the slab. Geometry is inline (the
369
+ reserve and the pixels come from one function); everything visual is a
370
+ variable the options write and a theme may override. The band takes the
371
+ pointer — the slab does not — so a press on it is the section's. */
372
+ .grafloria-html-layer > .axdb-slab > .axdb-slab-h {
373
+ position: absolute; box-sizing: border-box; pointer-events: auto; z-index: 1;
374
+ display: flex; align-items: center; gap: 6px; min-width: 0;
375
+ padding: var(--axdb-caption-pad, 0 10px);
376
+ background: var(--axdb-caption-bg, rgba(31, 36, 48, .045));
377
+ border-bottom: var(--axdb-caption-border, none);
378
+ color: var(--axdb-caption-fg, #1f2430);
379
+ font: var(--axdb-caption-font-weight, 600) var(--axdb-caption-font-size, 13px)/1.2 var(--axdb-caption-font-family, system-ui, -apple-system, "Segoe UI", sans-serif);
380
+ text-transform: var(--axdb-caption-transform, none);
381
+ letter-spacing: .01em;
382
+ border-radius: var(--axdb-rs-radius, 3px) var(--axdb-rs-radius, 3px) 0 0;
383
+ cursor: default; user-select: none; -webkit-user-select: none; overflow: hidden;
384
+ transition: opacity .12s;
385
+ }
386
+ /* The modifiers carry the band's own selector so they outrank its defaults. */
387
+ .grafloria-html-layer > .axdb-slab > .axdb-slab-h.axdb-slab-h--center { justify-content: center; }
388
+ .grafloria-html-layer > .axdb-slab > .axdb-slab-h.axdb-slab-h--end { justify-content: flex-end; }
389
+ .grafloria-html-layer > .axdb-slab > .axdb-slab-h.axdb-slab-h--vtop { align-items: flex-start; }
390
+ .grafloria-html-layer > .axdb-slab > .axdb-slab-h.axdb-slab-h--vbottom { align-items: flex-end; }
391
+ /* 'tab': above the frame, sized to its text */
392
+ .grafloria-html-layer > .axdb-slab > .axdb-slab-h.axdb-slab-h--tab { right: auto; max-width: 100%; }
393
+ /* 'hover': an overlay that appears with the pointer or the selection */
394
+ .grafloria-html-layer > .axdb-slab > .axdb-slab-h.axdb-slab-h--hover { opacity: 0; }
395
+ .grafloria-html-layer > .axdb-slab:hover > .axdb-slab-h.axdb-slab-h--hover,
396
+ .grafloria-html-layer > .axdb-slab.axdb-slab--selected > .axdb-slab-h.axdb-slab-h--hover,
397
+ .grafloria-html-layer > .axdb-slab > .axdb-slab-h.axdb-slab-h--hover:focus-within { opacity: 1; }
398
+ /* the tight tier: a section under 90 px */
399
+ .grafloria-html-layer > .axdb-slab > .axdb-slab-h.axdb-slab-h--tight { font-size: min(var(--axdb-caption-font-size, 12px), 12px); gap: 4px; }
400
+ .axdb-slab-h--tight .axdb-slab-h-sub, .axdb-slab-h--tight .axdb-slab-h-actions { display: none; }
401
+ .axdb-slab-h-icon { flex: none; }
402
+ .axdb-slab-h-body { display: flex; flex-direction: column; justify-content: center; min-width: 0; flex: 0 1 auto; }
403
+ .axdb-slab-h-text, .axdb-slab-h-sub { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
404
+ .axdb-slab-h-sub { font-size: .85em; font-weight: 500; opacity: .72; }
405
+ .axdb-slab-h-info { flex: none; opacity: .6; font-size: .9em; cursor: help; }
406
+ .axdb-slab-h-actions { flex: none; display: flex; gap: 2px; margin-inline-start: auto; opacity: 0; transition: opacity .12s; }
407
+ .axdb-slab-h--center .axdb-slab-h-actions, .axdb-slab-h--end .axdb-slab-h-actions { margin-inline-start: 0; }
408
+ .axdb-slab-h:hover > .axdb-slab-h-actions, .axdb-slab--selected > .axdb-slab-h > .axdb-slab-h-actions, .axdb-slab-h:focus-within > .axdb-slab-h-actions { opacity: 1; }
409
+ .axdb-slab-h-action {
410
+ all: unset; box-sizing: border-box; width: 24px; height: 24px; display: inline-flex; align-items: center; justify-content: center;
411
+ border-radius: 4px; cursor: pointer; font-size: 14px; line-height: 1; color: inherit;
412
+ }
413
+ .axdb-slab-h-action:hover { background: rgba(31, 36, 48, .08); }
414
+ .axdb-slab-h-action:focus-visible { outline: 2px solid var(--axdb-accent-ring, rgba(59, 82, 217, .55)); outline-offset: -2px; }
415
+ .axdb-slab-h-action[disabled] { opacity: .4; cursor: default; }
416
+ .axdb-slab-h-action[disabled]:hover { background: none; }
417
+ .grafloria-html-layer > .axdb-slab.axdb-slab--static > .axdb-slab-h { cursor: default; }
418
+ @media (prefers-color-scheme: dark) {
419
+ .grafloria-html-layer > .axdb-slab > .axdb-slab-h { background: var(--axdb-caption-bg, rgba(236, 238, 244, .06)); color: var(--axdb-caption-fg, #eceef4); }
420
+ .axdb-slab-h-action:hover { background: rgba(236, 238, 244, .1); }
421
+ }
422
+
368
423
  /* legend chips, shared by line and donut */
369
424
  .axdb-lg { display: flex; flex-wrap: wrap; gap: 4px 12px; margin-top: 9px; }
370
425
  .axdb-lg--col { flex-direction: column; flex-wrap: nowrap; gap: 6px; margin-top: 0; }