@shohojdhara/atomix 0.2.5 → 0.2.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shohojdhara/atomix",
3
- "version": "0.2.5",
3
+ "version": "0.2.6",
4
4
  "description": "Atomix Design System - A modern component library for web applications",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -561,7 +561,6 @@ export function AtomixGlass({
561
561
  width: adjustedSize.width,
562
562
  borderRadius: `${effectiveCornerRadius}px`,
563
563
  transform: baseStyle.transform,
564
- transition: baseStyle.transition,
565
564
  }}
566
565
  />
567
566
  <div
@@ -580,7 +579,6 @@ export function AtomixGlass({
580
579
  width: adjustedSize.width,
581
580
  borderRadius: `${effectiveCornerRadius}px`,
582
581
  transform: baseStyle.transform,
583
- transition: baseStyle.transition,
584
582
  }}
585
583
  />
586
584
  {shouldRenderOverLightLayers && (
@@ -334,7 +334,6 @@ export const AtomixGlassContainer = forwardRef<HTMLDivElement, AtomixGlassContai
334
334
  padding: `var(--atomix-glass-container-padding)`,
335
335
  borderRadius: `var(--atomix-glass-container-radius)`,
336
336
  boxShadow: `var(--atomix-glass-container-box-shadow)`,
337
- transition: effectiveReducedMotion ? 'none' : 'all 0.2s ease-out',
338
337
  }}
339
338
  onMouseEnter={onMouseEnter}
340
339
  onMouseLeave={onMouseLeave}
@@ -1,5 +1,7 @@
1
1
  import type { Meta, StoryObj } from '@storybook/react';
2
- import { ColorModeToggle } from './ColorModeToggle';
2
+ import { useState } from 'react';
3
+ import { ColorModeToggle, type ColorMode } from './ColorModeToggle';
4
+ import { Moon, Sun } from '@phosphor-icons/react';
3
5
 
