@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 +1 -1
- package/src/index.d.ts +1 -1
- package/src/lib/dashboard-kit/caption.d.ts +123 -0
- package/src/lib/dashboard-kit/caption.js +200 -0
- package/src/lib/dashboard-kit/dashboard.d.ts +27 -12
- package/src/lib/dashboard-kit/dashboard.js +87 -8
- package/src/lib/dashboard-kit/grid-binder.d.ts +14 -1
- package/src/lib/dashboard-kit/grid-binder.js +122 -24
- package/src/lib/dashboard-kit/index.d.ts +1 -0
- package/src/lib/dashboard-kit/index.js +1 -0
- package/src/lib/dashboard-kit/split-binder.js +53 -13
- package/src/lib/dashboard-kit/styles.js +55 -0
package/package.json
CHANGED
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 (((
|
|
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: (
|
|
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
|
-
|
|
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
|
-
|
|
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 (((
|
|
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 (
|
|
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
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
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 = ((
|
|
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
|
-
(
|
|
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 (((
|
|
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 = !!((
|
|
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 = ((
|
|
2231
|
-
const movable = ((
|
|
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: (
|
|
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
|
-
|
|
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
|
-
|
|
682
|
-
|
|
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 (((
|
|
716
|
+
if (((_l = node.state) === null || _l === void 0 ? void 0 : _l.locked) === true || isStatic)
|
|
692
717
|
return;
|
|
693
|
-
if (((
|
|
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
|
-
|
|
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
|
-
|
|
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; }
|