@umituz/web-dashboard 4.6.1 → 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.1",
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.0",
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: string;
144
+ key?: string;
143
145
  label: string;
144
- values: number[];
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 max = Math.max(1, ...series.flatMap((s) => [...s.values]));
163
- const toneOf = (s: BarSeries, i: number) => s.tone ?? PALETTE[i % PALETTE.length]!;
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.values[i] ?? 0) / max) * 100)}%`,
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.values[i] ?? 0)}</b>
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: string;
305
+ key?: string;
300
306
  label: string;
301
- values: number[];
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 = (v: number) => String(Math.round(v)),
318
- formatRight = (v: number) => String(Math.round(v)),
326
+ formatLeft,
327
+ formatRight,
328
+ formatValue,
319
329
  yLabelLeft,
320
330
  yLabelRight,
321
331
  }: {
322
- points: LineChartPoint[];
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 = points.length;
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.values))),
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.values))),
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.values), 1)), [series]);
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
- {formatLeft(v)}
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
- {formatRight((v / leftMax) * rightMax)}
401
+ {fmtRight((v / leftMax) * rightMax)}
386
402
  </text>
387
403
  ))}
388
404
  </g>
389
405
  )}
390
- {points.map((p, i) =>
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.values.slice(0, n).map((v, i) => (
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.values
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.values.slice(0, n).map((v, i) => (
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">{points[hover]!.label}</div>
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' ? formatRight : formatLeft)(s.values[hover]!)}</b>
500
+ <b>{(s.axis === 'right' ? fmtRight : fmtLeft)(vals(s)[hover] ?? 0)}</b>
485
501
  </div>
486
502
  ))}
487
503
  </div>
@@ -1,23 +1,43 @@
1
1
  import type { ReactNode } from "react";
2
2
 
3
+ export interface PageHeaderCrumb {
4
+ label: string;
5
+ to?: string;
6
+ }
7
+
3
8
  export interface PageHeaderProps {
4
- title: ReactNode;
9
+ title?: ReactNode;
5
10
  subtitle?: ReactNode;
6
11
  description?: ReactNode;
7
- breadcrumbs?: ReactNode;
12
+ breadcrumbs?: ReactNode | readonly PageHeaderCrumb[];
8
13
  actions?: ReactNode;
9
14
  className?: string;
10
15
  }
11
16
 
12
- export const PageHeader = ({ title, subtitle, description, breadcrumbs, actions, className }: PageHeaderProps) => (
13
- <div className={`udb-pageheader${className ? ` ${className}` : ""}`}>
14
- {breadcrumbs !== undefined && <div className="udb-pageheader__breadcrumbs">{breadcrumbs}</div>}
15
- <div className="udb-pageheader__main">
16
- <h1 className="udb-pageheader__title">{title}</h1>
17
- {(subtitle ?? description) !== undefined && (
18
- <p className="udb-pageheader__subtitle">{subtitle ?? description}</p>
19
- )}
17
+ const isCrumbList = (value: ReactNode | readonly PageHeaderCrumb[]): value is readonly PageHeaderCrumb[] =>
18
+ Array.isArray(value) && value.every((item) => typeof item === "object" && item !== null && "label" in item);
19
+
20
+ export const PageHeader = ({ title, subtitle, description, breadcrumbs, actions, className }: PageHeaderProps) => {
21
+ const crumbNode =
22
+ breadcrumbs === undefined
23
+ ? undefined
24
+ : isCrumbList(breadcrumbs)
25
+ ? breadcrumbs.map((crumb, index) => (
26
+ <span key={`${crumb.label}-${index}`} className="udb-pageheader__crumb">
27
+ {crumb.to !== undefined ? <a href={crumb.to}>{crumb.label}</a> : crumb.label}
28
+ </span>
29
+ ))
30
+ : breadcrumbs;
31
+ return (
32
+ <div className={`udb-pageheader${className ? ` ${className}` : ""}`}>
33
+ {crumbNode !== undefined && <div className="udb-pageheader__breadcrumbs">{crumbNode}</div>}
34
+ <div className="udb-pageheader__main">
35
+ {title !== undefined && <h1 className="udb-pageheader__title">{title}</h1>}
36
+ {(subtitle ?? description) !== undefined && (
37
+ <p className="udb-pageheader__subtitle">{subtitle ?? description}</p>
38
+ )}
39
+ </div>
40
+ {actions !== undefined && <div className="udb-pageheader__actions">{actions}</div>}
20
41
  </div>
21
- {actions !== undefined && <div className="udb-pageheader__actions">{actions}</div>}
22
- </div>
23
- );
42
+ );
43
+ };
@@ -7,4 +7,4 @@ export { DashboardHeader } from './DashboardHeader';
7
7
  export { DashboardSidebar } from './DashboardSidebar';
8
8
  export { BrandLogo } from './BrandLogo';
9
9
  export { PageHeader } from './PageHeader';
10
- export type { PageHeaderProps } from './PageHeader';
10
+ export type { PageHeaderProps, PageHeaderCrumb } from './PageHeader';
@@ -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;
@@ -3154,6 +3159,22 @@
3154
3159
  padding: 0 8px;
3155
3160
  }
3156
3161
 
3162
+ .udb-pageheader__breadcrumbs .udb-pageheader__crumb + .udb-pageheader__crumb::before {
3163
+ content: "/";
3164
+ margin-right: 0.375rem;
3165
+ color: var(--color-muted-foreground);
3166
+ opacity: 0.6;
3167
+ }
3168
+
3169
+ .udb-pageheader__breadcrumbs a {
3170
+ color: inherit;
3171
+ text-decoration: none;
3172
+ }
3173
+
3174
+ .udb-pageheader__breadcrumbs a:hover {
3175
+ text-decoration: underline;
3176
+ }
3177
+
3157
3178
  .udb-pageheader__breadcrumbs {
3158
3179
  display: flex;
3159
3180
  align-items: center;