4
6
  const meta = {
5
7
  title: 'Components/ColorModeToggle',
@@ -8,9 +10,26 @@ const meta = {
8
10
  layout: 'centered',
9
11
  },
10
12
  argTypes: {
11
- className: {
12
- control: 'text',
13
- description: 'Additional CSS class names',
13
+ size: {
14
+ control: 'select',
15
+ options: ['sm', 'md', 'lg'],
16
+ description: 'Size variant',
17
+ },
18
+ disabled: {
19
+ control: 'boolean',
20
+ description: 'Disable the toggle',
21
+ },
22
+ showTooltip: {
23
+ control: 'boolean',
24
+ description: 'Show tooltip on hover',
25
+ },
26
+ disableStorage: {
27
+ control: 'boolean',
28
+ description: 'Disable localStorage persistence',
29
+ },
30
+ disableSystemPreference: {
31
+ control: 'boolean',
32
+ description: 'Disable system preference detection',
14
33
  },
15
34
  },
16
35
  } satisfies Meta<typeof ColorModeToggle>;
@@ -23,22 +42,113 @@ export const Default: Story = {
23
42
  args: {},
24
43
  };
25
44
 
26
- // With Custom Class
27
- export const WithCustomClass: Story = {
45
+ // Size Variants
46
+ export const Sizes: Story = {
47
+ render: () => (
48
+ <div style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
49
+ <ColorModeToggle size="sm" />
50
+ <ColorModeToggle size="md" />
51
+ <ColorModeToggle size="lg" />
52
+ </div>
53
+ ),
54
+ };
55
+
56
+ // Disabled State
57
+ export const Disabled: Story = {
58
+ args: {
59
+ disabled: true,
60
+ },
61
+ };
62
+
63
+ // Controlled Mode
64
+ export const Controlled: Story = {
65
+ render: () => {
66
+ const [mode, setMode] = useState<ColorMode>('light');
67
+ return (
68
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem', alignItems: 'center' }}>
69
+ <ColorModeToggle value={mode} onChange={setMode} />
70
+ <p>Current mode: {mode}</p>
71
+ <button onClick={() => setMode(mode === 'light' ? 'dark' : 'light')}>
72
+ Toggle from outside
73
+ </button>
74
+ </div>
75
+ );
76
+ },
77
+ };
78
+
79
+ // Custom Icons
80
+ export const CustomIcons: Story = {
81
+ render: () => (
82
+ <ColorModeToggle
83
+ lightIcon={<Moon size={24} weight="fill" />}
84
+ darkIcon={<Sun size={24} weight="fill" />}
85
+ />
86
+ ),
87
+ };
88
+
89
+ // With Callback
90
+ export const WithCallback: Story = {
91
+ render: () => {
92
+ const [lastChanged, setLastChanged] = useState<string>('');
93
+ return (
94
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem', alignItems: 'center' }}>
95
+ <ColorModeToggle
96
+ onChange={(mode) => setLastChanged(`Changed to ${mode} at ${new Date().toLocaleTimeString()}`)}
97
+ />
98
+ {lastChanged && <p style={{ fontSize: '0.875rem' }}>{lastChanged}</p>}
99
+ </div>
100
+ );
101
+ },
102
+ };
103
+
104
+ // Custom Storage Key
105
+ export const CustomStorageKey: Story = {
106
+ args: {
107
+ storageKey: 'my-app-theme',
108
+ dataAttribute: 'data-theme',
109
+ },
110
+ };
111
+
112
+ // Without Storage
113
+ export const WithoutStorage: Story = {
28
114
  args: {
29
- className: 'custom-class',
115
+ disableStorage: true,
30
116
  },
31
117
  };
32
118
 
33
- // Example Usage
34
- export const ExampleUsage: Story = {
119
+ // Example Usage in Header
120
+ export const InHeader: Story = {
35
121
  render: () => (
36
122
  <div
37
123
  className="u-p-5 u-shadow u-d-flex u-justify-content-between u-align-items-center"
38
- style={{ width: '300px', borderRadius: '8px' }}
124
+ style={{ width: '400px', borderRadius: '8px' }}
39
125
  >
40
- <span>Toggle Theme</span>
126
+ <span style={{ fontWeight: 600 }}>Toggle Theme</span>
41
127
  <ColorModeToggle />
42
128
  </div>
43
129
  ),
44
130
  };
131
+
132
+ // Multiple Toggles
133
+ export const MultipleToggles: Story = {
134
+ render: () => (
135
+ <div style={{ display: 'flex', gap: '2rem' }}>
136
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem', alignItems: 'center' }}>
137
+ <ColorModeToggle size="sm" />
138
+ <span style={{ fontSize: '0.75rem' }}>Small</span>
139
+ </div>
140
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem', alignItems: 'center' }}>
141
+ <ColorModeToggle size="md" />
142
+ <span style={{ fontSize: '0.75rem' }}>Medium</span>
143
+ </div>
144
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem', alignItems: 'center' }}>
145
+ <ColorModeToggle size="lg" />
146
+ <span style={{ fontSize: '0.75rem' }}>Large</span>
147
+ </div>
148
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem', alignItems: 'center' }}>
149
+ <ColorModeToggle disabled />
150
+ <span style={{ fontSize: '0.75rem' }}>Disabled</span>
151
+ </div>
152
+ </div>
153
+ ),
154
+ };
@@ -1,84 +1,185 @@
1
- import React, { useEffect, useState } from 'react';
1
+ import React, { useEffect, useState, useCallback } from 'react';
2
+
3
+ export type ColorMode = 'light' | 'dark';
2
4
 
3
5
  export interface ColorModeToggleProps {
6
+ /** Additional CSS class names */
4
7
  className?: string;
8
+ /** Inline styles */
5
9
  style?: React.CSSProperties;
10
+ /** Controlled mode value */
11
+ value?: ColorMode;
12
+ /** Default mode (uncontrolled) */
13
+ defaultValue?: ColorMode;
14
+ /** Callback when mode changes */
15
+ onChange?: (mode: ColorMode) => void;
16
+ /** Custom light mode icon */
17
+ lightIcon?: React.ReactNode;
18
+ /** Custom dark mode icon */
19
+ darkIcon?: React.ReactNode;
20
+ /** Size variant */
21
+ size?: 'sm' | 'md' | 'lg';
22
+ /** Disable the toggle */
23
+ disabled?: boolean;
24
+ /** localStorage key for persistence */
25
+ storageKey?: string;
26
+ /** data attribute name for body element */
27
+ dataAttribute?: string;
28
+ /** Disable localStorage persistence */
29
+ disableStorage?: boolean;
30
+ /** Disable system preference detection */
31
+ disableSystemPreference?: boolean;
32
+ /** Custom aria-label */
33
+ 'aria-label'?: string;
34
+ /** Show tooltip */
35
+ showTooltip?: boolean;
6
36
  }
7
37
 
8
- export const ColorModeToggle: React.FC<ColorModeToggleProps> = ({ className = '', style }) => {
9
- const [colorMode, setColorMode] = useState<'light' | 'dark'>('light');
38
+ const DEFAULT_STORAGE_KEY = 'atomix-color-mode';
39
+ const DEFAULT_DATA_ATTRIBUTE = 'data-atomix-color-mode';
40
+
41
+ const SIZE_MAP = {
42
+ sm: 16,
43
+ md: 24,
44
+ lg: 32,
45
+ };
46
+
47
+ export const ColorModeToggle: React.FC<ColorModeToggleProps> = ({
48
+ className = '',
49
+ style,
50
+ value: controlledValue,
51
+ defaultValue = 'light',
52
+ onChange,
53
+ lightIcon,
54
+ darkIcon,
55
+ size = 'md',
56
+ disabled = false,
57
+ storageKey = DEFAULT_STORAGE_KEY,
58
+ dataAttribute = DEFAULT_DATA_ATTRIBUTE,
59
+ disableStorage = false,
60
+ disableSystemPreference = false,
61
+ 'aria-label': ariaLabel,
62
+ showTooltip = true,
63
+ }) => {
64
+ const isControlled = controlledValue !== undefined;
65
+ const [internalMode, setInternalMode] = useState<ColorMode>(defaultValue);
66
+ const colorMode = isControlled ? controlledValue : internalMode;
10
67
 
11
68
  // Initialize color mode from localStorage or system preference
12
69
  useEffect(() => {
70
+ if (isControlled) return;
71
+
72
+ // SSR check
73
+ if (typeof window === 'undefined') return;
74
+
13
75
  // Check if color mode is already set in localStorage
14
- const storedColorMode = localStorage.getItem('atomix-color-mode');
76
+ if (!disableStorage) {
77
+ try {
78
+ const storedColorMode = localStorage.getItem(storageKey);
79
+ if (storedColorMode === 'light' || storedColorMode === 'dark') {
80
+ setInternalMode(storedColorMode);
81
+ return;
82
+ }
83
+ } catch (error) {
84
+ console.warn('ColorModeToggle: Failed to read from localStorage', error);
85
+ }
86
+ }
15
87
 
16
- if (storedColorMode === 'light' || storedColorMode === 'dark') {
17
- setColorMode(storedColorMode);
18
- } else if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
19
- // Use system preference if no stored preference
20
- setColorMode('dark');
88
+ // Use system preference if no stored preference
89
+ if (!disableSystemPreference && window.matchMedia) {
90
+ const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
91
+ if (prefersDark) {
92
+ setInternalMode('dark');
93
+ }
21
94
  }
22
- }, []);
95
+ }, [isControlled, disableStorage, disableSystemPreference, storageKey]);
23
96
 
24
97
  // Update the document theme attribute when colorMode changes
25
98
  useEffect(() => {
99
+ if (typeof window === 'undefined') return;
100
+
26
101
  const validColorMode = colorMode === 'dark' ? 'dark' : 'light';
27
- document.body.setAttribute('data-atomix-color-mode', validColorMode);
28
- localStorage.setItem('atomix-color-mode', validColorMode);
29
- }, [colorMode]);
102
+ document.body.setAttribute(dataAttribute, validColorMode);
103
+
104
+ if (!disableStorage) {
105
+ try {
106
+ localStorage.setItem(storageKey, validColorMode);
107
+ } catch (error) {
108
+ console.warn('ColorModeToggle: Failed to write to localStorage', error);
109
+ }
110
+ }
111
+ }, [colorMode, dataAttribute, disableStorage, storageKey]);
30
112
 
31
113
  // Listen for system color scheme changes
32
114
  useEffect(() => {
115
+ if (isControlled || disableSystemPreference || typeof window === 'undefined') return;
116
+
33
117
  const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
34
118
 
35
119
  const handleSystemThemeChange = (event: MediaQueryListEvent) => {
36
120
  // Only update if user hasn't explicitly set a preference
37
- if (!localStorage.getItem('atomix-color-mode')) {
38
- setColorMode(event.matches ? 'dark' : 'light');
121
+ if (disableStorage) {
122
+ setInternalMode(event.matches ? 'dark' : 'light');
123
+ } else {
124
+ try {
125
+ const hasStoredPreference = localStorage.getItem(storageKey);
126
+ if (!hasStoredPreference) {
127
+ setInternalMode(event.matches ? 'dark' : 'light');
128
+ }
129
+ } catch (error) {
130
+ console.warn('ColorModeToggle: Failed to check localStorage', error);
131
+ }
39
132
  }
40
133
  };
41
134
 
42
- // Add event listener for system theme changes
43
- if (darkModeMediaQuery.addEventListener) {
44
- darkModeMediaQuery.addEventListener('change', handleSystemThemeChange);
45
- } else {
46
- // Fallback for older browsers
47
- darkModeMediaQuery.addListener(handleSystemThemeChange);
48
- }
135
+ darkModeMediaQuery.addEventListener('change', handleSystemThemeChange);
49
136
 
50
- // Clean up event listener
51
137
  return () => {
52
- if (darkModeMediaQuery.removeEventListener) {
53
- darkModeMediaQuery.removeEventListener('change', handleSystemThemeChange);
54
- } else {
55
- // Fallback for older browsers
56
- darkModeMediaQuery.removeListener(handleSystemThemeChange);
57
- }
138
+ darkModeMediaQuery.removeEventListener('change', handleSystemThemeChange);
58
139
  };
59
- }, []);
140
+ }, [isControlled, disableSystemPreference, disableStorage, storageKey]);
60
141
 
