@grafloria/element 0.4.22 → 0.4.24

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.22",
3
+ "version": "0.4.24",
4
4
  "type": "module",
5
5
  "main": "./src/index.js",
6
6
  "types": "./src/index.d.ts",
package/src/index.d.ts CHANGED
@@ -79,7 +79,7 @@ export type { JoinColumn, JoinEnd, MatchTier, JoinGuidanceApi, JoinGuidanceOptio
79
79
  export type { ErColumn, ErEntitySpec, ErRelationshipSpec, ErCardinality, ErSide, ErDiagramOptions, UmlClassSpec, UmlRelationshipSpec, UmlRelationKind, UmlSide, UmlDiagramOptions, ErEntityDelta, UmlClassDelta, CardEditingHandle, } from './lib/diagram-kit/index.js';
80
80
  export { erTable, umlClass, erTables, umlClasses, CardHandle, ErTable, ErField, ErColumnList, UmlClass, UmlMemberList, } from './lib/diagram-kit/index.js';
81
81
  export type { HandleApi } from './lib/diagram-kit/index.js';
82
- export { bindDashboardGrid, rowHeightFor, boardHeightFor, columnUnitFor, cellToRect, pointToCell, sizeToSpan, gridItemFromCell, cellFromGridItem, buildCommitCommands, ensureDashboardKitStyles, DASHBOARD_KIT_STYLE_ID, dashboard, defaultWidgetRenderer, renderKpiWidget, renderLineWidget, renderBarWidget, renderDonutWidget, renderFunnelWidget, renderTableWidget, BUILT_IN_WIDGET_KINDS, } from './lib/dashboard-kit/index.js';
82
+ export { bindDashboardGrid, normalizeCaption, captionReserve, captionBandHeight, paintCaptionBand, CAPTION_HEIGHT, CAPTION_HEIGHT_SUBTITLE, CAPTION_HEIGHT_TIGHT, CAPTION_PASS_THROUGH, rowHeightFor, boardHeightFor, columnUnitFor, cellToRect, pointToCell, sizeToSpan, gridItemFromCell, cellFromGridItem, buildCommitCommands, ensureDashboardKitStyles, DASHBOARD_KIT_STYLE_ID, dashboard, defaultWidgetRenderer, renderKpiWidget, renderLineWidget, renderBarWidget, renderDonutWidget, renderFunnelWidget, renderTableWidget, BUILT_IN_WIDGET_KINDS, } from './lib/dashboard-kit/index.js';
83
83
  export type { DashboardGridApi, DashboardGridOptions, DashboardGridHandle, CellRect, WorldRect, DashboardGridGeometry, TileDelta, DashboardOptions, DashboardSpec, DashboardSnapshot, DragHandleOption, DragGripOptions, DashboardHandle, DashboardViewSpec, DashboardWidgetSpec, SectionCaption, SectionCaptionOptions, SectionCaptionAction, SectionCaptionFont, WidgetHandle, WidgetRenderer, KpiWidgetData, LineWidgetData, LineSeries, BarWidgetData, DonutWidgetData, FunnelWidgetData, TableWidgetData, } from './lib/dashboard-kit/index.js';
84
84
  export { bindStencilPalette, bindShapeDataPanel, ensureStencilKitStyles } from './lib/stencil-kit/index.js';
85
85
  export type { StencilPaletteApi, StencilPaletteOptions, StencilPaletteHandle, ShapeDataPanelApi, ShapeDataPanelOptions, ShapeDataPanelHandle, } from './lib/stencil-kit/index.js';
package/src/index.js CHANGED
@@ -230,7 +230,7 @@ export { erTable, umlClass, erTables, umlClasses, CardHandle, ErTable, ErField,
230
230
  // GridItemConfig. Element-local code, so the Phase-0 wholesale re-export
231
231
  // below does NOT cover it — this curated block is its only door.
232
232
  // ===========================================================================
233
- export { bindDashboardGrid, rowHeightFor, boardHeightFor, columnUnitFor, cellToRect, pointToCell, sizeToSpan, gridItemFromCell, cellFromGridItem, buildCommitCommands, ensureDashboardKitStyles, DASHBOARD_KIT_STYLE_ID,
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,
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
@@ -40,7 +40,12 @@ export interface SectionCaptionOptions {
40
40
  description?: string;
41
41
  /** A glyph before the text (emoji or short text). */
42
42
  icon?: string;
43
- /** 'inside' (default): a band inside the frame, reserved. 'tab': above the frame, nothing reserved. */
43
+ /**
44
+ * 'inside' (default): a band across the top of the frame. 'tab': the same
45
+ * band sized to its text, a label chip at the leading corner. BOTH reserve
46
+ * their height inside the SECTION's own cell — a header that hangs over the
47
+ * neighbour above is the overlap bug this feature exists to end.
48
+ */
44
49
  position?: 'inside' | 'tab';
45
50
  /** Horizontal alignment (default 'start', mirrored on RTL). */
46
51
  align?: 'start' | 'center' | 'end';
@@ -73,6 +78,10 @@ export declare const CAPTION_HEIGHT_SUBTITLE = 44;
73
78
  export declare const CAPTION_HEIGHT_TIGHT = 22;
74
79
  /** A section shorter than this steps its band down to the tight tier. */
75
80
  export declare const CAPTION_TIGHT_BELOW = 90;
81
+ /** A band never paints thinner than this. */
82
+ export declare const CAPTION_MIN_HEIGHT = 16;
83
+ /** …nor takes the last pixels: the children keep at least this much. */
84
+ export declare const CAPTION_MIN_CONTENT = 20;
76
85
  export declare const CAPTION_PASS_THROUGH = "button, a, input, select, textarea, [data-axdb-pass]";
77
86
  /** The caption as options, or null when there is none. `title` fills the text. */
78
87
  export declare function normalizeCaption(c: SectionCaption | undefined, title?: string): SectionCaptionOptions | null;
@@ -82,11 +91,54 @@ export declare function captionOfGroup(grp: {
82
91
  name?: string;
83
92
  } | undefined): SectionCaptionOptions | null;
84
93
  export declare function pairOf(v: number | [number, number] | undefined, dflt: [number, number]): [number, number];
94
+ /** The shape of the model and groups this module needs — structural, so the kit's
95
+ * caption logic stays free of engine imports. */
96
+ interface CaptionGroup {
97
+ id: string;
98
+ name?: string;
99
+ members?: {
100
+ has(id: string): boolean;
101
+ } | null;
102
+ getMetadata(key: string): unknown;
103
+ }
104
+ interface CaptionDiagram {
105
+ getGroups?(): CaptionGroup[];
106
+ }
107
+ /**
108
+ * Does the board that HOLDS this section paint section chrome? Only the grid
109
+ * binder draws the slab overlay the band lives on, so a section inside a
110
+ * SPLIT board has no band — and must not reserve space for one.
111
+ *
112
+ * Read from the parent group's persisted layout rather than from the live
113
+ * binder registry: membership and metadata both exist before either binder is
114
+ * built, so the answer is the same on the first projection as on the hundredth
115
+ * (asking the registry made the reserve depend on which binder registered
116
+ * first, and a child binds BEFORE its parent).
117
+ */
118
+ export declare function parentPaintsSectionChrome(diagram: CaptionDiagram, group: CaptionGroup): boolean;
119
+ /** A section's own caption reserve: its band's pixels, or 0 when nothing paints one. */
120
+ export declare function sectionCaptionReserve(diagram: CaptionDiagram, group: CaptionGroup & {
121
+ size?: {
122
+ height: number;
123
+ } | null;
124
+ }, isStatic: boolean): number;
85
125
  /** Painted at all? `show: 'design'` disappears under static. */
86
126
  export declare function captionPainted(c: SectionCaptionOptions | null, isStatic: boolean): boolean;
87
- /** The band's height for a section of `sectionH` px. */
127
+ /**
128
+ * The band's height for a section of `sectionH` px: the authored height (or
129
+ * the tier), CLAMPED so the children always keep `CAPTION_MIN_CONTENT` px. A
130
+ * one-row section used to hand its whole 34 px to a 22 px band and paint a
131
+ * 12 px sliver of a child.
132
+ */
88
133
  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. */
134
+ /**
135
+ * Pixels the nested board's frame gives up at the top: the band plus its
136
+ * vertical margins. A TAB reserves too — it is a narrower band, not a header
137
+ * hanging over whatever the parent board put above the section (the stress
138
+ * page had one lying across a chart's legend, and one clipped off the top of
139
+ * the canvas). Only `show: 'hover'` reserves nothing: it is an overlay by
140
+ * definition, and it paints opaque so the content beneath stays readable.
141
+ */
90
142
  export declare function captionReserve(c: SectionCaptionOptions | null, ctx: {
91
143
  static: boolean;
92
144
  sectionH: number;
@@ -96,8 +148,12 @@ export declare function captionKey(c: SectionCaptionOptions, ctx: {
96
148
  rtl: boolean;
97
149
  static: boolean;
98
150
  }): string;
99
- /** Is a section of `sectionH` px in the tight tier? */
100
- export declare const captionTight: (sectionH: number) => boolean;
151
+ /**
152
+ * Is the band too short to carry a subtitle, actions or full-size text? Asked
153
+ * of the band's ACTUAL height, so an authored `height` that survives on a
154
+ * short section keeps its content instead of being styled as squeezed.
155
+ */
156
+ export declare const captionTight: (c: SectionCaptionOptions, sectionH: number) => boolean;
101
157
  /** The per-sync geometry of a painted band: height and tier follow the section's live size. */
102
158
  export declare function sizeCaptionBand(band: HTMLElement, c: SectionCaptionOptions, sectionH: number): void;
103
159
  /**
@@ -121,3 +177,4 @@ export declare function paintCaptionBand(band: HTMLElement, c: SectionCaptionOpt
121
177
  render?: (host: HTMLElement) => void;
122
178
  onAction?: (actionId: string) => void;
123
179
  }): void;
180
+ export {};
@@ -19,6 +19,10 @@ export const CAPTION_HEIGHT_SUBTITLE = 44;
19
19
  export const CAPTION_HEIGHT_TIGHT = 22;
20
20
  /** A section shorter than this steps its band down to the tight tier. */
21
21
  export const CAPTION_TIGHT_BELOW = 90;
22
+ /** A band never paints thinner than this. */
23
+ export const CAPTION_MIN_HEIGHT = 16;
24
+ /** …nor takes the last pixels: the children keep at least this much. */
25
+ export const CAPTION_MIN_CONTENT = 20;
22
26
  export const CAPTION_PASS_THROUGH = 'button, a, input, select, textarea, [data-axdb-pass]';
23
27
  /** The caption as options, or null when there is none. `title` fills the text. */
24
28
  export function normalizeCaption(c, title) {
@@ -44,24 +48,69 @@ export function pairOf(v, dflt) {
44
48
  return dflt;
45
49
  return typeof v === 'number' ? [v, v] : [v[0], v[1]];
46
50
  }
51
+ /**
52
+ * Does the board that HOLDS this section paint section chrome? Only the grid
53
+ * binder draws the slab overlay the band lives on, so a section inside a
54
+ * SPLIT board has no band — and must not reserve space for one.
55
+ *
56
+ * Read from the parent group's persisted layout rather than from the live
57
+ * binder registry: membership and metadata both exist before either binder is
58
+ * built, so the answer is the same on the first projection as on the hundredth
59
+ * (asking the registry made the reserve depend on which binder registered
60
+ * first, and a child binds BEFORE its parent).
61
+ */
62
+ export function parentPaintsSectionChrome(diagram, group) {
63
+ var _a, _b, _c, _d;
64
+ const groups = (_b = (_a = diagram.getGroups) === null || _a === void 0 ? void 0 : _a.call(diagram)) !== null && _b !== void 0 ? _b : [];
65
+ for (const g of groups) {
66
+ if (g === group || !((_c = g.members) === null || _c === void 0 ? void 0 : _c.has(group.id)))
67
+ continue;
68
+ const board = g.getMetadata('dashboardBoard');
69
+ return ((_d = board === null || board === void 0 ? void 0 : board.layout) !== null && _d !== void 0 ? _d : 'grid') !== 'split';
70
+ }
71
+ return false; // not a member of any board: a view, which carries no caption
72
+ }
73
+ /** A section's own caption reserve: its band's pixels, or 0 when nothing paints one. */
74
+ export function sectionCaptionReserve(diagram, group, isStatic) {
75
+ var _a, _b;
76
+ if (!parentPaintsSectionChrome(diagram, group))
77
+ return 0;
78
+ 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 });
79
+ }
47
80
  /** Painted at all? `show: 'design'` disappears under static. */
48
81
  export function captionPainted(c, isStatic) {
49
82
  if (!c)
50
83
  return false;
51
84
  return !(c.show === 'design' && isStatic);
52
85
  }
53
- /** The band's height for a section of `sectionH` px. */
86
+ /**
87
+ * The band's height for a section of `sectionH` px: the authored height (or
88
+ * the tier), CLAMPED so the children always keep `CAPTION_MIN_CONTENT` px. A
89
+ * one-row section used to hand its whole 34 px to a 22 px band and paint a
90
+ * 12 px sliver of a child.
91
+ */
54
92
  export function captionBandHeight(c, sectionH) {
55
93
  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);
94
+ // An authored `height` WINS over the squeezed tier — the tier is a default
95
+ // for a short section, not an override of what the app asked for. The clamp
96
+ // still protects the children either way.
97
+ const want = (_a = c.height) !== null && _a !== void 0 ? _a : (sectionH > 0 && sectionH < CAPTION_TIGHT_BELOW ? CAPTION_HEIGHT_TIGHT : c.subtitle ? CAPTION_HEIGHT_SUBTITLE : CAPTION_HEIGHT);
98
+ if (sectionH <= 0)
99
+ return want;
100
+ return Math.max(CAPTION_MIN_HEIGHT, Math.min(want, sectionH - CAPTION_MIN_CONTENT));
59
101
  }
60
- /** Pixels the nested board's frame gives up at the top: the band plus its vertical margins. */
102
+ /**
103
+ * Pixels the nested board's frame gives up at the top: the band plus its
104
+ * vertical margins. A TAB reserves too — it is a narrower band, not a header
105
+ * hanging over whatever the parent board put above the section (the stress
106
+ * page had one lying across a chart's legend, and one clipped off the top of
107
+ * the canvas). Only `show: 'hover'` reserves nothing: it is an overlay by
108
+ * definition, and it paints opaque so the content beneath stays readable.
109
+ */
61
110
  export function captionReserve(c, ctx) {
62
111
  if (!c || !captionPainted(c, ctx.static))
63
112
  return 0;
64
- if (c.position === 'tab' || c.show === 'hover')
113
+ if (c.show === 'hover')
65
114
  return 0;
66
115
  const [mv] = pairOf(c.margin, [0, 0]);
67
116
  return captionBandHeight(c, ctx.sectionH) + 2 * mv;
@@ -70,15 +119,22 @@ export function captionReserve(c, ctx) {
70
119
  export function captionKey(c, ctx) {
71
120
  return JSON.stringify([c, ctx.rtl, ctx.static]);
72
121
  }
73
- /** Is a section of `sectionH` px in the tight tier? */
74
- export const captionTight = (sectionH) => sectionH > 0 && sectionH < CAPTION_TIGHT_BELOW;
122
+ /**
123
+ * Is the band too short to carry a subtitle, actions or full-size text? Asked
124
+ * of the band's ACTUAL height, so an authored `height` that survives on a
125
+ * short section keeps its content instead of being styled as squeezed.
126
+ */
127
+ export const captionTight = (c, sectionH) => captionBandHeight(c, sectionH) <= CAPTION_HEIGHT_TIGHT;
128
+ /** Where the band's box sits horizontally: a tab hugs the LEADING edge, mirrored on RTL. */
129
+ function bandSides(c, mh, rtl) {
130
+ if (c.position !== 'tab')
131
+ return { left: `${mh}px`, right: `${mh}px` };
132
+ return rtl ? { left: 'auto', right: `${mh}px` } : { left: `${mh}px`, right: 'auto' };
133
+ }
75
134
  /** The per-sync geometry of a painted band: height and tier follow the section's live size. */
76
135
  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`;
136
+ band.classList.toggle('axdb-slab-h--tight', captionTight(c, sectionH));
137
+ band.style.height = `${captionBandHeight(c, sectionH)}px`;
82
138
  }
83
139
  /**
84
140
  * Is a press on `target`, inside `band`, content rather than the band? An
@@ -119,7 +175,7 @@ export function paintCaptionBand(band, c, ctx) {
119
175
  const cls = (name, on) => band.classList.toggle(name, on);
120
176
  cls('axdb-slab-h--tab', c.position === 'tab');
121
177
  cls('axdb-slab-h--hover', c.show === 'hover');
122
- cls('axdb-slab-h--tight', captionTight(ctx.sectionH));
178
+ cls('axdb-slab-h--tight', captionTight(c, ctx.sectionH));
123
179
  cls('axdb-slab-h--center', c.align === 'center');
124
180
  cls('axdb-slab-h--end', c.align === 'end');
125
181
  cls('axdb-slab-h--vtop', c.valign === 'top');
@@ -129,9 +185,10 @@ export function paintCaptionBand(band, c, ctx) {
129
185
  band.classList.add(k);
130
186
  band.setAttribute('dir', ctx.rtl ? 'rtl' : 'ltr');
131
187
  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
188
+ band.style.top = `${mv}px`;
189
+ const sides = bandSides(c, mh, ctx.rtl);
190
+ band.style.left = sides.left;
191
+ band.style.right = sides.right;
135
192
  const v = (name, value) => {
136
193
  if (value === undefined)
137
194
  band.style.removeProperty(name);
@@ -54,7 +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
+ import { captionOfGroup, captionPainted, captionPassThrough, captionKey, paintCaptionBand, sectionCaptionReserve, sizeCaptionBand } from './caption.js';
58
58
  /** The class of the painted grip element (a child of the node host). */
59
59
  export const GRIP_CLASS = 'axdb-grip';
60
60
  /** The container class that turns the caption strip's grip dots on. */
@@ -453,7 +453,7 @@ export function bindDashboardGrid(api, group, options = {}) {
453
453
  * band is outside `containsWorld`, so a press on it is the PARENT's (it
454
454
  * selects the section) rather than an empty press of this board.
455
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 }); };
456
+ const ownReserve = () => sectionCaptionReserve(diagram, group, isStatic);
457
457
  const frame = () => {
458
458
  var _a, _b, _c, _d;
459
459
  const r = ownReserve();
@@ -1177,6 +1177,7 @@ export function bindDashboardGrid(api, group, options = {}) {
1177
1177
  for (const [id, el] of slabEls) {
1178
1178
  if (!seen.has(id)) {
1179
1179
  el.remove();
1180
+ hoverSlabs.delete(el);
1180
1181
  slabEls.delete(id);
1181
1182
  }
1182
1183
  }
@@ -1193,9 +1194,18 @@ export function bindDashboardGrid(api, group, options = {}) {
1193
1194
  let band = el.querySelector(':scope > .axdb-slab-h');
1194
1195
  if (!cap || !captionPainted(cap, isStatic)) {
1195
1196
  band === null || band === void 0 ? void 0 : band.remove();
1197
+ hoverSlabs.delete(el);
1198
+ el.classList.remove('axdb-slab--hot');
1196
1199
  el.removeAttribute('aria-label');
1200
+ el.removeAttribute('role');
1197
1201
  return;
1198
1202
  }
1203
+ if (cap.show === 'hover')
1204
+ hoverSlabs.add(el);
1205
+ else {
1206
+ hoverSlabs.delete(el);
1207
+ el.classList.remove('axdb-slab--hot');
1208
+ }
1199
1209
  const ctx = { rtl, static: isStatic, sectionH };
1200
1210
  const key = captionKey(cap, ctx);
1201
1211
  if (band && band.getAttribute('data-key') === key) {
@@ -1208,10 +1218,18 @@ export function bindDashboardGrid(api, group, options = {}) {
1208
1218
  const render = options.renderCaption;
1209
1219
  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
1220
  band.setAttribute('data-key', key);
1211
- if (cap.text)
1221
+ // A named group, so the caption text is the section's name in the
1222
+ // accessibility tree rather than a stray label on an unnamed div. (The
1223
+ // band is not a tab stop yet — the actions are deliberately out of the
1224
+ // tab order so a board keeps exactly ONE stop; see the plan.)
1225
+ if (cap.text) {
1226
+ el.setAttribute('role', 'group');
1212
1227
  el.setAttribute('aria-label', cap.text);
1213
- else
1228
+ }
1229
+ else {
1230
+ el.removeAttribute('role');
1214
1231
  el.removeAttribute('aria-label');
1232
+ }
1215
1233
  };
1216
1234
  const insideMemberGroupFrame = (x, y) => {
1217
1235
  var _a;
@@ -2412,10 +2430,33 @@ export function bindDashboardGrid(api, group, options = {}) {
2412
2430
  * container; the corner handle keeps its own cursor from the stylesheet.
2413
2431
  */
2414
2432
  let hoverHost = null;
2433
+ /**
2434
+ * A `show: 'hover'` caption cannot ride CSS `:hover`: the slab overlay takes
2435
+ * no pointer (by design — it must never steal a press), and while the band
2436
+ * is hidden it takes none either, so nothing in the section is ever hovered
2437
+ * in CSS terms. The binder already tracks the pointer; it marks the section
2438
+ * under it instead.
2439
+ */
2440
+ /** Only the sections carrying a `show: 'hover'` band — usually none, so the
2441
+ * pointer handler costs nothing on a board that has no hover caption. */
2442
+ const hoverSlabs = new Set();
2443
+ const markHotSection = (clientX, clientY) => {
2444
+ if (!hoverSlabs.size)
2445
+ return;
2446
+ for (const el of hoverSlabs) {
2447
+ const r = el.getBoundingClientRect();
2448
+ el.classList.toggle('axdb-slab--hot', clientX >= r.left && clientX <= r.right && clientY >= r.top && clientY <= r.bottom);
2449
+ }
2450
+ };
2451
+ const onHoverLeave = () => {
2452
+ for (const el of hoverSlabs)
2453
+ el.classList.remove('axdb-slab--hot');
2454
+ };
2415
2455
  const onHover = (e) => {
2416
2456
  var _a, _b, _c, _d, _e, _f, _g, _h;
2417
2457
  if (disposed || gesture)
2418
2458
  return;
2459
+ markHotSection(e.clientX, e.clientY);
2419
2460
  // The event may target the host, its content, or (when a host's content
2420
2461
  // is pointer-transparent) the canvas under it — find the member host by
2421
2462
  // the pointer's position in that case.
@@ -2465,6 +2506,7 @@ export function bindDashboardGrid(api, group, options = {}) {
2465
2506
  host.removeAttribute('data-axdb-edge');
2466
2507
  };
2467
2508
  api.container.addEventListener('pointermove', onHover, { passive: true });
2509
+ api.container.addEventListener('pointerleave', onHoverLeave, { passive: true });
2468
2510
  /**
2469
2511
  * STATIC BOARDS LET CONTENT BE CLICKED. The renderer prevents the default of
2470
2512
  * every press a tool claims, which cancels the compatibility mouse events —
@@ -3041,6 +3083,7 @@ export function bindDashboardGrid(api, group, options = {}) {
3041
3083
  peersOnCanvas().delete(selfPeer);
3042
3084
  unregisterTool();
3043
3085
  api.container.removeEventListener('pointermove', onHover);
3086
+ api.container.removeEventListener('pointerleave', onHoverLeave);
3044
3087
  guardedLayer === null || guardedLayer === void 0 ? void 0 : guardedLayer.removeEventListener('pointerdown', staticGuard);
3045
3088
  api.container.removeEventListener('focusin', onFocusIn);
3046
3089
  api.container.removeEventListener('keydown', onKey);
@@ -3059,6 +3102,7 @@ export function bindDashboardGrid(api, group, options = {}) {
3059
3102
  for (const el of slabEls.values())
3060
3103
  el.remove();
3061
3104
  slabEls.clear();
3105
+ hoverSlabs.clear();
3062
3106
  // A rebind inside the 60 ms window (a layout switch right after an undo)
3063
3107
  // disposed this binder with the timer pending — the host kept its
3064
3108
  // lifted ghost for good (visual gate, nested-containers ⑤).
@@ -29,7 +29,7 @@ import { anyEdge, clearOtherSelections, dragHandleSelector, gripHostOf, gripOf,
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
+ import { sectionCaptionReserve } from './caption.js';
33
33
  /** Group metadata key the tree persists under. */
34
34
  export const SPLIT_TREE_KEY = 'dashboardTree';
35
35
  /**
@@ -105,8 +105,14 @@ export function bindDashboardSplit(api, group, options = {}) {
105
105
  let focusedId;
106
106
  const live = liveRegionFor(api.container);
107
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 }); };
108
+ /**
109
+ * Our own caption band's pixels, given up at the top of the pane but only
110
+ * when the parent board actually paints one (see BinderPeer.paintsCaptions).
111
+ * A split board paints no section chrome, so a captioned section inside one
112
+ * reserves nothing until the tab-container round gives the split binder its
113
+ * own overlays.
114
+ */
115
+ const ownReserve = () => sectionCaptionReserve(diagram, group, isStatic);
110
116
  const frame = () => {
111
117
  var _a, _b, _c, _d;
112
118
  const r = ownReserve();
@@ -373,8 +373,10 @@ const CSS = `
373
373
  position: absolute; box-sizing: border-box; pointer-events: auto; z-index: 1;
374
374
  display: flex; align-items: center; gap: 6px; min-width: 0;
375
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);
376
+ background: var(--axdb-caption-bg, rgba(31, 36, 48, .055));
377
+ /* A hairline by default: two bands meeting (a captioned section inside a
378
+ captioned section) read as two headers, not one grey block. */
379
+ border-bottom: var(--axdb-caption-border, 1px solid rgba(31, 36, 48, .1));
378
380
  color: var(--axdb-caption-fg, #1f2430);
379
381
  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
382
  text-transform: var(--axdb-caption-transform, none);
@@ -388,13 +390,27 @@ const CSS = `
388
390
  .grafloria-html-layer > .axdb-slab > .axdb-slab-h.axdb-slab-h--end { justify-content: flex-end; }
389
391
  .grafloria-html-layer > .axdb-slab > .axdb-slab-h.axdb-slab-h--vtop { align-items: flex-start; }
390
392
  .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; }
393
+ /* 'tab': the same band sized to its text, a chip at the leading corner. It
394
+ reserves its height like 'inside' see captionReserve. */
395
+ .grafloria-html-layer > .axdb-slab > .axdb-slab-h.axdb-slab-h--tab {
396
+ max-width: 100%; border-radius: var(--axdb-rs-radius, 3px);
397
+ border-bottom: var(--axdb-caption-border-tab, none);
398
+ box-shadow: inset 0 0 0 1px rgba(31, 36, 48, .08);
399
+ }
400
+ /* 'hover': an overlay. It reserves nothing, so while hidden it must not take
401
+ the pointer (an invisible band swallowed clicks on the content beneath),
402
+ and while shown it paints OPAQUE — a 5% tint over a chart is a smear. */
403
+ .grafloria-html-layer > .axdb-slab > .axdb-slab-h.axdb-slab-h--hover {
404
+ opacity: 0; pointer-events: none;
405
+ background: var(--axdb-caption-bg, #fff);
406
+ box-shadow: 0 1px 4px rgba(31, 36, 48, .16);
407
+ }
408
+ /* .axdb-slab--hot is set by the binder from the live pointer (the overlay
409
+ takes no pointer of its own, so CSS :hover can never fire on it). */
410
+ .grafloria-html-layer > .axdb-slab.axdb-slab--hot > .axdb-slab-h.axdb-slab-h--hover,
411
+ .grafloria-html-layer > .axdb-slab.axdb-slab--selected > .axdb-slab-h.axdb-slab-h--hover { pointer-events: auto; opacity: 1; }
412
+ /* the actions of a hovered or selected section */
413
+ .grafloria-html-layer > .axdb-slab.axdb-slab--hot > .axdb-slab-h > .axdb-slab-h-actions { opacity: 1; }
398
414
  /* the tight tier: a section under 90 px */
399
415
  .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
416
  .axdb-slab-h--tight .axdb-slab-h-sub, .axdb-slab-h--tight .axdb-slab-h-actions { display: none; }
@@ -416,7 +432,13 @@ const CSS = `
416
432
  .axdb-slab-h-action[disabled]:hover { background: none; }
417
433
  .grafloria-html-layer > .axdb-slab.axdb-slab--static > .axdb-slab-h { cursor: default; }
418
434
  @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); }
435
+ .grafloria-html-layer > .axdb-slab > .axdb-slab-h {
436
+ background: var(--axdb-caption-bg, rgba(236, 238, 244, .07));
437
+ color: var(--axdb-caption-fg, #eceef4);
438
+ border-bottom: var(--axdb-caption-border, 1px solid rgba(236, 238, 244, .12));
439
+ }
440
+ .grafloria-html-layer > .axdb-slab > .axdb-slab-h.axdb-slab-h--tab { box-shadow: inset 0 0 0 1px rgba(236, 238, 244, .12); }
441
+ .grafloria-html-layer > .axdb-slab > .axdb-slab-h.axdb-slab-h--hover { background: var(--axdb-caption-bg, #1a1d25); box-shadow: 0 1px 4px rgba(0, 0, 0, .5); }
420
442
  .axdb-slab-h-action:hover { background: rgba(236, 238, 244, .1); }
421
443
  }
422
444