@umituz/web-design-system 3.1.22 → 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.
Files changed (82) hide show
  1. package/README.md +137 -469
  2. package/dist/styles.css +2 -0
  3. package/package.json +30 -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/index.ts +12 -0
  14. package/src/presentation/atoms/IconButton.tsx +55 -0
  15. package/src/presentation/atoms/Progress.tsx +2 -1
  16. package/src/presentation/atoms/Spinner.tsx +1 -1
  17. package/src/presentation/atoms/Switch.tsx +2 -2
  18. package/src/presentation/atoms/Text.tsx +15 -13
  19. package/src/presentation/atoms/Tooltip.tsx +2 -1
  20. package/src/presentation/atoms/index.ts +3 -0
  21. package/src/presentation/hooks/index.ts +0 -3
  22. package/src/presentation/hooks/useClickOutside.ts +14 -10
  23. package/src/presentation/hooks/useClipboard.ts +30 -12
  24. package/src/presentation/hooks/useDebounce.ts +8 -10
  25. package/src/presentation/hooks/useLocalStorage.ts +28 -18
  26. package/src/presentation/hooks/useMediaQuery.ts +2 -1
  27. package/src/presentation/hooks/useScrollLock.ts +40 -12
  28. package/src/presentation/hooks/useToggle.ts +8 -6
  29. package/src/presentation/molecules/ActiveFilterTags.tsx +19 -18
  30. package/src/presentation/molecules/Avatar.tsx +1 -1
  31. package/src/presentation/molecules/CodeBlock.tsx +51 -48
  32. package/src/presentation/molecules/ListItem.tsx +40 -29
  33. package/src/presentation/molecules/Select.tsx +1 -1
  34. package/src/presentation/molecules/Toggle.tsx +4 -4
  35. package/src/presentation/molecules/index.ts +0 -3
  36. package/src/presentation/organisms/Alert.tsx +1 -1
  37. package/src/presentation/organisms/DataTable.tsx +24 -11
  38. package/src/presentation/organisms/Dialog.tsx +1 -1
  39. package/src/presentation/organisms/DropdownMenu.tsx +2 -2
  40. package/src/presentation/organisms/HoverCard.tsx +1 -1
  41. package/src/presentation/organisms/LoadingState.tsx +1 -5
  42. package/src/presentation/organisms/Popover.tsx +1 -1
  43. package/src/presentation/organisms/StatCard.tsx +2 -2
  44. package/src/presentation/organisms/Table.tsx +2 -2
  45. package/src/presentation/organisms/index.ts +0 -60
  46. package/src/styles/compile.css +18 -0
  47. package/src/styles/theme.css +131 -0
  48. package/src/domain/tokens/animation.tokens.ts +0 -87
  49. package/src/domain/tokens/color.tokens.ts +0 -95
  50. package/src/domain/tokens/radius.tokens.ts +0 -27
  51. package/src/domain/tokens/shadow.tokens.ts +0 -25
  52. package/src/domain/tokens/spacing.tokens.ts +0 -65
  53. package/src/domain/tokens/typography.tokens.ts +0 -74
  54. package/src/infrastructure/error/ErrorBoundary.tsx +0 -258
  55. package/src/infrastructure/error/ErrorDisplay.tsx +0 -346
  56. package/src/infrastructure/error/SuspenseWrapper.tsx +0 -133
  57. package/src/infrastructure/error/index.ts +0 -5
  58. package/src/infrastructure/performance/index.ts +0 -92
  59. package/src/infrastructure/performance/useLazyLoading.ts +0 -342
  60. package/src/infrastructure/performance/useMemoryOptimization.ts +0 -293
  61. package/src/infrastructure/performance/usePerformanceMonitor.ts +0 -158
  62. package/src/infrastructure/security/index.ts +0 -10
  63. package/src/infrastructure/security/security-config.ts +0 -171
  64. package/src/infrastructure/security/useFormValidation.ts +0 -216
  65. package/src/infrastructure/security/validation.ts +0 -246
  66. package/src/presentation/hooks/useLanguage.ts +0 -109
  67. package/src/presentation/molecules/Breadcrumb.tsx +0 -54
  68. package/src/presentation/organisms/AlertDialog.tsx +0 -181
  69. package/src/presentation/organisms/Calendar.tsx +0 -79
  70. package/src/presentation/organisms/Comments.tsx +0 -103
  71. package/src/presentation/organisms/ConfirmDialog.tsx +0 -97
  72. package/src/presentation/organisms/FilterBar.tsx +0 -244
  73. package/src/presentation/organisms/FilterSidebar.tsx +0 -172
  74. package/src/presentation/organisms/Footer.tsx +0 -86
  75. package/src/presentation/organisms/FormModal.tsx +0 -126
  76. package/src/presentation/organisms/Grid.tsx +0 -128
  77. package/src/presentation/organisms/ImageLightbox.tsx +0 -252
  78. package/src/presentation/organisms/MainNavbar.tsx +0 -320
  79. package/src/presentation/organisms/Modal.tsx +0 -173
  80. package/src/presentation/organisms/Navbar.tsx +0 -75
  81. package/src/presentation/organisms/NewsletterSignup.tsx +0 -165
  82. 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 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,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 = 'javascript',
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
- const timeout = setTimeout(() => setCopied(false), 2000);
32
- return () => clearTimeout(timeout);
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 > 10;
59
+ const isLongCode = lineCount > collapseThreshold;
51
60
 
52
61
  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">
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-text-secondary">{filename}</span>
71
+ <span className="text-xs font-medium text-muted-foreground">{filename}</span>
59
72
  )}
60
- <span className="text-xs px-2 py-1 bg-bg-primary text-text-primary rounded-md font-medium">
61
- {detectedLanguage}
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
- <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>
88
+ <ChevronUp className="h-4 w-4" aria-hidden="true" />
78
89
  ) : (
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>
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
- <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>
108
+ <Check className="h-3.5 w-3.5" aria-hidden="true" />
101
109
  ) : (
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>
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
- {/* Code */}
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={detectedLanguage}
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="px-4 py-2 bg-bg-secondary text-center border-t border-border">
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<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
@@ -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 { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from 'lucide-react';
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 = Math.ceil(data.length / pageSize);
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">{sortDirection === 'asc' ? '↑' : '↓'}</span>
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 {((currentPage - 1) * pageSize) + 1} to {Math.min(currentPage * pageSize, data.length)} of {data.length} results
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={currentPage === 1}
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) => Math.max(1, p - 1))}
203
- disabled={currentPage === 1}
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 {currentPage} of {totalPages}
221
+ Page {safePage} of {totalPages}
209
222
  </span>
210
223
  <Button
211
224
  size="sm"
212
225
  variant="outline"
213
- onClick={() => setCurrentPage((p) => Math.min(totalPages, p + 1))}
214
- disabled={currentPage === totalPages}
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={currentPage === totalPages}
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={cn(
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}