61
- const toggleColorMode = () => {
62
- setColorMode(prevMode => (prevMode === 'light' ? 'dark' : 'light'));
63
- };
142
+ const toggleColorMode = useCallback(() => {
143
+ if (disabled) return;
144
+
145
+ const newMode: ColorMode = colorMode === 'light' ? 'dark' : 'light';
146
+
147
+ if (!isControlled) {
148
+ setInternalMode(newMode);
149
+ }
150
+
151
+ onChange?.(newMode);
152
+ }, [disabled, colorMode, isControlled, onChange]);
153
+
154
+ const iconSize = SIZE_MAP[size];
155
+ const nextMode = colorMode === 'light' ? 'dark' : 'light';
156
+ const label = ariaLabel || `Switch to ${nextMode} mode`;
157
+ const title = showTooltip ? `Switch to ${nextMode} mode` : undefined;
158
+
159
+ const defaultLightIcon = (
160
+ <svg viewBox="0 0 24 24" width={iconSize} height={iconSize} fill="currentColor" aria-hidden="true">
161
+ <path d="M9.37 5.51c-.18.64-.27 1.31-.27 1.99 0 4.08 3.32 7.4 7.4 7.4.68 0 1.35-.09 1.99-.27C17.45 17.19 14.93 19 12 19c-3.86 0-7-3.14-7-7 0-2.93 1.81-5.45 4.37-6.49zM12 3c-4.97 0-9 4.03-9 9s4.03 9 9 9 9-4.03 9-9c0-.46-.04-.92-.1-1.36-.98 1.37-2.58 2.26-4.4 2.26-2.98 0-5.4-2.42-5.4-5.4 0-1.81.89-3.42 2.26-4.4-.44-.06-.9-.1-1.36-.1z" />
162
+ </svg>
163
+ );
164
+
165
+ const defaultDarkIcon = (
166
+ <svg viewBox="0 0 24 24" width={iconSize} height={iconSize} fill="currentColor" aria-hidden="true">
167
+ <path d="M12 7c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.24 5-5-2.24-5-5-5zM2 13h2c.55 0 1-.45 1-1s-.45-1-1-1H2c-.55 0-1 .45-1 1s.45 1 1 1zm18 0h2c.55 0 1-.45 1-1s-.45-1-1-1h-2c-.55 0-1 .45-1 1s.45 1 1 1zM11 2v2c0 .55.45 1 1 1s1-.45 1-1V2c0-.55-.45-1-1-1s-1 .45-1 1zm0 18v2c0 .55.45 1 1 1s1-.45 1-1v-2c0-.55-.45-1-1-1s-1 .45-1 1zM5.99 4.58c-.39-.39-1.03-.39-1.41 0-.39.39-.39 1.03 0 1.41l1.06 1.06c.39.39 1.03.39 1.41 0 .39-.39.39-1.03 0-1.41L5.99 4.58zm12.37 12.37c-.39-.39-1.03-.39-1.41 0-.39.39-.39 1.03 0 1.41l1.06 1.06c.39.39 1.03.39 1.41 0 .39-.39.39-1.03 0-1.41l-1.06-1.06zm1.06-10.96c.39-.39.39-1.03 0-1.41-.39-.39-1.03-.39-1.41 0l-1.06 1.06c-.39.39-.39 1.03 0 1.41.39.39 1.03.39 1.41 0l1.06-1.06zM7.05 18.36c.39-.39.39-1.03 0-1.41-.39-.39-1.03-.39-1.41 0l-1.06 1.06c-.39.39-.39 1.03 0 1.41.39.39 1.03.39 1.41 0l1.06-1.06z" />
168
+ </svg>
169
+ );
64
170
 
