@umituz/web-dashboard 4.1.3 → 4.3.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.1.3",
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",
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",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
7
7
  "types": "./src/index.ts",
@@ -50,7 +50,8 @@
50
50
  "./calendar/hooks": "./src/domains/calendar/hooks/index.ts",
51
51
  "./calendar/utils": "./src/domains/calendar/utils/index.ts",
52
52
  "./config": "./src/domain/config/index.ts",
53
- "./package.json": "./package.json"
53
+ "./package.json": "./package.json",
54
+ "./charts": "./src/domains/charts/index.ts"
54
55
  },
55
56
  "files": [
56
57
  "src",
@@ -0,0 +1,139 @@
1
+ import { useId } from 'react';
2
+ import type { ReactNode } from 'react';
3
+ import type { TipState } from './utils';
4
+
5
+ export function ChartTip({ tip }: { tip: TipState }) {
6
+ return (
7
+ <div className="udb-charttip" style={{ left: tip.x, top: tip.y }}>
8
+ <div className="udb-charttip__title">{tip.title}</div>
9
+ {tip.rows.map((r, i) => (
10
+ <div key={i} className="udb-charttip__row">
11
+ <span className="udb-charttip__label">
12
+ {r.color && <span className="udb-chartlegend__dot" style={{ background: r.color }} />}
13
+ {r.label}
14
+ </span>
15
+ <b>{r.value}</b>
16
+ </div>
17
+ ))}
18
+ </div>
19
+ );
20
+ }
21
+
22
+ export interface ChartCardProps {
23
+ title: string;
24
+ subtitle?: string;
25
+ legend?: ReactNode;
26
+ actions?: ReactNode;
27
+ children: ReactNode;
28
+ tip: TipState | null;
29
+ hostRef?: React.Ref<HTMLDivElement>;
30
+ onMouseLeave?: () => void;
31
+ bare?: boolean;
32
+ }
33
+
34
+ export function ChartCard({ title, subtitle, legend, actions, children, tip, hostRef, onMouseLeave, bare }: ChartCardProps) {
35
+ return (
36
+ <div className="udb-chartcard">
37
+ <div className="udb-chartcard__head">
38
+ <div className="udb-chartcard__heading">
39
+ <div className="udb-chartcard__title">{title}</div>
40
+ {subtitle && <div className="udb-chartcard__sub">{subtitle}</div>}
41
+ </div>
42
+ <div className="udb-chartcard__tools">
43
+ {legend}
44
+ {actions}
45
+ </div>
46
+ </div>
47
+ <div className="udb-chartcard__body" style={bare ? { padding: 0 } : undefined}>
48
+ <div ref={hostRef} className="udb-chartcard__plot" onMouseLeave={onMouseLeave}>
49
+ {children}
50
+ {tip && <ChartTip tip={tip} />}
51
+ </div>
52
+ </div>
53
+ </div>
54
+ );
55
+ }
56
+
57
+ export interface LegendItem {
58
+ label: string;
59
+ color: string;
60
+ value?: string;
61
+ }
62
+
63
+ export function Legend({ items }: { items: LegendItem[] }) {
64
+ return (
65
+ <div className="udb-chartlegend">
66
+ {items.map((i) => (
67
+ <span key={i.label} className="udb-chartlegend__item">
68
+ <span className="udb-chartlegend__dot" style={{ background: i.color }} />
69
+ {i.label}
70
+ {i.value && <span className="udb-chartlegend__value">{i.value}</span>}
71
+ </span>
72
+ ))}
73
+ </div>
74
+ );
75
+ }
76
+
77
+ export function Sparkline({
78
+ data,
79
+ height = 34,
80
+ width = 110,
81
+ color = 'currentColor',
82
+ }: {
83
+ data: number[];
84
+ height?: number;
85
+ width?: number;
86
+ color?: string;
87
+ }) {
88
+ const id = useId();
89
+ if (data.length < 2) return null;
90
+ const min = Math.min(...data);
91
+ const max = Math.max(...data);
92
+ const span = max - min || 1;
93
+ const pts = data.map((v, i) => [(i / (data.length - 1)) * width, height - 3 - ((v - min) / span) * (height - 8)]);
94
+ const line = pts.map((p, i) => `${i === 0 ? 'M' : 'L'}${p[0]!.toFixed(1)},${p[1]!.toFixed(1)}`).join(' ');
95
+ const area = `${line} L${width},${height} L0,${height} Z`;
96
+ return (
97
+ <svg width={width} height={height} viewBox={`0 0 ${width} ${height}`} aria-hidden>
98
+ <defs>
99
+ <linearGradient id={`sp${id}`} x1="0" y1="0" x2="0" y2="1">
100
+ <stop offset="0%" stopColor={color} stopOpacity="0.28" />
101
+ <stop offset="100%" stopColor={color} stopOpacity="0" />
102
+ </linearGradient>
103
+ </defs>
104
+ <path d={area} fill={`url(#sp${id})`} />
105
+ <path d={line} fill="none" stroke={color} strokeWidth="1.6" strokeLinejoin="round" strokeLinecap="round" />
106
+ <circle cx={pts.at(-1)![0]} cy={pts.at(-1)![1]} r="2.4" fill={color} />
107
+ </svg>
108
+ );
109
+ }
110
+
111
+ export interface HBarRow {
112
+ label: ReactNode;
113
+ sub?: ReactNode;
114
+ value: number;
115
+ tone?: string;
116
+ }
117
+
118
+ export function HBars({ rows, formatValue = (n: number) => String(n) }: { rows: HBarRow[]; formatValue?: (n: number) => string }) {
119
+ const max = Math.max(1, ...rows.map((r) => r.value));
120
+ return (
121
+ <div className="udb-hbars">
122
+ {rows.map((r, i) => (
123
+ <div key={i} className="udb-hbars__row">
124
+ <div className="udb-hbars__label">
125
+ <div className="udb-hbars__name">{r.label}</div>
126
+ {r.sub && <div className="udb-hbars__sub">{r.sub}</div>}
127
+ </div>
128
+ <div className="udb-hbars__track">
129
+ <div
130
+ className="udb-hbars__fill"
131
+ style={{ width: `${(r.value / max) * 100}%`, ...(r.tone ? { background: r.tone } : {}) }}
132
+ />
133
+ </div>
134
+ <span className="udb-hbars__value">{formatValue(r.value)}</span>
135
+ </div>
136
+ ))}
137
+ </div>
138
+ );
139
+ }
@@ -0,0 +1,4 @@
1
+ export { PALETTE, useTip, niceMax, gridTicks } from './utils';
2
+ export type { TipRow, TipState } from './utils';
3
+ export { ChartTip, ChartCard, Legend, Sparkline, HBars } from './components';
4
+ export type { ChartCardProps, LegendItem, HBarRow } from './components';
@@ -0,0 +1,42 @@
1
+ import { useRef, useState } from 'react';
2
+
3
+ export const PALETTE = ['#357344', '#b06e22', '#3e5f80', '#7a6ba8', '#8a5a3b', '#5e7d5a', '#a8b5a0', '#c9862f'];
4
+
5
+ export interface TipRow {
6
+ label: string;
7
+ value: string;
8
+ color?: string;
9
+ }
10
+
11
+ export interface TipState {
12
+ x: number;
13
+ y: number;
14
+ title: string;
15
+ rows: TipRow[];
16
+ }
17
+
18
+ export function useTip() {
19
+ const [tip, setTip] = useState<TipState | null>(null);
20
+ const hostRef = useRef<HTMLDivElement>(null);
21
+ const show = (e: React.MouseEvent, title: string, rows: TipRow[]) => {
22
+ const rect = hostRef.current?.getBoundingClientRect();
23
+ if (!rect) return;
24
+ setTip({ x: e.clientX - rect.left, y: e.clientY - rect.top, title, rows });
25
+ };
26
+ const hide = () => setTip(null);
27
+ return { tip, hostRef, show, hide };
28
+ }
29
+
30
+ export function niceMax(max: number): number {
31
+ if (max <= 0) return 1;
32
+ const exp = Math.floor(Math.log10(max));
33
+ const base = Math.pow(10, exp);
34
+ const frac = max / base;
35
+ const nice = frac <= 1 ? 1 : frac <= 2 ? 2 : frac <= 2.5 ? 2.5 : frac <= 5 ? 5 : 10;
36
+ return nice * base;
37
+ }
38
+
39
+ export function gridTicks(max: number, count = 4): number[] {
40
+ const nice = niceMax(max);
41
+ return Array.from({ length: count + 1 }, (_, i) => (nice / count) * i);
42
+ }
@@ -2729,3 +2729,182 @@
2729
2729
  white-space: nowrap;
2730
2730
  color: var(--u-color-muted);
2731
2731
  }
