@umituz/web-design-system 3.1.21 → 4.0.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/README.md +137 -469
- package/dist/styles.css +2 -0
- package/package.json +30 -31
- package/src/__tests__/cn.test.ts +24 -0
- package/src/__tests__/components.smoke.test.tsx +78 -0
- package/src/__tests__/styles.contract.test.ts +60 -0
- package/src/domain/tokens/index.ts +8 -49
- package/src/index.ts +8 -10
- package/src/infrastructure/constants/component.constants.ts +3 -17
- package/src/infrastructure/constants/index.ts +4 -2
- package/src/infrastructure/constants/timing.constants.ts +46 -0
- package/src/infrastructure/utils/calculation.utils.ts +74 -0
- package/src/infrastructure/utils/index.ts +12 -0
- package/src/presentation/atoms/IconButton.tsx +55 -0
- package/src/presentation/atoms/Progress.tsx +2 -1
- package/src/presentation/atoms/Spinner.tsx +1 -1
- package/src/presentation/atoms/Switch.tsx +2 -2
- package/src/presentation/atoms/Text.tsx +15 -13
- package/src/presentation/atoms/Tooltip.tsx +2 -1
- package/src/presentation/atoms/index.ts +3 -0
- package/src/presentation/hooks/index.ts +0 -3
- package/src/presentation/hooks/useClickOutside.ts +14 -10
- package/src/presentation/hooks/useClipboard.ts +30 -12
- package/src/presentation/hooks/useDebounce.ts +8 -10
- package/src/presentation/hooks/useLocalStorage.ts +28 -18
- package/src/presentation/hooks/useMediaQuery.ts +2 -1
- package/src/presentation/hooks/useScrollLock.ts +40 -12
- package/src/presentation/hooks/useToggle.ts +8 -6
- package/src/presentation/molecules/ActiveFilterTags.tsx +19 -18
- package/src/presentation/molecules/Avatar.tsx +1 -1
- package/src/presentation/molecules/CodeBlock.tsx +51 -48
- package/src/presentation/molecules/ListItem.tsx +40 -29
- package/src/presentation/molecules/Select.tsx +1 -1
- package/src/presentation/molecules/Toggle.tsx +4 -4
- package/src/presentation/molecules/index.ts +0 -3
- package/src/presentation/organisms/Alert.tsx +1 -1
- package/src/presentation/organisms/DataTable.tsx +24 -11
- package/src/presentation/organisms/Dialog.tsx +1 -1
- package/src/presentation/organisms/DropdownMenu.tsx +2 -2
- package/src/presentation/organisms/HoverCard.tsx +1 -1
- package/src/presentation/organisms/LoadingState.tsx +1 -5
- package/src/presentation/organisms/Popover.tsx +1 -1
- package/src/presentation/organisms/StatCard.tsx +2 -2
- package/src/presentation/organisms/Table.tsx +2 -2
- package/src/presentation/organisms/index.ts +0 -60
- package/src/styles/compile.css +18 -0
- package/src/styles/theme.css +131 -0
- package/src/domain/tokens/animation.tokens.ts +0 -87
- package/src/domain/tokens/color.tokens.ts +0 -95
- package/src/domain/tokens/radius.tokens.ts +0 -27
- package/src/domain/tokens/shadow.tokens.ts +0 -25
- package/src/domain/tokens/spacing.tokens.ts +0 -65
- package/src/domain/tokens/typography.tokens.ts +0 -74
- package/src/infrastructure/error/ErrorBoundary.tsx +0 -258
- package/src/infrastructure/error/ErrorDisplay.tsx +0 -346
- package/src/infrastructure/error/SuspenseWrapper.tsx +0 -133
- package/src/infrastructure/error/index.ts +0 -5
- package/src/infrastructure/performance/index.ts +0 -92
- package/src/infrastructure/performance/useLazyLoading.ts +0 -342
- package/src/infrastructure/performance/useMemoryOptimization.ts +0 -293
- package/src/infrastructure/performance/usePerformanceMonitor.ts +0 -158
- package/src/infrastructure/security/index.ts +0 -10
- package/src/infrastructure/security/security-config.ts +0 -171
- package/src/infrastructure/security/useFormValidation.ts +0 -216
- package/src/infrastructure/security/validation.ts +0 -246
- package/src/presentation/hooks/useLanguage.ts +0 -109
- package/src/presentation/molecules/Breadcrumb.tsx +0 -54
- package/src/presentation/organisms/AlertDialog.tsx +0 -181
- package/src/presentation/organisms/Calendar.tsx +0 -79
- package/src/presentation/organisms/Comments.tsx +0 -103
- package/src/presentation/organisms/ConfirmDialog.tsx +0 -97
- package/src/presentation/organisms/FilterBar.tsx +0 -244
- package/src/presentation/organisms/FilterSidebar.tsx +0 -172
- package/src/presentation/organisms/Footer.tsx +0 -86
- package/src/presentation/organisms/FormModal.tsx +0 -126
- package/src/presentation/organisms/Grid.tsx +0 -128
- package/src/presentation/organisms/ImageLightbox.tsx +0 -252
- package/src/presentation/organisms/MainNavbar.tsx +0 -320
- package/src/presentation/organisms/Modal.tsx +0 -173
- package/src/presentation/organisms/Navbar.tsx +0 -75
- package/src/presentation/organisms/NewsletterSignup.tsx +0 -165
- package/src/presentation/organisms/QuickActionCard.tsx +0 -93
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* @description Display active filters with remove buttons
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { X } from 'lucide-react';
|
|
7
|
+
import { cn } from '../../infrastructure/utils';
|
|
6
8
|
import type { BaseProps } from '../../domain/types';
|
|
7
9
|
|
|
8
10
|
export interface ActiveFilterTagsProps extends BaseProps {
|
|
@@ -24,7 +26,7 @@ export const ActiveFilterTags = ({
|
|
|
24
26
|
className,
|
|
25
27
|
}: ActiveFilterTagsProps) => {
|
|
26
28
|
const categoryName = selectedCategory
|
|
27
|
-
? categories.find(c => c.id === selectedCategory)?.name
|
|
29
|
+
? categories.find((c) => c.id === selectedCategory)?.name
|
|
28
30
|
: null;
|
|
29
31
|
|
|
30
32
|
if (!selectedCategory && selectedTags.length === 0) {
|
|
@@ -32,49 +34,48 @@ export const ActiveFilterTags = ({
|
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
return (
|
|
35
|
-
<div
|
|
36
|
-
|
|
37
|
+
<div
|
|
38
|
+
role="region"
|
|
39
|
+
aria-label="Active filters"
|
|
40
|
+
className={cn('mb-4 hidden flex-wrap items-center gap-2 md:mb-6 lg:flex', className)}
|
|
41
|
+
>
|
|
42
|
+
<span className="text-sm text-muted-foreground">Active filters:</span>
|
|
37
43
|
|
|
38
|
-
{/* Category Tag */}
|
|
39
44
|
{categoryName && (
|
|
40
|
-
<span className="inline-flex items-center gap-1 px-3 py-1.5
|
|
45
|
+
<span className="inline-flex items-center gap-1 rounded-full bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground">
|
|
41
46
|
{categoryName}
|
|
42
47
|
<button
|
|
48
|
+
type="button"
|
|
43
49
|
onClick={onCategoryRemove}
|
|
44
|
-
className="ml-1 hover:opacity-70 transition-opacity"
|
|
45
50
|
aria-label={`Remove ${categoryName} filter`}
|
|
51
|
+
className="ml-1 inline-flex h-4 w-4 items-center justify-center rounded-full transition-opacity hover:opacity-70 focus:outline-none focus:ring-2 focus:ring-ring"
|
|
46
52
|
>
|
|
47
|
-
<
|
|
48
|
-
<path d="M18 6L6 18M6 6l12 12" />
|
|
49
|
-
</svg>
|
|
53
|
+
<X className="h-3 w-3" aria-hidden="true" />
|
|
50
54
|
</button>
|
|
51
55
|
</span>
|
|
52
56
|
)}
|
|
53
57
|
|
|
54
|
-
{/* Tag Buttons */}
|
|
55
58
|
{selectedTags.map((tag) => (
|
|
56
59
|
<span
|
|
57
60
|
key={tag}
|
|
58
|
-
className="inline-flex items-center gap-1
|
|
61
|
+
className="inline-flex items-center gap-1 rounded-full border border-border bg-secondary px-3 py-1.5 text-xs font-medium text-foreground"
|
|
59
62
|
>
|
|
60
63
|
{tag}
|
|
61
64
|
<button
|
|
65
|
+
type="button"
|
|
62
66
|
onClick={() => onTagRemove(tag)}
|
|
63
|
-
className="ml-1 hover:opacity-70 transition-opacity"
|
|
64
67
|
aria-label={`Remove ${tag} filter`}
|
|
68
|
+
className="ml-1 inline-flex h-4 w-4 items-center justify-center rounded-full transition-opacity hover:opacity-70 focus:outline-none focus:ring-2 focus:ring-ring"
|
|
65
69
|
>
|
|
66
|
-
<
|
|
67
|
-
<path d="M18 6L6 18M6 6l12 12" />
|
|
68
|
-
</svg>
|
|
70
|
+
<X className="h-3 w-3" aria-hidden="true" />
|
|
69
71
|
</button>
|
|
70
72
|
</span>
|
|
71
73
|
))}
|
|
72
74
|
|
|
73
|
-
{/* Clear All Button */}
|
|
74
75
|
<button
|
|
75
|
-
onClick={onClearAll}
|
|
76
|
-
className="px-3 py-1.5 text-primary-light hover:underline text-xs font-medium"
|
|
77
76
|
type="button"
|
|
77
|
+
onClick={onClearAll}
|
|
78
|
+
className="px-3 py-1.5 text-xs font-medium text-primary underline-offset-4 transition-colors hover:underline"
|
|
78
79
|
>
|
|
79
80
|
Clear all
|
|
80
81
|
</button>
|
|
@@ -59,7 +59,7 @@ const AvatarImage = forwardRef<HTMLImageElement, AvatarImageProps>(
|
|
|
59
59
|
|
|
60
60
|
AvatarImage.displayName = 'AvatarImage';
|
|
61
61
|
|
|
62
|
-
export
|
|
62
|
+
export type AvatarFallbackProps = HTMLAttributes<HTMLDivElement>;
|
|
63
63
|
|
|
64
64
|
const AvatarFallback = forwardRef<HTMLDivElement, AvatarFallbackProps>(
|
|
65
65
|
({ className, ...props }, ref) => {
|
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
import { useState, useEffect } from 'react';
|
|
7
7
|
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
|
8
8
|
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
|
9
|
+
import { ChevronDown, ChevronUp, Check, Copy } from 'lucide-react';
|
|
10
|
+
import { cn } from '../../infrastructure/utils';
|
|
11
|
+
import { TIMING } from '../../infrastructure/constants/timing.constants';
|
|
9
12
|
import type { BaseProps } from '../../domain/types';
|
|
10
13
|
|
|
11
14
|
export interface CodeBlockProps extends BaseProps {
|
|
@@ -13,106 +16,107 @@ export interface CodeBlockProps extends BaseProps {
|
|
|
13
16
|
language?: string;
|
|
14
17
|
filename?: string;
|
|
15
18
|
showLineNumbers?: boolean;
|
|
19
|
+
/** Lines threshold to enable collapse/expand. */
|
|
20
|
+
collapseThreshold?: number;
|
|
16
21
|
}
|
|
17
22
|
|
|
23
|
+
const DEFAULT_LANGUAGE = 'javascript';
|
|
24
|
+
const DEFAULT_COLLAPSE_THRESHOLD = 10;
|
|
25
|
+
const COPY_RESET_MS = 2000;
|
|
26
|
+
const COLLAPSED_MAX_HEIGHT = 'max-h-80';
|
|
27
|
+
|
|
18
28
|
export const CodeBlock = ({
|
|
19
29
|
children,
|
|
20
|
-
language =
|
|
30
|
+
language = DEFAULT_LANGUAGE,
|
|
21
31
|
filename,
|
|
22
32
|
showLineNumbers = false,
|
|
33
|
+
collapseThreshold = DEFAULT_COLLAPSE_THRESHOLD,
|
|
23
34
|
className,
|
|
24
35
|
}: CodeBlockProps) => {
|
|
25
36
|
const [copied, setCopied] = useState(false);
|
|
26
37
|
const [isExpanded, setIsExpanded] = useState(false);
|
|
27
38
|
|
|
28
|
-
// Cleanup copy timeout
|
|
29
39
|
useEffect(() => {
|
|
30
|
-
if (copied)
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
40
|
+
if (!copied) return;
|
|
41
|
+
const timeout = setTimeout(() => setCopied(false), COPY_RESET_MS);
|
|
42
|
+
return () => clearTimeout(timeout);
|
|
34
43
|
}, [copied]);
|
|
35
44
|
|
|
36
45
|
const handleCopy = async () => {
|
|
46
|
+
if (typeof navigator === 'undefined' || !navigator.clipboard) {
|
|
47
|
+
console.error('[CodeBlock] Clipboard API not available');
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
37
50
|
try {
|
|
38
51
|
await navigator.clipboard.writeText(children);
|
|
39
52
|
setCopied(true);
|
|
40
53
|
} catch (error) {
|
|
41
|
-
console.error('Failed to copy code:', error);
|
|
54
|
+
console.error('[CodeBlock] Failed to copy code:', error);
|
|
42
55
|
}
|
|
43
56
|
};
|
|
44
57
|
|
|
45
|
-
// Auto-detect language if not provided
|
|
46
|
-
const detectedLanguage = language || 'javascript';
|
|
47
|
-
|
|
48
|
-
// Check if code is long enough for collapse
|
|
49
58
|
const lineCount = children.split('\n').length;
|
|
50
|
-
const isLongCode = lineCount >
|
|
59
|
+
const isLongCode = lineCount > collapseThreshold;
|
|
51
60
|
|
|
52
61
|
return (
|
|
53
|
-
<div
|
|
54
|
-
{
|
|
55
|
-
|
|
62
|
+
<div
|
|
63
|
+
className={cn(
|
|
64
|
+
'my-6 overflow-hidden rounded-lg border border-border bg-card transition-colors',
|
|
65
|
+
className
|
|
66
|
+
)}
|
|
67
|
+
>
|
|
68
|
+
<div className="flex items-center justify-between border-b border-border bg-secondary px-4 py-2">
|
|
56
69
|
<div className="flex items-center gap-2">
|
|
57
70
|
{filename && (
|
|
58
|
-
<span className="text-xs font-medium text-
|
|
71
|
+
<span className="text-xs font-medium text-muted-foreground">{filename}</span>
|
|
59
72
|
)}
|
|
60
|
-
<span className="
|
|
61
|
-
{
|
|
73
|
+
<span className="rounded-md bg-muted px-2 py-1 text-xs font-medium text-foreground">
|
|
74
|
+
{language}
|
|
62
75
|
</span>
|
|
63
76
|
</div>
|
|
64
77
|
|
|
65
78
|
<div className="flex items-center gap-2">
|
|
66
79
|
{isLongCode && (
|
|
67
80
|
<button
|
|
68
|
-
onClick={() => setIsExpanded(!isExpanded)}
|
|
69
|
-
className="p-1 text-text-secondary hover:text-text-primary rounded transition-colors"
|
|
70
|
-
title={isExpanded ? 'Collapse' : 'Expand'}
|
|
71
81
|
type="button"
|
|
82
|
+
onClick={() => setIsExpanded((prev) => !prev)}
|
|
72
83
|
aria-label={isExpanded ? 'Collapse code' : 'Expand code'}
|
|
84
|
+
title={isExpanded ? 'Collapse' : 'Expand'}
|
|
85
|
+
className="rounded p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus:outline-none focus:ring-2 focus:ring-ring"
|
|
73
86
|
>
|
|
74
87
|
{isExpanded ? (
|
|
75
|
-
<
|
|
76
|
-
<polyline points="18 15 12 9 6 15" />
|
|
77
|
-
</svg>
|
|
88
|
+
<ChevronUp className="h-4 w-4" aria-hidden="true" />
|
|
78
89
|
) : (
|
|
79
|
-
<
|
|
80
|
-
<polyline points="6 9 12 15 18 9" />
|
|
81
|
-
</svg>
|
|
90
|
+
<ChevronDown className="h-4 w-4" aria-hidden="true" />
|
|
82
91
|
)}
|
|
83
92
|
</button>
|
|
84
93
|
)}
|
|
85
94
|
|
|
86
95
|
<button
|
|
87
|
-
onClick={handleCopy}
|
|
88
|
-
className={`flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-lg transition-all ${
|
|
89
|
-
copied
|
|
90
|
-
? 'bg-green-500 text-white'
|
|
91
|
-
: 'bg-bg-secondary text-text-primary hover:bg-bg-tertiary border border-border'
|
|
92
|
-
}`}
|
|
93
|
-
title={copied ? 'Copied!' : 'Copy code'}
|
|
94
96
|
type="button"
|
|
97
|
+
onClick={handleCopy}
|
|
95
98
|
aria-label={copied ? 'Code copied' : 'Copy code'}
|
|
99
|
+
title={copied ? 'Copied!' : 'Copy code'}
|
|
100
|
+
className={cn(
|
|
101
|
+
'flex items-center gap-1.5 rounded-lg border border-border px-3 py-1.5 text-xs font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-ring',
|
|
102
|
+
copied
|
|
103
|
+
? 'border-success bg-success text-success-foreground'
|
|
104
|
+
: 'border-border bg-secondary text-foreground hover:bg-muted'
|
|
105
|
+
)}
|
|
96
106
|
>
|
|
97
107
|
{copied ? (
|
|
98
|
-
<
|
|
99
|
-
<polyline points="20 6 9 17 4 12" />
|
|
100
|
-
</svg>
|
|
108
|
+
<Check className="h-3.5 w-3.5" aria-hidden="true" />
|
|
101
109
|
) : (
|
|
102
|
-
<
|
|
103
|
-
<rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
|
|
104
|
-
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
|
|
105
|
-
</svg>
|
|
110
|
+
<Copy className="h-3.5 w-3.5" aria-hidden="true" />
|
|
106
111
|
)}
|
|
107
112
|
<span className="hidden sm:inline">{copied ? 'Copied!' : 'Copy'}</span>
|
|
108
113
|
</button>
|
|
109
114
|
</div>
|
|
110
115
|
</div>
|
|
111
116
|
|
|
112
|
-
{
|
|
113
|
-
<div className={isLongCode && !isExpanded ? 'max-h-80 overflow-hidden' : ''}>
|
|
117
|
+
<div className={cn(isLongCode && !isExpanded && COLLAPSED_MAX_HEIGHT, 'overflow-hidden')}>
|
|
114
118
|
<SyntaxHighlighter
|
|
115
|
-
language={
|
|
119
|
+
language={language}
|
|
116
120
|
style={vscDarkPlus}
|
|
117
121
|
showLineNumbers={showLineNumbers}
|
|
118
122
|
customStyle={{
|
|
@@ -126,14 +130,13 @@ export const CodeBlock = ({
|
|
|
126
130
|
</SyntaxHighlighter>
|
|
127
131
|
</div>
|
|
128
132
|
|
|
129
|
-
{/* Expand hint for collapsed code */}
|
|
130
133
|
{isLongCode && !isExpanded && (
|
|
131
|
-
<div className="
|
|
134
|
+
<div className="border-t border-border bg-secondary px-4 py-2 text-center">
|
|
132
135
|
<button
|
|
133
|
-
onClick={() => setIsExpanded(true)}
|
|
134
|
-
className="text-sm text-primary-light hover:underline font-medium"
|
|
135
136
|
type="button"
|
|
137
|
+
onClick={() => setIsExpanded(true)}
|
|
136
138
|
aria-label={`Show all ${lineCount} lines`}
|
|
139
|
+
className="text-sm font-medium text-primary underline-offset-4 transition-colors hover:underline focus:outline-none focus:ring-2 focus:ring-ring"
|
|
137
140
|
>
|
|
138
141
|
Show all {lineCount} lines
|
|
139
142
|
</button>
|
|
@@ -1,27 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ListItem Component (Molecule)
|
|
3
3
|
* @description Reusable list item with icon, content, and actions
|
|
4
|
-
* Reduces boilerplate in list components throughout the app
|
|
5
4
|
*/
|
|
6
5
|
|
|
7
|
-
import { forwardRef, type ReactNode } from 'react';
|
|
6
|
+
import { forwardRef, type ReactNode, type MouseEvent } from 'react';
|
|
8
7
|
import { cn } from '../../infrastructure/utils';
|
|
9
8
|
import type { BaseProps } from '../../domain/types';
|
|
10
9
|
|
|
11
|
-
export interface ListItemProps extends BaseProps {
|
|
10
|
+
export interface ListItemProps extends Omit<BaseProps, 'id'> {
|
|
12
11
|
title: string;
|
|
13
12
|
description?: string;
|
|
14
13
|
icon?: ReactNode;
|
|
15
14
|
actions?: ReactNode;
|
|
16
15
|
leftContent?: ReactNode;
|
|
17
16
|
rightContent?: ReactNode;
|
|
18
|
-
onClick?: () => void;
|
|
17
|
+
onClick?: (event: MouseEvent<HTMLElement>) => void;
|
|
19
18
|
href?: string;
|
|
20
19
|
disabled?: boolean;
|
|
21
20
|
selected?: boolean;
|
|
22
21
|
size?: 'sm' | 'md' | 'lg';
|
|
23
22
|
variant?: 'default' | 'bordered' | 'ghost';
|
|
24
|
-
className?: string;
|
|
25
23
|
id?: string;
|
|
26
24
|
}
|
|
27
25
|
|
|
@@ -29,27 +27,27 @@ const sizeStyles = {
|
|
|
29
27
|
sm: 'p-3 gap-3',
|
|
30
28
|
md: 'p-4 gap-4',
|
|
31
29
|
lg: 'p-5 gap-5',
|
|
32
|
-
};
|
|
30
|
+
} as const;
|
|
33
31
|
|
|
34
32
|
const titleSizeStyles = {
|
|
35
33
|
sm: 'text-sm',
|
|
36
34
|
md: 'text-base',
|
|
37
35
|
lg: 'text-lg',
|
|
38
|
-
};
|
|
36
|
+
} as const;
|
|
39
37
|
|
|
40
38
|
const descriptionSizeStyles = {
|
|
41
39
|
sm: 'text-xs',
|
|
42
40
|
md: 'text-sm',
|
|
43
41
|
lg: 'text-base',
|
|
44
|
-
};
|
|
42
|
+
} as const;
|
|
45
43
|
|
|
46
44
|
const iconSizeStyles = {
|
|
47
45
|
sm: 'h-4 w-4',
|
|
48
46
|
md: 'h-5 w-5',
|
|
49
47
|
lg: 'h-6 w-6',
|
|
50
|
-
};
|
|
48
|
+
} as const;
|
|
51
49
|
|
|
52
|
-
export const ListItem = forwardRef<
|
|
50
|
+
export const ListItem = forwardRef<HTMLElement, ListItemProps>(
|
|
53
51
|
(
|
|
54
52
|
{
|
|
55
53
|
className,
|
|
@@ -70,46 +68,57 @@ export const ListItem = forwardRef<HTMLDivElement, ListItemProps>(
|
|
|
70
68
|
ref
|
|
71
69
|
) => {
|
|
72
70
|
const baseClasses = cn(
|
|
73
|
-
'flex items-center justify-between
|
|
71
|
+
'flex w-full items-center justify-between transition-all duration-200',
|
|
74
72
|
sizeStyles[size],
|
|
75
73
|
!disabled && onClick && 'cursor-pointer hover:bg-muted/50 active:scale-[0.98]',
|
|
76
|
-
selected && 'bg-muted/50
|
|
77
|
-
variant === 'bordered' && 'border border-border
|
|
74
|
+
selected && 'border-primary bg-muted/50',
|
|
75
|
+
variant === 'bordered' && 'rounded-lg border border-border',
|
|
78
76
|
variant === 'ghost' && 'hover:bg-muted/30',
|
|
79
|
-
disabled && '
|
|
77
|
+
disabled && 'cursor-not-allowed opacity-50',
|
|
80
78
|
className
|
|
81
79
|
);
|
|
82
80
|
|
|
83
81
|
const content = (
|
|
84
82
|
<>
|
|
85
|
-
{leftContent || icon
|
|
86
|
-
<div className="flex items-center gap-3
|
|
87
|
-
{icon
|
|
88
|
-
|
|
83
|
+
{(leftContent || icon) && (
|
|
84
|
+
<div className="flex shrink-0 items-center gap-3">
|
|
85
|
+
{icon ? (
|
|
86
|
+
<div className={cn(iconSizeStyles[size], 'text-muted-foreground')}>{icon}</div>
|
|
87
|
+
) : (
|
|
88
|
+
leftContent
|
|
89
|
+
)}
|
|
89
90
|
</div>
|
|
90
|
-
)
|
|
91
|
+
)}
|
|
91
92
|
|
|
92
|
-
<div className="
|
|
93
|
-
<p className={cn('font-medium text-foreground
|
|
93
|
+
<div className="min-w-0 flex-1">
|
|
94
|
+
<p className={cn('truncate font-medium text-foreground', titleSizeStyles[size])}>
|
|
95
|
+
{title}
|
|
96
|
+
</p>
|
|
94
97
|
{description && (
|
|
95
|
-
<p className={cn('text-muted-foreground
|
|
98
|
+
<p className={cn('truncate text-muted-foreground', descriptionSizeStyles[size])}>
|
|
99
|
+
{description}
|
|
100
|
+
</p>
|
|
96
101
|
)}
|
|
97
102
|
</div>
|
|
98
103
|
|
|
99
|
-
{rightContent || actions
|
|
100
|
-
<div className="flex items-center gap-2
|
|
104
|
+
{(rightContent || actions) && (
|
|
105
|
+
<div className="flex shrink-0 items-center gap-2">
|
|
101
106
|
{rightContent}
|
|
102
107
|
{actions}
|
|
103
108
|
</div>
|
|
104
|
-
)
|
|
109
|
+
)}
|
|
105
110
|
</>
|
|
106
111
|
);
|
|
107
112
|
|
|
108
|
-
// NOTE: Polymorphic component - renders as anchor when href is provided
|
|
109
|
-
// "ref as any" is necessary due to TypeScript limitations with polymorphic refs
|
|
110
113
|
if (href && !disabled) {
|
|
111
114
|
return (
|
|
112
|
-
<a
|
|
115
|
+
<a
|
|
116
|
+
ref={ref as React.Ref<HTMLAnchorElement>}
|
|
117
|
+
href={href}
|
|
118
|
+
className={baseClasses}
|
|
119
|
+
id={id}
|
|
120
|
+
onClick={onClick}
|
|
121
|
+
>
|
|
113
122
|
{content}
|
|
114
123
|
</a>
|
|
115
124
|
);
|
|
@@ -117,10 +126,12 @@ export const ListItem = forwardRef<HTMLDivElement, ListItemProps>(
|
|
|
117
126
|
|
|
118
127
|
return (
|
|
119
128
|
<div
|
|
120
|
-
ref={ref}
|
|
129
|
+
ref={ref as React.Ref<HTMLDivElement>}
|
|
121
130
|
className={baseClasses}
|
|
122
131
|
onClick={disabled ? undefined : onClick}
|
|
123
132
|
id={id}
|
|
133
|
+
role={onClick && !disabled ? 'button' : undefined}
|
|
134
|
+
tabIndex={onClick && !disabled ? 0 : undefined}
|
|
124
135
|
>
|
|
125
136
|
{content}
|
|
126
137
|
</div>
|
|
@@ -76,7 +76,7 @@ const SelectContent = React.forwardRef<
|
|
|
76
76
|
<SelectPrimitive.Content
|
|
77
77
|
ref={ref}
|
|
78
78
|
className={cn(
|
|
79
|
-
'relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
|
79
|
+
'relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border border-border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
|
80
80
|
position === 'popper' &&
|
|
81
81
|
'data-[side=bottom]:translate-y-1 data-[side=left]:translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:translate-y-1',
|
|
82
82
|
className
|
|
@@ -16,13 +16,13 @@ export interface ToggleProps extends ButtonHTMLAttributes<HTMLButtonElement>, Ba
|
|
|
16
16
|
const sizeStyles = {
|
|
17
17
|
sm: 'h-5 w-9',
|
|
18
18
|
md: 'h-6 w-11',
|
|
19
|
-
lg: 'h-7 w-
|
|
19
|
+
lg: 'h-7 w-14',
|
|
20
20
|
};
|
|
21
21
|
|
|
22
22
|
const thumbSizeStyles = {
|
|
23
|
-
sm: 'h-4 w-4 data-[state=checked:translate-x-4',
|
|
24
|
-
md: 'h-5 w-5 data-[state=checked:translate-x-5',
|
|
25
|
-
lg: 'h-6 w-6 data-[state=checked:translate-x-
|
|
23
|
+
sm: 'h-4 w-4 data-[state=checked]:translate-x-4',
|
|
24
|
+
md: 'h-5 w-5 data-[state=checked]:translate-x-5',
|
|
25
|
+
lg: 'h-6 w-6 data-[state=checked]:translate-x-7',
|
|
26
26
|
};
|
|
27
27
|
|
|
28
28
|
export const Toggle = forwardRef<HTMLButtonElement, ToggleProps>(
|
|
@@ -42,8 +42,5 @@ export type { ListItemProps } from './ListItem';
|
|
|
42
42
|
export { ActiveFilterTags } from './ActiveFilterTags';
|
|
43
43
|
export type { ActiveFilterTagsProps } from './ActiveFilterTags';
|
|
44
44
|
|
|
45
|
-
export { Breadcrumb } from './Breadcrumb';
|
|
46
|
-
export type { BreadcrumbProps, BreadcrumbItem } from './Breadcrumb';
|
|
47
|
-
|
|
48
45
|
export { CodeBlock } from './CodeBlock';
|
|
49
46
|
export type { CodeBlockProps } from './CodeBlock';
|
|
@@ -16,7 +16,7 @@ const Alert = React.forwardRef<HTMLDivElement, AlertProps>(
|
|
|
16
16
|
ref={ref}
|
|
17
17
|
role="alert"
|
|
18
18
|
className={cn(
|
|
19
|
-
'relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground',
|
|
19
|
+
'relative w-full rounded-lg border border-border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground',
|
|
20
20
|
variant === 'default' && 'bg-background text-foreground',
|
|
21
21
|
variant === 'destructive' && 'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive',
|
|
22
22
|
className
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { useState, useMemo } from 'react';
|
|
7
|
+
import { ArrowDown, ArrowUp, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from 'lucide-react';
|
|
7
8
|
import { cn } from '../../infrastructure/utils';
|
|
8
9
|
import {
|
|
9
10
|
Table,
|
|
@@ -16,7 +17,11 @@ import {
|
|
|
16
17
|
TableCaption,
|
|
17
18
|
} from './Table';
|
|
18
19
|
import { Button } from '../atoms/Button';
|
|
19
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
computeTotalPages,
|
|
22
|
+
clampPageNumber,
|
|
23
|
+
computePageBounds,
|
|
24
|
+
} from '../../infrastructure/utils/calculation.utils';
|
|
20
25
|
import type { BaseProps, ColorVariant } from '../../domain/types';
|
|
21
26
|
import type { MediumSizes } from '../../infrastructure/constants';
|
|
22
27
|
|
|
@@ -100,7 +105,9 @@ export function DataTable<T extends Record<string, unknown>>({
|
|
|
100
105
|
return sortedData.slice(start, end);
|
|
101
106
|
}, [sortedData, currentPage, paginated, pageSize]);
|
|
102
107
|
|
|
103
|
-
const totalPages =
|
|
108
|
+
const totalPages = computeTotalPages(data.length, pageSize);
|
|
109
|
+
const safePage = clampPageNumber(currentPage, totalPages);
|
|
110
|
+
const pageBounds = computePageBounds(currentPage, pageSize, data.length);
|
|
104
111
|
|
|
105
112
|
const renderCell = (row: T, column: Column<T>) => {
|
|
106
113
|
if (column.cell) {
|
|
@@ -154,7 +161,13 @@ export function DataTable<T extends Record<string, unknown>>({
|
|
|
154
161
|
<div className="flex items-center gap-2">
|
|
155
162
|
{column.header}
|
|
156
163
|
{column.sortable && sortable && sortColumn === column.id && (
|
|
157
|
-
<span className="text-xs">
|
|
164
|
+
<span className="text-xs">
|
|
165
|
+
{sortDirection === 'asc' ? (
|
|
166
|
+
<ArrowUp className="h-3 w-3" aria-hidden="true" />
|
|
167
|
+
) : (
|
|
168
|
+
<ArrowDown className="h-3 w-3" aria-hidden="true" />
|
|
169
|
+
)}
|
|
170
|
+
</span>
|
|
158
171
|
)}
|
|
159
172
|
</div>
|
|
160
173
|
</TableHead>
|
|
@@ -185,33 +198,33 @@ export function DataTable<T extends Record<string, unknown>>({
|
|
|
185
198
|
<TableCell colSpan={columns.length}>
|
|
186
199
|
<div className="flex items-center justify-between">
|
|
187
200
|
<p className={cn('text-muted-foreground', sizeStyles[size])}>
|
|
188
|
-
Showing {
|
|
201
|
+
Showing {pageBounds.startIndex} to {pageBounds.endIndex} of {data.length} results
|
|
189
202
|
</p>
|
|
190
203
|
<div className="flex items-center gap-2">
|
|
191
204
|
<Button
|
|
192
205
|
size="sm"
|
|
193
206
|
variant="outline"
|
|
194
207
|
onClick={() => setCurrentPage(1)}
|
|
195
|
-
disabled={
|
|
208
|
+
disabled={safePage === 1}
|
|
196
209
|
>
|
|
197
210
|
<ChevronsLeft className="h-4 w-4" />
|
|
198
211
|
</Button>
|
|
199
212
|
<Button
|
|
200
213
|
size="sm"
|
|
201
214
|
variant="outline"
|
|
202
|
-
onClick={() => setCurrentPage((p) =>
|
|
203
|
-
disabled={
|
|
215
|
+
onClick={() => setCurrentPage((p) => clampPageNumber(p - 1, totalPages))}
|
|
216
|
+
disabled={safePage === 1}
|
|
204
217
|
>
|
|
205
218
|
<ChevronLeft className="h-4 w-4" />
|
|
206
219
|
</Button>
|
|
207
220
|
<span className={cn('text-sm font-medium', sizeStyles[size])}>
|
|
208
|
-
Page {
|
|
221
|
+
Page {safePage} of {totalPages}
|
|
209
222
|
</span>
|
|
210
223
|
<Button
|
|
211
224
|
size="sm"
|
|
212
225
|
variant="outline"
|
|
213
|
-
onClick={() => setCurrentPage((p) =>
|
|
214
|
-
disabled={
|
|
226
|
+
onClick={() => setCurrentPage((p) => clampPageNumber(p + 1, totalPages))}
|
|
227
|
+
disabled={safePage === totalPages}
|
|
215
228
|
>
|
|
216
229
|
<ChevronRight className="h-4 w-4" />
|
|
217
230
|
</Button>
|
|
@@ -219,7 +232,7 @@ export function DataTable<T extends Record<string, unknown>>({
|
|
|
219
232
|
size="sm"
|
|
220
233
|
variant="outline"
|
|
221
234
|
onClick={() => setCurrentPage(totalPages)}
|
|
222
|
-
disabled={
|
|
235
|
+
disabled={safePage === totalPages}
|
|
223
236
|
>
|
|
224
237
|
<ChevronsRight className="h-4 w-4" />
|
|
225
238
|
</Button>
|
|
@@ -40,7 +40,7 @@ const DialogContent = React.forwardRef<
|
|
|
40
40
|
<DialogPrimitive.Content
|
|
41
41
|
ref={ref}
|
|
42
42
|
className={cn(
|
|
43
|
-
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
|
|
43
|
+
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border border-border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
|
|
44
44
|
className
|
|
45
45
|
)}
|
|
46
46
|
{...props}
|
|
@@ -49,7 +49,7 @@ const DropdownMenuSubContent = React.forwardRef<
|
|
|
49
49
|
<DropdownMenuPrimitive.SubContent
|
|
50
50
|
ref={ref}
|
|
51
51
|
className={cn(
|
|
52
|
-
'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
|
52
|
+
'z-50 min-w-[8rem] overflow-hidden rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
|
53
53
|
className
|
|
54
54
|
)}
|
|
55
55
|
{...props}
|
|
@@ -67,7 +67,7 @@ const DropdownMenuContent = React.forwardRef<
|
|
|
67
67
|
collisionPadding={collisionPadding}
|
|
68
68
|
sideOffset={sideOffset}
|
|
69
69
|
className={cn(
|
|
70
|
-
'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md',
|
|
70
|
+
'z-50 min-w-[8rem] overflow-hidden rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md',
|
|
71
71
|
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
|
72
72
|
className
|
|
73
73
|
)}
|
|
@@ -20,7 +20,7 @@ const HoverCardContent = React.forwardRef<
|
|
|
20
20
|
align={align}
|
|
21
21
|
sideOffset={sideOffset}
|
|
22
22
|
className={cn(
|
|
23
|
-
'z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
|
23
|
+
'z-50 w-64 rounded-md border border-border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
|
24
24
|
className
|
|
25
25
|
)}
|
|
26
26
|
{...props}
|
|
@@ -46,11 +46,7 @@ export const LoadingState = forwardRef<HTMLDivElement, LoadingStateProps>(
|
|
|
46
46
|
{[0, 1, 2].map((i) => (
|
|
47
47
|
<div
|
|
48
48
|
key={i}
|
|
49
|
-
className=
|
|
50
|
-
'w-2 h-2 bg-primary rounded-full animate-bounce',
|
|
51
|
-
'w-1.5 h-1.5 sm:w-2 sm:h-2',
|
|
52
|
-
'lg:w-2.5 h-2.5'
|
|
53
|
-
)}
|
|
49
|
+
className="h-1.5 w-1.5 animate-bounce rounded-full bg-primary sm:h-2 sm:w-2 lg:h-2.5 lg:w-2.5"
|
|
54
50
|
style={{ animationDelay: `${i * 0.15}s` }}
|
|
55
51
|
/>
|
|
56
52
|
))}
|
|
@@ -21,7 +21,7 @@ const PopoverContent = React.forwardRef<
|
|
|
21
21
|
align={align}
|
|
22
22
|
sideOffset={sideOffset}
|
|
23
23
|
className={cn(
|
|
24
|
-
'z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
|
24
|
+
'z-50 w-72 rounded-md border border-border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
|
25
25
|
className
|
|
26
26
|
)}
|
|
27
27
|
{...props}
|