65
171
  return (
66
172
  <button
67
- className={`c-color-mode-toggle ${className}`}
173
+ type="button"
174
+ className={`c-color-mode-toggle c-color-mode-toggle--${size} ${disabled ? 'c-color-mode-toggle--disabled' : ''} ${className}`}
68
175
  onClick={toggleColorMode}
69
- aria-label={`Switch to ${colorMode === 'light' ? 'dark' : 'light'} mode`}
70
- title={`Switch to ${colorMode === 'light' ? 'dark' : 'light'} mode`}
176
+ disabled={disabled}
177
+ aria-label={label}
178
+ aria-pressed={colorMode === 'dark'}
179
+ title={title}
71
180
  style={style}
72
181
  >
73
- {colorMode === 'light' ? (
74
- <svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor">
75
- <path d="M9.37 5.51c-.18.64-.27 1.31-.27 1.99 0 4.08 3.32 7.4 7.4 7.4.68 0 1.35-.09 1.99-.27C17.45 17.19 14.93 19 12 19c-3.86 0-7-3.14-7-7 0-2.93 1.81-5.45 4.37-6.49zM12 3c-4.97 0-9 4.03-9 9s4.03 9 9 9 9-4.03 9-9c0-.46-.04-.92-.1-1.36-.98 1.37-2.58 2.26-4.4 2.26-2.98 0-5.4-2.42-5.4-5.4 0-1.81.89-3.42 2.26-4.4-.44-.06-.9-.1-1.36-.1z" />
76
- </svg>
77
- ) : (
78
- <svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor">
79
- <path d="M12 7c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.24 5-5-2.24-5-5-5zM2 13h2c.55 0 1-.45 1-1s-.45-1-1-1H2c-.55 0-1 .45-1 1s.45 1 1 1zm18 0h2c.55 0 1-.45 1-1s-.45-1-1-1h-2c-.55 0-1 .45-1 1s.45 1 1 1zM11 2v2c0 .55.45 1 1 1s1-.45 1-1V2c0-.55-.45-1-1-1s-1 .45-1 1zm0 18v2c0 .55.45 1 1 1s1-.45 1-1v-2c0-.55-.45-1-1-1s-1 .45-1 1zM5.99 4.58c-.39-.39-1.03-.39-1.41 0-.39.39-.39 1.03 0 1.41l1.06 1.06c.39.39 1.03.39 1.41 0 .39-.39.39-1.03 0-1.41L5.99 4.58zm12.37 12.37c-.39-.39-1.03-.39-1.41 0-.39.39-.39 1.03 0 1.41l1.06 1.06c.39.39 1.03.39 1.41 0 .39-.39.39-1.03 0-1.41l-1.06-1.06zm1.06-10.96c.39-.39.39-1.03 0-1.41-.39-.39-1.03-.39-1.41 0l-1.06 1.06c-.39.39-.39 1.03 0 1.41.39.39 1.03.39 1.41 0l1.06-1.06zM7.05 18.36c.39-.39.39-1.03 0-1.41-.39-.39-1.03-.39-1.41 0l-1.06 1.06c-.39.39-.39 1.03 0 1.41.39.39 1.03.39 1.41 0l1.06-1.06z" />
80
- </svg>
81
- )}
182
+ {colorMode === 'light' ? (lightIcon || defaultLightIcon) : (darkIcon || defaultDarkIcon)}
82
183
  </button>
83
184
  );
