@grafloria/element 0.4.23 → 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.23",
3
+ "version": "0.4.24",
4
4
  "type": "module",
5
5
  "main": "./src/index.js",
6
6
  "types": "./src/index.d.ts",
@@ -91,6 +91,37 @@ export declare function captionOfGroup(grp: {
91
91
  name?: string;
92
92
  } | undefined): SectionCaptionOptions | null;
93
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;
94
125
  /** Painted at all? `show: 'design'` disappears under static. */
95
126
  export declare function captionPainted(c: SectionCaptionOptions | null, isStatic: boolean): boolean;
96
127
  /**
@@ -117,8 +148,12 @@ export declare function captionKey(c: SectionCaptionOptions, ctx: {
117
148
  rtl: boolean;
118
149
  static: boolean;
119
150
  }): string;
120
- /** Is a section of `sectionH` px in the tight tier? */
121
- 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;
122
157
  /** The per-sync geometry of a painted band: height and tier follow the section's live size. */
123
158
  export declare function sizeCaptionBand(band: HTMLElement, c: SectionCaptionOptions, sectionH: number): void;
124
159
  /**
@@ -142,3 +177,4 @@ export declare function paintCaptionBand(band: HTMLElement, c: SectionCaptionOpt
142
177
  render?: (host: HTMLElement) => void;
143
178
  onAction?: (actionId: string) => void;
144
179
  }): void;
180
+ export {};
@@ -48,6 +48,35 @@ export function pairOf(v, dflt) {
48
48
  return dflt;
49
49
  return typeof v === 'number' ? [v, v] : [v[0], v[1]];
50
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
+ }
51
80
  /** Painted at all? `show: 'design'` disappears under static. */
