@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.
Files changed (84) hide show
  1. package/README.md +137 -469
  2. package/dist/styles.css +2 -0
  3. package/package.json +31 -31
  4. package/src/__tests__/cn.test.ts +24 -0
  5. package/src/__tests__/components.smoke.test.tsx +78 -0
  6. package/src/__tests__/styles.contract.test.ts +60 -0
  7. package/src/domain/tokens/index.ts +8 -49
  8. package/src/index.ts +8 -10
  9. package/src/infrastructure/constants/component.constants.ts +3 -17
  10. package/src/infrastructure/constants/index.ts +4 -2
  11. package/src/infrastructure/constants/timing.constants.ts +46 -0
  12. package/src/infrastructure/utils/calculation.utils.ts +74 -0
  13. package/src/infrastructure/utils/cn.util.ts +1 -1
  14. package/src/infrastructure/utils/index.ts +12 -0
  15. package/src/presentation/atoms/IconButton.tsx +55 -0
  16. package/src/presentation/atoms/Progress.tsx +2 -1
  17. package/src/presentation/atoms/Spinner.tsx +1 -1
  18. package/src/presentation/atoms/Switch.tsx +2 -2
  19. package/src/presentation/atoms/Text.tsx +15 -13
  20. package/src/presentation/atoms/Tooltip.tsx +2 -1
  21. package/src/presentation/atoms/index.ts +3 -0
  22. package/src/presentation/hooks/index.ts +0 -3
  23. package/src/presentation/hooks/useClickOutside.ts +16 -10
  24. package/src/presentation/hooks/useClipboard.ts +30 -12
  25. package/src/presentation/hooks/useDebounce.ts +9 -11
  26. package/src/presentation/hooks/useLocalStorage.ts +32 -20
  27. package/src/presentation/hooks/useMediaQuery.ts +20 -15
  28. package/src/presentation/hooks/useScrollLock.ts +40 -12
  29. package/src/presentation/hooks/useToggle.ts +8 -6
  30. package/src/presentation/molecules/ActiveFilterTags.tsx +19 -18
  31. package/src/presentation/molecules/Avatar.tsx +1 -1
  32. package/src/presentation/molecules/CodeBlock.tsx +50 -48
  33. package/src/presentation/molecules/InputGroup.tsx +1 -1
  34. package/src/presentation/molecules/ListItem.tsx +40 -29
  35. package/src/presentation/molecules/Select.tsx +1 -1
  36. package/src/presentation/molecules/Toggle.tsx +4 -4
  37. package/src/presentation/molecules/index.ts +0 -3
  38. package/src/presentation/organisms/Alert.tsx +1 -1
  39. package/src/presentation/organisms/DataTable.tsx +26 -15
  40. package/src/presentation/organisms/Dialog.tsx +1 -1
  41. package/src/presentation/organisms/DropdownMenu.tsx +2 -2
  42. package/src/presentation/organisms/HoverCard.tsx +1 -1
  43. package/src/presentation/organisms/LoadingState.tsx +1 -5
  44. package/src/presentation/organisms/Popover.tsx +1 -1
  45. package/src/presentation/organisms/StatCard.tsx +2 -2
  46. package/src/presentation/organisms/Table.tsx +2 -2
  47. package/src/presentation/organisms/index.ts +0 -60
  48. package/src/styles/compile.css +18 -0
  49. package/src/styles/theme.css +131 -0
  50. package/src/domain/tokens/animation.tokens.ts +0 -87
  51. package/src/domain/tokens/color.tokens.ts +0 -95
  52. package/src/domain/tokens/radius.tokens.ts +0 -27
  53. package/src/domain/tokens/shadow.tokens.ts +0 -25
  54. package/src/domain/tokens/spacing.tokens.ts +0 -65
  55. package/src/domain/tokens/typography.tokens.ts +0 -74
  56. package/src/infrastructure/error/ErrorBoundary.tsx +0 -258
  57. package/src/infrastructure/error/ErrorDisplay.tsx +0 -346
  58. package/src/infrastructure/error/SuspenseWrapper.tsx +0 -133
  59. package/src/infrastructure/error/index.ts +0 -5
  60. package/src/infrastructure/performance/index.ts +0 -92
  61. package/src/infrastructure/performance/useLazyLoading.ts +0 -342
  62. package/src/infrastructure/performance/useMemoryOptimization.ts +0 -293
  63. package/src/infrastructure/performance/usePerformanceMonitor.ts +0 -158
  64. package/src/infrastructure/security/index.ts +0 -10
  65. package/src/infrastructure/security/security-config.ts +0 -171
  66. package/src/infrastructure/security/useFormValidation.ts +0 -216
  67. package/src/infrastructure/security/validation.ts +0 -246
  68. package/src/presentation/hooks/useLanguage.ts +0 -109
  69. package/src/presentation/molecules/Breadcrumb.tsx +0 -54
  70. package/src/presentation/organisms/AlertDialog.tsx +0 -181
  71. package/src/presentation/organisms/Calendar.tsx +0 -79
  72. package/src/presentation/organisms/Comments.tsx +0 -103
  73. package/src/presentation/organisms/ConfirmDialog.tsx +0 -97
  74. package/src/presentation/organisms/FilterBar.tsx +0 -244
  75. package/src/presentation/organisms/FilterSidebar.tsx +0 -172
  76. package/src/presentation/organisms/Footer.tsx +0 -86
  77. package/src/presentation/organisms/FormModal.tsx +0 -126
  78. package/src/presentation/organisms/Grid.tsx +0 -128
  79. package/src/presentation/organisms/ImageLightbox.tsx +0 -252
  80. package/src/presentation/organisms/MainNavbar.tsx +0 -320
  81. package/src/presentation/organisms/Modal.tsx +0 -173
  82. package/src/presentation/organisms/Navbar.tsx +0 -75
  83. package/src/presentation/organisms/NewsletterSignup.tsx +0 -165
  84. 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