84
185
  };
@@ -1,2 +1,2 @@
1
1
  export { default as ColorModeToggle } from './ColorModeToggle';
2
- export type { ColorModeToggleProps } from './ColorModeToggle';
2
+ export type { ColorModeToggleProps, ColorMode } from './ColorModeToggle';
@@ -1604,7 +1604,7 @@ export const ATOMIX_GLASS = {
1604
1604
  CORNER_RADIUS: 16, // Default border-radius matching design system
1605
1605
  PADDING: '0 0',
1606
1606
  MODE: 'standard' as const,
1607
- OVER_LIGHT: 'auto' as const,
1607
+ OVER_LIGHT: false as const,
1608
1608
  ENABLE_OVER_LIGHT_LAYERS: true,
1609
1609
  },
1610
1610
  CONSTANTS: {
@@ -7,7 +7,7 @@
7
7
  // CSS custom property defaults
8
8
  --atomix-glass-radius: var(--atomix-radius-md, 16px);
9
9
  --atomix-glass-transform: none;
10
- --atomix-glass-transition: opacity var(--atomix-transition-duration, 0.2s) ease-out;
10
+ --atomix-glass-transition: all var(--atomix-transition-duration, 0s) ease-out;
11
11
  --atomix-glass-position: absolute;
12
12
  --atomix-glass-container-width: 100%;
13
13
  --atomix-glass-container-height: 100%;
@@ -138,14 +138,13 @@
138
138
  height: var(--atomix-glass-container-height);
139
139
  position: relative;
140
140
  border-radius: var(--atomix-glass-radius);
141
- transition: border-radius 0.25s ease-out;
141
+ transition: var(--atomix-glass-transition);
142
142
  }
