@umituz/web-design-system 6.0.2 → 6.1.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/dist/styles.css +1 -1
- package/package.json +3 -1
- package/src/__tests__/charts/ActivityHeatmap.test.tsx +48 -0
- package/src/__tests__/charts/BarChart.test.tsx +91 -0
- package/src/__tests__/charts/ChartPrimitives.test.tsx +90 -0
- package/src/__tests__/charts/Donut.test.tsx +51 -0
- package/src/__tests__/charts/LineChart.test.tsx +81 -0
- package/src/__tests__/charts/ssr.test.tsx +51 -0
- package/src/__tests__/layout-components.test.tsx +100 -0
- package/src/presentation/molecules/TabNav.tsx +46 -0
- package/src/presentation/molecules/index.ts +3 -0
- package/src/presentation/organisms/AuditLogList.tsx +58 -0
- package/src/presentation/organisms/charts/ActivityHeatmap.tsx +58 -0
- package/src/presentation/organisms/charts/BarChart.tsx +143 -0
- package/src/presentation/organisms/charts/ChartCard.tsx +39 -0
- package/src/presentation/organisms/charts/ChartTip.tsx +18 -0
- package/src/presentation/organisms/charts/Donut.tsx +100 -0
- package/src/presentation/organisms/charts/HBars.tsx +31 -0
- package/src/presentation/organisms/charts/Legend.tsx +21 -0
- package/src/presentation/organisms/charts/LineChart.tsx +244 -0
- package/src/presentation/organisms/charts/Sparkline.tsx +35 -0
- package/src/presentation/organisms/charts/index.ts +17 -0
- package/src/presentation/organisms/charts/utils.ts +57 -0
- package/src/presentation/organisms/index.ts +6 -0
- package/src/presentation/templates/PageHeader.tsx +103 -33
- package/src/presentation/templates/SectionHeader.tsx +54 -0
- package/src/presentation/templates/index.ts +4 -1
- package/src/styles/app.css +0 -1
- package/src/styles/charts.css +455 -0
- package/src/styles/compile.css +1 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { useRef, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
export const PALETTE = ['#357344', '#b06e22', '#3e5f80', '#7a6ba8', '#8a5a3b', '#5e7d5a', '#a8b5a0', '#c9862f'];
|
|
4
|
+
|
|
5
|
+
export const SERIES_COLORS = [
|
|
6
|
+
'var(--udb-chart-1, #357344)',
|
|
7
|
+
'var(--udb-chart-2, #b06e22)',
|
|
8
|
+
'var(--udb-chart-3, #3e5f80)',
|
|
9
|
+
'var(--udb-chart-4, #7a6ba8)',
|
|
10
|
+
'var(--udb-chart-5, #8a5a3b)',
|
|
11
|
+
'var(--udb-chart-6, #5e7d5a)',
|
|
12
|
+
'var(--udb-chart-7, #a8b5a0)',
|
|
13
|
+
'var(--udb-chart-8, #c9862f)',
|
|
14
|
+
] as const;
|
|
15
|
+
|
|
16
|
+
export function seriesColor(index: number): string {
|
|
17
|
+
return SERIES_COLORS[index % SERIES_COLORS.length]!;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface TipRow {
|
|
21
|
+
label: string;
|
|
22
|
+
value: string;
|
|
23
|
+
color?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface TipState {
|
|
27
|
+
x: number;
|
|
28
|
+
y: number;
|
|
29
|
+
title: string;
|
|
30
|
+
rows: TipRow[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function useTip() {
|
|
34
|
+
const [tip, setTip] = useState<TipState | null>(null);
|
|
35
|
+
const hostRef = useRef<HTMLDivElement>(null);
|
|
36
|
+
const show = (e: React.MouseEvent, title: string, rows: TipRow[]) => {
|
|
37
|
+
const rect = hostRef.current?.getBoundingClientRect();
|
|
38
|
+
if (!rect) return;
|
|
39
|
+
setTip({ x: e.clientX - rect.left, y: e.clientY - rect.top, title, rows });
|
|
40
|
+
};
|
|
41
|
+
const hide = () => setTip(null);
|
|
42
|
+
return { tip, hostRef, show, hide };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function niceMax(max: number): number {
|
|
46
|
+
if (max <= 0) return 1;
|
|
47
|
+
const exp = Math.floor(Math.log10(max));
|
|
48
|
+
const base = Math.pow(10, exp);
|
|
49
|
+
const frac = max / base;
|
|
50
|
+
const nice = frac <= 1 ? 1 : frac <= 2 ? 2 : frac <= 2.5 ? 2.5 : frac <= 5 ? 5 : 10;
|
|
51
|
+
return nice * base;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function gridTicks(max: number, count = 4): number[] {
|
|
55
|
+
const nice = niceMax(max);
|
|
56
|
+
return Array.from({ length: count + 1 }, (_, i) => (nice / count) * i);
|
|
57
|
+
}
|
|
@@ -185,3 +185,9 @@ export type { StatsGridProps, StatItem } from './StatsGrid';
|
|
|
185
185
|
|
|
186
186
|
export { DescriptionList } from './DescriptionList';
|
|
187
187
|
export type { DescriptionListProps, DescriptionItem } from './DescriptionList';
|
|
188
|
+
|
|
189
|
+
export { AuditLogList } from './AuditLogList';
|
|
190
|
+
export type { AuditLogListProps, AuditEntry } from './AuditLogList';
|
|
191
|
+
|
|
192
|
+
// Visual Data Visualization & Charts
|
|
193
|
+
export * from './charts';
|
|
@@ -1,32 +1,46 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
import { type ReactNode } from 'react';
|
|
1
|
+
import { forwardRef, type HTMLAttributes, type ReactNode } from 'react';
|
|
2
|
+
import { Link } from 'react-router';
|
|
4
3
|
import { cn } from '../../infrastructure/utils';
|
|
5
4
|
import type { BaseProps } from '../../domain/types';
|
|
6
5
|
|
|
7
6
|
export type TextAlign = 'left' | 'center' | 'right';
|
|
8
7
|
export type HeaderSize = 'small' | 'medium' | 'large';
|
|
9
8
|
|
|
9
|
+
export interface PageHeaderCrumb {
|
|
10
|
+
label: string;
|
|
11
|
+
to?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
10
14
|
export interface PageHeaderProps extends Omit<HTMLAttributes<HTMLDivElement>, 'title'>, BaseProps {
|
|
11
|
-
title
|
|
12
|
-
subtitle?:
|
|
15
|
+
title?: ReactNode;
|
|
16
|
+
subtitle?: ReactNode;
|
|
17
|
+
description?: ReactNode;
|
|
18
|
+
kicker?: ReactNode;
|
|
19
|
+
breadcrumbs?: ReactNode | readonly PageHeaderCrumb[];
|
|
20
|
+
meta?: ReactNode;
|
|
13
21
|
align?: TextAlign;
|
|
14
22
|
size?: HeaderSize;
|
|
15
23
|
actions?: ReactNode;
|
|
16
24
|
}
|
|
17
25
|
|
|
26
|
+
const isCrumbList = (
|
|
27
|
+
value: ReactNode | readonly PageHeaderCrumb[]
|
|
28
|
+
): value is readonly PageHeaderCrumb[] =>
|
|
29
|
+
Array.isArray(value) &&
|
|
30
|
+
value.every((item) => typeof item === 'object' && item !== null && 'label' in item);
|
|
31
|
+
|
|
18
32
|
const sizeClasses: Record<HeaderSize, { title: string; subtitle: string }> = {
|
|
19
33
|
small: {
|
|
20
|
-
title: 'text-
|
|
21
|
-
subtitle: 'text-
|
|
34
|
+
title: 'text-xl sm:text-2xl font-bold tracking-tight',
|
|
35
|
+
subtitle: 'text-xs sm:text-sm',
|
|
22
36
|
},
|
|
23
37
|
medium: {
|
|
24
|
-
title: 'text-
|
|
25
|
-
subtitle: 'text-
|
|
38
|
+
title: 'text-2xl sm:text-3xl font-bold tracking-tight',
|
|
39
|
+
subtitle: 'text-sm sm:text-base',
|
|
26
40
|
},
|
|
27
41
|
large: {
|
|
28
|
-
title: 'text-
|
|
29
|
-
subtitle: 'text-
|
|
42
|
+
title: 'text-3xl sm:text-4xl lg:text-5xl font-bold tracking-tight',
|
|
43
|
+
subtitle: 'text-base sm:text-lg',
|
|
30
44
|
},
|
|
31
45
|
};
|
|
32
46
|
|
|
@@ -36,36 +50,92 @@ const alignClasses: Record<TextAlign, string> = {
|
|
|
36
50
|
right: 'text-right',
|
|
37
51
|
};
|
|
38
52
|
|
|
39
|
-
const getAlignFlexClass = (align: TextAlign, hasActions: boolean): string => {
|
|
40
|
-
if (!hasActions) return '';
|
|
41
|
-
return align === 'center'
|
|
42
|
-
? 'flex justify-center'
|
|
43
|
-
: align === 'right'
|
|
44
|
-
? 'flex justify-end'
|
|
45
|
-
: 'flex justify-start';
|
|
46
|
-
};
|
|
47
|
-
|
|
48
53
|
export const PageHeader = forwardRef<HTMLDivElement, PageHeaderProps>(
|
|
49
|
-
(
|
|
54
|
+
(
|
|
55
|
+
{
|
|
56
|
+
title,
|
|
57
|
+
subtitle,
|
|
58
|
+
description,
|
|
59
|
+
kicker,
|
|
60
|
+
breadcrumbs,
|
|
61
|
+
meta,
|
|
62
|
+
align = 'left',
|
|
63
|
+
size = 'small',
|
|
64
|
+
actions,
|
|
65
|
+
className,
|
|
66
|
+
...props
|
|
67
|
+
},
|
|
68
|
+
ref
|
|
69
|
+
) => {
|
|
70
|
+
const desc = subtitle ?? description;
|
|
71
|
+
|
|
72
|
+
const crumbNode =
|
|
73
|
+
breadcrumbs === undefined
|
|
74
|
+
? undefined
|
|
75
|
+
: isCrumbList(breadcrumbs)
|
|
76
|
+
? breadcrumbs.map((crumb, index) => (
|
|
77
|
+
<span key={`${crumb.label}-${index}`} className="inline-flex items-center gap-1">
|
|
78
|
+
{index > 0 && <span className="opacity-40 select-none">/</span>}
|
|
79
|
+
{crumb.to !== undefined ? (
|
|
80
|
+
<Link to={crumb.to} className="hover:underline hover:text-foreground transition-colors">
|
|
81
|
+
{crumb.label}
|
|
82
|
+
</Link>
|
|
83
|
+
) : (
|
|
84
|
+
<span>{crumb.label}</span>
|
|
85
|
+
)}
|
|
86
|
+
</span>
|
|
87
|
+
))
|
|
88
|
+
: breadcrumbs;
|
|
89
|
+
|
|
50
90
|
return (
|
|
51
91
|
<div
|
|
52
92
|
ref={ref}
|
|
53
|
-
className={cn(
|
|
93
|
+
className={cn(
|
|
94
|
+
'flex flex-col gap-2 mb-6',
|
|
95
|
+
alignClasses[align],
|
|
96
|
+
className
|
|
97
|
+
)}
|
|
54
98
|
{...props}
|
|
55
99
|
>
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
{subtitle && (
|
|
60
|
-
<p className={cn(sizeClasses[size].subtitle, 'text-text-secondary')}>
|
|
61
|
-
{subtitle}
|
|
62
|
-
</p>
|
|
63
|
-
)}
|
|
64
|
-
{actions && (
|
|
65
|
-
<div className={cn('mt-6 md:mt-8', getAlignFlexClass(align, true))}>
|
|
66
|
-
{actions}
|
|
100
|
+
{crumbNode !== undefined && (
|
|
101
|
+
<div className="flex items-center gap-1.5 text-xs text-muted-foreground mb-1">
|
|
102
|
+
{crumbNode}
|
|
67
103
|
</div>
|
|
68
104
|
)}
|
|
105
|
+
|
|
106
|
+
<div className="flex flex-wrap items-start justify-between gap-3">
|
|
107
|
+
<div className="min-w-0 flex-1">
|
|
108
|
+
{kicker !== undefined && (
|
|
109
|
+
<p className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground mb-1">
|
|
110
|
+
{kicker}
|
|
111
|
+
</p>
|
|
112
|
+
)}
|
|
113
|
+
|
|
114
|
+
{title !== undefined && (
|
|
115
|
+
<h1 className={cn(sizeClasses[size].title, 'text-foreground')}>
|
|
116
|
+
{title}
|
|
117
|
+
</h1>
|
|
118
|
+
)}
|
|
119
|
+
|
|
120
|
+
{desc !== undefined && (
|
|
121
|
+
<p className={cn(sizeClasses[size].subtitle, 'text-muted-foreground mt-1 leading-relaxed')}>
|
|
122
|
+
{desc}
|
|
123
|
+
</p>
|
|
124
|
+
)}
|
|
125
|
+
|
|
126
|
+
{meta !== undefined && (
|
|
127
|
+
<div className="flex flex-wrap items-center gap-2 mt-2 text-xs text-muted-foreground">
|
|
128
|
+
{meta}
|
|
129
|
+
</div>
|
|
130
|
+
)}
|
|
131
|
+
</div>
|
|
132
|
+
|
|
133
|
+
{actions && (
|
|
134
|
+
<div className="flex flex-wrap items-center gap-2 shrink-0">
|
|
135
|
+
{actions}
|
|
136
|
+
</div>
|
|
137
|
+
)}
|
|
138
|
+
</div>
|
|
69
139
|
</div>
|
|
70
140
|
);
|
|
71
141
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { forwardRef, type HTMLAttributes, type ReactNode } from 'react';
|
|
2
|
+
import { cn } from '../../infrastructure/utils';
|
|
3
|
+
import type { BaseProps } from '../../domain/types';
|
|
4
|
+
|
|
5
|
+
export interface SectionHeaderProps
|
|
6
|
+
extends Omit<HTMLAttributes<HTMLDivElement>, 'title'>,
|
|
7
|
+
BaseProps {
|
|
8
|
+
kicker?: ReactNode;
|
|
9
|
+
title?: ReactNode;
|
|
10
|
+
subtitle?: ReactNode;
|
|
11
|
+
description?: ReactNode;
|
|
12
|
+
actions?: ReactNode;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const SectionHeader = forwardRef<HTMLDivElement, SectionHeaderProps>(
|
|
16
|
+
({ kicker, title, subtitle, description, actions, className, ...props }, ref) => {
|
|
17
|
+
const desc = subtitle ?? description;
|
|
18
|
+
return (
|
|
19
|
+
<div
|
|
20
|
+
ref={ref}
|
|
21
|
+
className={cn(
|
|
22
|
+
'flex flex-wrap items-start justify-between gap-3 mb-3.5',
|
|
23
|
+
className
|
|
24
|
+
)}
|
|
25
|
+
{...props}
|
|
26
|
+
>
|
|
27
|
+
<div className="min-w-0">
|
|
28
|
+
{kicker && (
|
|
29
|
+
<p className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground mb-1">
|
|
30
|
+
{kicker}
|
|
31
|
+
</p>
|
|
32
|
+
)}
|
|
33
|
+
{title && (
|
|
34
|
+
<h2 className="text-base sm:text-lg font-semibold tracking-tight text-foreground">
|
|
35
|
+
{title}
|
|
36
|
+
</h2>
|
|
37
|
+
)}
|
|
38
|
+
{desc && (
|
|
39
|
+
<p className="text-xs sm:text-sm text-muted-foreground mt-0.5 leading-relaxed">
|
|
40
|
+
{desc}
|
|
41
|
+
</p>
|
|
42
|
+
)}
|
|
43
|
+
</div>
|
|
44
|
+
{actions && (
|
|
45
|
+
<div className="flex flex-wrap items-center gap-2 shrink-0">
|
|
46
|
+
{actions}
|
|
47
|
+
</div>
|
|
48
|
+
)}
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
SectionHeader.displayName = 'SectionHeader';
|
|
@@ -11,7 +11,10 @@ export { PageLayout } from './PageLayout';
|
|
|
11
11
|
export type { PageLayoutProps, MaxWidth } from './PageLayout';
|
|
12
12
|
|
|
13
13
|
export { PageHeader } from './PageHeader';
|
|
14
|
-
export type { PageHeaderProps, TextAlign, HeaderSize } from './PageHeader';
|
|
14
|
+
export type { PageHeaderProps, PageHeaderCrumb, TextAlign, HeaderSize } from './PageHeader';
|
|
15
|
+
|
|
16
|
+
export { SectionHeader } from './SectionHeader';
|
|
17
|
+
export type { SectionHeaderProps } from './SectionHeader';
|
|
15
18
|
|
|
16
19
|
export { ProjectSkeleton } from './ProjectSkeleton';
|
|
17
20
|
export type { ProjectSkeletonProps } from './ProjectSkeleton';
|
package/src/styles/app.css
CHANGED