@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.
Files changed (30) hide show
  1. package/dist/styles.css +1 -1
  2. package/package.json +3 -1
  3. package/src/__tests__/charts/ActivityHeatmap.test.tsx +48 -0
  4. package/src/__tests__/charts/BarChart.test.tsx +91 -0
  5. package/src/__tests__/charts/ChartPrimitives.test.tsx +90 -0
  6. package/src/__tests__/charts/Donut.test.tsx +51 -0
  7. package/src/__tests__/charts/LineChart.test.tsx +81 -0
  8. package/src/__tests__/charts/ssr.test.tsx +51 -0
  9. package/src/__tests__/layout-components.test.tsx +100 -0
  10. package/src/presentation/molecules/TabNav.tsx +46 -0
  11. package/src/presentation/molecules/index.ts +3 -0
  12. package/src/presentation/organisms/AuditLogList.tsx +58 -0
  13. package/src/presentation/organisms/charts/ActivityHeatmap.tsx +58 -0
  14. package/src/presentation/organisms/charts/BarChart.tsx +143 -0
  15. package/src/presentation/organisms/charts/ChartCard.tsx +39 -0
  16. package/src/presentation/organisms/charts/ChartTip.tsx +18 -0
  17. package/src/presentation/organisms/charts/Donut.tsx +100 -0
  18. package/src/presentation/organisms/charts/HBars.tsx +31 -0
  19. package/src/presentation/organisms/charts/Legend.tsx +21 -0
  20. package/src/presentation/organisms/charts/LineChart.tsx +244 -0
  21. package/src/presentation/organisms/charts/Sparkline.tsx +35 -0
  22. package/src/presentation/organisms/charts/index.ts +17 -0
  23. package/src/presentation/organisms/charts/utils.ts +57 -0
  24. package/src/presentation/organisms/index.ts +6 -0
  25. package/src/presentation/templates/PageHeader.tsx +103 -33
  26. package/src/presentation/templates/SectionHeader.tsx +54 -0
  27. package/src/presentation/templates/index.ts +4 -1
  28. package/src/styles/app.css +0 -1
  29. package/src/styles/charts.css +455 -0
  30. 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 { forwardRef, type HTMLAttributes } from 'react';
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: ReactNode;
12
- subtitle?: string;
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-2xl md:text-3xl font-bold',
21
- subtitle: 'text-base md:text-lg',
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-3xl md:text-4xl font-bold',
25
- subtitle: 'text-lg md:text-xl',
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-4xl md:text-5xl lg:text-6xl font-bold',
29
- subtitle: 'text-lg md:text-xl',
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
- ({ title, subtitle, align = 'center', size = 'large', actions, className, ...props }, ref) => {
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('mb-6 md:mb-12', alignClasses[align], className)}
93
+ className={cn(
94
+ 'flex flex-col gap-2 mb-6',
95
+ alignClasses[align],
96
+ className
97
+ )}
54
98
  {...props}
55
99
  >
56
- <h1 className={cn(sizeClasses[size].title, 'text-text-primary mb-4')}>
57
- {title}
58
- </h1>
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';
@@ -1,3 +1,2 @@
1
-
2
1
  @import "./source.css";
3
2
  @import "./bridge.css";