- export function useScrollLock(enabled: boolean = true) {
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
- const originalStyle = window.getComputedStyle(document.body).overflow;
13
- const originalPaddingRight = window.getComputedStyle(document.body).paddingRight;
34
+ ensureScrollbarGutterStyle();
35
+ const html = document.documentElement;
36
+ const body = document.body;
14
37
 
15
- // FIX: Ensure scrollbar width is never negative
16
- const scrollbarWidth = Math.max(0, window.innerWidth - document.documentElement.clientWidth);
38
+ const previousOverflow = body.style.overflow;
39
+ const previousPaddingRight = body.style.paddingRight;
40
+ const scrollbarWidth = measureScrollbarWidth();
17
41
 
18
- document.body.style.overflow = 'hidden';
19
- document.body.style.paddingRight = `${scrollbarWidth}px`;
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
- document.body.style.overflow = originalStyle;
23
- document.body.style.paddingRight = originalPaddingRight;
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 function useToggle(initialValue: boolean = false): [boolean, () => void, (value: boolean) => void] {
9
- const [value, setValue] = useState(initialValue);
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((v) => !v);
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 className={`hidden lg:flex flex-wrap items-center gap-2 mb-4 md:mb-6 ${className || ''}`}>
36
- <span className="text-sm text-text-secondary">Active filters:</span>
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 bg-primary-light text-text-primary rounded-full text-xs font-medium">
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
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
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 px-3 py-1.5 bg-bg-secondary text-text-primary rounded-full text-xs font-medium border border-border"
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
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
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 interface AvatarFallbackProps extends HTMLAttributes<HTMLDivElement> {}
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 = 'javascript',
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
- const timeout = setTimeout(() => setCopied(false), 2000);
32
- return () => clearTimeout(timeout);
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 > 10;
58
+ const isLongCode = lineCount > collapseThreshold;
51
59
 
52
60
  return (
53
- <div className={`my-6 rounded-lg border border-border overflow-hidden bg-bg-card transition-theme ${className || ''}`}>
54
- {/* Header */}
55
- <div className="flex items-center justify-between px-4 py-2 bg-bg-tertiary border-b border-border">
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-text-secondary">{filename}</span>
70
+ <span className="text-xs font-medium text-muted-foreground">{filename}</span>
59
71
  )}
60
- <span className="text-xs px-2 py-1 bg-bg-primary text-text-primary rounded-md font-medium">
61
- {detectedLanguage}
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
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
76
- <polyline points="18 15 12 9 6 15" />
77
- </svg>
87
+ <ChevronUp className="h-4 w-4" aria-hidden="true" />
78
88
  ) : (
79
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
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
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
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
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
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
- {/* Code */}
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={detectedLanguage}
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="px-4 py-2 bg-bg-secondary text-center border-t border-border">
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>
@@ -18,7 +18,7 @@ export const InputGroup = forwardRef<HTMLDivElement, InputGroupProps>(
18
18
  return (
19
19
  <div
20
20
  ref={ref}
21
- className="relative"
21
+ className={cn('relative', className)}
22
22
  {...props}
23
23
  >
24
24
  {leftElement && (
@@ -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<HTMLDivElement, ListItemProps>(
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 w-full transition-all duration-200',
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 border-primary',
77
- variant === 'bordered' && 'border border-border rounded-lg',
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 && 'opacity-50 cursor-not-allowed',
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 flex-shrink-0">
87
- {icon && <div className={cn(iconSizeStyles[size], 'text-muted-foreground')}>{icon}</div>}
88
- {!icon && leftContent}
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
- ) : null}
91
+ )}
91
92
 
92
- <div className="flex-1 min-w-0">
93
- <p className={cn('font-medium text-foreground truncate', titleSizeStyles[size])}>{title}</p>
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 truncate', descriptionSizeStyles[size])}>{description}</p>
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 flex-shrink-0">
104
+ {(rightContent || actions) && (
105
+ <div className="flex shrink-0 items-center gap-2">
101
106
  {rightContent}
102
107
  {actions}
103
108
  </div>
104
- ) : null}
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 href={href} className={baseClasses} id={id} ref={ref as any}>
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-13',
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-6',
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