@umituz/web-dashboard 4.6.2 → 4.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/web-dashboard",
|
|
3
|
-
"version": "4.6.
|
|
3
|
+
"version": "4.6.3",
|
|
4
4
|
"description": "Installable app skeleton for the fleet — layouts, auth, onboarding, analytics (dependency-free SVG charts), calendar, i18n injection, permissions/session/module registry; ships its own udb-* stylesheet and --u-* tokens",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
"react-router-dom": "^6.0.0 || ^7.0.0"
|
|
73
73
|
},
|
|
74
74
|
"dependencies": {
|
|
75
|
-
"@umituz/web-design-system": "^4.5.
|
|
75
|
+
"@umituz/web-design-system": "^4.5.3",
|
|
76
76
|
"lucide-react": "^0.577.0"
|
|
77
77
|
},
|
|
78
78
|
"devDependencies": {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
import { renderToString } from 'react-dom/server';
|
|
3
|
-
import { BarChart, Donut, LineChart, Sparkline } from './components';
|
|
3
|
+
import { BarChart, Donut, Legend, LineChart, Sparkline } from './components';
|
|
4
4
|
|
|
5
5
|
describe('charts markup smoke', () => {
|
|
6
6
|
it('Sparkline renders an svg path for enough data and null for less', () => {
|
|
@@ -64,4 +64,37 @@ describe('charts markup smoke', () => {
|
|
|
64
64
|
it('LineChart renders nothing without points', () => {
|
|
65
65
|
expect(renderToString(<LineChart points={[]} series={[]} />)).toBe('');
|
|
66
66
|
});
|
|
67
|
+
|
|
68
|
+
it('fleet aliases: LineChart accepts labels/formatValue and key-less series with data/color', () => {
|
|
69
|
+
const html = renderToString(
|
|
70
|
+
<LineChart
|
|
71
|
+
labels={['m1', 'm2']}
|
|
72
|
+
series={[{ label: 'views', data: [1, 3], color: '#123456' }]}
|
|
73
|
+
formatValue={(v) => String(v)}
|
|
74
|
+
/>,
|
|
75
|
+
);
|
|
76
|
+
expect(html).toContain('#123456');
|
|
77
|
+
expect(html).toContain('views');
|
|
78
|
+
expect(html).toContain('m1');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('fleet aliases: BarChart accepts key-less series with data/color', () => {
|
|
82
|
+
const html = renderToString(
|
|
83
|
+
<BarChart labels={['a', 'b']} series={[{ label: 'A', data: [2, 4], color: '#654321' }]} />,
|
|
84
|
+
);
|
|
85
|
+
expect(html).toContain('#654321');
|
|
86
|
+
expect(html).toContain('height:100%');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('fleet aliases: Legend renders hint suffix and Donut accepts color', () => {
|
|
90
|
+
const html = renderToString(
|
|
91
|
+
<>
|
|
92
|
+
<Legend items={[{ label: 'A', color: '#111111', value: '12', hint: '(per month)' }]} />
|
|
93
|
+
<Donut segments={[{ label: 'x', value: 2, color: '#333333' }]} />
|
|
94
|
+
</>,
|
|
95
|
+
);
|
|
96
|
+
expect(html).toContain('udb-chartlegend__hint');
|
|
97
|
+
expect(html).toContain('(per month)');
|
|
98
|
+
expect(html).toContain('#333333');
|
|
99
|
+
});
|
|
67
100
|
});
|
|
@@ -58,6 +58,7 @@ export interface LegendItem {
|
|
|
58
58
|
label: string;
|
|
59
59
|
color: string;
|
|
60
60
|
value?: string;
|
|
61
|
+
hint?: string;
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
export function Legend({ items }: { items: LegendItem[] }) {
|
|
@@ -68,6 +69,7 @@ export function Legend({ items }: { items: LegendItem[] }) {
|
|
|
68
69
|
<span className="udb-chartlegend__dot" style={{ background: i.color }} />
|
|
69
70
|
{i.label}
|
|
70
71
|
{i.value && <span className="udb-chartlegend__value">{i.value}</span>}
|
|
72
|
+
{i.hint && <span className="udb-chartlegend__hint">{i.hint}</span>}
|
|
71
73
|
</span>
|
|
72
74
|
))}
|
|
73
75
|
</div>
|
|
@@ -139,10 +141,12 @@ export function HBars({ rows, formatValue = (n: number) => String(n) }: { rows:
|
|
|
139
141
|
}
|
|
140
142
|
|
|
141
143
|
export interface BarSeries {
|
|
142
|
-
key
|
|
144
|
+
key?: string;
|
|
143
145
|
label: string;
|
|
144
|
-
values
|
|
146
|
+
values?: number[];
|
|
147
|
+
data?: number[];
|
|
145
148
|
tone?: string;
|
|
149
|
+
color?: string;
|
|
146
150
|
}
|
|
147
151
|
|
|
148
152
|
export function BarChart({
|
|
@@ -159,8 +163,9 @@ export function BarChart({
|
|
|
159
163
|
formatLabel?: (l: string) => string;
|
|
160
164
|
}) {
|
|
161
165
|
const [hover, setHover] = useState<number | null>(null);
|
|
162
|
-
const
|
|
163
|
-
const
|
|
166
|
+
const vals = (s: BarSeries) => s.values ?? s.data ?? [];
|
|
167
|
+
const max = Math.max(1, ...series.flatMap((s) => vals(s)));
|
|
168
|
+
const toneOf = (s: BarSeries, i: number) => s.tone ?? s.color ?? PALETTE[i % PALETTE.length]!;
|
|
164
169
|
return (
|
|
165
170
|
<div className="udb-barchart" style={{ height }}>
|
|
166
171
|
{labels.map((label, i) => (
|
|
@@ -173,10 +178,10 @@ export function BarChart({
|
|
|
173
178
|
<div className="udb-barchart__bars">
|
|
174
179
|
{series.map((s, si) => (
|
|
175
180
|
<div
|
|
176
|
-
key={s.key}
|
|
181
|
+
key={s.key ?? s.label}
|
|
177
182
|
className="udb-barchart__bar"
|
|
178
183
|
style={{
|
|
179
|
-
height: `${Math.max(2, ((s
|
|
184
|
+
height: `${Math.max(2, ((vals(s)[i] ?? 0) / max) * 100)}%`,
|
|
180
185
|
background: toneOf(s, si),
|
|
181
186
|
opacity: hover === null || hover === i ? 1 : 0.4,
|
|
182
187
|
}}
|
|
@@ -187,12 +192,12 @@ export function BarChart({
|
|
|
187
192
|
<div className="udb-barchart__tip">
|
|
188
193
|
<div className="udb-barchart__tiptitle">{formatLabel(label)}</div>
|
|
189
194
|
{series.map((s, si) => (
|
|
190
|
-
<div key={s.key} className="udb-barchart__tiprow">
|
|
195
|
+
<div key={s.key ?? s.label} className="udb-barchart__tiprow">
|
|
191
196
|
<span className="udb-barchart__tiplabel">
|
|
192
197
|
<span className="udb-chartlegend__dot" style={{ background: toneOf(s, si) }} />
|
|
193
198
|
{s.label}
|
|
194
199
|
</span>
|
|
195
|
-
<b>{formatValue(s
|
|
200
|
+
<b>{formatValue(vals(s)[i] ?? 0)}</b>
|
|
196
201
|
</div>
|
|
197
202
|
))}
|
|
198
203
|
</div>
|
|
@@ -207,6 +212,7 @@ export interface DonutSegment {
|
|
|
207
212
|
label: string;
|
|
208
213
|
value: number;
|
|
209
214
|
tone?: string;
|
|
215
|
+
color?: string;
|
|
210
216
|
}
|
|
211
217
|
|
|
212
218
|
export function Donut({
|
|
@@ -250,7 +256,7 @@ export function Donut({
|
|
|
250
256
|
cy={size / 2}
|
|
251
257
|
r={radius}
|
|
252
258
|
fill="none"
|
|
253
|
-
stroke={seg.tone ?? PALETTE[i % PALETTE.length]!}
|
|
259
|
+
stroke={seg.tone ?? seg.color ?? PALETTE[i % PALETTE.length]!}
|
|
254
260
|
strokeWidth={active === i ? thickness + 3 : thickness}
|
|
255
261
|
strokeDasharray={`${dash} ${circumference - dash}`}
|
|
256
262
|
strokeDashoffset={-offset}
|
|
@@ -281,7 +287,7 @@ export function Donut({
|
|
|
281
287
|
onMouseLeave={() => setActive(null)}
|
|
282
288
|
>
|
|
283
289
|
<span className="udb-donut__itemlabel">
|
|
284
|
-
<span className="udb-donut__dot" style={{ background: seg.tone ?? PALETTE[i % PALETTE.length]! }} />
|
|
290
|
+
<span className="udb-donut__dot" style={{ background: seg.tone ?? seg.color ?? PALETTE[i % PALETTE.length]! }} />
|
|
285
291
|
{seg.label}
|
|
286
292
|
</span>
|
|
287
293
|
<span className="udb-donut__itemvalue">
|
|
@@ -296,10 +302,12 @@ export function Donut({
|
|
|
296
302
|
}
|
|
297
303
|
|
|
298
304
|
export interface LineSeries {
|
|
299
|
-
key
|
|
305
|
+
key?: string;
|
|
300
306
|
label: string;
|
|
301
|
-
values
|
|
307
|
+
values?: number[];
|
|
308
|
+
data?: number[];
|
|
302
309
|
tone?: string;
|
|
310
|
+
color?: string;
|
|
303
311
|
kind?: 'line' | 'bars';
|
|
304
312
|
axis?: 'left' | 'right';
|
|
305
313
|
dashed?: boolean;
|
|
@@ -312,22 +320,30 @@ export interface LineChartPoint {
|
|
|
312
320
|
|
|
313
321
|
export function LineChart({
|
|
314
322
|
points,
|
|
323
|
+
labels,
|
|
315
324
|
series,
|
|
316
325
|
height = 220,
|
|
317
|
-
formatLeft
|
|
318
|
-
formatRight
|
|
326
|
+
formatLeft,
|
|
327
|
+
formatRight,
|
|
328
|
+
formatValue,
|
|
319
329
|
yLabelLeft,
|
|
320
330
|
yLabelRight,
|
|
321
331
|
}: {
|
|
322
|
-
points
|
|
332
|
+
points?: LineChartPoint[];
|
|
333
|
+
labels?: string[];
|
|
323
334
|
series: LineSeries[];
|
|
324
335
|
height?: number;
|
|
325
336
|
formatLeft?: (v: number) => string;
|
|
326
337
|
formatRight?: (v: number) => string;
|
|
338
|
+
formatValue?: (v: number) => string;
|
|
327
339
|
yLabelLeft?: string;
|
|
328
340
|
yLabelRight?: string;
|
|
329
341
|
}) {
|
|
330
342
|
const [hover, setHover] = useState<number | null>(null);
|
|
343
|
+
const pts = points ?? (labels ?? []).map((label) => ({ label }));
|
|
344
|
+
const fmtLeft = formatLeft ?? formatValue ?? ((v: number) => String(Math.round(v)));
|
|
345
|
+
const fmtRight = formatRight ?? formatValue ?? ((v: number) => String(Math.round(v)));
|
|
346
|
+
const vals = (s: LineSeries) => s.values ?? s.data ?? [];
|
|
331
347
|
const w = 720;
|
|
332
348
|
const padLeft = 52;
|
|
333
349
|
const padRight = series.some((s) => s.axis === 'right') ? 46 : 14;
|
|
@@ -335,19 +351,19 @@ export function LineChart({
|
|
|
335
351
|
const padBottom = 24;
|
|
336
352
|
const innerW = w - padLeft - padRight;
|
|
337
353
|
const innerH = height - padTop - padBottom;
|
|
338
|
-
const n =
|
|
354
|
+
const n = pts.length;
|
|
339
355
|
|
|
340
|
-
const toneOf = (s: LineSeries, i: number) => s.tone ?? PALETTE[i % PALETTE.length]!;
|
|
356
|
+
const toneOf = (s: LineSeries, i: number) => s.tone ?? s.color ?? PALETTE[i % PALETTE.length]!;
|
|
341
357
|
|
|
342
358
|
const leftMax = useMemo(
|
|
343
|
-
() => niceMax(Math.max(1, ...series.filter((s) => s.axis !== 'right').flatMap((s) => s
|
|
359
|
+
() => niceMax(Math.max(1, ...series.filter((s) => s.axis !== 'right').flatMap((s) => vals(s)))),
|
|
344
360
|
[series],
|
|
345
361
|
);
|
|
346
362
|
const rightMax = useMemo(
|
|
347
|
-
() => niceMax(Math.max(1, ...series.filter((s) => s.axis === 'right').flatMap((s) => s
|
|
363
|
+
() => niceMax(Math.max(1, ...series.filter((s) => s.axis === 'right').flatMap((s) => vals(s)))),
|
|
348
364
|
[series],
|
|
349
365
|
);
|
|
350
|
-
const leftTicks = useMemo(() => gridTicks(Math.max(...series.filter((s) => s.axis !== 'right').flatMap((s) => s
|
|
366
|
+
const leftTicks = useMemo(() => gridTicks(Math.max(...series.filter((s) => s.axis !== 'right').flatMap((s) => vals(s)), 1)), [series]);
|
|
351
367
|
|
|
352
368
|
const xAt = (i: number) => padLeft + (n <= 1 ? innerW / 2 : (i / (n - 1)) * innerW);
|
|
353
369
|
const yLeft = (v: number) => padTop + innerH - (v / leftMax) * innerH;
|
|
@@ -374,7 +390,7 @@ export function LineChart({
|
|
|
374
390
|
<g key={i}>
|
|
375
391
|
<line x1={padLeft} x2={w - padRight} y1={yLeft(v)} y2={yLeft(v)} className="udb-linechart__grid" />
|
|
376
392
|
<text x={padLeft - 6} y={yLeft(v) + 3} textAnchor="end" className="udb-linechart__axis">
|
|
377
|
-
{
|
|
393
|
+
{fmtLeft(v)}
|
|
378
394
|
</text>
|
|
379
395
|
</g>
|
|
380
396
|
))}
|
|
@@ -382,12 +398,12 @@ export function LineChart({
|
|
|
382
398
|
<g>
|
|
383
399
|
{leftTicks.map((v, i) => (
|
|
384
400
|
<text key={i} x={w - padRight + 6} y={yLeft(v) + 3} textAnchor="start" className="udb-linechart__axis">
|
|
385
|
-
{
|
|
401
|
+
{fmtRight((v / leftMax) * rightMax)}
|
|
386
402
|
</text>
|
|
387
403
|
))}
|
|
388
404
|
</g>
|
|
389
405
|
)}
|
|
390
|
-
{
|
|
406
|
+
{pts.map((p, i) =>
|
|
391
407
|
i % labelEvery === 0 ? (
|
|
392
408
|
<text key={i} x={xAt(i)} y={height - 8} textAnchor="middle" className="udb-linechart__axis">
|
|
393
409
|
{p.label}
|
|
@@ -399,8 +415,8 @@ export function LineChart({
|
|
|
399
415
|
if (s.kind === 'bars') {
|
|
400
416
|
const barW = Math.max(3, (innerW / Math.max(1, n - 1)) * 0.5);
|
|
401
417
|
return (
|
|
402
|
-
<g key={s.key}>
|
|
403
|
-
{s.
|
|
418
|
+
<g key={s.key ?? s.label}>
|
|
419
|
+
{vals(s).slice(0, n).map((v, i) => (
|
|
404
420
|
<rect
|
|
405
421
|
key={i}
|
|
406
422
|
x={xAt(i) - barW / 2}
|
|
@@ -415,13 +431,13 @@ export function LineChart({
|
|
|
415
431
|
</g>
|
|
416
432
|
);
|
|
417
433
|
}
|
|
418
|
-
const path = s
|
|
434
|
+
const path = vals(s)
|
|
419
435
|
.slice(0, n)
|
|
420
436
|
.map((v, i) => `${i === 0 ? 'M' : 'L'}${xAt(i).toFixed(1)},${yFn(v).toFixed(1)}`)
|
|
421
437
|
.join(' ');
|
|
422
438
|
const areaPath = `${path} L${xAt(n - 1).toFixed(1)},${padTop + innerH} L${xAt(0).toFixed(1)},${padTop + innerH} Z`;
|
|
423
439
|
return (
|
|
424
|
-
<g key={s.key}>
|
|
440
|
+
<g key={s.key ?? s.label}>
|
|
425
441
|
{s.area !== false && !s.dashed && <path d={areaPath} fill={toneOf(s, si)} opacity={0.07} />}
|
|
426
442
|
<path
|
|
427
443
|
d={path}
|
|
@@ -432,7 +448,7 @@ export function LineChart({
|
|
|
432
448
|
strokeLinecap="round"
|
|
433
449
|
strokeDasharray={s.dashed ? '4 4' : undefined}
|
|
434
450
|
/>
|
|
435
|
-
{s.
|
|
451
|
+
{vals(s).slice(0, n).map((v, i) => (
|
|
436
452
|
<circle
|
|
437
453
|
key={i}
|
|
438
454
|
cx={xAt(i)}
|
|
@@ -474,14 +490,14 @@ export function LineChart({
|
|
|
474
490
|
transform: hover > n / 2 ? 'translateX(calc(-100% - 10px))' : 'translateX(10px)',
|
|
475
491
|
}}
|
|
476
492
|
>
|
|
477
|
-
<div className="udb-linechart__tiptitle">{
|
|
493
|
+
<div className="udb-linechart__tiptitle">{pts[hover]!.label}</div>
|
|
478
494
|
{series.map((s, si) => (
|
|
479
|
-
<div key={s.key} className="udb-linechart__tiprow">
|
|
495
|
+
<div key={s.key ?? s.label} className="udb-linechart__tiprow">
|
|
480
496
|
<span>
|
|
481
497
|
<span className="udb-linechart__swatch" style={{ background: toneOf(s, si) }} />
|
|
482
498
|
{s.label}
|
|
483
499
|
</span>
|
|
484
|
-
<b>{(s.axis === 'right' ?
|
|
500
|
+
<b>{(s.axis === 'right' ? fmtRight : fmtLeft)(vals(s)[hover] ?? 0)}</b>
|
|
485
501
|
</div>
|
|
486
502
|
))}
|
|
487
503
|
</div>
|
|
@@ -6,7 +6,7 @@ export interface PageHeaderCrumb {
|
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
export interface PageHeaderProps {
|
|
9
|
-
title
|
|
9
|
+
title?: ReactNode;
|
|
10
10
|
subtitle?: ReactNode;
|
|
11
11
|
description?: ReactNode;
|
|
12
12
|
breadcrumbs?: ReactNode | readonly PageHeaderCrumb[];
|
|
@@ -32,7 +32,7 @@ export const PageHeader = ({ title, subtitle, description, breadcrumbs, actions,
|
|
|
32
32
|
<div className={`udb-pageheader${className ? ` ${className}` : ""}`}>
|
|
33
33
|
{crumbNode !== undefined && <div className="udb-pageheader__breadcrumbs">{crumbNode}</div>}
|
|
34
34
|
<div className="udb-pageheader__main">
|
|
35
|
-
<h1 className="udb-pageheader__title">{title}</h1>
|
|
35
|
+
{title !== undefined && <h1 className="udb-pageheader__title">{title}</h1>}
|
|
36
36
|
{(subtitle ?? description) !== undefined && (
|
|
37
37
|
<p className="udb-pageheader__subtitle">{subtitle ?? description}</p>
|
|
38
38
|
)}
|
package/src/styles/dashboard.css
CHANGED
|
@@ -2819,6 +2819,11 @@
|
|
|
2819
2819
|
font-size: var(--u-font-size-xs);
|
|
2820
2820
|
}
|
|
2821
2821
|
|
|
2822
|
+
.udb-chartlegend__hint {
|
|
2823
|
+
color: var(--u-color-muted);
|
|
2824
|
+
font-size: var(--u-font-size-xs);
|
|
2825
|
+
}
|
|
2826
|
+
|
|
2822
2827
|
.udb-charttip {
|
|
2823
2828
|
position: absolute;
|
|
2824
2829
|
pointer-events: none;
|