143
143
 
144
144
  &__inner {
145
145
  width: var(--atomix-glass-container-width);
146
146
  height: var(--atomix-glass-container-height);
147
147
  position: relative;
148
- // Padding and border-radius are set dynamically via inline styles
149
148
  border-radius: var(--atomix-glass-radius);
150
149
  }
151
150
 
@@ -202,10 +201,12 @@
202
201
  // When both --dark and --over-light modifiers are present
203
202
  &--dark#{&}--over-light {
204
203
  opacity: var(--atomix-opacity-50, 0.5);
204
+ z-index: -1;
205
205
  }
206
206
 
207
207
  &--black#{&}--over-light {
208
208
  opacity: var(--atomix-opacity-25, 0.25);
209
+
209
210
  }
210
211
 
211
212
  // Hidden state modifier
@@ -12,26 +12,63 @@
12
12
  cursor: pointer;
13
13
  padding: rem(8px);
14
14
  border-radius: 50%;
15
- transition: background-color 0.2s ease;
15
+ transition: background-color 0.2s ease, opacity 0.2s ease;
16
16
  color: var(--#{$prefix}body-color);
17
17
 
18
- &:hover {
18
+ &:hover:not(:disabled) {
19
19
  @include dynamic-background(rgba(0, 0, 0, 0.05));
20
20
  }
21
21
 
22
- &:focus {
22
+ &:focus-visible {
23
23
  outline: none;
24
24
  box-shadow: 0 0 0 2px var(--#{$prefix}primary);
25
25
  }
26
26
 
27
+ &:active:not(:disabled) {
28
+ transform: scale(0.95);
29
+ }
30
+
27
31
  svg {
28
- width: rem(24px);
29
- height: rem(24px);
32
+ display: block;
33
+ transition: transform 0.2s ease;
34
+ }
35
+
36
+ // Size variants
37
+ &--sm {
38
+ padding: rem(6px);
39
+ }
40
+
41
+ &--md {
42
+ padding: rem(8px);
43
+ }
44
+
45
+ &--lg {
46
+ padding: rem(10px);
47
+ }
48
+
49
+ // Disabled state
50
+ &--disabled,
51
+ &:disabled {
52
+ cursor: not-allowed;
53
+ opacity: 0.5;
30
54
  }
31
55
 
32
56
  [data-#{$prefix}theme='dark'] & {
33
- &:hover {
57
+ &:hover:not(:disabled) {
34
58
  @include dynamic-background(rgba(255, 255, 255, 0.1));
35
59
  }
36
60
  }
61
+
62
+ // Reduced motion support
63
+ @media (prefers-reduced-motion: reduce) {
64
+ transition: none;
65
+
66
+ svg {
67
+ transition: none;
68
+ }
69
+
70
+ &:active:not(:disabled) {
71
+ transform: none;
72
+ }
73
+ }
37
74
  }