@grafloria/element 0.3.23 → 0.4.1
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/README.md +9 -7
- package/package.json +7 -9
- package/src/index.d.ts +16 -16
- package/src/index.js +58 -339
- package/src/lib/dashboard-kit/dashboard.d.ts +1 -1
- package/src/lib/dashboard-kit/dashboard.js +22 -26
- package/src/lib/dashboard-kit/grid-binder.d.ts +1 -1
- package/src/lib/dashboard-kit/grid-binder.js +62 -65
- package/src/lib/dashboard-kit/grid-mapping.js +14 -26
- package/src/lib/dashboard-kit/index.d.ts +5 -5
- package/src/lib/dashboard-kit/index.js +5 -30
- package/src/lib/dashboard-kit/styles.js +4 -8
- package/src/lib/dashboard-kit/widgets.d.ts +1 -1
- package/src/lib/dashboard-kit/widgets.js +16 -26
- package/src/lib/diagram-kit/card.d.ts +2 -2
- package/src/lib/diagram-kit/card.js +22 -32
- package/src/lib/diagram-kit/editing.js +15 -20
- package/src/lib/diagram-kit/er.d.ts +1 -1
- package/src/lib/diagram-kit/er.js +15 -18
- package/src/lib/diagram-kit/handles.d.ts +4 -4
- package/src/lib/diagram-kit/handles.js +16 -29
- package/src/lib/diagram-kit/index.d.ts +10 -10
- package/src/lib/diagram-kit/index.js +10 -50
- package/src/lib/diagram-kit/join-guidance.js +9 -18
- package/src/lib/diagram-kit/rows.js +1 -4
- package/src/lib/diagram-kit/styles.js +4 -8
- package/src/lib/diagram-kit/uml.js +10 -13
- package/src/lib/diagram-kit/update.d.ts +1 -1
- package/src/lib/diagram-kit/update.js +19 -26
- package/src/lib/grafloria-flow-element.js +19 -24
- package/src/lib/grafloria.d.ts +4 -4
- package/src/lib/grafloria.js +12 -17
- package/src/lib/load.d.ts +3 -3
- package/src/lib/load.js +19 -22
- package/src/lib/node-type-registry.js +6 -14
- package/src/lib/stencil-kit/builders.js +4 -10
- package/src/lib/stencil-kit/card-builders.js +14 -17
- package/src/lib/stencil-kit/index.d.ts +7 -7
- package/src/lib/stencil-kit/index.js +6 -16
- package/src/lib/stencil-kit/palette.js +14 -17
- package/src/lib/stencil-kit/shape-data.js +25 -28
- package/src/lib/stencil-kit/styles.js +1 -4
- package/esm/src/index.js +0 -295
- package/esm/src/lib/dashboard-kit/dashboard.js +0 -590
- package/esm/src/lib/dashboard-kit/grid-binder.js +0 -1633
- package/esm/src/lib/dashboard-kit/grid-mapping.js +0 -164
- package/esm/src/lib/dashboard-kit/index.js +0 -8
- package/esm/src/lib/dashboard-kit/styles.js +0 -205
- package/esm/src/lib/dashboard-kit/widgets.js +0 -379
- package/esm/src/lib/diagram-kit/card.js +0 -199
- package/esm/src/lib/diagram-kit/editing.js +0 -448
- package/esm/src/lib/diagram-kit/er.js +0 -160
- package/esm/src/lib/diagram-kit/handles.js +0 -312
- package/esm/src/lib/diagram-kit/index.js +0 -15
- package/esm/src/lib/diagram-kit/join-guidance.js +0 -322
- package/esm/src/lib/diagram-kit/rows.js +0 -144
- package/esm/src/lib/diagram-kit/styles.js +0 -118
- package/esm/src/lib/diagram-kit/uml.js +0 -135
- package/esm/src/lib/diagram-kit/update.js +0 -244
- package/esm/src/lib/grafloria-flow-element.js +0 -266
- package/esm/src/lib/grafloria.js +0 -69
- package/esm/src/lib/load.js +0 -252
- package/esm/src/lib/node-type-registry.js +0 -42
- package/esm/src/lib/stencil-kit/builders.js +0 -18
- package/esm/src/lib/stencil-kit/card-builders.js +0 -105
- package/esm/src/lib/stencil-kit/index.js +0 -9
- package/esm/src/lib/stencil-kit/palette.js +0 -427
- package/esm/src/lib/stencil-kit/shape-data.js +0 -537
- package/esm/src/lib/stencil-kit/styles.js +0 -126
|
@@ -1,379 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* BUILT-IN WIDGET RENDERERS — what `kind` means when you write no `renderWidget`.
|
|
3
|
-
*
|
|
4
|
-
* `dashboard({ views: [...] })` is data-first: a widget is `{ id, kind, span,
|
|
5
|
-
* rows, data }`. Until now `kind` was a free-form string that only meant
|
|
6
|
-
* something if the page supplied `renderWidget`; omitting it drew a titled
|
|
7
|
-
* empty frame. This module is the other half — six real renderers driven by
|
|
8
|
-
* the DEVELOPER'S OWN `data`, so a dashboard is useful from pure data alone:
|
|
9
|
-
*
|
|
10
|
-
* ```js
|
|
11
|
-
* dashboard({ widgets: [
|
|
12
|
-
* { id: 'rev', kind: 'kpi', span: 3, data: { label: 'Revenue', value: '$6.8M', delta: 12.4 } },
|
|
13
|
-
* { id: 'trend', kind: 'line', span: 8, rows: 2, data: { series: [10, 14, 12, 19] } },
|
|
14
|
-
* ]}); // no renderWidget, no charting library, real widgets
|
|
15
|
-
* ```
|
|
16
|
-
*
|
|
17
|
-
* TWO THINGS THIS DELIBERATELY IS NOT:
|
|
18
|
-
* - A charting library. Every mark is hand-drawn inline SVG (the same
|
|
19
|
-
* technique the dashboard demo uses), so the kit still adds ZERO
|
|
20
|
-
* dependencies and the whole file is readable.
|
|
21
|
-
* - A dataset. Nothing here ships sample numbers; every renderer reads the
|
|
22
|
-
* `data` the developer declared and degrades to an empty-state note when it
|
|
23
|
-
* is missing, partial or the wrong shape. A widget renderer must NEVER
|
|
24
|
-
* throw: it paints into a live board, mid-gesture, on every reflow.
|
|
25
|
-
*
|
|
26
|
-
* `renderWidget` remains the seam for anything richer — and it composes:
|
|
27
|
-
* call `defaultWidgetRenderer(widget, host)` first, then decorate the host
|
|
28
|
-
* (that is exactly what demos/dashboard/dashboard-builder.html does for its
|
|
29
|
-
* focus ring and pin marker).
|
|
30
|
-
*/
|
|
31
|
-
import { ensureDashboardKitStyles } from './styles';
|
|
32
|
-
/** The kinds `defaultWidgetRenderer` knows; anything else gets the placeholder. */
|
|
33
|
-
export const BUILT_IN_WIDGET_KINDS = ['kpi', 'line', 'bar', 'donut', 'funnel', 'table'];
|
|
34
|
-
/**
|
|
35
|
-
* Categorical default palette — readable on both light and dark cards. Per-mark
|
|
36
|
-
* overrides win (`slices[].color`); everything else cycles this list.
|
|
37
|
-
*/
|
|
38
|
-
const PALETTE = ['#3b52d9', '#0ea5e9', '#14b8a6', '#f59e0b', '#8b5cf6', '#64748b'];
|
|
39
|
-
const colorAt = (i) => PALETTE[((i % PALETTE.length) + PALETTE.length) % PALETTE.length];
|
|
40
|
-
// -- primitives ---------------------------------------------------------------
|
|
41
|
-
const esc = (v) => String(v !== null && v !== void 0 ? v : '').replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' })[c]);
|
|
42
|
-
/** Finite numbers only — a NaN in the data must not become a NaN in a path. */
|
|
43
|
-
const num = (v, fallback = 0) => (typeof v === 'number' && Number.isFinite(v) ? v : fallback);
|
|
44
|
-
const isNum = (v) => typeof v === 'number' && Number.isFinite(v);
|
|
45
|
-
/** 6730 → '6.7k'. Axis and centre figures only; your `value` is never touched. */
|
|
46
|
-
function compact(n) {
|
|
47
|
-
const a = Math.abs(n);
|
|
48
|
-
if (a >= 1e9)
|
|
49
|
-
return `${(n / 1e9).toFixed(1)}B`;
|
|
50
|
-
if (a >= 1e6)
|
|
51
|
-
return `${(n / 1e6).toFixed(1)}M`;
|
|
52
|
-
if (a >= 1e4)
|
|
53
|
-
return `${(n / 1e3).toFixed(1)}k`;
|
|
54
|
-
return `${Math.round(n * 100) / 100}`;
|
|
55
|
-
}
|
|
56
|
-
/** Round an axis maximum up to a friendly step (762 → 800, 2860 → 3000). */
|
|
57
|
-
function niceMax(v) {
|
|
58
|
-
if (!(v > 0))
|
|
59
|
-
return 1;
|
|
60
|
-
const step = Math.pow(10, Math.floor(Math.log10(v))) / 2;
|
|
61
|
-
return Math.ceil(v / step) * step;
|
|
62
|
-
}
|
|
63
|
-
/** The card shell: `<div class="axdb-widget"><div h/><div b/></div>`, body returned. */
|
|
64
|
-
function card(host, widget, title) {
|
|
65
|
-
var _a, _b;
|
|
66
|
-
const doc = (_a = host.ownerDocument) !== null && _a !== void 0 ? _a : document;
|
|
67
|
-
ensureDashboardKitStyles(doc);
|
|
68
|
-
host.innerHTML = '';
|
|
69
|
-
const root = doc.createElement('div');
|
|
70
|
-
root.className = `axdb-widget axdb-widget--${(_b = widget.kind) !== null && _b !== void 0 ? _b : 'widget'}`;
|
|
71
|
-
const head = doc.createElement('div');
|
|
72
|
-
head.className = 'axdb-widget-h';
|
|
73
|
-
head.textContent = title;
|
|
74
|
-
const body = doc.createElement('div');
|
|
75
|
-
body.className = 'axdb-widget-b';
|
|
76
|
-
root.appendChild(head);
|
|
77
|
-
root.appendChild(body);
|
|
78
|
-
host.appendChild(root);
|
|
79
|
-
return body;
|
|
80
|
-
}
|
|
81
|
-
/** Header text: an explicit title wins, then a data-supplied label, then the kind. */
|
|
82
|
-
function titleOf(widget, label) {
|
|
83
|
-
var _a, _b, _c;
|
|
84
|
-
const t = (_c = (_b = (_a = widget.title) !== null && _a !== void 0 ? _a : (typeof label === 'string' && label ? label : undefined)) !== null && _b !== void 0 ? _b : widget.kind) !== null && _c !== void 0 ? _c : widget.id;
|
|
85
|
-
return String(t);
|
|
86
|
-
}
|
|
87
|
-
/** The one empty state, so every kind fails the same readable way. */
|
|
88
|
-
function empty(body, note = 'no data') {
|
|
89
|
-
body.innerHTML = `<div class="axdb-widget-empty">${esc(note)}</div>`;
|
|
90
|
-
}
|
|
91
|
-
const data = (widget) => { var _a; return ((_a = widget.data) !== null && _a !== void 0 ? _a : {}); };
|
|
92
|
-
const legend = (items, column = false) => `<div class="axdb-lg${column ? ' axdb-lg--col' : ''}">` +
|
|
93
|
-
items.map((i) => `<i><b style="background:${esc(i.color)}"></b>${esc(i.label)}</i>`).join('') +
|
|
94
|
-
'</div>';
|
|
95
|
-
// -- kpi ----------------------------------------------------------------------
|
|
96
|
-
/** `{ label, value, delta?, spark? }` — headline number + change + sparkline. */
|
|
97
|
-
export const renderKpiWidget = (widget, host) => {
|
|
98
|
-
var _a;
|
|
99
|
-
const d = data(widget);
|
|
100
|
-
const body = card(host, widget, titleOf(widget, d.label));
|
|
101
|
-
const value = d.value === undefined || d.value === null || d.value === '' ? '—' : d.value;
|
|
102
|
-
let html = `<div class="axdb-kpi-v">${esc(value)}</div>`;
|
|
103
|
-
if (isNum(d.delta)) {
|
|
104
|
-
const up = d.delta >= 0;
|
|
105
|
-
html +=
|
|
106
|
-
`<div class="axdb-kpi-d ${up ? 'up' : 'down'}">${up ? '▲' : '▼'} ${Math.abs(d.delta)}% ` +
|
|
107
|
-
`<span>${esc((_a = d.deltaLabel) !== null && _a !== void 0 ? _a : 'vs previous')}</span></div>`;
|
|
108
|
-
}
|
|
109
|
-
const spark = (Array.isArray(d.spark) ? d.spark : []).filter(isNum);
|
|
110
|
-
if (spark.length > 1) {
|
|
111
|
-
const W = 240;
|
|
112
|
-
const H = 40;
|
|
113
|
-
const max = Math.max(...spark);
|
|
114
|
-
const min = Math.min(...spark);
|
|
115
|
-
const pts = spark.map((v, i) => {
|
|
116
|
-
const x = (i / (spark.length - 1)) * W;
|
|
117
|
-
const y = H - ((v - min) / (max - min || 1)) * (H - 6) - 3;
|
|
118
|
-
return `${x.toFixed(1)},${y.toFixed(1)}`;
|
|
119
|
-
});
|
|
120
|
-
html +=
|
|
121
|
-
`<svg class="axdb-kpi-s" viewBox="0 0 ${W} ${H}" preserveAspectRatio="none" aria-hidden="true">` +
|
|
122
|
-
`<path d="M0,${H} L${pts.join(' L')} L${W},${H} Z" fill="${colorAt(0)}" fill-opacity="0.12"></path>` +
|
|
123
|
-
`<polyline points="${pts.join(' ')}" fill="none" stroke="${colorAt(0)}" stroke-width="2" ` +
|
|
124
|
-
`stroke-linejoin="round" stroke-linecap="round"></polyline></svg>`;
|
|
125
|
-
}
|
|
126
|
-
body.classList.add('axdb-kpi');
|
|
127
|
-
body.innerHTML = html;
|
|
128
|
-
};
|
|
129
|
-
// -- line ---------------------------------------------------------------------
|
|
130
|
-
/** Accept both `number[]` and `{name, values}[]`, drop anything unusable. */
|
|
131
|
-
function normalizeSeries(raw) {
|
|
132
|
-
if (!Array.isArray(raw) || raw.length === 0)
|
|
133
|
-
return [];
|
|
134
|
-
if (raw.every((s) => typeof s === 'number')) {
|
|
135
|
-
const values = raw.filter(isNum);
|
|
136
|
-
return values.length ? [{ values }] : [];
|
|
137
|
-
}
|
|
138
|
-
return raw
|
|
139
|
-
.filter((s) => !!s && Array.isArray(s.values))
|
|
140
|
-
.map((s) => ({ name: s.name, values: s.values.filter(isNum) }))
|
|
141
|
-
.filter((s) => s.values.length > 0);
|
|
142
|
-
}
|
|
143
|
-
/** `{ series, labels? }` — area under the first series, a line per series. */
|
|
144
|
-
export const renderLineWidget = (widget, host) => {
|
|
145
|
-
const d = data(widget);
|
|
146
|
-
const body = card(host, widget, titleOf(widget));
|
|
147
|
-
const series = normalizeSeries(d.series);
|
|
148
|
-
if (!series.length)
|
|
149
|
-
return empty(body);
|
|
150
|
-
const W = 640;
|
|
151
|
-
const H = 250;
|
|
152
|
-
const pad = { l: 34, r: 12, t: 12, b: 22 };
|
|
153
|
-
const iw = W - pad.l - pad.r;
|
|
154
|
-
const ih = H - pad.t - pad.b;
|
|
155
|
-
const all = series.flatMap((s) => s.values);
|
|
156
|
-
const count = Math.max(...series.map((s) => s.values.length));
|
|
157
|
-
const max = niceMax(Math.max(...all));
|
|
158
|
-
const min = Math.min(0, ...all);
|
|
159
|
-
const xAt = (i) => pad.l + (count > 1 ? (i / (count - 1)) * iw : iw / 2);
|
|
160
|
-
const yAt = (v) => pad.t + ih - ((v - min) / (max - min || 1)) * ih;
|
|
161
|
-
const grid = [0, 0.25, 0.5, 0.75, 1]
|
|
162
|
-
.map((f) => {
|
|
163
|
-
const y = pad.t + ih - f * ih;
|
|
164
|
-
return (`<line x1="${pad.l}" y1="${y.toFixed(1)}" x2="${W - pad.r}" y2="${y.toFixed(1)}" ` +
|
|
165
|
-
`stroke="var(--axdb-grid)" stroke-width="1"></line>` +
|
|
166
|
-
`<text x="${pad.l - 6}" y="${(y + 3).toFixed(1)}" text-anchor="end" font-size="9" ` +
|
|
167
|
-
`fill="var(--axdb-muted)">${esc(compact(min + f * (max - min)))}</text>`);
|
|
168
|
-
})
|
|
169
|
-
.join('');
|
|
170
|
-
const labels = Array.isArray(d.labels) ? d.labels : [];
|
|
171
|
-
const every = labels.length > 8 ? 2 : 1;
|
|
172
|
-
const ticks = labels
|
|
173
|
-
.slice(0, count)
|
|
174
|
-
.map((l, i) => i % every === 0
|
|
175
|
-
? `<text x="${xAt(i).toFixed(1)}" y="${H - 6}" text-anchor="middle" font-size="9" ` +
|
|
176
|
-
`fill="var(--axdb-muted)">${esc(l)}</text>`
|
|
177
|
-
: '')
|
|
178
|
-
.join('');
|
|
179
|
-
const marks = series
|
|
180
|
-
.map((s, si) => {
|
|
181
|
-
const pts = s.values.map((v, i) => `${xAt(i).toFixed(1)},${yAt(v).toFixed(1)}`).join(' ');
|
|
182
|
-
const area = si === 0 && s.values.length > 1
|
|
183
|
-
? `<path d="M${xAt(0).toFixed(1)},${(pad.t + ih).toFixed(1)} L${pts.replace(/ /g, ' L')} ` +
|
|
184
|
-
`L${xAt(s.values.length - 1).toFixed(1)},${(pad.t + ih).toFixed(1)} Z" ` +
|
|
185
|
-
`fill="${colorAt(si)}" fill-opacity="0.10"></path>`
|
|
186
|
-
: '';
|
|
187
|
-
return (area +
|
|
188
|
-
`<polyline points="${pts}" fill="none" stroke="${colorAt(si)}" stroke-width="${si === 0 ? 2.4 : 1.8}" ` +
|
|
189
|
-
`stroke-linejoin="round" stroke-linecap="round"></polyline>`);
|
|
190
|
-
})
|
|
191
|
-
.join('');
|
|
192
|
-
const named = series.filter((s) => s.name);
|
|
193
|
-
if (named.length)
|
|
194
|
-
body.classList.add('axdb-has-lg');
|
|
195
|
-
body.innerHTML =
|
|
196
|
-
`<svg viewBox="0 0 ${W} ${H}" preserveAspectRatio="xMidYMid meet" role="img" ` +
|
|
197
|
-
`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
|
-
};
|
|
200
|
-
// -- bar ----------------------------------------------------------------------
|
|
201
|
-
/** `{ bars: [{label, value}] }` — columns, value above, category below. */
|
|
202
|
-
export const renderBarWidget = (widget, host) => {
|
|
203
|
-
const d = data(widget);
|
|
204
|
-
const body = card(host, widget, titleOf(widget));
|
|
205
|
-
const bars = (Array.isArray(d.bars) ? d.bars : []).filter((b) => !!b);
|
|
206
|
-
if (!bars.length)
|
|
207
|
-
return empty(body);
|
|
208
|
-
const W = 640;
|
|
209
|
-
const H = 250;
|
|
210
|
-
const pad = { l: 34, r: 12, t: 12, b: 26 };
|
|
211
|
-
const iw = W - pad.l - pad.r;
|
|
212
|
-
const ih = H - pad.t - pad.b;
|
|
213
|
-
const max = niceMax(Math.max(...bars.map((b) => num(b.value))));
|
|
214
|
-
const slot = iw / bars.length;
|
|
215
|
-
const bw = slot * 0.56;
|
|
216
|
-
const grid = [0, 0.5, 1]
|
|
217
|
-
.map((f) => {
|
|
218
|
-
const y = pad.t + ih - f * ih;
|
|
219
|
-
return (`<line x1="${pad.l}" y1="${y.toFixed(1)}" x2="${W - pad.r}" y2="${y.toFixed(1)}" ` +
|
|
220
|
-
`stroke="var(--axdb-grid)" stroke-width="1"></line>` +
|
|
221
|
-
`<text x="${pad.l - 6}" y="${(y + 3).toFixed(1)}" text-anchor="end" font-size="9" ` +
|
|
222
|
-
`fill="var(--axdb-muted)">${esc(compact(f * max))}</text>`);
|
|
223
|
-
})
|
|
224
|
-
.join('');
|
|
225
|
-
const marks = bars
|
|
226
|
-
.map((b, i) => {
|
|
227
|
-
var _a;
|
|
228
|
-
const v = Math.max(0, num(b.value));
|
|
229
|
-
const h = (v / max) * ih;
|
|
230
|
-
const x = pad.l + i * slot + (slot - bw) / 2;
|
|
231
|
-
const y = pad.t + ih - h;
|
|
232
|
-
return (`<rect x="${x.toFixed(1)}" y="${y.toFixed(1)}" width="${bw.toFixed(1)}" height="${h.toFixed(1)}" ` +
|
|
233
|
-
`rx="4" fill="${colorAt(i)}"></rect>` +
|
|
234
|
-
`<text x="${(x + bw / 2).toFixed(1)}" y="${(y - 4).toFixed(1)}" text-anchor="middle" font-size="9.5" ` +
|
|
235
|
-
`font-weight="600" fill="var(--axdb-ink)">${esc(compact(num(b.value)))}</text>` +
|
|
236
|
-
`<text x="${(x + bw / 2).toFixed(1)}" y="${H - 8}" text-anchor="middle" font-size="9" ` +
|
|
237
|
-
`fill="var(--axdb-muted)">${esc((_a = b.label) !== null && _a !== void 0 ? _a : '')}</text>`);
|
|
238
|
-
})
|
|
239
|
-
.join('');
|
|
240
|
-
body.innerHTML =
|
|
241
|
-
`<svg viewBox="0 0 ${W} ${H}" preserveAspectRatio="xMidYMid meet" role="img" ` +
|
|
242
|
-
`aria-label="${esc(titleOf(widget))}">${grid}${marks}</svg>`;
|
|
243
|
-
};
|
|
244
|
-
// -- donut --------------------------------------------------------------------
|
|
245
|
-
/** One arc of the ring, as an SVG path command. */
|
|
246
|
-
function arcPath(cx, cy, r, a0, a1) {
|
|
247
|
-
const at = (a) => [cx + r * Math.cos(a), cy + r * Math.sin(a)];
|
|
248
|
-
const [x0, y0] = at(a0);
|
|
249
|
-
const [x1, y1] = at(a1);
|
|
250
|
-
return `M${x0.toFixed(2)},${y0.toFixed(2)} A${r},${r} 0 ${a1 - a0 > Math.PI ? 1 : 0} 1 ${x1.toFixed(2)},${y1.toFixed(2)}`;
|
|
251
|
-
}
|
|
252
|
-
/** `{ slices: [{label, value, color?}], centerLabel? }` — ring + legend. */
|
|
253
|
-
export const renderDonutWidget = (widget, host) => {
|
|
254
|
-
var _a, _b;
|
|
255
|
-
const d = data(widget);
|
|
256
|
-
const body = card(host, widget, titleOf(widget));
|
|
257
|
-
const slices = (Array.isArray(d.slices) ? d.slices : []).filter((s) => !!s && num(s.value) > 0);
|
|
258
|
-
const total = slices.reduce((s, x) => s + num(x.value), 0);
|
|
259
|
-
if (!slices.length || total <= 0)
|
|
260
|
-
return empty(body);
|
|
261
|
-
const cx = 90;
|
|
262
|
-
const cy = 90;
|
|
263
|
-
const r = 66;
|
|
264
|
-
let a = -Math.PI / 2;
|
|
265
|
-
const segs = slices
|
|
266
|
-
.map((s, i) => {
|
|
267
|
-
var _a;
|
|
268
|
-
const sweep = (num(s.value) / total) * Math.PI * 2;
|
|
269
|
-
const a1 = a + sweep;
|
|
270
|
-
// A hair of padding each side keeps neighbouring arcs visually separate —
|
|
271
|
-
// clamped so a very thin slice cannot invert into a backwards arc.
|
|
272
|
-
const inset = Math.min(0.02, sweep / 4);
|
|
273
|
-
const path = arcPath(cx, cy, r, a + inset, a1 - inset);
|
|
274
|
-
a = a1;
|
|
275
|
-
return `<path d="${path}" fill="none" stroke="${esc((_a = s.color) !== null && _a !== void 0 ? _a : colorAt(i))}" stroke-width="26" stroke-linecap="round"></path>`;
|
|
276
|
-
})
|
|
277
|
-
.join('');
|
|
278
|
-
body.classList.add('axdb-donut');
|
|
279
|
-
body.innerHTML =
|
|
280
|
-
`<svg viewBox="0 0 180 180" preserveAspectRatio="xMidYMid meet" role="img" aria-label="${esc(titleOf(widget))}">` +
|
|
281
|
-
segs +
|
|
282
|
-
`<text x="${cx}" y="${cy - 2}" text-anchor="middle" font-size="20" font-weight="700" ` +
|
|
283
|
-
`fill="var(--axdb-ink)">${esc((_a = d.centerLabel) !== null && _a !== void 0 ? _a : compact(total))}</text>` +
|
|
284
|
-
`<text x="${cx}" y="${cy + 16}" text-anchor="middle" font-size="10" ` +
|
|
285
|
-
`fill="var(--axdb-muted)">${esc((_b = d.centerCaption) !== null && _b !== void 0 ? _b : 'total')}</text>` +
|
|
286
|
-
'</svg>' +
|
|
287
|
-
legend(slices.map((s, i) => {
|
|
288
|
-
var _a, _b;
|
|
289
|
-
return ({
|
|
290
|
-
label: `${(_a = s.label) !== null && _a !== void 0 ? _a : ''} · ${Math.round((num(s.value) / total) * 100)}%`,
|
|
291
|
-
color: (_b = s.color) !== null && _b !== void 0 ? _b : colorAt(i),
|
|
292
|
-
});
|
|
293
|
-
}), true);
|
|
294
|
-
};
|
|
295
|
-
// -- funnel -------------------------------------------------------------------
|
|
296
|
-
/** `{ stages: [{label, value}] }` — centred bars scaled against the first stage. */
|
|
297
|
-
export const renderFunnelWidget = (widget, host) => {
|
|
298
|
-
const d = data(widget);
|
|
299
|
-
const body = card(host, widget, titleOf(widget));
|
|
300
|
-
const stages = (Array.isArray(d.stages) ? d.stages : []).filter((s) => !!s);
|
|
301
|
-
if (!stages.length)
|
|
302
|
-
return empty(body);
|
|
303
|
-
const W = 260;
|
|
304
|
-
const rowH = 34;
|
|
305
|
-
const gap = 8;
|
|
306
|
-
const H = stages.length * (rowH + gap);
|
|
307
|
-
const max = Math.max(...stages.map((s) => num(s.value)), 1);
|
|
308
|
-
const track = W - 90;
|
|
309
|
-
const marks = stages
|
|
310
|
-
.map((s, i) => {
|
|
311
|
-
var _a;
|
|
312
|
-
const w = Math.max(2, (num(s.value) / max) * track);
|
|
313
|
-
const x = (track - w) / 2 + 8;
|
|
314
|
-
const y = i * (rowH + gap);
|
|
315
|
-
return (`<rect x="${x.toFixed(1)}" y="${y}" width="${w.toFixed(1)}" height="${rowH}" rx="6" fill="${colorAt(i)}"></rect>` +
|
|
316
|
-
`<text x="${(x + w / 2).toFixed(1)}" y="${y + rowH / 2 + 4}" text-anchor="middle" font-size="11" ` +
|
|
317
|
-
`font-weight="600" fill="#fff">${esc(compact(num(s.value)))}</text>` +
|
|
318
|
-
`<text x="${W - 78}" y="${y + rowH / 2 + 4}" font-size="10.5" fill="var(--axdb-muted)">${esc((_a = s.label) !== null && _a !== void 0 ? _a : '')}</text>`);
|
|
319
|
-
})
|
|
320
|
-
.join('');
|
|
321
|
-
body.innerHTML =
|
|
322
|
-
`<svg viewBox="0 0 ${W} ${H}" preserveAspectRatio="xMidYMid meet" role="img" ` +
|
|
323
|
-
`aria-label="${esc(titleOf(widget))}">${marks}</svg>`;
|
|
324
|
-
};
|
|
325
|
-
// -- table --------------------------------------------------------------------
|
|
326
|
-
/** `{ columns, rows }` — plain rows; numeric cells right-align themselves. */
|
|
327
|
-
export const renderTableWidget = (widget, host) => {
|
|
328
|
-
const d = data(widget);
|
|
329
|
-
const body = card(host, widget, titleOf(widget));
|
|
330
|
-
const columns = Array.isArray(d.columns) ? d.columns : [];
|
|
331
|
-
const rows = (Array.isArray(d.rows) ? d.rows : []).filter((r) => Array.isArray(r));
|
|
332
|
-
if (!columns.length && !rows.length)
|
|
333
|
-
return empty(body);
|
|
334
|
-
const head = columns.length
|
|
335
|
-
? `<thead><tr>${columns.map((c) => `<th>${esc(c)}</th>`).join('')}</tr></thead>`
|
|
336
|
-
: '';
|
|
337
|
-
const cols = columns.length || Math.max(0, ...rows.map((r) => r.length));
|
|
338
|
-
const tbody = rows
|
|
339
|
-
.map((r) => '<tr>' +
|
|
340
|
-
Array.from({ length: cols }, (_, i) => r[i])
|
|
341
|
-
.map((cell) => `<td class="${isNum(cell) ? 'num' : ''}">${esc(cell !== null && cell !== void 0 ? cell : '')}</td>`)
|
|
342
|
-
.join('') +
|
|
343
|
-
'</tr>')
|
|
344
|
-
.join('');
|
|
345
|
-
body.classList.add('axdb-scroll');
|
|
346
|
-
body.innerHTML = `<table class="axdb-table">${head}<tbody>${tbody}</tbody></table>`;
|
|
347
|
-
};
|
|
348
|
-
// -- dispatch -----------------------------------------------------------------
|
|
349
|
-
const BY_KIND = {
|
|
350
|
-
kpi: renderKpiWidget,
|
|
351
|
-
line: renderLineWidget,
|
|
352
|
-
bar: renderBarWidget,
|
|
353
|
-
donut: renderDonutWidget,
|
|
354
|
-
funnel: renderFunnelWidget,
|
|
355
|
-
table: renderTableWidget,
|
|
356
|
-
};
|
|
357
|
-
/**
|
|
358
|
-
* Paint `widget` into `host` by its `kind` — `dashboard()`'s default
|
|
359
|
-
* `renderWidget`, and the composable base for your own.
|
|
360
|
-
*
|
|
361
|
-
* An unknown (or absent) kind falls back to the titled placeholder frame, so a
|
|
362
|
-
* board of not-yet-implemented widgets still lays out and is still testable.
|
|
363
|
-
* Never throws: bad data paints an empty state.
|
|
364
|
-
*/
|
|
365
|
-
export const defaultWidgetRenderer = (widget, host) => {
|
|
366
|
-
var _a;
|
|
367
|
-
const renderer = BY_KIND[(_a = widget === null || widget === void 0 ? void 0 : widget.kind) !== null && _a !== void 0 ? _a : ''];
|
|
368
|
-
if (renderer) {
|
|
369
|
-
try {
|
|
370
|
-
renderer(widget, host);
|
|
371
|
-
return;
|
|
372
|
-
}
|
|
373
|
-
catch (_b) {
|
|
374
|
-
/* a broken widget must not take the board down — fall through to the frame */
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
card(host, widget !== null && widget !== void 0 ? widget : { id: '' }, titleOf(widget !== null && widget !== void 0 ? widget : { id: '' }));
|
|
378
|
-
};
|
|
379
|
-
//# sourceMappingURL=widgets.js.map
|
|
@@ -1,199 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Card building — the ONE source of truth for the ER/UML card HTML trees, the
|
|
3
|
-
* layout constants and the size math.
|
|
4
|
-
*
|
|
5
|
-
* Both the builders (er.ts / uml.ts, which emit the initial render() spec) and
|
|
6
|
-
* the live editor (update.ts, which regenerates a card after an edit) call
|
|
7
|
-
* these. Keeping them here is what stops the two paths from drifting: an edited
|
|
8
|
-
* card is byte-for-byte the card the builder would have produced for the same
|
|
9
|
-
* data, so a re-rendered table looks identical to a freshly built one.
|
|
10
|
-
*
|
|
11
|
-
* `editable` threads through: when true the card grows the in-canvas editing
|
|
12
|
-
* chrome (a per-row delete control and a trailing "add" affordance). That
|
|
13
|
-
* chrome is DELIBERATELY not a `.axk-row`/`.axk-member` — the row-selection and
|
|
14
|
-
* port-reconciliation code count those, and the affordance is neither a column
|
|
15
|
-
* nor a member.
|
|
16
|
-
*/
|
|
17
|
-
/** Row height / header height of the entity card — sizing is derived from these. */
|
|
18
|
-
export const ER_ROW_H = 25;
|
|
19
|
-
export const ER_HEAD_H = 28;
|
|
20
|
-
/**
|
|
21
|
-
* Auto-height slack: the html wrapper's padding (8px) + the card's 1px
|
|
22
|
-
* top/bottom borders + the head rendering slightly under ER_HEAD_H. Measured
|
|
23
|
-
* live — with less, an auto-sized card overflows a few px, and the wheel
|
|
24
|
-
* delegation would steal that much scroll on EVERY card.
|
|
25
|
-
*/
|
|
26
|
-
export const ER_BORDER_SLACK = 9;
|
|
27
|
-
/** The editable "add column" affordance row's height (excluded from row math). */
|
|
28
|
-
export const ER_ADD_H = 26;
|
|
29
|
-
/** UML compartment metrics (mirrors the pre-kit demos, measured live). */
|
|
30
|
-
export const UML_LINE_H = 19;
|
|
31
|
-
export const UML_NAME_H = 30;
|
|
32
|
-
export const UML_STEREO_H = 14;
|
|
33
|
-
export const UML_PAD = 8;
|
|
34
|
-
/** Node-local y of a row's centre (the +1 offsets past the card's top border). */
|
|
35
|
-
export function erRowCenterY(rowIndex) {
|
|
36
|
-
return ER_HEAD_H + rowIndex * ER_ROW_H + ER_ROW_H / 2 + 1;
|
|
37
|
-
}
|
|
38
|
-
/** Inverse of {@link erRowCenterY}: which column row a pinned port sits on. */
|
|
39
|
-
export function rowIndexFromY(y) {
|
|
40
|
-
return Math.round((y - ER_HEAD_H - ER_ROW_H / 2 - 1) / ER_ROW_H);
|
|
41
|
-
}
|
|
42
|
-
/**
|
|
43
|
-
* The `.axk-entity` content tree — the SAME tree er.ts ships and update.ts
|
|
44
|
-
* regenerates. `editable` adds the delete control per row and the trailing add
|
|
45
|
-
* affordance.
|
|
46
|
-
*/
|
|
47
|
-
export function entityCardContent(entity, editable = false) {
|
|
48
|
-
var _a;
|
|
49
|
-
const rows = entity.columns.map((c) => {
|
|
50
|
-
var _a;
|
|
51
|
-
const children = [
|
|
52
|
-
{ tag: 'span', className: 'axk-key' + (c.fk ? ' axk-fk' : ''), text: c.pk ? 'PK' : c.fk ? 'FK' : '' },
|
|
53
|
-
{ tag: 'span', className: 'axk-col', text: c.name },
|
|
54
|
-
{ tag: 'span', className: 'axk-ty', text: (_a = c.type) !== null && _a !== void 0 ? _a : '' },
|
|
55
|
-
];
|
|
56
|
-
// The delete control is a real child so it survives re-renders (the card is
|
|
57
|
-
// regenerated wholesale on every edit) and hover-reveals via CSS only.
|
|
58
|
-
if (editable)
|
|
59
|
-
children.push({ tag: 'span', className: 'axk-col-del', text: '×' });
|
|
60
|
-
return { tag: 'div', className: 'axk-row' + (c.pk ? ' axk-pk' : ''), children };
|
|
61
|
-
});
|
|
62
|
-
const body = {
|
|
63
|
-
// Scroll is OPT-IN via an explicit height: an auto-sized card fits by
|
|
64
|
-
// construction and must never trap the wheel, not even by a pixel.
|
|
65
|
-
tag: 'div',
|
|
66
|
-
className: entity.height != null ? 'axk-entity-body axk-scroll' : 'axk-entity-body',
|
|
67
|
-
children: rows,
|
|
68
|
-
};
|
|
69
|
-
const children = [
|
|
70
|
-
{ tag: 'div', className: 'axk-entity-head', text: (_a = entity.name) !== null && _a !== void 0 ? _a : entity.id },
|
|
71
|
-
body,
|
|
72
|
-
];
|
|
73
|
-
// The affordance sits OUTSIDE the (scrollable) body so it is always reachable.
|
|
74
|
-
if (editable)
|
|
75
|
-
children.push({ tag: 'div', className: 'axk-entity-add', text: '+ add column' });
|
|
76
|
-
return { tag: 'div', className: 'axk-entity', children };
|
|
77
|
-
}
|
|
78
|
-
/** Card height for an entity — explicit height wins, else derived from columns. */
|
|
79
|
-
export function entityAutoHeight(entity, editable = false) {
|
|
80
|
-
if (entity.height != null)
|
|
81
|
-
return entity.height;
|
|
82
|
-
return ER_HEAD_H + entity.columns.length * ER_ROW_H + ER_BORDER_SLACK + (editable ? ER_ADD_H : 0);
|
|
83
|
-
}
|
|
84
|
-
/**
|
|
85
|
-
* The `.axk-uml` content tree. `editable` adds a delete control per member and
|
|
86
|
-
* a trailing add affordance in each compartment.
|
|
87
|
-
*/
|
|
88
|
-
export function classCardContent(cls, editable = false) {
|
|
89
|
-
var _a, _b, _c;
|
|
90
|
-
const attrs = (_a = cls.attributes) !== null && _a !== void 0 ? _a : [];
|
|
91
|
-
const methods = (_b = cls.methods) !== null && _b !== void 0 ? _b : [];
|
|
92
|
-
const italic = cls.abstract || cls.stereotype === 'abstract' || cls.stereotype === 'interface';
|
|
93
|
-
// Non-editable output stays BYTE-IDENTICAL to the pre-editing builder (a
|
|
94
|
-
// member is a text div); editable additions are purely additive.
|
|
95
|
-
const member = (text) => editable
|
|
96
|
-
? {
|
|
97
|
-
tag: 'div',
|
|
98
|
-
className: 'axk-member',
|
|
99
|
-
children: [
|
|
100
|
-
{ tag: 'span', className: 'axk-mtext', text },
|
|
101
|
-
{ tag: 'span', className: 'axk-col-del', text: '×' },
|
|
102
|
-
],
|
|
103
|
-
}
|
|
104
|
-
: { tag: 'div', className: 'axk-member', text };
|
|
105
|
-
const compartment = (members, section) => {
|
|
106
|
-
const children = members.map(member);
|
|
107
|
-
if (editable) {
|
|
108
|
-
children.push({
|
|
109
|
-
tag: 'div',
|
|
110
|
-
className: 'axk-uml-add',
|
|
111
|
-
text: section === 'attributes' ? '+ attribute' : '+ method',
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
return {
|
|
115
|
-
tag: 'div',
|
|
116
|
-
className: 'axk-uml-comp' + (members.length ? '' : ' axk-empty'),
|
|
117
|
-
children,
|
|
118
|
-
};
|
|
119
|
-
};
|
|
120
|
-
const name = {
|
|
121
|
-
tag: 'div',
|
|
122
|
-
className: 'axk-uml-name' + (italic ? ' axk-abstract' : ''),
|
|
123
|
-
children: [
|
|
124
|
-
...(cls.stereotype ? [{ tag: 'span', className: 'axk-uml-stereo', text: `«${cls.stereotype}»` }] : []),
|
|
125
|
-
{ tag: 'span', text: (_c = cls.name) !== null && _c !== void 0 ? _c : cls.id },
|
|
126
|
-
],
|
|
127
|
-
};
|
|
128
|
-
return {
|
|
129
|
-
tag: 'div',
|
|
130
|
-
className: 'axk-uml',
|
|
131
|
-
children: [
|
|
132
|
-
name,
|
|
133
|
-
{
|
|
134
|
-
tag: 'div',
|
|
135
|
-
className: cls.height != null ? 'axk-uml-body axk-scroll' : 'axk-uml-body',
|
|
136
|
-
children: [compartment(attrs, 'attributes'), compartment(methods, 'methods')],
|
|
137
|
-
},
|
|
138
|
-
],
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
/** Card height for a class — explicit height wins, else derived from members. */
|
|
142
|
-
export function classAutoHeight(cls, editable = false) {
|
|
143
|
-
var _a, _b;
|
|
144
|
-
if (cls.height != null)
|
|
145
|
-
return cls.height;
|
|
146
|
-
const attrs = (_a = cls.attributes) !== null && _a !== void 0 ? _a : [];
|
|
147
|
-
const methods = (_b = cls.methods) !== null && _b !== void 0 ? _b : [];
|
|
148
|
-
const addRows = editable ? 2 : 0; // one "add" affordance per compartment
|
|
149
|
-
return (UML_NAME_H +
|
|
150
|
-
(cls.stereotype ? UML_STEREO_H : 0) +
|
|
151
|
-
(attrs.length + methods.length + addRows) * UML_LINE_H +
|
|
152
|
-
UML_PAD * 2 +
|
|
153
|
-
12);
|
|
154
|
-
}
|
|
155
|
-
/**
|
|
156
|
-
* Match old columns to new columns for port reconciliation. Returns a map from
|
|
157
|
-
* OLD index → NEW index; an old index absent from the map is a REMOVED column
|
|
158
|
-
* (its pinned ports and their edges are dropped).
|
|
159
|
-
*
|
|
160
|
-
* Three passes, most-certain first, so a single edit is always read the way a
|
|
161
|
-
* human means it:
|
|
162
|
-
* 1. object identity — reorder / add / remove that preserved references;
|
|
163
|
-
* 2. name — a retype (same name, new object) or a rebuilt array;
|
|
164
|
-
* 3. positional remainder — the leftovers matched in order, which is exactly
|
|
165
|
-
* what a rename is (one old + one new in the same slot). Without pass 3 a
|
|
166
|
-
* rename reads as remove+add and the edge is severed.
|
|
167
|
-
*/
|
|
168
|
-
export function matchColumns(oldCols, newCols) {
|
|
169
|
-
const out = new Map();
|
|
170
|
-
const usedNew = new Set();
|
|
171
|
-
const matchedOld = new Set();
|
|
172
|
-
const claim = (oldIdx, newIdx) => {
|
|
173
|
-
out.set(oldIdx, newIdx);
|
|
174
|
-
usedNew.add(newIdx);
|
|
175
|
-
matchedOld.add(oldIdx);
|
|
176
|
-
};
|
|
177
|
-
// Pass 1: object identity.
|
|
178
|
-
oldCols.forEach((oc, oi) => {
|
|
179
|
-
const ni = newCols.indexOf(oc);
|
|
180
|
-
if (ni !== -1 && !usedNew.has(ni))
|
|
181
|
-
claim(oi, ni);
|
|
182
|
-
});
|
|
183
|
-
// Pass 2: by name, among the unmatched.
|
|
184
|
-
oldCols.forEach((oc, oi) => {
|
|
185
|
-
if (matchedOld.has(oi))
|
|
186
|
-
return;
|
|
187
|
-
const ni = newCols.findIndex((nc, i) => !usedNew.has(i) && nc.name === oc.name);
|
|
188
|
-
if (ni !== -1)
|
|
189
|
-
claim(oi, ni);
|
|
190
|
-
});
|
|
191
|
-
// Pass 3: positional remainder (rename / retype-and-rename).
|
|
192
|
-
const remOld = oldCols.map((_, i) => i).filter((i) => !matchedOld.has(i));
|
|
193
|
-
const remNew = newCols.map((_, i) => i).filter((i) => !usedNew.has(i));
|
|
194
|
-
const n = Math.min(remOld.length, remNew.length);
|
|
195
|
-
for (let k = 0; k < n; k++)
|
|
196
|
-
claim(remOld[k], remNew[k]);
|
|
197
|
-
return out;
|
|
198
|
-
}
|
|
199
|
-
//# sourceMappingURL=card.js.map
|