@umituz/web-dashboard 4.3.0 → 4.5.0

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,7 +1,7 @@
1
1
  {
2
2
  "name": "@umituz/web-dashboard",
3
- "version": "4.3.0",
4
- "description": "Installable app skeleton for the fleet \u2014 layouts, auth, onboarding, analytics (dependency-free SVG charts), calendar, i18n injection, permissions/session/module registry; ships its own udb-* stylesheet and --u-* tokens",
3
+ "version": "4.5.0",
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",
7
7
  "types": "./src/index.ts",
@@ -0,0 +1,67 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { renderToString } from 'react-dom/server';
3
+ import { BarChart, Donut, LineChart, Sparkline } from './components';
4
+
5
+ describe('charts markup smoke', () => {
6
+ it('Sparkline renders an svg path for enough data and null for less', () => {
7
+ const html = renderToString(<Sparkline data={[1, 3, 2, 5]} />);
8
+ expect(html).toContain('<svg');
9
+ expect(html).toContain('<path');
10
+ expect(renderToString(<Sparkline data={[1]} />)).toBe('');
11
+ });
12
+
13
+ it('BarChart renders one grouped column per label with one bar per series', () => {
14
+ const html = renderToString(
15
+ <BarChart
16
+ labels={['mon', 'tue', 'wed']}
17
+ series={[
18
+ { key: 'a', label: 'A', values: [4, 8, 0] },
19
+ { key: 'b', label: 'B', values: [2, 4, 6], tone: '#111111' },
20
+ ]}
21
+ />,
22
+ );
23
+ expect(html.match(/udb-barchart__col(?![a-z])/g)?.length).toBe(3);
24
+ expect(html.match(/udb-barchart__bar(?![a-z_])/g)?.length).toBe(6);
25
+ expect(html).toContain('height:100%');
26
+ expect(html).toContain('height:50%');
27
+ expect(html).toContain('#111111');
28
+ });
29
+
30
+ it('Donut renders a segment circle per label with dash math and center fallback', () => {
31
+ const html = renderToString(
32
+ <Donut
33
+ segments={[
34
+ { label: 'a', value: 3, tone: '#111111' },
35
+ { label: 'b', value: 1, tone: '#222222' },
36
+ ]}
37
+ centerValue="4"
38
+ centerLabel="total"
39
+ />,
40
+ );
41
+ expect(html.match(/udb-donut__item(?![a-z])/g)?.length).toBe(2);
42
+ expect(html).toContain('75<!-- -->%');
43
+ expect(html).toContain('#111111');
44
+ expect(html).toContain('>total<');
45
+ });
46
+
47
+ it('LineChart renders series paths, grid lines and a legend entry per series', () => {
48
+ const html = renderToString(
49
+ <LineChart
50
+ points={[{ label: 'w1' }, { label: 'w2' }, { label: 'w3' }]}
51
+ series={[
52
+ { key: 's1', label: 'revenue', values: [2, 4, 8] },
53
+ { key: 's2', label: 'cost', values: [5, 3, 1], kind: 'bars', axis: 'right' },
54
+ ]}
55
+ />,
56
+ );
57
+ expect(html).toContain('udb-linechart__grid');
58
+ expect(html).toContain('revenue');
59
+ expect(html).toContain('cost');
60
+ expect(html.match(/<rect /g)?.length).toBe(3);
61
+ expect(html.match(/udb-chartlegend__dot/g)?.length).toBeGreaterThanOrEqual(2);
62
+ });
63
+
64
+ it('LineChart renders nothing without points', () => {
65
+ expect(renderToString(<LineChart points={[]} series={[]} />)).toBe('');
66
+ });
67
+ });
@@ -1,6 +1,6 @@
1
- import { useId } from 'react';
1
+ import { useId, useMemo, useState } from 'react';
2
2
  import type { ReactNode } from 'react';
3
- import type { TipState } from './utils';
3
+ import { PALETTE, gridTicks, niceMax, type TipState } from './utils';
4
4
 
5
5
  export function ChartTip({ tip }: { tip: TipState }) {
6
6
  return (
@@ -137,3 +137,358 @@ export function HBars({ rows, formatValue = (n: number) => String(n) }: { rows:
137
137
  </div>
138
138
  );
139
139
  }
140
+
141
+ export interface BarSeries {
142
+ key: string;
143
+ label: string;
144
+ values: number[];
145
+ tone?: string;
146
+ }
147
+
148
+ export function BarChart({
149
+ labels,
150
+ series,
151
+ height = 130,
152
+ formatValue = (v: number) => String(v),
153
+ formatLabel = (l: string) => l,
154
+ }: {
155
+ labels: string[];
156
+ series: BarSeries[];
157
+ height?: number;
158
+ formatValue?: (v: number) => string;
159
+ formatLabel?: (l: string) => string;
160
+ }) {
161
+ 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]!;
164
+ return (
165
+ <div className="udb-barchart" style={{ height }}>
166
+ {labels.map((label, i) => (
167
+ <div
168
+ key={label}
169
+ className="udb-barchart__col"
170
+ onMouseEnter={() => setHover(i)}
171
+ onMouseLeave={() => setHover(null)}
172
+ >
173
+ <div className="udb-barchart__bars">
174
+ {series.map((s, si) => (
175
+ <div
176
+ key={s.key}
177
+ className="udb-barchart__bar"
178
+ style={{
179
+ height: `${Math.max(2, ((s.values[i] ?? 0) / max) * 100)}%`,
180
+ background: toneOf(s, si),
181
+ opacity: hover === null || hover === i ? 1 : 0.4,
182
+ }}
183
+ />
184
+ ))}
185
+ </div>
186
+ {hover === i && (
187
+ <div className="udb-barchart__tip">
188
+ <div className="udb-barchart__tiptitle">{formatLabel(label)}</div>
189
+ {series.map((s, si) => (
190
+ <div key={s.key} className="udb-barchart__tiprow">
191
+ <span className="udb-barchart__tiplabel">
192
+ <span className="udb-chartlegend__dot" style={{ background: toneOf(s, si) }} />
193
+ {s.label}
194
+ </span>
195
+ <b>{formatValue(s.values[i] ?? 0)}</b>
196
+ </div>
197
+ ))}
198
+ </div>
199
+ )}
200
+ </div>
201
+ ))}
202
+ </div>
203
+ );
204
+ }
205
+
206
+ export interface DonutSegment {
207
+ label: string;
208
+ value: number;
209
+ tone?: string;
210
+ }
211
+
212
+ export function Donut({
213
+ segments,
214
+ size = 148,
215
+ thickness = 18,
216
+ centerValue,
217
+ centerLabel,
218
+ formatValue = (v: number) => String(v),
219
+ }: {
220
+ segments: DonutSegment[];
221
+ size?: number;
222
+ thickness?: number;
223
+ centerValue?: string;
224
+ centerLabel?: string;
225
+ formatValue?: (v: number) => string;
226
+ }) {
227
+ const [active, setActive] = useState<number | null>(null);
228
+ const total = segments.reduce((sum, seg) => sum + seg.value, 0) || 1;
229
+ const radius = (size - thickness) / 2;
230
+ const circumference = 2 * Math.PI * radius;
231
+ let offset = 0;
232
+ return (
233
+ <div className="udb-donut">
234
+ <div className="udb-donut__wrap" style={{ width: size, height: size }}>
235
+ <svg width={size} height={size} style={{ transform: 'rotate(-90deg)' }}>
236
+ <circle
237
+ cx={size / 2}
238
+ cy={size / 2}
239
+ r={radius}
240
+ fill="none"
241
+ stroke="var(--u-color-surface-muted)"
242
+ strokeWidth={thickness}
243
+ />
244
+ {segments.map((seg, i) => {
245
+ const dash = (seg.value / total) * circumference;
246
+ const el = (
247
+ <circle
248
+ key={seg.label}
249
+ cx={size / 2}
250
+ cy={size / 2}
251
+ r={radius}
252
+ fill="none"
253
+ stroke={seg.tone ?? PALETTE[i % PALETTE.length]!}
254
+ strokeWidth={active === i ? thickness + 3 : thickness}
255
+ strokeDasharray={`${dash} ${circumference - dash}`}
256
+ strokeDashoffset={-offset}
257
+ strokeLinecap="butt"
258
+ opacity={active === null || active === i ? 1 : 0.35}
259
+ className="udb-donut__seg"
260
+ onMouseEnter={() => setActive(i)}
261
+ onMouseLeave={() => setActive(null)}
262
+ />
263
+ );
264
+ offset += dash;
265
+ return el;
266
+ })}
267
+ </svg>
268
+ <div className="udb-donut__center">
269
+ <span className="udb-donut__value">
270
+ {active !== null ? formatValue(segments[active]!.value) : centerValue}
271
+ </span>
272
+ <span className="udb-donut__label">{active !== null ? segments[active]!.label : centerLabel}</span>
273
+ </div>
274
+ </div>
275
+ <div className="udb-donut__list">
276
+ {segments.map((seg, i) => (
277
+ <div
278
+ key={seg.label}
279
+ className="udb-donut__item"
280
+ onMouseEnter={() => setActive(i)}
281
+ onMouseLeave={() => setActive(null)}
282
+ >
283
+ <span className="udb-donut__itemlabel">
284
+ <span className="udb-donut__dot" style={{ background: seg.tone ?? PALETTE[i % PALETTE.length]! }} />
285
+ {seg.label}
286
+ </span>
287
+ <span className="udb-donut__itemvalue">
288
+ {formatValue(seg.value)}
289
+ <span className="udb-donut__pct">{Math.round((seg.value / total) * 100)}%</span>
290
+ </span>
291
+ </div>
292
+ ))}
293
+ </div>
294
+ </div>
295
+ );
296
+ }
297
+
298
+ export interface LineSeries {
299
+ key: string;
300
+ label: string;
301
+ values: number[];
302
+ tone?: string;
303
+ kind?: 'line' | 'bars';
304
+ axis?: 'left' | 'right';
305
+ dashed?: boolean;
306
+ area?: boolean;
307
+ }
308
+
309
+ export interface LineChartPoint {
310
+ label: string;
311
+ }
312
+
313
+ export function LineChart({
314
+ points,
315
+ series,
316
+ height = 220,
317
+ formatLeft = (v: number) => String(Math.round(v)),
318
+ formatRight = (v: number) => String(Math.round(v)),
319
+ yLabelLeft,
320
+ yLabelRight,
321
+ }: {
322
+ points: LineChartPoint[];
323
+ series: LineSeries[];
324
+ height?: number;
325
+ formatLeft?: (v: number) => string;
326
+ formatRight?: (v: number) => string;
327
+ yLabelLeft?: string;
328
+ yLabelRight?: string;
329
+ }) {
330
+ const [hover, setHover] = useState<number | null>(null);
331
+ const w = 720;
332
+ const padLeft = 52;
333
+ const padRight = series.some((s) => s.axis === 'right') ? 46 : 14;
334
+ const padTop = 12;
335
+ const padBottom = 24;
336
+ const innerW = w - padLeft - padRight;
337
+ const innerH = height - padTop - padBottom;
338
+ const n = points.length;
339
+
340
+ const toneOf = (s: LineSeries, i: number) => s.tone ?? PALETTE[i % PALETTE.length]!;
341
+
342
+ const leftMax = useMemo(
343
+ () => niceMax(Math.max(1, ...series.filter((s) => s.axis !== 'right').flatMap((s) => s.values))),
344
+ [series],
345
+ );
346
+ const rightMax = useMemo(
347
+ () => niceMax(Math.max(1, ...series.filter((s) => s.axis === 'right').flatMap((s) => s.values))),
348
+ [series],
349
+ );
350
+ const leftTicks = useMemo(() => gridTicks(Math.max(...series.filter((s) => s.axis !== 'right').flatMap((s) => s.values), 1)), [series]);
351
+
352
+ const xAt = (i: number) => padLeft + (n <= 1 ? innerW / 2 : (i / (n - 1)) * innerW);
353
+ const yLeft = (v: number) => padTop + innerH - (v / leftMax) * innerH;
354
+ const yRight = (v: number) => padTop + innerH - (v / rightMax) * innerH;
355
+ const labelEvery = Math.max(1, Math.ceil(n / 8));
356
+
357
+ if (n === 0) return null;
358
+
359
+ return (
360
+ <div className="udb-linechart">
361
+ <svg
362
+ viewBox={`0 0 ${w} ${height}`}
363
+ className="udb-linechart__svg"
364
+ style={{ height }}
365
+ onMouseLeave={() => setHover(null)}
366
+ onMouseMove={(e) => {
367
+ const rect = e.currentTarget.getBoundingClientRect();
368
+ const x = ((e.clientX - rect.left) / rect.width) * w;
369
+ const idx = Math.round(((x - padLeft) / innerW) * (n - 1));
370
+ setHover(Math.max(0, Math.min(n - 1, idx)));
371
+ }}
372
+ >
373
+ {leftTicks.map((v, i) => (
374
+ <g key={i}>
375
+ <line x1={padLeft} x2={w - padRight} y1={yLeft(v)} y2={yLeft(v)} className="udb-linechart__grid" />
376
+ <text x={padLeft - 6} y={yLeft(v) + 3} textAnchor="end" className="udb-linechart__axis">
377
+ {formatLeft(v)}
378
+ </text>
379
+ </g>
380
+ ))}
381
+ {series.some((s) => s.axis === 'right') && (
382
+ <g>
383
+ {leftTicks.map((v, i) => (
384
+ <text key={i} x={w - padRight + 6} y={yLeft(v) + 3} textAnchor="start" className="udb-linechart__axis">
385
+ {formatRight((v / leftMax) * rightMax)}
386
+ </text>
387
+ ))}
388
+ </g>
389
+ )}
390
+ {points.map((p, i) =>
391
+ i % labelEvery === 0 ? (
392
+ <text key={i} x={xAt(i)} y={height - 8} textAnchor="middle" className="udb-linechart__axis">
393
+ {p.label}
394
+ </text>
395
+ ) : null,
396
+ )}
397
+ {series.map((s, si) => {
398
+ const yFn = s.axis === 'right' ? yRight : yLeft;
399
+ if (s.kind === 'bars') {
400
+ const barW = Math.max(3, (innerW / Math.max(1, n - 1)) * 0.5);
401
+ return (
402
+ <g key={s.key}>
403
+ {s.values.slice(0, n).map((v, i) => (
404
+ <rect
405
+ key={i}
406
+ x={xAt(i) - barW / 2}
407
+ y={yFn(v)}
408
+ width={barW}
409
+ height={Math.max(0, padTop + innerH - yFn(v))}
410
+ rx={1.5}
411
+ fill={toneOf(s, si)}
412
+ opacity={hover === null || hover === i ? 0.85 : 0.35}
413
+ />
414
+ ))}
415
+ </g>
416
+ );
417
+ }
418
+ const path = s.values
419
+ .slice(0, n)
420
+ .map((v, i) => `${i === 0 ? 'M' : 'L'}${xAt(i).toFixed(1)},${yFn(v).toFixed(1)}`)
421
+ .join(' ');
422
+ const areaPath = `${path} L${xAt(n - 1).toFixed(1)},${padTop + innerH} L${xAt(0).toFixed(1)},${padTop + innerH} Z`;
423
+ return (
424
+ <g key={s.key}>
425
+ {s.area !== false && !s.dashed && <path d={areaPath} fill={toneOf(s, si)} opacity={0.07} />}
426
+ <path
427
+ d={path}
428
+ fill="none"
429
+ stroke={toneOf(s, si)}
430
+ strokeWidth={2}
431
+ strokeLinejoin="round"
432
+ strokeLinecap="round"
433
+ strokeDasharray={s.dashed ? '4 4' : undefined}
434
+ />
435
+ {s.values.slice(0, n).map((v, i) => (
436
+ <circle
437
+ key={i}
438
+ cx={xAt(i)}
439
+ cy={yFn(v)}
440
+ r={hover === i ? 3.4 : 0}
441
+ fill="var(--u-color-surface)"
442
+ stroke={toneOf(s, si)}
443
+ strokeWidth={2}
444
+ />
445
+ ))}
446
+ </g>
447
+ );
448
+ })}
449
+ {hover !== null && (
450
+ <line
451
+ x1={xAt(hover)}
452
+ x2={xAt(hover)}
453
+ y1={padTop}
454
+ y2={padTop + innerH}
455
+ className="udb-linechart__crosshair"
456
+ />
457
+ )}
458
+ {yLabelLeft && (
459
+ <text x={4} y={10} className="udb-linechart__axis">
460
+ {yLabelLeft}
461
+ </text>
462
+ )}
463
+ {yLabelRight && (
464
+ <text x={w - 4} y={10} textAnchor="end" className="udb-linechart__axis">
465
+ {yLabelRight}
466
+ </text>
467
+ )}
468
+ </svg>
469
+ {hover !== null && (
470
+ <div
471
+ className="udb-linechart__tip"
472
+ style={{
473
+ left: `${(xAt(hover) / w) * 100}%`,
474
+ transform: hover > n / 2 ? 'translateX(calc(-100% - 10px))' : 'translateX(10px)',
475
+ }}
476
+ >
477
+ <div className="udb-linechart__tiptitle">{points[hover]!.label}</div>
478
+ {series.map((s, si) => (
479
+ <div key={s.key} className="udb-linechart__tiprow">
480
+ <span>
481
+ <span className="udb-linechart__swatch" style={{ background: toneOf(s, si) }} />
482
+ {s.label}
483
+ </span>
484
+ <b>{(s.axis === 'right' ? formatRight : formatLeft)(s.values[hover]!)}</b>
485
+ </div>
486
+ ))}
487
+ </div>
488
+ )}
489
+ <div className="udb-linechart__legendhost">
490
+ <Legend items={series.map((s, si) => ({ label: s.label, color: toneOf(s, si) }))} />
491
+ </div>
492
+ </div>
493
+ );
494
+ }
@@ -1,4 +1,12 @@
1
1
  export { PALETTE, useTip, niceMax, gridTicks } from './utils';
2
2
  export type { TipRow, TipState } from './utils';
3
- export { ChartTip, ChartCard, Legend, Sparkline, HBars } from './components';
4
- export type { ChartCardProps, LegendItem, HBarRow } from './components';
3
+ export { ChartTip, ChartCard, Legend, Sparkline, HBars, BarChart, Donut, LineChart } from './components';
4
+ export type {
5
+ ChartCardProps,
6
+ LegendItem,
7
+ HBarRow,
8
+ BarSeries,
9
+ DonutSegment,
10
+ LineSeries,
11
+ LineChartPoint,
12
+ } from './components';
@@ -2908,3 +2908,248 @@
2908
2908
  text-align: right;
2909
2909
  font-variant-numeric: tabular-nums;
2910
2910
  }
2911
+
2912
+ .udb-barchart {
2913
+ display: flex;
2914
+ align-items: flex-end;
2915
+ gap: 3px;
2916
+ width: 100%;
2917
+ }
2918
+
2919
+ .udb-barchart__col {
2920
+ position: relative;
2921
+ display: flex;
2922
+ flex: 1;
2923
+ align-items: flex-end;
2924
+ min-width: 0;
2925
+ height: 100%;
2926
+ }
2927
+
2928
+ .udb-barchart__bars {
2929
+ display: flex;
2930
+ align-items: flex-end;
2931
+ justify-content: center;
2932
+ gap: 2px;
2933
+ width: 100%;
2934
+ height: 100%;
2935
+ }
2936
+
2937
+ .udb-barchart__bars .udb-barchart__bar {
2938
+ flex: 1;
2939
+ width: auto;
2940
+ }
2941
+
2942
+ .udb-barchart__bar {
2943
+ width: 100%;
2944
+ min-height: 2px;
2945
+ border-radius: 3px 3px 0 0;
2946
+ transition: opacity 150ms ease, height 300ms ease;
2947
+ }
2948
+
2949
+ .udb-barchart__tip {
2950
+ position: absolute;
2951
+ bottom: calc(100% + 6px);
2952
+ left: 50%;
2953
+ z-index: 10;
2954
+ padding: 4px 8px;
2955
+ white-space: nowrap;
2956
+ pointer-events: none;
2957
+ border: 1px solid var(--u-color-border);
2958
+ border-radius: var(--u-radius-sm);
2959
+ background: var(--u-color-surface);
2960
+ box-shadow: var(--u-shadow-sm);
2961
+ font-size: var(--u-font-size-xs);
2962
+ color: var(--u-color-muted);
2963
+ }
2964
+
2965
+ .udb-barchart__tiptitle {
2966
+ font-weight: var(--u-font-weight-medium);
2967
+ color: var(--u-color-text);
2968
+ }
2969
+
2970
+ .udb-barchart__tiprow {
2971
+ display: flex;
2972
+ align-items: center;
2973
+ justify-content: space-between;
2974
+ gap: 10px;
2975
+ }
2976
+
2977
+ .udb-barchart__tiplabel {
2978
+ display: inline-flex;
2979
+ align-items: center;
2980
+ gap: 5px;
2981
+ }
2982
+
2983
+ .udb-barchart__tip b {
2984
+ margin-right: 4px;
2985
+ font-weight: var(--u-font-weight-medium);
2986
+ color: var(--u-color-text);
2987
+ }
2988
+
2989
+ .udb-donut {
2990
+ display: flex;
2991
+ align-items: center;
2992
+ gap: 20px;
2993
+ }
2994
+
2995
+ .udb-donut__wrap {
2996
+ position: relative;
2997
+ flex-shrink: 0;
2998
+ }
2999
+
3000
+ .udb-donut__seg {
3001
+ cursor: pointer;
3002
+ transition: stroke-width 150ms ease, opacity 150ms ease;
3003
+ }
3004
+
3005
+ .udb-donut__center {
3006
+ position: absolute;
3007
+ inset: 0;
3008
+ display: flex;
3009
+ flex-direction: column;
3010
+ align-items: center;
3011
+ justify-content: center;
3012
+ }
3013
+
3014
+ .udb-donut__value {
3015
+ font-size: var(--u-font-size-lg);
3016
+ font-weight: var(--u-font-weight-bold);
3017
+ color: var(--u-color-text);
3018
+ font-variant-numeric: tabular-nums;
3019
+ }
3020
+
3021
+ .udb-donut__label {
3022
+ margin-top: 2px;
3023
+ font-size: var(--u-font-size-xs);
3024
+ color: var(--u-color-muted);
3025
+ }
3026
+
3027
+ .udb-donut__list {
3028
+ display: flex;
3029
+ min-width: 0;
3030
+ flex: 1;
3031
+ flex-direction: column;
3032
+ gap: 6px;
3033
+ }
3034
+
3035
+ .udb-donut__item {
3036
+ display: flex;
3037
+ align-items: center;
3038
+ justify-content: space-between;
3039
+ gap: 8px;
3040
+ padding: 4px 6px;
3041
+ border-radius: var(--u-radius-sm);
3042
+ }
3043
+
3044
+ .udb-donut__item:hover {
3045
+ background: var(--u-color-surface-muted);
3046
+ }
3047
+
3048
+ .udb-donut__itemlabel {
3049
+ display: flex;
3050
+ min-width: 0;
3051
+ align-items: center;
3052
+ gap: 8px;
3053
+ font-size: var(--u-font-size-sm);
3054
+ color: var(--u-color-text);
3055
+ }
3056
+
3057
+ .udb-donut__dot {
3058
+ width: 8px;
3059
+ height: 8px;
3060
+ flex-shrink: 0;
3061
+ border-radius: 3px;
3062
+ }
3063
+
3064
+ .udb-donut__itemvalue {
3065
+ flex-shrink: 0;
3066
+ font-size: var(--u-font-size-sm);
3067
+ font-weight: var(--u-font-weight-medium);
3068
+ color: var(--u-color-text);
3069
+ font-variant-numeric: tabular-nums;
3070
+ }
3071
+
3072
+ .udb-donut__pct {
3073
+ margin-left: 6px;
3074
+ color: var(--u-color-muted);
3075
+ }
3076
+
3077
+ .udb-linechart {
3078
+ position: relative;
3079
+ width: 100%;
3080
+ user-select: none;
3081
+ }
3082
+
3083
+ .udb-linechart__svg {
3084
+ display: block;
3085
+ width: 100%;
3086
+ }
3087
+
3088
+ .udb-linechart__axis {
3089
+ fill: var(--u-color-muted);
3090
+ font-size: var(--u-font-size-xs);
3091
+ font-family: var(--u-font-sans);
3092
+ }
3093
+
3094
+ .udb-linechart__grid {
3095
+ stroke: var(--u-color-border);
3096
+ stroke-width: 1;
3097
+ }
3098
+
3099
+ .udb-linechart__crosshair {
3100
+ stroke: var(--u-color-border-strong);
3101
+ stroke-width: 1;
3102
+ stroke-dasharray: 3 3;
3103
+ }
3104
+
3105
+ .udb-linechart__tip {
3106
+ position: absolute;
3107
+ top: 8px;
3108
+ z-index: 10;
3109
+ min-width: 132px;
3110
+ padding: 8px;
3111
+ pointer-events: none;
3112
+ border: 1px solid var(--u-color-border);
3113
+ border-radius: var(--u-radius-md);
3114
+ background: var(--u-color-surface);
3115
+ box-shadow: var(--u-shadow-md);
3116
+ }
3117
+
3118
+ .udb-linechart__tiptitle {
3119
+ margin-bottom: 4px;
3120
+ font-size: var(--u-font-size-xs);
3121
+ color: var(--u-color-muted);
3122
+ }
3123
+
3124
+ .udb-linechart__tiprow {
3125
+ display: flex;
3126
+ align-items: center;
3127
+ justify-content: space-between;
3128
+ gap: 12px;
3129
+ font-size: var(--u-font-size-sm);
3130
+ }
3131
+
3132
+ .udb-linechart__tiprow span {
3133
+ display: flex;
3134
+ align-items: center;
3135
+ gap: 6px;
3136
+ color: var(--u-color-muted);
3137
+ }
3138
+
3139
+ .udb-linechart__tiprow b {
3140
+ font-weight: var(--u-font-weight-medium);
3141
+ color: var(--u-color-text);
3142
+ font-variant-numeric: tabular-nums;
3143
+ }
3144
+
3145
+ .udb-linechart__swatch {
3146
+ display: inline-block;
3147
+ width: 12px;
3148
+ height: 3px;
3149
+ border-radius: 999px;
3150
+ }
3151
+
3152
+ .udb-linechart__legendhost {
3153
+ margin-top: 4px;
3154
+ padding: 0 8px;
3155
+ }