@umituz/web-design-system 3.1.22 → 4.0.2
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 +31 -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/cn.util.ts +1 -1
- 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 +16 -10
- package/src/presentation/hooks/useClipboard.ts +30 -12
- package/src/presentation/hooks/useDebounce.ts +9 -11
- package/src/presentation/hooks/useLocalStorage.ts +32 -20
- package/src/presentation/hooks/useMediaQuery.ts +20 -15
- 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 +50 -48
- package/src/presentation/molecules/InputGroup.tsx +1 -1
- 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 +26 -15
- 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
|
@@ -1,26 +1,54 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* useScrollLock Hook
|
|
3
|
-
* @description Lock/unlock body scroll
|
|
3
|
+
* @description Lock/unlock body scroll while preserving scrollbar gutter
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { useEffect } from 'react';
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
/** Width of the vertical scrollbar, so locking scroll doesn't shift layout. */
|
|
9
|
+
const measureScrollbarWidth = (): number => {
|
|
10
|
+
if (typeof window === 'undefined' || typeof document === 'undefined') return 0;
|
|
11
|
+
return Math.max(0, window.innerWidth - document.documentElement.clientWidth);
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const SCROLLBAR_GUTTER_STYLE_ID = 'use-scroll-lock-gutter-style';
|
|
15
|
+
|
|
16
|
+
const ensureScrollbarGutterStyle = () => {
|
|
17
|
+
if (typeof document === 'undefined') return;
|
|
18
|
+
if (document.getElementById(SCROLLBAR_GUTTER_STYLE_ID)) return;
|
|
19
|
+
|
|
20
|
+
const style = document.createElement('style');
|
|
21
|
+
style.id = SCROLLBAR_GUTTER_STYLE_ID;
|
|
22
|
+
style.textContent = `
|
|
23
|
+
html.use-scroll-lock-active body {
|
|
24
|
+
overflow: hidden;
|
|
25
|
+
}
|
|
26
|
+
`;
|
|
27
|
+
document.head.appendChild(style);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const useScrollLock = (enabled: boolean = true): void => {
|
|
9
31
|
useEffect(() => {
|
|
10
|
-
if (!enabled) return;
|
|
32
|
+
if (!enabled || typeof document === 'undefined') return;
|
|
11
33
|
|
|
12
|
-
|
|
13
|
-
const
|
|
34
|
+
ensureScrollbarGutterStyle();
|
|
35
|
+
const html = document.documentElement;
|
|
36
|
+
const body = document.body;
|
|
14
37
|
|
|
15
|
-
|
|
16
|
-
const
|
|
38
|
+
const previousOverflow = body.style.overflow;
|
|
39
|
+
const previousPaddingRight = body.style.paddingRight;
|
|
40
|
+
const scrollbarWidth = measureScrollbarWidth();
|
|
17
41
|
|
|
18
|
-
|
|
19
|
-
|
|
42
|
+
body.style.overflow = 'hidden';
|
|
43
|
+
if (scrollbarWidth > 0) {
|
|
44
|
+
body.style.paddingRight = `${scrollbarWidth}px`;
|
|
45
|
+
}
|
|
46
|
+
html.classList.add('use-scroll-lock-active');
|
|
20
47
|
|
|
21
48
|
return () => {
|
|
22
|
-
|
|
23
|
-
|
|
49
|
+
body.style.overflow = previousOverflow;
|
|
50
|
+
body.style.paddingRight = previousPaddingRight;
|
|
51
|
+
html.classList.remove('use-scroll-lock-active');
|
|
24
52
|
};
|
|
25
53
|
}, [enabled]);
|
|
26
|
-
}
|
|
54
|
+
};
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* useToggle Hook
|
|
3
|
-
* @description Toggle boolean state
|
|
3
|
+
* @description Toggle boolean state with explicit setValue
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { useCallback, useState } from 'react';
|
|
6
|
+
import { useCallback, useState, useMemo } from 'react';
|
|
7
7
|
|
|
8
|
-
export
|
|
9
|
-
|
|
8
|
+
export type UseToggleReturn = [boolean, () => void, (value: boolean) => void];
|
|
9
|
+
|
|
10
|
+
export function useToggle(initialValue: boolean = false): UseToggleReturn {
|
|
11
|
+
const [value, setValue] = useState<boolean>(initialValue);
|
|
10
12
|
|
|
11
13
|
const toggle = useCallback(() => {
|
|
12
|
-
setValue((
|
|
14
|
+
setValue((prev) => !prev);
|
|
13
15
|
}, []);
|
|
14
16
|
|
|
15
|
-
return [value, toggle, setValue];
|
|
17
|
+
return useMemo<UseToggleReturn>(() => [value, toggle, setValue], [value, toggle]);
|
|
16
18
|
}
|
|
@@ -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,8 @@
|
|
|
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';
|
|
9
11
|
import type { BaseProps } from '../../domain/types';
|
|
10
12
|
|
|
11
13
|
export interface CodeBlockProps extends BaseProps {
|
|
@@ -13,106 +15,107 @@ export interface CodeBlockProps extends BaseProps {
|
|
|
13
15
|
language?: string;
|
|
14
16
|
filename?: string;
|
|
15
17
|
showLineNumbers?: boolean;
|
|
18
|
+
/** Lines threshold to enable collapse/expand. */
|
|
19
|
+
collapseThreshold?: number;
|
|
16
20
|
}
|
|
17
21
|
|
|
22
|
+
const DEFAULT_LANGUAGE = 'javascript';
|
|
23
|
+
const DEFAULT_COLLAPSE_THRESHOLD = 10;
|
|
24
|
+
const COPY_RESET_MS = 2000;
|
|
25
|
+
const COLLAPSED_MAX_HEIGHT = 'max-h-80';
|
|
26
|
+
|
|
18
27
|
export const CodeBlock = ({
|
|
19
28
|
children,
|
|
20
|
-
language =
|
|
29
|
+
language = DEFAULT_LANGUAGE,
|
|
21
30
|
filename,
|
|
22
31
|
showLineNumbers = false,
|
|
32
|
+
collapseThreshold = DEFAULT_COLLAPSE_THRESHOLD,
|
|
23
33
|
className,
|
|
24
34
|
}: CodeBlockProps) => {
|
|
25
35
|
const [copied, setCopied] = useState(false);
|
|
26
36
|
const [isExpanded, setIsExpanded] = useState(false);
|
|
27
37
|
|
|
28
|
-
// Cleanup copy timeout
|
|
29
38
|
useEffect(() => {
|
|
30
|
-
if (copied)
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
39
|
+
if (!copied) return;
|
|
40
|
+
const timeout = setTimeout(() => setCopied(false), COPY_RESET_MS);
|
|
41
|
+
return () => clearTimeout(timeout);
|
|
34
42
|
}, [copied]);
|
|
35
43
|
|
|
36
44
|
const handleCopy = async () => {
|
|
45
|
+
if (typeof navigator === 'undefined' || !navigator.clipboard) {
|
|
46
|
+
console.error('[CodeBlock] Clipboard API not available');
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
37
49
|
try {
|
|
38
50
|
await navigator.clipboard.writeText(children);
|
|
39
51
|
setCopied(true);
|
|
40
52
|
} catch (error) {
|
|
41
|
-
console.error('Failed to copy code:', error);
|
|
53
|
+
console.error('[CodeBlock] Failed to copy code:', error);
|
|
42
54
|
}
|
|
43
55
|
};
|
|
44
56
|
|
|
45
|
-
// Auto-detect language if not provided
|
|
46
|
-
const detectedLanguage = language || 'javascript';
|
|
47
|
-
|
|
48
|
-
// Check if code is long enough for collapse
|
|
49
57
|
const lineCount = children.split('\n').length;
|
|
50
|
-
const isLongCode = lineCount >
|
|
58
|
+
const isLongCode = lineCount > collapseThreshold;
|
|
51
59
|
|
|
52
60
|
return (
|
|
53
|
-
<div
|
|
54
|
-
{
|
|
55
|
-
|
|
61
|
+
<div
|
|
62
|
+
className={cn(
|
|
63
|
+
'my-6 overflow-hidden rounded-lg border border-border bg-card transition-colors',
|
|
64
|
+
className
|
|
65
|
+
)}
|
|
66
|
+
>
|
|
67
|
+
<div className="flex items-center justify-between border-b border-border bg-secondary px-4 py-2">
|
|
56
68
|
<div className="flex items-center gap-2">
|
|
57
69
|
{filename && (
|
|
58
|
-
<span className="text-xs font-medium text-
|
|
70
|
+
<span className="text-xs font-medium text-muted-foreground">{filename}</span>
|
|
59
71
|
)}
|
|
60
|
-
<span className="
|
|
61
|
-
{
|
|
72
|
+
<span className="rounded-md bg-muted px-2 py-1 text-xs font-medium text-foreground">
|
|
73
|
+
{language}
|
|
62
74
|
</span>
|
|
63
75
|
</div>
|
|
64
76
|
|
|
65
77
|
<div className="flex items-center gap-2">
|
|
66
78
|
{isLongCode && (
|
|
67
79
|
<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
80
|
type="button"
|
|
81
|
+
onClick={() => setIsExpanded((prev) => !prev)}
|
|
72
82
|
aria-label={isExpanded ? 'Collapse code' : 'Expand code'}
|
|
83
|
+
title={isExpanded ? 'Collapse' : 'Expand'}
|
|
84
|
+
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
85
|
>
|
|
74
86
|
{isExpanded ? (
|
|
75
|
-
<
|
|
76
|
-
<polyline points="18 15 12 9 6 15" />
|
|
77
|
-
</svg>
|
|
87
|
+
<ChevronUp className="h-4 w-4" aria-hidden="true" />
|
|
78
88
|
) : (
|
|
79
|
-
<
|
|
80
|
-
<polyline points="6 9 12 15 18 9" />
|
|
81
|
-
</svg>
|
|
89
|
+
<ChevronDown className="h-4 w-4" aria-hidden="true" />
|
|
82
90
|
)}
|
|
83
91
|
</button>
|
|
84
92
|
)}
|
|
85
93
|
|
|
86
94
|
<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
95
|
type="button"
|
|
96
|
+
onClick={handleCopy}
|
|
95
97
|
aria-label={copied ? 'Code copied' : 'Copy code'}
|
|
98
|
+
title={copied ? 'Copied!' : 'Copy code'}
|
|
99
|
+
className={cn(
|
|
100
|
+
'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',
|
|
101
|
+
copied
|
|
102
|
+
? 'border-success bg-success text-success-foreground'
|
|
103
|
+
: 'border-border bg-secondary text-foreground hover:bg-muted'
|
|
104
|
+
)}
|
|
96
105
|
>
|
|
97
106
|
{copied ? (
|
|
98
|
-
<
|
|
99
|
-
<polyline points="20 6 9 17 4 12" />
|
|
100
|
-
</svg>
|
|
107
|
+
<Check className="h-3.5 w-3.5" aria-hidden="true" />
|
|
101
108
|
) : (
|
|
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>
|
|
109
|
+
<Copy className="h-3.5 w-3.5" aria-hidden="true" />
|
|
106
110
|
)}
|
|
107
111
|
<span className="hidden sm:inline">{copied ? 'Copied!' : 'Copy'}</span>
|
|
108
112
|
</button>
|
|
109
113
|
</div>
|
|
110
114
|
</div>
|
|
111
115
|
|
|
112
|
-
{
|
|
113
|
-
<div className={isLongCode && !isExpanded ? 'max-h-80 overflow-hidden' : ''}>
|
|
116
|
+
<div className={cn(isLongCode && !isExpanded && COLLAPSED_MAX_HEIGHT, 'overflow-hidden')}>
|
|
114
117
|
<SyntaxHighlighter
|
|
115
|
-
language={
|
|
118
|
+
language={language}
|
|
116
119
|
style={vscDarkPlus}
|
|
117
120
|
showLineNumbers={showLineNumbers}
|
|
118
121
|
customStyle={{
|
|
@@ -126,14 +129,13 @@ export const CodeBlock = ({
|
|
|
126
129
|
</SyntaxHighlighter>
|
|
127
130
|
</div>
|
|
128
131
|
|
|
129
|
-
{/* Expand hint for collapsed code */}
|
|
130
132
|
{isLongCode && !isExpanded && (
|
|
131
|
-
<div className="
|
|
133
|
+
<div className="border-t border-border bg-secondary px-4 py-2 text-center">
|
|
132
134
|
<button
|
|
133
|
-
onClick={() => setIsExpanded(true)}
|
|
134
|
-
className="text-sm text-primary-light hover:underline font-medium"
|
|
135
135
|
type="button"
|
|
136
|
+
onClick={() => setIsExpanded(true)}
|
|
136
137
|
aria-label={`Show all ${lineCount} lines`}
|
|
138
|
+
className="text-sm font-medium text-primary underline-offset-4 transition-colors hover:underline focus:outline-none focus:ring-2 focus:ring-ring"
|
|
137
139
|
>
|
|
138
140
|
Show all {lineCount} lines
|
|
139
141
|
</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
|