52
81
  export function captionPainted(c, isStatic) {
53
82
  if (!c)
@@ -62,7 +91,10 @@ export function captionPainted(c, isStatic) {
62
91
  */
63
92
  export function captionBandHeight(c, sectionH) {
64
93
  var _a;
65
- const want = captionTight(sectionH) ? CAPTION_HEIGHT_TIGHT : (_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);
66
98
  if (sectionH <= 0)
67
99
  return want;
68
100
  return Math.max(CAPTION_MIN_HEIGHT, Math.min(want, sectionH - CAPTION_MIN_CONTENT));
@@ -87,8 +119,12 @@ export function captionReserve(c, ctx) {
87
119
  export function captionKey(c, ctx) {
88
120
  return JSON.stringify([c, ctx.rtl, ctx.static]);
89
121
  }
90
- /** Is a section of `sectionH` px in the tight tier? */
91
- 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;
92
128
  /** Where the band's box sits horizontally: a tab hugs the LEADING edge, mirrored on RTL. */
93
129
  function bandSides(c, mh, rtl) {
94
130
  if (c.position !== 'tab')
@@ -97,7 +133,7 @@ function bandSides(c, mh, rtl) {
97
133
  }
98
134
  /** The per-sync geometry of a painted band: height and tier follow the section's live size. */
99
135
  export function sizeCaptionBand(band, c, sectionH) {
100
- band.classList.toggle('axdb-slab-h--tight', captionTight(sectionH));
136
+ band.classList.toggle('axdb-slab-h--tight', captionTight(c, sectionH));
101
137
  band.style.height = `${captionBandHeight(c, sectionH)}px`;
102
138
  }
103
139
  /**
@@ -139,7 +175,7 @@ export function paintCaptionBand(band, c, ctx) {
139
175
  const cls = (name, on) => band.classList.toggle(name, on);
140
176
  cls('axdb-slab-h--tab', c.position === 'tab');
141
177
  cls('axdb-slab-h--hover', c.show === 'hover');
142
- cls('axdb-slab-h--tight', captionTight(ctx.sectionH));
178
+ cls('axdb-slab-h--tight', captionTight(c, ctx.sectionH));
143
179
  cls('axdb-slab-h--center', c.align === 'center');
144
180
  cls('axdb-slab-h--end', c.align === 'end');
145
181
  cls('axdb-slab-h--vtop', c.valign === 'top');
@@ -436,14 +436,6 @@ export interface DashboardGridHandle {
436
436
  * the feature).
437
437
  */
438
438
  interface BinderPeer {
439
- /**
440
- * Does this board paint SECTION CHROME (the slab overlay that carries the
441
- * ring, the corner handle and the caption band) for its member groups? Only
442
- * the grid binder does. A section reserves its caption's pixels ONLY when
443
- * its parent paints one — otherwise a captioned section inside a SPLIT
444
- * board pushed its children down by 44 px under a band nobody drew.
445
- */
446
- paintsCaptions?: boolean;
447
439
  group: GroupModel;
448
440
  /** True when this board's engine holds `id` as an item (member lookup). */
449
441
  hasItem(id: string): boolean;
@@ -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,12 +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 = () => {
457
- var _a, _b, _c;
458
- return ((_a = parentPeer()) === null || _a === void 0 ? void 0 : _a.paintsCaptions) === true
459
- ? captionReserve(captionOfGroup(group), { static: isStatic, sectionH: (_c = (_b = group.size) === null || _b === void 0 ? void 0 : _b.height) !== null && _c !== void 0 ? _c : 0 })
460
- : 0;
461
- };
456
+ const ownReserve = () => sectionCaptionReserve(diagram, group, isStatic);
462
457
  const frame = () => {
463
458
  var _a, _b, _c, _d;
464
459
  const r = ownReserve();
@@ -1182,6 +1177,7 @@ export function bindDashboardGrid(api, group, options = {}) {
1182
1177
  for (const [id, el] of slabEls) {
1183
1178
  if (!seen.has(id)) {
1184
1179
  el.remove();
1180
+ hoverSlabs.delete(el);
1185
1181
  slabEls.delete(id);
1186
1182
  }
1187
1183
  }
@@ -1198,9 +1194,18 @@ export function bindDashboardGrid(api, group, options = {}) {
1198
1194
  let band = el.querySelector(':scope > .axdb-slab-h');
1199
1195
  if (!cap || !captionPainted(cap, isStatic)) {
1200
1196
  band === null || band === void 0 ? void 0 : band.remove();
1197
+ hoverSlabs.delete(el);
1198
+ el.classList.remove('axdb-slab--hot');
1201
1199
  el.removeAttribute('aria-label');
1200
+ el.removeAttribute('role');
1202
1201
  return;
1203
1202
  }
1203
+ if (cap.show === 'hover')
1204
+ hoverSlabs.add(el);
1205
+ else {
1206
+ hoverSlabs.delete(el);
1207
+ el.classList.remove('axdb-slab--hot');
1208
+ }
1204
1209
  const ctx = { rtl, static: isStatic, sectionH };
1205
1210
  const key = captionKey(cap, ctx);
1206
1211
  if (band && band.getAttribute('data-key') === key) {
@@ -1213,10 +1218,18 @@ export function bindDashboardGrid(api, group, options = {}) {
1213
1218
  const render = options.renderCaption;
1214
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); } }));
1215
1220
  band.setAttribute('data-key', key);
1216
- 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');
1217
1227
  el.setAttribute('aria-label', cap.text);
1218
- else
1228
+ }
1229
+ else {
1230
+ el.removeAttribute('role');
1219
1231
  el.removeAttribute('aria-label');
1232
+ }
1220
1233
  };
1221
1234
  const insideMemberGroupFrame = (x, y) => {
1222
1235
  var _a;
@@ -2191,7 +2204,6 @@ export function bindDashboardGrid(api, group, options = {}) {
2191
2204
  containsWorldExtended: worldInsideBoardExtended,
2192
2205
  frameArea: boardArea,
2193
2206
  adopt,
2194
- paintsCaptions: true, // the grid binder owns the slab overlays (syncSlabs)
2195
2207
  };
2196
2208
  peersOnCanvas().add(selfPeer);
2197
2209
  selfPeerRef = selfPeer;
@@ -2425,20 +2437,19 @@ export function bindDashboardGrid(api, group, options = {}) {
2425
2437
  * in CSS terms. The binder already tracks the pointer; it marks the section
2426
2438
  * under it instead.
2427
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();
2428
2443
  const markHotSection = (clientX, clientY) => {
2429
- if (!slabEls.size)
2444
+ if (!hoverSlabs.size)
2430
2445
  return;
2431
- for (const [id, el] of slabEls) {
2432
- if (!el.querySelector(':scope > .axdb-slab-h--hover'))
2433
- continue;
2446
+ for (const el of hoverSlabs) {
2434
2447
  const r = el.getBoundingClientRect();
2435
- const hot = clientX >= r.left && clientX <= r.right && clientY >= r.top && clientY <= r.bottom;
2436
- el.classList.toggle('axdb-slab--hot', hot);
2437
- void id;
2448
+ el.classList.toggle('axdb-slab--hot', clientX >= r.left && clientX <= r.right && clientY >= r.top && clientY <= r.bottom);
2438
2449
  }
2439
2450
  };
2440
2451
  const onHoverLeave = () => {
2441
- for (const el of slabEls.values())
2452
+ for (const el of hoverSlabs)
2442
2453
  el.classList.remove('axdb-slab--hot');
2443
2454
  };
2444
2455
  const onHover = (e) => {
@@ -3091,6 +3102,7 @@ export function bindDashboardGrid(api, group, options = {}) {
3091
3102
  for (const el of slabEls.values())
3092
3103
  el.remove();
3093
3104
  slabEls.clear();
3105
+ hoverSlabs.clear();
3094
3106
  // A rebind inside the 60 ms window (a layout switch right after an undo)
3095
3107
  // disposed this binder with the timer pending — the host kept its
3096
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
  /**
@@ -112,12 +112,7 @@ export function bindDashboardSplit(api, group, options = {}) {
112
112
  * reserves nothing until the tab-container round gives the split binder its
113
113
  * own overlays.
114
114
  */
115
- const ownReserve = () => {
116
- var _a, _b, _c;
117
- return ((_a = parentPeerOf(api.container, group.id)) === null || _a === void 0 ? void 0 : _a.paintsCaptions) === true
118
- ? captionReserve(captionOfGroup(group), { static: isStatic, sectionH: (_c = (_b = group.size) === null || _b === void 0 ? void 0 : _b.height) !== null && _c !== void 0 ? _c : 0 })
119
- : 0;
120
- };
115
+ const ownReserve = () => sectionCaptionReserve(diagram, group, isStatic);
121
116
  const frame = () => {
122
117
  var _a, _b, _c, _d;
123
118
  const r = ownReserve();