@grafloria/element 0.4.2 → 0.4.4
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/lib/dashboard-kit/dashboard.d.ts +147 -1
- package/src/lib/dashboard-kit/dashboard.js +524 -113
- package/src/lib/dashboard-kit/grid-binder.d.ts +43 -0
- package/src/lib/dashboard-kit/grid-binder.js +719 -106
- package/src/lib/dashboard-kit/index.d.ts +2 -0
- package/src/lib/dashboard-kit/index.js +2 -0
- package/src/lib/dashboard-kit/split-binder.d.ts +69 -0
- package/src/lib/dashboard-kit/split-binder.js +1078 -0
- package/src/lib/dashboard-kit/split-layout.d.ts +128 -0
- package/src/lib/dashboard-kit/split-layout.js +425 -0
- package/src/lib/dashboard-kit/styles.js +137 -20
- package/src/lib/dashboard-kit/widgets.d.ts +16 -1
- package/src/lib/dashboard-kit/widgets.js +146 -33
- package/src/lib/diagram-kit/card.d.ts +46 -0
- package/src/lib/diagram-kit/card.js +149 -0
- package/src/lib/diagram-kit/er.js +5 -3
- package/src/lib/diagram-kit/styles.js +40 -5
- package/src/lib/diagram-kit/uml.js +3 -4
- package/src/lib/diagram-kit/update.js +7 -3
- package/src/lib/grafloria.js +4 -4
- package/src/lib/load.d.ts +5 -0
- package/src/lib/load.js +72 -8
|
@@ -28,6 +28,18 @@ const CSS = `
|
|
|
28
28
|
width .28s cubic-bezier(.2, 0, .2, 1), height .28s cubic-bezier(.2, 0, .2, 1);
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
/* Motion is a preference (WCAG 2.3.3): no glide for those who asked for none. */
|
|
32
|
+
@media (prefers-reduced-motion: reduce) {
|
|
33
|
+
.grafloria-html-layer.axdb-glide > .grafloria-node-host { transition: none; }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/* ===== keyboard focus: the roving tab stop shows where it is (WCAG 2.4.7) ===== */
|
|
37
|
+
.grafloria-html-layer > .grafloria-node-host:focus-visible {
|
|
38
|
+
outline: 2px solid #3b52d9;
|
|
39
|
+
outline-offset: 2px;
|
|
40
|
+
border-radius: var(--axdb-rs-radius, 6px);
|
|
41
|
+
}
|
|
42
|
+
|
|
31
43
|
/* ===== the held tile: transition-exempt ghost, above everything ===== */
|
|
32
44
|
.grafloria-html-layer > .grafloria-node-host.axdb-ghost,
|
|
33
45
|
.grafloria-html-layer.axdb-glide > .grafloria-node-host.axdb-ghost {
|
|
@@ -43,7 +55,7 @@ const CSS = `
|
|
|
43
55
|
/* ===== the placeholder: dashed slab, truthful, never animated ===== */
|
|
44
56
|
.grafloria-html-layer > .axdb-ph {
|
|
45
57
|
position: absolute;
|
|
46
|
-
border-radius: var(--axdb-rs-radius,
|
|
58
|
+
border-radius: var(--axdb-rs-radius, 6px);
|
|
47
59
|
background: rgba(30, 34, 45, .14);
|
|
48
60
|
border: 2px dashed rgba(30, 34, 45, .28);
|
|
49
61
|
box-sizing: border-box;
|
|
@@ -60,17 +72,20 @@ const CSS = `
|
|
|
60
72
|
position: absolute;
|
|
61
73
|
right: 0;
|
|
62
74
|
bottom: 0;
|
|
63
|
-
width:
|
|
64
|
-
height:
|
|
75
|
+
width: 24px; /* WCAG 2.5.8: a 24-px minimum target (was 18) */
|
|
76
|
+
height: 24px;
|
|
65
77
|
cursor: nwse-resize;
|
|
66
78
|
border-right: 3px solid rgba(120, 130, 148, .55);
|
|
67
79
|
border-bottom: 3px solid rgba(120, 130, 148, .55);
|
|
68
|
-
border-bottom-right-radius: var(--axdb-rs-radius,
|
|
80
|
+
border-bottom-right-radius: var(--axdb-rs-radius, 6px);
|
|
69
81
|
opacity: 0;
|
|
70
82
|
transition: opacity .12s;
|
|
71
83
|
z-index: 5;
|
|
72
84
|
}
|
|
73
85
|
.grafloria-node-host:hover > .axdb-rs { opacity: 1; }
|
|
86
|
+
/* A finger never hovers: on touch devices the corner handle is always visible
|
|
87
|
+
(review D8 — hover-only meant no resize at all on a tablet). */
|
|
88
|
+
@media (hover: none) { .grafloria-node-host > .axdb-rs { opacity: .8; } }
|
|
74
89
|
.grafloria-node-host > .axdb-rs:hover { border-color: #3b52d9; }
|
|
75
90
|
/* RTL boards grow leftwards, so the grab corner mirrors with them. */
|
|
76
91
|
.grafloria-node-host > .axdb-rs.axdb-rs--rtl {
|
|
@@ -80,7 +95,7 @@ const CSS = `
|
|
|
80
95
|
border-right: none;
|
|
81
96
|
border-left: 3px solid rgba(120, 130, 148, .55);
|
|
82
97
|
border-bottom-right-radius: 0;
|
|
83
|
-
border-bottom-left-radius: var(--axdb-rs-radius,
|
|
98
|
+
border-bottom-left-radius: var(--axdb-rs-radius, 6px);
|
|
84
99
|
}
|
|
85
100
|
|
|
86
101
|
/* ===== palette drag-in chip (screen-space clone following the cursor) ===== */
|
|
@@ -90,7 +105,12 @@ const CSS = `
|
|
|
90
105
|
pointer-events: none;
|
|
91
106
|
opacity: .9;
|
|
92
107
|
filter: drop-shadow(0 8px 14px rgba(16, 24, 40, .3));
|
|
108
|
+
transition: opacity .12s, filter .12s;
|
|
93
109
|
}
|
|
110
|
+
/* A bounded board with no room refuses the entry: the chip dims to say so
|
|
111
|
+
(the same signal a tile ghost gives outside the board). Without this rule
|
|
112
|
+
the class was set and nothing showed — a refusal a user could not see. */
|
|
113
|
+
.axdb-drag-chip.axdb-out { opacity: .35; filter: grayscale(.7) drop-shadow(0 8px 14px rgba(16, 24, 40, .2)); }
|
|
94
114
|
|
|
95
115
|
/* ===========================================================================
|
|
96
116
|
BUILT-IN WIDGET CARDS — what widgets.ts paints when a page writes no
|
|
@@ -102,11 +122,19 @@ const CSS = `
|
|
|
102
122
|
=========================================================================== */
|
|
103
123
|
.axdb-widget {
|
|
104
124
|
--axdb-ink: #1f2430;
|
|
105
|
-
|
|
125
|
+
/* 5.95:1 on the card — WCAG 1.4.3 for the 9–11px captions this paints
|
|
126
|
+
(the #7a8496 it replaced sat at 3.77:1, axe-core's first finding). */
|
|
127
|
+
--axdb-muted: #5a6478;
|
|
106
128
|
--axdb-grid: rgba(120, 130, 148, .22);
|
|
107
129
|
--axdb-card: #fff;
|
|
108
130
|
--axdb-line: #e7eaf1;
|
|
109
131
|
--axdb-soft: rgba(120, 130, 148, .14);
|
|
132
|
+
--axdb-up: #0f7a3d; /* 5.42:1 */
|
|
133
|
+
--axdb-down: #be123c; /* 6.29:1 */
|
|
134
|
+
/* The categorical palette, as tokens so the dark card can carry its own
|
|
135
|
+
steps: every entry clears 3:1 against its card (WCAG 1.4.11). */
|
|
136
|
+
--axdb-c1: #3b52d9; --axdb-c2: #0369a1; --axdb-c3: #0f766e;
|
|
137
|
+
--axdb-c4: #b45309; --axdb-c5: #6d28d9; --axdb-c6: #475569;
|
|
110
138
|
box-sizing: border-box;
|
|
111
139
|
width: 100%;
|
|
112
140
|
height: 100%;
|
|
@@ -116,11 +144,29 @@ const CSS = `
|
|
|
116
144
|
padding: 13px 15px 12px;
|
|
117
145
|
background: var(--axdb-card);
|
|
118
146
|
border: 1px solid var(--axdb-line);
|
|
119
|
-
border-radius: var(--axdb-rs-radius,
|
|
147
|
+
border-radius: var(--axdb-rs-radius, 6px);
|
|
120
148
|
box-shadow: 0 1px 2px rgba(16, 24, 40, .05), 0 1px 3px rgba(16, 24, 40, .05);
|
|
121
149
|
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
122
150
|
color: var(--axdb-ink);
|
|
123
151
|
}
|
|
152
|
+
/* The host is the card's size container (named, so a widget's inner parts can
|
|
153
|
+
ask the TILE's size as well as their own body's), so the card can shed its
|
|
154
|
+
padding as the tile gets short instead of clipping its own content. */
|
|
155
|
+
.grafloria-html-layer > .grafloria-node-host { container: axdb-tile / size; }
|
|
156
|
+
/* Two class hops on the header: these blocks precede the header's own rule
|
|
157
|
+
below and would lose the cascade at equal specificity — at 54 px the margin
|
|
158
|
+
stayed 8 px, the body shrank to 14 px and the KPI figure hid. */
|
|
159
|
+
@container axdb-tile (max-height: 90px) {
|
|
160
|
+
.axdb-widget { padding: 8px 14px 7px; }
|
|
161
|
+
.axdb-widget > .axdb-widget-h { margin-bottom: 4px; }
|
|
162
|
+
}
|
|
163
|
+
@container axdb-tile (max-height: 46px) {
|
|
164
|
+
.axdb-widget { padding: 4px 12px 3px; }
|
|
165
|
+
.axdb-widget > .axdb-widget-h { margin-bottom: 0; }
|
|
166
|
+
}
|
|
167
|
+
@container axdb-tile (max-height: 26px) {
|
|
168
|
+
.axdb-widget { padding: 2px 12px 1px; }
|
|
169
|
+
}
|
|
124
170
|
.axdb-widget-h {
|
|
125
171
|
display: flex;
|
|
126
172
|
align-items: center;
|
|
@@ -138,9 +184,15 @@ const CSS = `
|
|
|
138
184
|
own (without this the 100%-tall svg pushes the legend out of the card). */
|
|
139
185
|
.axdb-widget-b.axdb-has-lg { display: flex; flex-direction: column; }
|
|
140
186
|
.axdb-widget-b.axdb-has-lg > svg { flex: 1; height: auto; min-height: 0; }
|
|
187
|
+
/* The data behind a chart, for readers that cannot see the chart: present in
|
|
188
|
+
the accessibility tree, absent from the picture (WCAG 1.1.1). */
|
|
189
|
+
.axdb-sr {
|
|
190
|
+
position: absolute; top: 0; left: 0; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden;
|
|
191
|
+
clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; border: 0;
|
|
192
|
+
}
|
|
141
193
|
.axdb-widget-empty {
|
|
142
194
|
display: flex; align-items: center; justify-content: center; height: 100%;
|
|
143
|
-
font: 500 11.5px/1.3 system-ui, sans-serif; color: var(--axdb-muted);
|
|
195
|
+
font: 500 11.5px/1.3 system-ui, sans-serif; color: var(--axdb-muted);
|
|
144
196
|
}
|
|
145
197
|
|
|
146
198
|
/* kpi: value + delta stack, the spark yields its height before they do */
|
|
@@ -150,17 +202,73 @@ const CSS = `
|
|
|
150
202
|
sliced mid-glyph by the card's overflow — the audited "trimmed" widgets.
|
|
151
203
|
cqh = 1% of the body's own height, clamped so full-size tiles look exactly
|
|
152
204
|
as before and short tiles compress instead of cutting. */
|
|
153
|
-
.axdb-widget-b.axdb-kpi { display: flex; flex-direction: column; container
|
|
154
|
-
|
|
155
|
-
|
|
205
|
+
.axdb-widget-b.axdb-kpi { display: flex; flex-direction: column; container: axdb-kpi / size; }
|
|
206
|
+
/* STEP DOWN, NEVER CLIP. As the body gets shorter the sparkline goes first, then the
|
|
207
|
+
delta, then the value — the header alone at the row floor. Before this a 99-px row
|
|
208
|
+
drew the sparkline as a 13-px sliver and a 28-px row cut the value mid-glyph (the
|
|
209
|
+
fluid board, after a drag pushed the KPI row down). Thresholds are body heights. */
|
|
210
|
+
/* Two class hops, so these outrank ".axdb-widget-b > svg { display: block }" —
|
|
211
|
+
a bare .axdb-kpi-s lost that cascade and the sparkline stayed. */
|
|
212
|
+
@container (max-height: 78px) { .axdb-kpi > .axdb-kpi-s { display: none; } }
|
|
213
|
+
@container (max-height: 40px) { .axdb-kpi > .axdb-kpi-d { display: none; } }
|
|
214
|
+
@container (max-height: 16px) { .axdb-kpi > .axdb-kpi-v { display: none; } }
|
|
215
|
+
.axdb-kpi-v { font: 700 clamp(15px, 44cqh, 30px)/1.05 system-ui, sans-serif; letter-spacing: -.02em; color: var(--axdb-ink); white-space: nowrap; }
|
|
216
|
+
.axdb-kpi-d { margin-top: clamp(1px, 5cqh, 6px); font: 600 clamp(9px, 19cqh, 12px)/1.2 system-ui, sans-serif; white-space: nowrap; }
|
|
156
217
|
.axdb-kpi-d span { color: var(--axdb-muted); font-weight: 500; }
|
|
157
|
-
.axdb-kpi-d.up { color:
|
|
158
|
-
.axdb-kpi-d.down { color:
|
|
159
|
-
|
|
218
|
+
.axdb-kpi-d.up { color: var(--axdb-up); }
|
|
219
|
+
.axdb-kpi-d.down { color: var(--axdb-down); }
|
|
220
|
+
/* Grows into a tall tile (a 3-row KPI is not a number over a void) and never
|
|
221
|
+
takes more than two fifths of the body. */
|
|
222
|
+
.axdb-kpi-s { margin-top: auto; height: auto; min-height: 0; flex: 1 1 34px; max-height: 40%; }
|
|
223
|
+
/* SHORT AND WIDE — a one-row KPI on a fluid board. Stacked, a short tile dropped
|
|
224
|
+
its sparkline and left a small figure in the corner of an empty card. Here the
|
|
225
|
+
figure, its delta and the sparkline sit in a row and the spark fills the width.
|
|
226
|
+
The outer query asks the TILE (host), the inner one the body, so a strip too
|
|
227
|
+
thin for a readable spark (under 24 px) keeps the figure alone. 340 px is what
|
|
228
|
+
a figure, its delta, the gaps and a spark worth reading need — the builder's
|
|
229
|
+
236-px tiles got a 40-px wedge of spark beside the delta. */
|
|
230
|
+
@container axdb-tile (max-height: 125px) and (min-width: 340px) {
|
|
231
|
+
.axdb-widget--kpi > .axdb-kpi { flex-direction: row; align-items: center; gap: 14px; }
|
|
232
|
+
.axdb-widget--kpi .axdb-kpi-d { margin-top: 0; }
|
|
233
|
+
@container axdb-kpi (min-height: 24px) {
|
|
234
|
+
.axdb-kpi > .axdb-kpi-s { display: block; flex: 1 1 40%; min-width: 80px; height: 100%; max-height: 100%; margin-top: 0; align-self: stretch; }
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
/* THE STRIP (row floor): header and figure on one line, the spark beside them
|
|
238
|
+
when the tile is wide enough. */
|
|
239
|
+
@container axdb-tile (max-height: 46px) {
|
|
240
|
+
.axdb-widget--kpi { flex-direction: row; align-items: center; gap: 12px; }
|
|
241
|
+
.axdb-widget--kpi > .axdb-widget-h { flex: none; }
|
|
242
|
+
/* The body is a SIZE container: in a centred row it would collapse to zero
|
|
243
|
+
and every height query would fire. It takes the card's full inner height. */
|
|
244
|
+
.axdb-widget--kpi > .axdb-kpi { flex: 1; align-self: stretch; }
|
|
245
|
+
}
|
|
160
246
|
|
|
161
|
-
/*
|
|
247
|
+
/* SPLIT LAYOUT chrome: a divider sits in the gap between two siblings (a
|
|
248
|
+
24-px hit zone around a 10-px gap) and shows its line on hover or while
|
|
249
|
+
dragged; the insertion line marks the edge a dragged widget will land on. */
|
|
250
|
+
.grafloria-html-layer > .axdb-div { position: absolute; z-index: 2; pointer-events: auto; background: transparent; }
|
|
251
|
+
.grafloria-html-layer > .axdb-div--row { cursor: col-resize; }
|
|
252
|
+
.grafloria-html-layer > .axdb-div--column { cursor: row-resize; }
|
|
253
|
+
.grafloria-html-layer > .axdb-div::after {
|
|
254
|
+
content: ""; position: absolute; border-radius: 2px; background: #3b52d9; opacity: 0; transition: opacity .12s ease;
|
|
255
|
+
}
|
|
256
|
+
.grafloria-html-layer > .axdb-div--row::after { left: 50%; top: 8px; bottom: 8px; width: 4px; margin-left: -2px; }
|
|
257
|
+
.grafloria-html-layer > .axdb-div--column::after { top: 50%; left: 8px; right: 8px; height: 4px; margin-top: -2px; }
|
|
258
|
+
.grafloria-html-layer > .axdb-div:hover::after, .grafloria-html-layer > .axdb-div.axdb-active::after { opacity: .9; }
|
|
259
|
+
.grafloria-html-layer > .axdb-ins {
|
|
260
|
+
position: absolute; z-index: 3; pointer-events: none; border-radius: 2px;
|
|
261
|
+
background: #3b52d9; box-shadow: 0 0 0 3px rgba(59, 82, 217, .22);
|
|
262
|
+
}
|
|
263
|
+
@media (prefers-reduced-motion: reduce) { .grafloria-html-layer > .axdb-div::after { transition: none; } }
|
|
264
|
+
|
|
265
|
+
/* donut: ring beside its legend. The ring takes the body's height (a tall
|
|
266
|
+
tile gets a bigger ring, not dead card), square, capped so its centre figure
|
|
267
|
+
stays a figure and not a headline. */
|
|
162
268
|
.axdb-widget-b.axdb-donut { display: flex; align-items: center; gap: 10px; }
|
|
163
|
-
.axdb-widget-b.axdb-donut > svg {
|
|
269
|
+
.axdb-widget-b.axdb-donut > svg {
|
|
270
|
+
flex: 0 0 auto; width: auto; height: 100%; max-height: 260px; max-width: 60%; aspect-ratio: 1 / 1;
|
|
271
|
+
}
|
|
164
272
|
|
|
165
273
|
/* legend chips, shared by line and donut */
|
|
166
274
|
.axdb-lg { display: flex; flex-wrap: wrap; gap: 4px 12px; margin-top: 9px; }
|
|
@@ -190,16 +298,25 @@ const CSS = `
|
|
|
190
298
|
--axdb-card: #1a1d25;
|
|
191
299
|
--axdb-line: #2b3040;
|
|
192
300
|
--axdb-soft: rgba(150, 160, 182, .16);
|
|
301
|
+
--axdb-up: #4ade80;
|
|
302
|
+
--axdb-down: #fb7185;
|
|
303
|
+
--axdb-c1: #8b9cff; --axdb-c2: #38bdf8; --axdb-c3: #2dd4bf;
|
|
304
|
+
--axdb-c4: #fbbf24; --axdb-c5: #a78bfa; --axdb-c6: #94a3b8;
|
|
193
305
|
}
|
|
194
306
|
}
|
|
195
307
|
`;
|
|
196
308
|
/** Idempotently inject the kit stylesheet (safe to call per binder). */
|
|
197
|
-
export function ensureDashboardKitStyles(doc
|
|
198
|
-
|
|
309
|
+
export function ensureDashboardKitStyles(doc) {
|
|
310
|
+
// SERVER-SAFE: `dashboard()` is a pure spec builder and runs where there is
|
|
311
|
+
// no document (SSR, a Node script sizing a board). A default parameter of
|
|
312
|
+
// `document` threw ReferenceError there — the packaging gate's raw-Node
|
|
313
|
+
// import caught it. No document, no stylesheet, no error.
|
|
314
|
+
const d = doc !== null && doc !== void 0 ? doc : (typeof document !== 'undefined' ? document : undefined);
|
|
315
|
+
if (!d || d.getElementById(DASHBOARD_KIT_STYLE_ID))
|
|
199
316
|
return;
|
|
200
|
-
const style =
|
|
317
|
+
const style = d.createElement('style');
|
|
201
318
|
style.id = DASHBOARD_KIT_STYLE_ID;
|
|
202
319
|
style.textContent = CSS;
|
|
203
|
-
|
|
320
|
+
d.head.appendChild(style);
|
|
204
321
|
}
|
|
205
322
|
//# sourceMappingURL=styles.js.map
|
|
@@ -89,6 +89,21 @@ export interface TableWidgetData {
|
|
|
89
89
|
}
|
|
90
90
|
/** The kinds `defaultWidgetRenderer` knows; anything else gets the placeholder. */
|
|
91
91
|
export declare const BUILT_IN_WIDGET_KINDS: readonly ["kpi", "line", "bar", "donut", "funnel", "table"];
|
|
92
|
+
/**
|
|
93
|
+
* The drawing box a chart should lay itself out in: the body it is painted
|
|
94
|
+
* into, less the legend's strip. A fixed 640×250 viewBox scaled to "meet" left
|
|
95
|
+
* a wide fluid tile with ~170 px of dead card either side; drawing to the
|
|
96
|
+
* body's own aspect fills it. Outside a layout (jsdom, a host not yet sized)
|
|
97
|
+
* the classic 640×250 stands in, and the size watcher below repaints once the
|
|
98
|
+
* real box exists.
|
|
99
|
+
*/
|
|
100
|
+
export declare function chartBox(body: {
|
|
101
|
+
clientWidth: number;
|
|
102
|
+
clientHeight: number;
|
|
103
|
+
}, legend?: boolean): {
|
|
104
|
+
W: number;
|
|
105
|
+
H: number;
|
|
106
|
+
};
|
|
92
107
|
/** `{ label, value, delta?, spark? }` — headline number + change + sparkline. */
|
|
93
108
|
export declare const renderKpiWidget: WidgetRenderer;
|
|
94
109
|
/** `{ series, labels? }` — area under the first series, a line per series. */
|
|
@@ -97,7 +112,7 @@ export declare const renderLineWidget: WidgetRenderer;
|
|
|
97
112
|
export declare const renderBarWidget: WidgetRenderer;
|
|
98
113
|
/** `{ slices: [{label, value, color?}], centerLabel? }` — ring + legend. */
|
|
99
114
|
export declare const renderDonutWidget: WidgetRenderer;
|
|
100
|
-
/** `{ stages: [{label, value}] }` — centred bars scaled against the
|
|
115
|
+
/** `{ stages: [{label, value}] }` — centred bars scaled against the widest stage. */
|
|
101
116
|
export declare const renderFunnelWidget: WidgetRenderer;
|
|
102
117
|
/** `{ columns, rows }` — plain rows; numeric cells right-align themselves. */
|
|
103
118
|
export declare const renderTableWidget: WidgetRenderer;
|
|
@@ -35,8 +35,12 @@ export const BUILT_IN_WIDGET_KINDS = ['kpi', 'line', 'bar', 'donut', 'funnel', '
|
|
|
35
35
|
* Categorical default palette — readable on both light and dark cards. Per-mark
|
|
36
36
|
* overrides win (`slices[].color`); everything else cycles this list.
|
|
37
37
|
*/
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
/* Every entry clears 3:1 against the light AND the dark card (WCAG 1.4.11,
|
|
39
|
+
non-text contrast): the sky, teal and amber of the first palette sat at
|
|
40
|
+
2.9, 2.5 and 2.2 against white and were replaced by their darker steps. */
|
|
41
|
+
const PALETTE_SIZE = 6;
|
|
42
|
+
/** The i-th palette token — the stylesheet resolves it per theme (`--axdb-c1…c6`). */
|
|
43
|
+
const colorAt = (i) => `var(--axdb-c${(((i % PALETTE_SIZE) + PALETTE_SIZE) % PALETTE_SIZE) + 1})`;
|
|
40
44
|
// -- primitives ---------------------------------------------------------------
|
|
41
45
|
const esc = (v) => String(v !== null && v !== void 0 ? v : '').replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' })[c]);
|
|
42
46
|
/** Finite numbers only — a NaN in the data must not become a NaN in a path. */
|
|
@@ -89,6 +93,67 @@ function empty(body, note = 'no data') {
|
|
|
89
93
|
body.innerHTML = `<div class="axdb-widget-empty">${esc(note)}</div>`;
|
|
90
94
|
}
|
|
91
95
|
const data = (widget) => { var _a; return ((_a = widget.data) !== null && _a !== void 0 ? _a : {}); };
|
|
96
|
+
/**
|
|
97
|
+
* The drawing box a chart should lay itself out in: the body it is painted
|
|
98
|
+
* into, less the legend's strip. A fixed 640×250 viewBox scaled to "meet" left
|
|
99
|
+
* a wide fluid tile with ~170 px of dead card either side; drawing to the
|
|
100
|
+
* body's own aspect fills it. Outside a layout (jsdom, a host not yet sized)
|
|
101
|
+
* the classic 640×250 stands in, and the size watcher below repaints once the
|
|
102
|
+
* real box exists.
|
|
103
|
+
*/
|
|
104
|
+
export function chartBox(body, legend = false) {
|
|
105
|
+
const w = body.clientWidth || 0;
|
|
106
|
+
const h = (body.clientHeight || 0) - (legend ? 26 : 0);
|
|
107
|
+
if (w < 60 || h < 40)
|
|
108
|
+
return { W: 640, H: 250 };
|
|
109
|
+
return { W: Math.round(w), H: Math.round(h) };
|
|
110
|
+
}
|
|
111
|
+
const sizeWatchers = new WeakMap();
|
|
112
|
+
/**
|
|
113
|
+
* Re-lay a chart out when the HOST's box changes (a fit-mode reflow, a
|
|
114
|
+
* resize, a narrower container) — coalesced to a frame, skipping sub-2-px
|
|
115
|
+
* jitter. `relayout` redraws ONLY the chart body: the card and anything an
|
|
116
|
+
* app painted onto it after `defaultWidgetRenderer` (a focus ring, a pin
|
|
117
|
+
* marker — the documented composition) are never touched. The first version
|
|
118
|
+
* re-ran the whole painter and wiped exactly those, which the save/load gate
|
|
119
|
+
* caught as a reloaded board reading differently from the original.
|
|
120
|
+
*/
|
|
121
|
+
function watchSize(host, relayout) {
|
|
122
|
+
var _a;
|
|
123
|
+
if (typeof ResizeObserver === 'undefined')
|
|
124
|
+
return;
|
|
125
|
+
(_a = sizeWatchers.get(host)) === null || _a === void 0 ? void 0 : _a.disconnect();
|
|
126
|
+
let last = { w: host.clientWidth, h: host.clientHeight };
|
|
127
|
+
let frame = 0;
|
|
128
|
+
const ro = new ResizeObserver(() => {
|
|
129
|
+
const w = host.clientWidth;
|
|
130
|
+
const h = host.clientHeight;
|
|
131
|
+
if (Math.abs(w - last.w) < 2 && Math.abs(h - last.h) < 2)
|
|
132
|
+
return;
|
|
133
|
+
last = { w, h };
|
|
134
|
+
if (frame)
|
|
135
|
+
cancelAnimationFrame(frame);
|
|
136
|
+
frame = requestAnimationFrame(() => {
|
|
137
|
+
frame = 0;
|
|
138
|
+
if (host.isConnected)
|
|
139
|
+
relayout();
|
|
140
|
+
else
|
|
141
|
+
ro.disconnect();
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
ro.observe(host);
|
|
145
|
+
sizeWatchers.set(host, ro);
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* The chart's numbers as a visually-hidden table — what a screen reader gets
|
|
149
|
+
* instead of an svg it cannot read (WCAG 1.1.1). `role="img"` + aria-label on
|
|
150
|
+
* the svg names the chart; this carries the values.
|
|
151
|
+
*/
|
|
152
|
+
const srTable = (caption, columns, rows) => `<table class="axdb-sr"><caption>${esc(caption)}</caption><thead><tr>${columns
|
|
153
|
+
.map((c) => `<th scope="col">${esc(c)}</th>`)
|
|
154
|
+
.join('')}</tr></thead><tbody>${rows
|
|
155
|
+
.map((r) => `<tr>${r.map((v) => `<td>${esc(v)}</td>`).join('')}</tr>`)
|
|
156
|
+
.join('')}</tbody></table>`;
|
|
92
157
|
const legend = (items, column = false) => `<div class="axdb-lg${column ? ' axdb-lg--col' : ''}">` +
|
|
93
158
|
items.map((i) => `<i><b style="background:${esc(i.color)}"></b>${esc(i.label)}</i>`).join('') +
|
|
94
159
|
'</div>';
|
|
@@ -142,13 +207,20 @@ function normalizeSeries(raw) {
|
|
|
142
207
|
}
|
|
143
208
|
/** `{ series, labels? }` — area under the first series, a line per series. */
|
|
144
209
|
export const renderLineWidget = (widget, host) => {
|
|
145
|
-
const d = data(widget);
|
|
146
210
|
const body = card(host, widget, titleOf(widget));
|
|
211
|
+
layoutLine(widget, body);
|
|
212
|
+
watchSize(host, () => layoutLine(widget, body));
|
|
213
|
+
};
|
|
214
|
+
/** The line chart's body, drawn to the body's current box. */
|
|
215
|
+
function layoutLine(widget, body) {
|
|
216
|
+
const d = data(widget);
|
|
147
217
|
const series = normalizeSeries(d.series);
|
|
148
218
|
if (!series.length)
|
|
149
219
|
return empty(body);
|
|
150
|
-
const
|
|
151
|
-
|
|
220
|
+
const named = series.filter((s) => s.name);
|
|
221
|
+
if (named.length)
|
|
222
|
+
body.classList.add('axdb-has-lg');
|
|
223
|
+
const { W, H } = chartBox(body, named.length > 0);
|
|
152
224
|
const pad = { l: 34, r: 12, t: 12, b: 22 };
|
|
153
225
|
const iw = W - pad.l - pad.r;
|
|
154
226
|
const ih = H - pad.t - pad.b;
|
|
@@ -168,6 +240,9 @@ export const renderLineWidget = (widget, host) => {
|
|
|
168
240
|
})
|
|
169
241
|
.join('');
|
|
170
242
|
const labels = Array.isArray(d.labels) ? d.labels : [];
|
|
243
|
+
// The label SET must not depend on the box: a board reloaded at another size
|
|
244
|
+
// has to paint the same text as the original (the save/load gate's contract).
|
|
245
|
+
// Only the geometry follows the box.
|
|
171
246
|
const every = labels.length > 8 ? 2 : 1;
|
|
172
247
|
const ticks = labels
|
|
173
248
|
.slice(0, count)
|
|
@@ -184,29 +259,42 @@ export const renderLineWidget = (widget, host) => {
|
|
|
184
259
|
`L${xAt(s.values.length - 1).toFixed(1)},${(pad.t + ih).toFixed(1)} Z" ` +
|
|
185
260
|
`fill="${colorAt(si)}" fill-opacity="0.10"></path>`
|
|
186
261
|
: '';
|
|
262
|
+
// A series with ONE point has nothing for a polyline to connect, so the
|
|
263
|
+
// plot rendered its axes and legend around an invisible chart — the
|
|
264
|
+
// classic day-one-of-data tile. A single value is still data: draw it as
|
|
265
|
+
// a dot. (Two or more points keep exactly the look they had.)
|
|
266
|
+
const dots = s.values.length < 2
|
|
267
|
+
? s.values
|
|
268
|
+
.map((v, i) => `<circle cx="${xAt(i).toFixed(1)}" cy="${yAt(v).toFixed(1)}" r="3.5" ` +
|
|
269
|
+
`fill="${colorAt(si)}"></circle>`)
|
|
270
|
+
.join('')
|
|
271
|
+
: '';
|
|
187
272
|
return (area +
|
|
188
273
|
`<polyline points="${pts}" fill="none" stroke="${colorAt(si)}" stroke-width="${si === 0 ? 2.4 : 1.8}" ` +
|
|
189
|
-
`stroke-linejoin="round" stroke-linecap="round"></polyline>`
|
|
274
|
+
`stroke-linejoin="round" stroke-linecap="round"></polyline>` +
|
|
275
|
+
dots);
|
|
190
276
|
})
|
|
191
277
|
.join('');
|
|
192
|
-
const named = series.filter((s) => s.name);
|
|
193
|
-
if (named.length)
|
|
194
|
-
body.classList.add('axdb-has-lg');
|
|
195
278
|
body.innerHTML =
|
|
196
279
|
`<svg viewBox="0 0 ${W} ${H}" preserveAspectRatio="xMidYMid meet" role="img" ` +
|
|
197
280
|
`aria-label="${esc(titleOf(widget))}">${grid}${ticks}${marks}</svg>` +
|
|
198
|
-
(named.length ? legend(series.map((s, i) => { var _a; return ({ label: String((_a = s.name) !== null && _a !== void 0 ? _a : ''), color: colorAt(i) }); })) : '')
|
|
199
|
-
};
|
|
281
|
+
(named.length ? legend(series.map((s, i) => { var _a; return ({ label: String((_a = s.name) !== null && _a !== void 0 ? _a : ''), color: colorAt(i) }); })) : '') +
|
|
282
|
+
srTable(titleOf(widget), ['', ...series.map((s, i) => { var _a; return String((_a = s.name) !== null && _a !== void 0 ? _a : `Series ${i + 1}`); })], Array.from({ length: count }, (_, i) => { var _a; return [(_a = labels[i]) !== null && _a !== void 0 ? _a : String(i + 1), ...series.map((s) => { var _a; return (_a = s.values[i]) !== null && _a !== void 0 ? _a : ''; })]; }));
|
|
283
|
+
}
|
|
200
284
|
// -- bar ----------------------------------------------------------------------
|
|
201
285
|
/** `{ bars: [{label, value}] }` — columns, value above, category below. */
|
|
202
286
|
export const renderBarWidget = (widget, host) => {
|
|
203
|
-
const d = data(widget);
|
|
204
287
|
const body = card(host, widget, titleOf(widget));
|
|
288
|
+
layoutBar(widget, body);
|
|
289
|
+
watchSize(host, () => layoutBar(widget, body));
|
|
290
|
+
};
|
|
291
|
+
/** The bar chart's body, drawn to the body's current box. */
|
|
292
|
+
function layoutBar(widget, body) {
|
|
293
|
+
const d = data(widget);
|
|
205
294
|
const bars = (Array.isArray(d.bars) ? d.bars : []).filter((b) => !!b);
|
|
206
295
|
if (!bars.length)
|
|
207
296
|
return empty(body);
|
|
208
|
-
const W =
|
|
209
|
-
const H = 250;
|
|
297
|
+
const { W, H } = chartBox(body);
|
|
210
298
|
const pad = { l: 34, r: 12, t: 12, b: 26 };
|
|
211
299
|
const iw = W - pad.l - pad.r;
|
|
212
300
|
const ih = H - pad.t - pad.b;
|
|
@@ -239,8 +327,9 @@ export const renderBarWidget = (widget, host) => {
|
|
|
239
327
|
.join('');
|
|
240
328
|
body.innerHTML =
|
|
241
329
|
`<svg viewBox="0 0 ${W} ${H}" preserveAspectRatio="xMidYMid meet" role="img" ` +
|
|
242
|
-
`aria-label="${esc(titleOf(widget))}">${grid}${marks}</svg
|
|
243
|
-
};
|
|
330
|
+
`aria-label="${esc(titleOf(widget))}">${grid}${marks}</svg>` +
|
|
331
|
+
srTable(titleOf(widget), ['Category', 'Value'], bars.map((b) => { var _a; return [(_a = b.label) !== null && _a !== void 0 ? _a : '', num(b.value)]; }));
|
|
332
|
+
}
|
|
244
333
|
// -- donut --------------------------------------------------------------------
|
|
245
334
|
/** One arc of the ring, as an SVG path command. */
|
|
246
335
|
function arcPath(cx, cy, r, a0, a1) {
|
|
@@ -290,38 +379,57 @@ export const renderDonutWidget = (widget, host) => {
|
|
|
290
379
|
label: `${(_a = s.label) !== null && _a !== void 0 ? _a : ''} · ${Math.round((num(s.value) / total) * 100)}%`,
|
|
291
380
|
color: (_b = s.color) !== null && _b !== void 0 ? _b : colorAt(i),
|
|
292
381
|
});
|
|
293
|
-
}), true)
|
|
382
|
+
}), true) +
|
|
383
|
+
srTable(titleOf(widget), ['Slice', 'Value', 'Share'], slices.map((s) => { var _a; return [(_a = s.label) !== null && _a !== void 0 ? _a : '', num(s.value), `${Math.round((num(s.value) / total) * 100)}%`]; }));
|
|
294
384
|
};
|
|
295
385
|
// -- funnel -------------------------------------------------------------------
|
|
296
|
-
/** `{ stages: [{label, value}] }` — centred bars scaled against the
|
|
386
|
+
/** `{ stages: [{label, value}] }` — centred bars scaled against the widest stage. */
|
|
297
387
|
export const renderFunnelWidget = (widget, host) => {
|
|
298
|
-
const d = data(widget);
|
|
299
388
|
const body = card(host, widget, titleOf(widget));
|
|
389
|
+
layoutFunnel(widget, body);
|
|
390
|
+
watchSize(host, () => layoutFunnel(widget, body));
|
|
391
|
+
};
|
|
392
|
+
/** Room an 11-px value needs, so the smallest stage's bar still holds its number. */
|
|
393
|
+
const valueWidth = (text) => text.length * 6.6 + 14;
|
|
394
|
+
/**
|
|
395
|
+
* The funnel's body, drawn to the body's current box. The old fixed 260-wide
|
|
396
|
+
* viewBox scaled bars AND type with the tile: 24-px digits in a tall tile,
|
|
397
|
+
* "188" spilling out of a stage narrower than its own label. Here the type is
|
|
398
|
+
* fixed, bars share the height between a readable 18 px and a 40 px cap, the
|
|
399
|
+
* stack is centred, and a bar is never narrower than its value.
|
|
400
|
+
*/
|
|
401
|
+
function layoutFunnel(widget, body) {
|
|
402
|
+
const d = data(widget);
|
|
300
403
|
const stages = (Array.isArray(d.stages) ? d.stages : []).filter((s) => !!s);
|
|
301
404
|
if (!stages.length)
|
|
302
405
|
return empty(body);
|
|
303
|
-
const W =
|
|
304
|
-
const
|
|
305
|
-
const gap =
|
|
306
|
-
const
|
|
406
|
+
const { W, H } = chartBox(body);
|
|
407
|
+
const n = stages.length;
|
|
408
|
+
const gap = 6;
|
|
409
|
+
const rowH = Math.max(18, Math.min(40, (H - gap * (n - 1)) / n));
|
|
410
|
+
const top = Math.max(0, (H - (n * rowH + (n - 1) * gap)) / 2);
|
|
411
|
+
const labelCol = Math.min(120, Math.max(60, W * 0.28));
|
|
412
|
+
const track = Math.max(40, W - labelCol - 16);
|
|
307
413
|
const max = Math.max(...stages.map((s) => num(s.value)), 1);
|
|
308
|
-
const track = W - 90;
|
|
309
414
|
const marks = stages
|
|
310
415
|
.map((s, i) => {
|
|
311
416
|
var _a;
|
|
312
|
-
const
|
|
313
|
-
const
|
|
314
|
-
const
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
`<text x="${
|
|
417
|
+
const text = compact(num(s.value));
|
|
418
|
+
const w = Math.min(track, Math.max(valueWidth(text), (num(s.value) / max) * track));
|
|
419
|
+
const x = 8 + (track - w) / 2;
|
|
420
|
+
const y = top + i * (rowH + gap);
|
|
421
|
+
const mid = (y + rowH / 2 + 4).toFixed(1);
|
|
422
|
+
return (`<rect x="${x.toFixed(1)}" y="${y.toFixed(1)}" width="${w.toFixed(1)}" height="${rowH.toFixed(1)}" rx="6" fill="${colorAt(i)}"></rect>` +
|
|
423
|
+
`<text x="${(x + w / 2).toFixed(1)}" y="${mid}" text-anchor="middle" font-size="11" ` +
|
|
424
|
+
`font-weight="600" fill="#fff">${esc(text)}</text>` +
|
|
425
|
+
`<text x="${8 + track + 8}" y="${mid}" font-size="10.5" fill="var(--axdb-muted)">${esc((_a = s.label) !== null && _a !== void 0 ? _a : '')}</text>`);
|
|
319
426
|
})
|
|
320
427
|
.join('');
|
|
321
428
|
body.innerHTML =
|
|
322
429
|
`<svg viewBox="0 0 ${W} ${H}" preserveAspectRatio="xMidYMid meet" role="img" ` +
|
|
323
|
-
`aria-label="${esc(titleOf(widget))}">${marks}</svg
|
|
324
|
-
};
|
|
430
|
+
`aria-label="${esc(titleOf(widget))}">${marks}</svg>` +
|
|
431
|
+
srTable(titleOf(widget), ['Stage', 'Value'], stages.map((s) => { var _a; return [(_a = s.label) !== null && _a !== void 0 ? _a : '', num(s.value)]; }));
|
|
432
|
+
}
|
|
325
433
|
// -- table --------------------------------------------------------------------
|
|
326
434
|
/** `{ columns, rows }` — plain rows; numeric cells right-align themselves. */
|
|
327
435
|
export const renderTableWidget = (widget, host) => {
|
|
@@ -343,6 +451,11 @@ export const renderTableWidget = (widget, host) => {
|
|
|
343
451
|
'</tr>')
|
|
344
452
|
.join('');
|
|
345
453
|
body.classList.add('axdb-scroll');
|
|
454
|
+
// A scrollable region must be reachable by keyboard (axe: scrollable-
|
|
455
|
+
// region-focusable) — it is a tab stop with the table's name.
|
|
456
|
+
body.setAttribute('tabindex', '0');
|
|
457
|
+
body.setAttribute('role', 'region');
|
|
458
|
+
body.setAttribute('aria-label', titleOf(widget));
|
|
346
459
|
body.innerHTML = `<table class="axdb-table">${head}<tbody>${tbody}</tbody></table>`;
|
|
347
460
|
};
|
|
348
461
|
// -- dispatch -----------------------------------------------------------------
|
|
@@ -52,6 +52,50 @@ export interface HtmlNode {
|
|
|
52
52
|
export declare function entityCardContent(entity: ErEntitySpec, editable?: boolean): HtmlNode;
|
|
53
53
|
/** Card height for an entity — explicit height wins, else derived from columns. */
|
|
54
54
|
export declare function entityAutoHeight(entity: ErEntitySpec, editable?: boolean): number;
|
|
55
|
+
/** Horizontal padding shared by `.axk-row`, `.axk-entity-head`, `.axk-member`
|
|
56
|
+
* and `.axk-uml-name` — all of them are `padding: Npx 10px`. */
|
|
57
|
+
export declare const CARD_PAD_X = 20;
|
|
58
|
+
/** `.axk-row`'s `gap: 8px`. */
|
|
59
|
+
export declare const ER_ROW_GAP = 8;
|
|
60
|
+
/** `.axk-key`'s fixed `width: 22px`. */
|
|
61
|
+
export declare const ER_KEY_W = 22;
|
|
62
|
+
/** `.axk-ty`'s `min-width: 52px` — it never measures narrower than its target. */
|
|
63
|
+
export declare const ER_TYPE_MIN_W = 52;
|
|
64
|
+
/** `.axk-col-del`'s `width: 14px` (present only when editable). */
|
|
65
|
+
export declare const CARD_DEL_W = 14;
|
|
66
|
+
/**
|
|
67
|
+
* Horizontal counterpart of ER_BORDER_SLACK: the html wrapper's padding
|
|
68
|
+
* (`html.padding ?? 4`, so 8px across) plus the card's own 1px left/right
|
|
69
|
+
* borders. The node's width is the OUTER box; the row only ever gets what is
|
|
70
|
+
* left after these, so a derived width that ignored them came out 10px short
|
|
71
|
+
* and truncated by a hair — which is exactly how it was first measured.
|
|
72
|
+
*/
|
|
73
|
+
export declare const CARD_SLACK_X = 10;
|
|
74
|
+
/** Defaults, and the floor every derived width is taken against. */
|
|
75
|
+
export declare const ER_DEFAULT_WIDTH = 190;
|
|
76
|
+
export declare const UML_DEFAULT_WIDTH = 200;
|
|
77
|
+
/**
|
|
78
|
+
* Ceiling for a DERIVED width. A card that sized itself to a pathological
|
|
79
|
+
* identifier would dominate the diagram, so past this the stylesheet's ellipsis
|
|
80
|
+
* takes over — visibly, which is the point — and an author who needs the whole
|
|
81
|
+
* string sets an explicit `width`. An explicit width is never clamped.
|
|
82
|
+
*/
|
|
83
|
+
export declare const CARD_MAX_AUTO_W = 420;
|
|
84
|
+
/**
|
|
85
|
+
* Width of `text` in CSS pixels, as the browser will really lay it out.
|
|
86
|
+
*
|
|
87
|
+
* Canvas `measureText` was calibrated against live `getBoundingClientRect()`
|
|
88
|
+
* widths for these exact fonts and agreed to within 0.02px, which is what makes
|
|
89
|
+
* deriving a card's width from its content safe rather than a guess.
|
|
90
|
+
*
|
|
91
|
+
* Outside a browser (jsdom, SSR) there is no 2d context and this falls back to
|
|
92
|
+
* a per-character average. The fallback only has to be roughly right: it feeds
|
|
93
|
+
* a `Math.max` against the default width, and anything it underestimates is
|
|
94
|
+
* ellipsised by the stylesheet rather than lost.
|
|
95
|
+
*/
|
|
96
|
+
export declare function measureCardText(text: string, font: string, tracking?: number): number;
|
|
97
|
+
/** Card width for an entity — explicit width wins, else derived from content. */
|
|
98
|
+
export declare function entityAutoWidth(entity: ErEntitySpec, editable?: boolean): number;
|
|
55
99
|
/**
|
|
56
100
|
* The `.axk-uml` content tree. `editable` adds a delete control per member and
|
|
57
101
|
* a trailing add affordance in each compartment.
|
|
@@ -59,6 +103,8 @@ export declare function entityAutoHeight(entity: ErEntitySpec, editable?: boolea
|
|
|
59
103
|
export declare function classCardContent(cls: UmlClassSpec, editable?: boolean): HtmlNode;
|
|
60
104
|
/** Card height for a class — explicit height wins, else derived from members. */
|
|
61
105
|
export declare function classAutoHeight(cls: UmlClassSpec, editable?: boolean): number;
|
|
106
|
+
/** Card width for a class — explicit width wins, else derived from content. */
|
|
107
|
+
export declare function classAutoWidth(cls: UmlClassSpec, editable?: boolean): number;
|
|
62
108
|
/**
|
|
63
109
|
* Match old columns to new columns for port reconciliation. Returns a map from
|
|
64
110
|
* OLD index → NEW index; an old index absent from the map is a REMOVED column
|