@umituz/web-design-system 1.8.7 โ†’ 1.8.8

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 CHANGED
@@ -68,7 +68,11 @@ import {
68
68
  import {
69
69
  Form,
70
70
  List,
71
- Section
71
+ Section,
72
+ PageLayout,
73
+ PageHeader,
74
+ ResponsiveContainer,
75
+ ProjectSkeleton
72
76
  } from '@umituz/web-design-system/templates';
73
77
  ```
74
78
 
@@ -78,7 +82,15 @@ import {
78
82
  useTheme,
79
83
  useMediaQuery,
80
84
  useBreakpoint,
81
- useLocalStorage
85
+ useLocalStorage,
86
+ useLanguage,
87
+ useClickOutside,
88
+ useKeyboard,
89
+ useEscape,
90
+ useDebounce,
91
+ useClipboard,
92
+ useToggle,
93
+ useScrollLock
82
94
  } from '@umituz/web-design-system/hooks';
83
95
  ```
84
96
 
@@ -196,6 +208,81 @@ function ThemeToggle() {
196
208
  }
197
209
  ```
198
210
 
211
+ ### Language Hook
212
+
213
+ ```tsx
214
+ import { useLanguage } from '@umituz/web-design-system/hooks';
215
+
216
+ function LanguageSelector() {
217
+ const { currentLanguage, changeLanguage, t, supportedLanguages } = useLanguage({
218
+ defaultLanguage: 'en-US',
219
+ supportedLanguages: {
220
+ 'en-US': { name: 'English', flag: '๐Ÿ‡บ๐Ÿ‡ธ' },
221
+ 'tr-TR': { name: 'Tรผrkรงe', flag: '๐Ÿ‡น๐Ÿ‡ท' },
222
+ 'de-DE': { name: 'Deutsch', flag: '๐Ÿ‡ฉ๐Ÿ‡ช' },
223
+ }
224
+ });
225
+
226
+ return (
227
+ <select value={currentLanguage} onChange={(e) => changeLanguage(e.target.value)}>
228
+ {Object.entries(supportedLanguages).map(([code, { name, flag }]) => (
229
+ <option key={code} value={code}>{flag} {name}</option>
230
+ ))}
231
+ </select>
232
+ );
233
+ }
234
+ ```
235
+
236
+ ### Breakpoint Hook
237
+
238
+ ```tsx
239
+ import { useBreakpoint, useMediaQuery } from '@umituz/web-design-system/hooks';
240
+
241
+ function ResponsiveComponent() {
242
+ const breakpoint = useBreakpoint();
243
+ const isDesktop = useMediaQuery('lg');
244
+ const isTablet = useMediaQuery('md');
245
+
246
+ return (
247
+ <div>
248
+ Current breakpoint: {breakpoint || 'mobile'}
249
+ {isDesktop && <DesktopNavigation />}
250
+ {isTablet && !isDesktop && <TabletNavigation />}
251
+ </div>
252
+ );
253
+ }
254
+ ```
255
+
256
+ ## ๐Ÿ“ฑ Responsive Templates
257
+
258
+ ### ResponsiveContainer
259
+
260
+ ```tsx
261
+ import { ResponsiveContainer } from '@umituz/web-design-system/templates';
262
+
263
+ // Auto-responsive container with different max widths per device
264
+ <ResponsiveContainer
265
+ mobileMaxWidth="full"
266
+ tabletMaxWidth="lg"
267
+ desktopMaxWidth="xl"
268
+ gradient
269
+ minHeight="screen"
270
+ >
271
+ <h1>Auto-responsive content</h1>
272
+ <p>This container automatically adjusts based on screen size</p>
273
+ </ResponsiveContainer>
274
+
275
+ // Custom responsive container
276
+ <ResponsiveContainer
277
+ mobilePadding={false}
278
+ tabletPadding={true}
279
+ desktopPadding={true}
280
+ centered={false}
281
+ >
282
+ <div>Custom responsive layout</div>
283
+ </ResponsiveContainer>
284
+ ```
285
+
199
286
  ## ๐ŸŽฏ Design Tokens
200
287
 
201
288
  ### Colors
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/web-design-system",
3
- "version": "1.8.7",
3
+ "version": "1.8.8",
4
4
  "private": false,
5
5
  "description": "Web Design System - Atomic Design components (Atoms, Molecules, Organisms, Templates) for React applications",
6
6
  "main": "./src/index.ts",
@@ -57,16 +57,26 @@
57
57
  "@radix-ui/react-alert-dialog": ">=1.0.0",
58
58
  "@radix-ui/react-collapsible": ">=1.0.0",
59
59
  "@radix-ui/react-dialog": ">=1.0.0",
60
+ "@radix-ui/react-dropdown-menu": ">=1.0.0",
60
61
  "@radix-ui/react-hover-card": ">=1.0.0",
61
62
  "@radix-ui/react-label": ">=2.0.0",
62
63
  "@radix-ui/react-popover": ">=1.0.0",
63
64
  "@radix-ui/react-scroll-area": ">=1.0.0",
64
65
  "@radix-ui/react-select": ">=2.0.0",
66
+ "@radix-ui/react-separator": ">=1.0.0",
67
+ "@radix-ui/react-slot": ">=1.0.0",
68
+ "@radix-ui/react-switch": ">=1.0.0",
69
+ "@radix-ui/react-tabs": ">=1.0.0",
70
+ "@radix-ui/react-toggle": ">=1.0.0",
71
+ "@radix-ui/react-toggle-group": ">=1.0.0",
72
+ "@radix-ui/react-tooltip": ">=1.0.0",
73
+ "class-variance-authority": ">=0.7.0",
65
74
  "clsx": ">=2.0.0",
66
75
  "lucide-react": ">=0.400.0",
67
76
  "react": ">=18.0.0",
68
77
  "react-day-picker": ">=9.0.0",
69
78
  "react-dom": ">=18.0.0",
79
+ "react-i18next": ">=13.0.0",
70
80
  "tailwind-merge": ">=2.0.0"
71
81
  },
72
82
  "devDependencies": {
@@ -74,18 +84,28 @@
74
84
  "@radix-ui/react-alert-dialog": "^1.1.8",
75
85
  "@radix-ui/react-collapsible": "^1.1.12",
76
86
  "@radix-ui/react-dialog": "^1.1.15",
87
+ "@radix-ui/react-dropdown-menu": "^2.1.15",
77
88
  "@radix-ui/react-hover-card": "^1.1.8",
78
89
  "@radix-ui/react-label": "^2.1.8",
79
90
  "@radix-ui/react-popover": "^1.1.15",
80
91
  "@radix-ui/react-scroll-area": "^1.2.10",
81
92
  "@radix-ui/react-select": "^2.2.6",
93
+ "@radix-ui/react-separator": "^1.1.7",
94
+ "@radix-ui/react-slot": "^1.2.3",
95
+ "@radix-ui/react-switch": "^1.2.5",
96
+ "@radix-ui/react-tabs": "^1.1.12",
97
+ "@radix-ui/react-toggle": "^1.1.9",
98
+ "@radix-ui/react-toggle-group": "^1.1.10",
99
+ "@radix-ui/react-tooltip": "^1.2.7",
82
100
  "@types/react": "^18.0.0",
83
101
  "@types/react-dom": "^18.0.0",
102
+ "class-variance-authority": "^0.7.1",
84
103
  "clsx": "^2.1.1",
85
104
  "lucide-react": "^0.577.0",
86
105
  "react": "^18.0.0",
87
106
  "react-day-picker": "^9.14.0",
88
107
  "react-dom": "^18.0.0",
108
+ "react-i18next": "^13.0.0",
89
109
  "tailwind-merge": "^3.5.0",
90
110
  "typescript": "~5.9.2"
91
111
  },
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Separator Component (Atom) - Shadcn/ui format
3
+ * @description Visual separator/divider component
4
+ */
5
+
6
+ import * as React from 'react';
7
+ import * as SeparatorPrimitive from '@radix-ui/react-separator';
8
+
9
+ import { cn } from '../../infrastructure/utils';
10
+
11
+ const Separator = React.forwardRef<
12
+ React.ElementRef<typeof SeparatorPrimitive.Root>,
13
+ React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
14
+ >(
15
+ (
16
+ { className, orientation = 'horizontal', decorative = true, ...props },
17
+ ref
18
+ ) => (
19
+ <SeparatorPrimitive.Root
20
+ ref={ref}
21
+ decorative={decorative}
22
+ orientation={orientation}
23
+ className={cn(
24
+ 'shrink-0 bg-border',
25
+ orientation === 'horizontal' ? 'h-[1px] w-full' : 'h-full w-[1px]',
26
+ className
27
+ )}
28
+ {...props}
29
+ />
30
+ )
31
+ );
32
+ Separator.displayName = SeparatorPrimitive.Root.displayName;
33
+
34
+ export { Separator };
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Switch Component (Atom) - Shadcn/ui format
3
+ * @description Toggle switch component using class-variance-authority
4
+ */
5
+
6
+ import * as React from 'react';
7
+ import * as SwitchPrimitives from '@radix-ui/react-switch';
8
+ import { cva, type VariantProps } from 'class-variance-authority';
9
+
10
+ import { cn } from '../../infrastructure/utils';
11
+
12
+ const switchVariants = cva(
13
+ 'peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input',
14
+ {
15
+ variants: {
16
+ size: {
17
+ default: 'h-6 w-11',
18
+ sm: 'h-5 w-9',
19
+ lg: 'h-7 w-13',
20
+ },
21
+ },
22
+ defaultVariants: {
23
+ size: 'default',
24
+ },
25
+ }
26
+ );
27
+
28
+ const switchThumbVariants = cva(
29
+ 'pointer-events-none block rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0',
30
+ {
31
+ variants: {
32
+ size: {
33
+ default: 'h-5 w-5 data-[state=checked]:translate-x-5',
34
+ sm: 'h-4 w-4 data-[state=checked]:translate-x-4',
35
+ lg: 'h-6 w-6 data-[state=checked]:translate-x-6',
36
+ },
37
+ },
38
+ defaultVariants: {
39
+ size: 'default',
40
+ },
41
+ }
42
+ );
43
+
44
+ export interface SwitchProps extends React.ComponentProps<typeof SwitchPrimitives.Root>, VariantProps<typeof switchVariants> {}
45
+
46
+ const Switch = React.forwardRef<React.ElementRef<typeof SwitchPrimitives.Root>, SwitchProps>(
47
+ ({ className, size, ...props }, ref) => (
48
+ <SwitchPrimitives.Root className={cn(switchVariants({ size, className }))} {...props} ref={ref}>
49
+ <SwitchPrimitives.Thumb className={cn(switchThumbVariants({ size }))} />
50
+ </SwitchPrimitives.Root>
51
+ )
52
+ );
53
+ Switch.displayName = SwitchPrimitives.Root.displayName;
54
+
55
+ export { Switch };
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Toggle Component (Atom) - Shadcn/ui format
3
+ * @description Toggle button component with variants
4
+ */
5
+
6
+ import * as React from 'react';
7
+ import * as TogglePrimitive from '@radix-ui/react-toggle';
8
+ import { cva, type VariantProps } from 'class-variance-authority';
9
+
10
+ import { cn } from '../../infrastructure/utils';
11
+
12
+ const toggleVariants = cva(
13
+ 'inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground',
14
+ {
15
+ variants: {
16
+ variant: {
17
+ default: 'bg-transparent',
18
+ outline:
19
+ 'border border-input bg-transparent hover:bg-accent hover:text-accent-foreground',
20
+ },
21
+ size: {
22
+ default: 'h-10 px-3',
23
+ sm: 'h-9 px-2.5',
24
+ lg: 'h-11 px-5',
25
+ },
26
+ },
27
+ defaultVariants: {
28
+ variant: 'default',
29
+ size: 'default',
30
+ },
31
+ }
32
+ );
33
+
34
+ const Toggle = React.forwardRef<
35
+ React.ElementRef<typeof TogglePrimitive.Root>,
36
+ React.ComponentPropsWithoutRef<typeof TogglePrimitive.Root> &
37
+ VariantProps<typeof toggleVariants>
38
+ >(({ className, variant, size, ...props }, ref) => (
39
+ <TogglePrimitive.Root
40
+ ref={ref}
41
+ className={cn(toggleVariants({ variant, size, className }))}
42
+ {...props}
43
+ />
44
+ ));
45
+
46
+ Toggle.displayName = TogglePrimitive.Root.displayName;
47
+
48
+ export { Toggle, toggleVariants };
@@ -50,3 +50,12 @@ export { Label } from './Label';
50
50
 
51
51
  export { AspectRatio } from './AspectRatio';
52
52
  export type { AspectRatioProps } from './AspectRatio';
53
+
54
+ export { Switch } from './Switch';
55
+ export type { SwitchProps } from './Switch';
56
+
57
+ export { Separator } from './Separator';
58
+ export type { SeparatorProps } from './Separator';
59
+
60
+ export { Toggle, toggleVariants } from './Toggle';
61
+ export type { ToggleProps } from './Toggle';
@@ -25,3 +25,6 @@ export type { UseClipboardReturn } from './useClipboard';
25
25
  export { useToggle } from './useToggle';
26
26
 
27
27
  export { useScrollLock } from './useScrollLock';
28
+
29
+ export { useLanguage } from './useLanguage';
30
+ export type { Language, SupportedLanguage, UseLanguageReturn } from './useLanguage';
@@ -0,0 +1,109 @@
1
+ /**
2
+ * useLanguage Hook
3
+ * @description Language management with i18n integration
4
+ */
5
+
6
+ import { useTranslation } from 'react-i18next';
7
+
8
+ export type Language = string;
9
+
10
+ export interface SupportedLanguage {
11
+ name: string;
12
+ flag: string;
13
+ }
14
+
15
+ export interface UseLanguageReturn {
16
+ currentLanguage: Language;
17
+ changeLanguage: (language: Language) => void;
18
+ t: (key: string, params?: Record<string, unknown>) => string;
19
+ supportedLanguages: Record<Language, SupportedLanguage>;
20
+ }
21
+
22
+ /**
23
+ * Validates if a language code exists in supported languages
24
+ */
25
+ function isValidLanguage(
26
+ code: string,
27
+ supportedLanguages: Record<Language, SupportedLanguage>
28
+ ): boolean {
29
+ return Object.keys(supportedLanguages).includes(code);
30
+ }
31
+
32
+ /**
33
+ * Normalizes language code to match supported languages
34
+ * Tries exact match first, then tries to match by language code prefix
35
+ */
36
+ function normalizeLanguage(
37
+ code: string,
38
+ supportedLanguages: Record<Language, SupportedLanguage>,
39
+ defaultLanguage: Language
40
+ ): Language {
41
+ // If code is valid, return it
42
+ if (isValidLanguage(code, supportedLanguages)) {
43
+ return code;
44
+ }
45
+
46
+ // Try to extract language code (e.g., 'en-US' -> 'en')
47
+ const [language] = code.split('-');
48
+ const matchedKey = Object.keys(supportedLanguages).find((key) =>
49
+ key.startsWith(language)
50
+ );
51
+
52
+ return (matchedKey as Language) || defaultLanguage;
53
+ }
54
+
55
+ /**
56
+ * Language management hook with i18n integration
57
+ *
58
+ * @example
59
+ * ```tsx
60
+ * const { currentLanguage, changeLanguage, t, supportedLanguages } = useLanguage({
61
+ * defaultLanguage: 'en-US',
62
+ * supportedLanguages: {
63
+ * 'en-US': { name: 'English', flag: '๐Ÿ‡บ๐Ÿ‡ธ' },
64
+ * 'tr-TR': { name: 'Tรผrkรงe', flag: '๐Ÿ‡น๐Ÿ‡ท' },
65
+ * }
66
+ * });
67
+ *
68
+ * // Change language
69
+ * changeLanguage('tr-TR');
70
+ *
71
+ * // Translate
72
+ * const title = t('common.title');
73
+ * ```
74
+ */
75
+ export function useLanguage(options?: {
76
+ defaultLanguage?: Language;
77
+ supportedLanguages?: Record<Language, SupportedLanguage>;
78
+ }): UseLanguageReturn {
79
+ const {
80
+ defaultLanguage = 'en-US',
81
+ supportedLanguages = {} as Record<Language, SupportedLanguage>,
82
+ } = options || {};
83
+
84
+ const { t, i18n } = useTranslation();
85
+
86
+ // Safely get and validate current language
87
+ const currentLanguage: Language = normalizeLanguage(
88
+ i18n.language,
89
+ supportedLanguages,
90
+ defaultLanguage
91
+ );
92
+
93
+ const changeLanguage = (language: Language) => {
94
+ // Validate language before changing
95
+ if (isValidLanguage(language, supportedLanguages)) {
96
+ i18n.changeLanguage(language);
97
+ } else {
98
+ // Fall back to default language
99
+ i18n.changeLanguage(defaultLanguage);
100
+ }
101
+ };
102
+
103
+ return {
104
+ currentLanguage,
105
+ changeLanguage,
106
+ t,
107
+ supportedLanguages,
108
+ };
109
+ }
@@ -0,0 +1,186 @@
1
+ /**
2
+ * Dropdown Menu Component (Organism) - Shadcn/ui format
3
+ * @description Dropdown menu component using Radix UI
4
+ */
5
+
6
+ import * as React from 'react';
7
+ import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
8
+ import { Check, ChevronRight, Circle } from 'lucide-react';
9
+
10
+ import { cn } from '../../infrastructure/utils';
11
+
12
+ const DropdownMenu = DropdownMenuPrimitive.Root;
13
+
14
+ const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
15
+
16
+ const DropdownMenuGroup = DropdownMenuPrimitive.Group;
17
+
18
+ const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
19
+
20
+ const DropdownMenuSub = DropdownMenuPrimitive.Sub;
21
+
22
+ const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
23
+
24
+ const DropdownMenuSubTrigger = React.forwardRef<
25
+ React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
26
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
27
+ inset?: boolean;
28
+ }
29
+ >(({ className, inset, children, ...props }, ref) => (
30
+ <DropdownMenuPrimitive.SubTrigger
31
+ ref={ref}
32
+ className={cn(
33
+ 'flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
34
+ inset && 'pl-8',
35
+ className
36
+ )}
37
+ {...props}
38
+ >
39
+ {children}
40
+ <ChevronRight className="ml-auto" />
41
+ </DropdownMenuPrimitive.SubTrigger>
42
+ ));
43
+ DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName;
44
+
45
+ const DropdownMenuSubContent = React.forwardRef<
46
+ React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
47
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
48
+ >(({ className, ...props }, ref) => (
49
+ <DropdownMenuPrimitive.SubContent
50
+ ref={ref}
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',
53
+ className
54
+ )}
55
+ {...props}
56
+ />
57
+ ));
58
+ DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName;
59
+
60
+ const DropdownMenuContent = React.forwardRef<
61
+ React.ElementRef<typeof DropdownMenuPrimitive.Content>,
62
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
63
+ >(({ className, collisionPadding = 8, sideOffset = 4, ...props }, ref) => (
64
+ <DropdownMenuPrimitive.Portal>
65
+ <DropdownMenuPrimitive.Content
66
+ ref={ref}
67
+ collisionPadding={collisionPadding}
68
+ sideOffset={sideOffset}
69
+ className={cn(
70
+ 'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md',
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
+ className
73
+ )}
74
+ {...props}
75
+ />
76
+ </DropdownMenuPrimitive.Portal>
77
+ ));
78
+ DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
79
+
80
+ const DropdownMenuItem = React.forwardRef<
81
+ React.ElementRef<typeof DropdownMenuPrimitive.Item>,
82
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
83
+ inset?: boolean;
84
+ }
85
+ >(({ className, inset, ...props }, ref) => (
86
+ <DropdownMenuPrimitive.Item
87
+ ref={ref}
88
+ className={cn(
89
+ 'relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
90
+ inset && 'pl-8',
91
+ className
92
+ )}
93
+ {...props}
94
+ />
95
+ ));
96
+ DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
97
+
98
+ const DropdownMenuCheckboxItem = React.forwardRef<
99
+ React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
100
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
101
+ >(({ className, children, checked, ...props }, ref) => (
102
+ <DropdownMenuPrimitive.CheckboxItem
103
+ ref={ref}
104
+ className={cn(
105
+ 'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
106
+ className
107
+ )}
108
+ checked={checked}
109
+ {...props}
110
+ >
111
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
112
+ <DropdownMenuPrimitive.ItemIndicator>
113
+ <Check className="h-4 w-4" />
114
+ </DropdownMenuPrimitive.ItemIndicator>
115
+ </span>
116
+ {children}
117
+ </DropdownMenuPrimitive.CheckboxItem>
118
+ ));
119
+ DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName;
120
+
121
+ const DropdownMenuRadioItem = React.forwardRef<
122
+ React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
123
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
124
+ >(({ className, children, ...props }, ref) => (
125
+ <DropdownMenuPrimitive.RadioItem
126
+ ref={ref}
127
+ className={cn(
128
+ 'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
129
+ className
130
+ )}
131
+ {...props}
132
+ >
133
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
134
+ <DropdownMenuPrimitive.ItemIndicator>
135
+ <Circle className="h-2 w-2 fill-current" />
136
+ </DropdownMenuPrimitive.ItemIndicator>
137
+ </span>
138
+ {children}
139
+ </DropdownMenuPrimitive.RadioItem>
140
+ ));
141
+ DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
142
+
143
+ const DropdownMenuLabel = React.forwardRef<
144
+ React.ElementRef<typeof DropdownMenuPrimitive.Label>,
145
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
146
+ inset?: boolean;
147
+ }
148
+ >(({ className, inset, ...props }, ref) => (
149
+ <DropdownMenuPrimitive.Label
150
+ ref={ref}
151
+ className={cn('px-2 py-1.5 text-sm font-semibold', inset && 'pl-8', className)}
152
+ {...props}
153
+ />
154
+ ));
155
+ DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
156
+
157
+ const DropdownMenuSeparator = React.forwardRef<
158
+ React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
159
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
160
+ >(({ className, ...props }, ref) => (
161
+ <DropdownMenuPrimitive.Separator ref={ref} className={cn('-mx-1 my-1 h-px bg-muted', className)} {...props} />
162
+ ));
163
+ DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
164
+
165
+ const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
166
+ return <span className={cn('ml-auto text-xs tracking-widest opacity-60', className)} {...props} />;
167
+ };
168
+ DropdownMenuShortcut.displayName = 'DropdownMenuShortcut';
169
+
170
+ export {
171
+ DropdownMenu,
172
+ DropdownMenuTrigger,
173
+ DropdownMenuContent,
174
+ DropdownMenuItem,
175
+ DropdownMenuCheckboxItem,
176
+ DropdownMenuRadioItem,
177
+ DropdownMenuLabel,
178
+ DropdownMenuSeparator,
179
+ DropdownMenuShortcut,
180
+ DropdownMenuGroup,
181
+ DropdownMenuPortal,
182
+ DropdownMenuSub,
183
+ DropdownMenuSubContent,
184
+ DropdownMenuSubTrigger,
185
+ DropdownMenuRadioGroup,
186
+ };
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Toggle Group Component (Organism) - Shadcn/ui format
3
+ * @description Group of toggle buttons with single or multiple selection
4
+ */
5
+
6
+ import * as React from 'react';
7
+ import * as ToggleGroupPrimitive from '@radix-ui/react-toggle-group';
8
+ import { cva, type VariantProps } from 'class-variance-authority';
9
+
10
+ import { cn } from '../../infrastructure/utils';
11
+ import { Toggle, toggleVariants } from '../atoms/Toggle';
12
+
13
+ const ToggleGroupContext = React.createContext<VariantProps<typeof toggleVariants>>({
14
+ size: 'default',
15
+ variant: 'default',
16
+ });
17
+
18
+ const ToggleGroup = React.forwardRef<
19
+ React.ElementRef<typeof ToggleGroupPrimitive.Root>,
20
+ React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root> &
21
+ VariantProps<typeof toggleVariants>
22
+ >(({ className, variant, size, children, ...props }, ref) => (
23
+ <ToggleGroupPrimitive.Root
24
+ ref={ref}
25
+ className={cn('flex items-center justify-center gap-1', className)}
26
+ {...props}
27
+ >
28
+ <ToggleGroupContext.Provider value={{ variant, size }}>
29
+ {children}
30
+ </ToggleGroupContext.Provider>
31
+ </ToggleGroupPrimitive.Root>
32
+ ));
33
+
34
+ ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName;
35
+
36
+ const ToggleGroupItem = React.forwardRef<
37
+ React.ElementRef<typeof ToggleGroupPrimitive.Item>,
38
+ React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item> &
39
+ VariantProps<typeof toggleVariants>
40
+ >(({ className, children, variant, size, ...props }, ref) => {
41
+ const context = React.useContext(ToggleGroupContext);
42
+
43
+ return (
44
+ <ToggleGroupPrimitive.Item
45
+ ref={ref}
46
+ className={cn(
47
+ toggleVariants({
48
+ variant: variant || context.variant,
49
+ size: size || context.size,
50
+ }),
51
+ className
52
+ )}
53
+ {...props}
54
+ >
55
+ {children}
56
+ </ToggleGroupPrimitive.Item>
57
+ );
58
+ });
59
+
60
+ ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName;
61
+
62
+ export { ToggleGroup, ToggleGroupItem };
@@ -111,3 +111,25 @@ export {
111
111
  AlertDialogAction,
112
112
  AlertDialogCancel,
113
113
  } from './AlertDialog';
114
+
115
+ export {
116
+ DropdownMenu,
117
+ DropdownMenuTrigger,
118
+ DropdownMenuContent,
119
+ DropdownMenuItem,
120
+ DropdownMenuCheckboxItem,
121
+ DropdownMenuRadioItem,
122
+ DropdownMenuLabel,
123
+ DropdownMenuSeparator,
124
+ DropdownMenuShortcut,
125
+ DropdownMenuGroup,
126
+ DropdownMenuPortal,
127
+ DropdownMenuSub,
128
+ DropdownMenuSubContent,
129
+ DropdownMenuSubTrigger,
130
+ DropdownMenuRadioGroup,
131
+ } from './DropdownMenu';
132
+ export type { DropdownMenuProps, DropdownMenuContentProps } from './DropdownMenu';
133
+
134
+ export { ToggleGroup, ToggleGroupItem } from './ToggleGroup';
135
+ export type { ToggleGroupProps, ToggleGroupItemProps } from './ToggleGroup';
@@ -0,0 +1,178 @@
1
+ /**
2
+ * ResponsiveContainer Template Component
3
+ * @description Responsive wrapper with mobile/desktop optimizations
4
+ */
5
+
6
+ import { forwardRef, type HTMLAttributes } from 'react';
7
+ import { cn } from '../../infrastructure/utils';
8
+ import { useBreakpoint } from '../hooks/useMediaQuery';
9
+ import type { BaseProps, ChildrenProps } from '../../domain/types';
10
+
11
+ export type ResponsiveSize = 'sm' | 'md' | 'lg' | 'xl' | 'full';
12
+
13
+ export interface ResponsiveContainerProps
14
+ extends HTMLAttributes<HTMLDivElement>,
15
+ BaseProps,
16
+ ChildrenProps {
17
+ /**
18
+ * Maximum width on mobile devices
19
+ * @default 'full'
20
+ */
21
+ mobileMaxWidth?: ResponsiveSize;
22
+
23
+ /**
24
+ * Maximum width on tablet devices
25
+ * @default 'lg'
26
+ */
27
+ tabletMaxWidth?: ResponsiveSize;
28
+
29
+ /**
30
+ * Maximum width on desktop devices
31
+ * @default 'xl'
32
+ */
33
+ desktopMaxWidth?: ResponsiveSize;
34
+
35
+ /**
36
+ * Padding on mobile devices
37
+ * @default true
38
+ */
39
+ mobilePadding?: boolean;
40
+
41
+ /**
42
+ * Padding on tablet devices
43
+ * @default true
44
+ */
45
+ tabletPadding?: boolean;
46
+
47
+ /**
48
+ * Padding on desktop devices
49
+ * @default true
50
+ */
51
+ desktopPadding?: boolean;
52
+
53
+ /**
54
+ * Center content horizontally
55
+ * @default true
56
+ */
57
+ centered?: boolean;
58
+
59
+ /**
60
+ * Background gradient
61
+ * @default false
62
+ */
63
+ gradient?: boolean;
64
+
65
+ /**
66
+ * Minimum height
67
+ * @default 'auto'
68
+ */
69
+ minHeight?: 'auto' | 'screen' | 'full';
70
+ }
71
+
72
+ const maxWidthClasses: Record<ResponsiveSize, string> = {
73
+ sm: 'max-w-sm',
74
+ md: 'max-w-md',
75
+ lg: 'max-w-lg',
76
+ xl: 'max-w-xl',
77
+ full: 'max-w-full',
78
+ };
79
+
80
+ const sizeBreakpointMap: Record<string, ResponsiveSize> = {
81
+ sm: 'mobileMaxWidth',
82
+ md: 'tabletMaxWidth',
83
+ lg: 'desktopMaxWidth',
84
+ };
85
+
86
+ export const ResponsiveContainer = forwardRef<
87
+ HTMLDivElement,
88
+ ResponsiveContainerProps
89
+ >(
90
+ (
91
+ {
92
+ children,
93
+ mobileMaxWidth = 'full',
94
+ tabletMaxWidth = 'lg',
95
+ desktopMaxWidth = 'xl',
96
+ mobilePadding = true,
97
+ tabletPadding = true,
98
+ desktopPadding = true,
99
+ centered = true,
100
+ gradient = false,
101
+ minHeight = 'auto',
102
+ className,
103
+ ...props
104
+ },
105
+ ref
106
+ ) => {
107
+ const breakpoint = useBreakpoint();
108
+
109
+ // Determine current max width based on breakpoint
110
+ const getCurrentMaxWidth = (): string => {
111
+ if (!breakpoint) return maxWidthClasses[mobileMaxWidth];
112
+
113
+ if (breakpoint === 'sm') {
114
+ return maxWidthClasses[mobileMaxWidth];
115
+ }
116
+ if (breakpoint === 'md') {
117
+ return maxWidthClasses[tabletMaxWidth];
118
+ }
119
+ return maxWidthClasses[desktopMaxWidth];
120
+ };
121
+
122
+ // Determine current padding based on breakpoint
123
+ const getCurrentPadding = (): string => {
124
+ if (!breakpoint) return mobilePadding ? 'px-4' : '';
125
+
126
+ if (breakpoint === 'sm') {
127
+ return mobilePadding ? 'px-4' : '';
128
+ }
129
+ if (breakpoint === 'md') {
130
+ return tabletPadding ? 'px-6' : '';
131
+ }
132
+ return desktopPadding ? 'px-8' : '';
133
+ };
134
+
135
+ // Determine current py padding based on breakpoint
136
+ const getCurrentPyPadding = (): string => {
137
+ if (!breakpoint) return 'py-8';
138
+
139
+ if (breakpoint === 'sm') {
140
+ return 'py-8';
141
+ }
142
+ if (breakpoint === 'md') {
143
+ return 'py-12';
144
+ }
145
+ return 'py-16';
146
+ };
147
+
148
+ const minHeightClasses: Record<
149
+ typeof minHeight,
150
+ string
151
+ > = {
152
+ auto: '',
153
+ screen: 'min-h-screen',
154
+ full: 'min-h-full',
155
+ };
156
+
157
+ return (
158
+ <div
159
+ ref={ref}
160
+ className={cn(
161
+ 'transition-theme',
162
+ gradient && 'bg-surface-gradient',
163
+ minHeightClasses[minHeight],
164
+ getCurrentPyPadding(),
165
+ getCurrentPadding(),
166
+ centered && 'mx-auto',
167
+ getCurrentMaxWidth(),
168
+ className
169
+ )}
170
+ {...props}
171
+ >
172
+ {children}
173
+ </div>
174
+ );
175
+ }
176
+ );
177
+
178
+ ResponsiveContainer.displayName = 'ResponsiveContainer';
@@ -21,3 +21,6 @@ export type { PageHeaderProps, TextAlign, HeaderSize } from './PageHeader';
21
21
 
22
22
  export { ProjectSkeleton } from './ProjectSkeleton';
23
23
  export type { ProjectSkeletonProps } from './ProjectSkeleton';
24
+
25
+ export { ResponsiveContainer } from './ResponsiveContainer';
26
+ export type { ResponsiveContainerProps, ResponsiveSize } from './ResponsiveContainer';