2732
+
2733
+ /* ============================================================
2734
+ * 12. Charts — card frame, legend, hover tooltip
2735
+ * ============================================================ */
2736
+
2737
+ .udb-chartcard {
2738
+ display: flex;
2739
+ flex-direction: column;
2740
+ min-width: 0;
2741
+ background: var(--u-color-surface);
2742
+ border: 1px solid var(--u-color-border);
2743
+ border-radius: var(--u-radius-lg);
2744
+ box-shadow: var(--u-shadow-sm);
2745
+ }
2746
+
2747
+ .udb-chartcard__head {
2748
+ display: flex;
2749
+ align-items: flex-start;
2750
+ justify-content: space-between;
2751
+ gap: var(--u-space-3);
2752
+ padding: 14px 18px 4px;
2753
+ flex-wrap: wrap;
2754
+ }
2755
+
2756
+ .udb-chartcard__heading {
2757
+ min-width: 0;
2758
+ }
2759
+
2760
+ .udb-chartcard__title {
2761
+ font-size: var(--u-font-size-md);
2762
+ font-weight: 600;
2763
+ color: var(--u-color-text);
2764
+ }
2765
+
2766
+ .udb-chartcard__sub {
2767
+ font-size: var(--u-font-size-sm);
2768
+ color: var(--u-color-muted);
2769
+ margin-top: 1px;
2770
+ }
2771
+
2772
+ .udb-chartcard__tools {
2773
+ display: flex;
2774
+ align-items: center;
2775
+ gap: 12px;
2776
+ flex-wrap: wrap;
2777
+ }
2778
+
2779
+ .udb-chartcard__body {
2780
+ padding: 10px 14px 14px;
2781
+ min-width: 0;
2782
+ }
2783
+
2784
+ .udb-chartcard__body svg {
2785
+ display: block;
2786
+ width: 100%;
2787
+ height: auto;
2788
+ }
2789
+
2790
+ .udb-chartcard__plot {
2791
+ position: relative;
2792
+ }
2793
+
2794
+ .udb-chartlegend {
2795
+ display: flex;
2796
+ flex-wrap: wrap;
2797
+ gap: 5px 14px;
2798
+ }
2799
+
2800
+ .udb-chartlegend__item {
2801
+ display: inline-flex;
2802
+ align-items: center;
2803
+ gap: 6px;
2804
+ font-size: var(--u-font-size-sm);
2805
+ color: var(--u-color-text);
2806
+ white-space: nowrap;
2807
+ }
2808
+
2809
+ .udb-chartlegend__dot {
2810
+ width: 8px;
2811
+ height: 8px;
2812
+ border-radius: 2.5px;
2813
+ flex: none;
2814
+ }
2815
+
2816
+ .udb-chartlegend__value {
2817
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
2818
+ color: var(--u-color-muted);
2819
+ font-size: var(--u-font-size-xs);
2820
+ }
2821
+
2822
+ .udb-charttip {
2823
+ position: absolute;
2824
+ pointer-events: none;
2825
+ background: var(--u-color-text);
2826
+ color: var(--u-color-bg);
2827
+ border-radius: var(--u-radius-md);
2828
+ padding: 7px 10px;
2829
+ font-size: var(--u-font-size-sm);
2830
+ box-shadow: var(--u-shadow-lg);
2831
+ z-index: 30;
2832
+ min-width: 120px;
2833
+ transform: translate(-50%, calc(-100% - 10px));
2834
+ }
2835
+
2836
+ .udb-charttip__title {
2837
+ font-weight: 600;
2838
+ margin-bottom: 2px;
2839
+ white-space: nowrap;
2840
+ }
2841
+
2842
+ .udb-charttip__row {
2843
+ display: flex;
2844
+ align-items: center;
2845
+ gap: 6px;
2846
+ justify-content: space-between;
2847
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
2848
+ font-size: var(--u-font-size-xs);
2849
+ }
2850
+
2851
+ .udb-charttip__row b {
2852
+ font-weight: 600;
2853
+ }
2854
+
2855
+ .udb-charttip__label {
2856
+ display: inline-flex;
2857
+ align-items: center;
2858
+ gap: 5px;
2859
+ }
2860
+
2861
+ .udb-hbars {
2862
+ display: flex;
2863
+ flex-direction: column;
2864
+ gap: 10px;
2865
+ }
2866
+
2867
+ .udb-hbars__row {
2868
+ display: grid;
2869
+ grid-template-columns: minmax(120px, 1.2fr) 2fr auto;
2870
+ align-items: center;
2871
+ gap: 12px;
2872
+ }
2873
+
2874
+ .udb-hbars__label {
2875
+ min-width: 0;
2876
+ }
2877
+
2878
+ .udb-hbars__name {
2879
+ font-weight: 600;
2880
+ font-size: var(--u-font-size-sm);
2881
+ overflow: hidden;
2882
+ text-overflow: ellipsis;
2883
+ white-space: nowrap;
2884
+ color: var(--u-color-text);
2885
+ }
2886
+
2887
+ .udb-hbars__sub {
2888
+ font-size: var(--u-font-size-xs);
2889
+ color: var(--u-color-muted);
2890
+ }
2891
+
2892
+ .udb-hbars__track {
2893
+ height: 8px;
2894
+ background: var(--u-color-border);
2895
+ border-radius: 999px;
2896
+ overflow: hidden;
2897
+ }
2898
+
2899
+ .udb-hbars__fill {
2900
+ height: 100%;
2901
+ background: var(--u-color-primary);
2902
+ border-radius: 999px;
2903
+ }
2904
+
2905
+ .udb-hbars__value {
2906
+ font-size: var(--u-font-size-sm);
2907
+ color: var(--u-color-text);
2908
+ text-align: right;
2909
+ font-variant-numeric: tabular-nums;
2910
+ }