@fragments-sdk/ui 0.16.1 → 0.17.1

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 (139) hide show
  1. package/dist/assets/ui.css +2021 -1606
  2. package/dist/components/Accordion/Accordion.module.scss.cjs +8 -8
  3. package/dist/components/Accordion/Accordion.module.scss.js +8 -8
  4. package/dist/components/Alert/Alert.module.scss.cjs +12 -12
  5. package/dist/components/Alert/Alert.module.scss.js +12 -12
  6. package/dist/components/Alert/index.cjs +2 -1
  7. package/dist/components/Alert/index.d.ts.map +1 -1
  8. package/dist/components/Alert/index.js +2 -1
  9. package/dist/components/Avatar/index.cjs +12 -4
  10. package/dist/components/Avatar/index.d.ts.map +1 -1
  11. package/dist/components/Avatar/index.js +12 -4
  12. package/dist/components/Badge/Badge.module.scss.cjs +13 -13
  13. package/dist/components/Badge/Badge.module.scss.js +13 -13
  14. package/dist/components/Button/Button.module.scss.cjs +11 -11
  15. package/dist/components/Button/Button.module.scss.js +11 -11
  16. package/dist/components/Button/index.cjs +51 -4
  17. package/dist/components/Button/index.d.ts.map +1 -1
  18. package/dist/components/Button/index.js +51 -4
  19. package/dist/components/Card/Card.module.scss.cjs +14 -14
  20. package/dist/components/Card/Card.module.scss.js +14 -14
  21. package/dist/components/Card/index.cjs +35 -2
  22. package/dist/components/Card/index.d.ts.map +1 -1
  23. package/dist/components/Card/index.js +35 -2
  24. package/dist/components/Checkbox/Checkbox.module.scss.cjs +10 -10
  25. package/dist/components/Checkbox/Checkbox.module.scss.js +10 -10
  26. package/dist/components/Chip/Chip.module.scss.cjs +15 -15
  27. package/dist/components/Chip/Chip.module.scss.js +15 -15
  28. package/dist/components/CodeBlock/CodeBlock.module.scss.cjs +21 -21
  29. package/dist/components/CodeBlock/CodeBlock.module.scss.js +21 -21
  30. package/dist/components/Collapsible/Collapsible.module.scss.cjs +10 -10
  31. package/dist/components/Collapsible/Collapsible.module.scss.js +10 -10
  32. package/dist/components/ColorPicker/ColorPicker.module.scss.cjs +14 -14
  33. package/dist/components/ColorPicker/ColorPicker.module.scss.js +14 -14
  34. package/dist/components/Combobox/Combobox.module.scss.cjs +24 -24
  35. package/dist/components/Combobox/Combobox.module.scss.js +24 -24
  36. package/dist/components/DataTable/DataTable.module.scss.cjs +26 -26
  37. package/dist/components/DataTable/DataTable.module.scss.js +26 -26
  38. package/dist/components/DatePicker/DatePicker.module.scss.cjs +31 -31
  39. package/dist/components/DatePicker/DatePicker.module.scss.js +31 -31
  40. package/dist/components/Dialog/Dialog.module.scss.cjs +14 -14
  41. package/dist/components/Dialog/Dialog.module.scss.js +14 -14
  42. package/dist/components/Drawer/Drawer.module.scss.cjs +26 -26
  43. package/dist/components/Drawer/Drawer.module.scss.js +26 -26
  44. package/dist/components/Editor/Editor.module.scss.cjs +17 -17
  45. package/dist/components/Editor/Editor.module.scss.js +17 -17
  46. package/dist/components/Fieldset/Fieldset.module.scss.cjs +6 -3
  47. package/dist/components/Fieldset/Fieldset.module.scss.js +6 -3
  48. package/dist/components/Fieldset/index.cjs +7 -1
  49. package/dist/components/Fieldset/index.d.ts +6 -1
  50. package/dist/components/Fieldset/index.d.ts.map +1 -1
  51. package/dist/components/Fieldset/index.js +7 -1
  52. package/dist/components/Header/Header.module.scss.cjs +42 -21
  53. package/dist/components/Header/Header.module.scss.js +42 -21
  54. package/dist/components/Header/index.cjs +121 -3
  55. package/dist/components/Header/index.d.ts +26 -3
  56. package/dist/components/Header/index.d.ts.map +1 -1
  57. package/dist/components/Header/index.js +122 -4
  58. package/dist/components/Input/Input.module.scss.cjs +27 -15
  59. package/dist/components/Input/Input.module.scss.js +27 -15
  60. package/dist/components/Input/index.cjs +20 -7
  61. package/dist/components/Input/index.d.ts +8 -2
  62. package/dist/components/Input/index.d.ts.map +1 -1
  63. package/dist/components/Input/index.js +20 -7
  64. package/dist/components/Link/Link.module.scss.cjs +10 -10
  65. package/dist/components/Link/Link.module.scss.js +10 -10
  66. package/dist/components/Listbox/Listbox.module.scss.cjs +8 -8
  67. package/dist/components/Listbox/Listbox.module.scss.js +8 -8
  68. package/dist/components/NavigationMenu/NavigationMenu.module.scss.cjs +28 -28
  69. package/dist/components/NavigationMenu/NavigationMenu.module.scss.js +28 -28
  70. package/dist/components/Pagination/Pagination.module.scss.cjs +7 -7
  71. package/dist/components/Pagination/Pagination.module.scss.js +7 -7
  72. package/dist/components/Popover/Popover.module.scss.cjs +10 -10
  73. package/dist/components/Popover/Popover.module.scss.js +10 -10
  74. package/dist/components/Prompt/Prompt.module.scss.cjs +14 -14
  75. package/dist/components/Prompt/Prompt.module.scss.js +14 -14
  76. package/dist/components/RadioGroup/RadioGroup.module.scss.cjs +16 -16
  77. package/dist/components/RadioGroup/RadioGroup.module.scss.js +16 -16
  78. package/dist/components/Select/Select.module.scss.cjs +17 -17
  79. package/dist/components/Select/Select.module.scss.js +17 -17
  80. package/dist/components/Sidebar/Sidebar.module.scss.cjs +42 -42
  81. package/dist/components/Sidebar/Sidebar.module.scss.js +42 -42
  82. package/dist/components/Slider/Slider.module.scss.cjs +12 -12
  83. package/dist/components/Slider/Slider.module.scss.js +12 -12
  84. package/dist/components/Tabs/Tabs.module.scss.cjs +9 -9
  85. package/dist/components/Tabs/Tabs.module.scss.js +9 -9
  86. package/dist/components/Textarea/Textarea.module.scss.cjs +34 -19
  87. package/dist/components/Textarea/Textarea.module.scss.js +34 -19
  88. package/dist/components/Textarea/index.cjs +36 -6
  89. package/dist/components/Textarea/index.d.ts +6 -2
  90. package/dist/components/Textarea/index.d.ts.map +1 -1
  91. package/dist/components/Textarea/index.js +36 -6
  92. package/dist/components/Theme/ThemeToggle.module.scss.cjs +6 -6
  93. package/dist/components/Theme/ThemeToggle.module.scss.js +6 -6
  94. package/dist/components/Toast/Toast.module.scss.cjs +22 -22
  95. package/dist/components/Toast/Toast.module.scss.js +22 -22
  96. package/dist/components/Toggle/Toggle.module.scss.cjs +13 -13
  97. package/dist/components/Toggle/Toggle.module.scss.js +13 -13
  98. package/dist/components/Toggle/index.cjs +7 -3
  99. package/dist/components/Toggle/index.d.ts.map +1 -1
  100. package/dist/components/Toggle/index.js +7 -3
  101. package/dist/components/ToggleGroup/ToggleGroup.module.scss.cjs +17 -17
  102. package/dist/components/ToggleGroup/ToggleGroup.module.scss.js +17 -17
  103. package/dist/index.d.ts +2 -2
  104. package/dist/index.d.ts.map +1 -1
  105. package/fragments.json +1 -1
  106. package/package.json +1 -1
  107. package/src/components/Alert/Alert.module.scss +8 -8
  108. package/src/components/Alert/index.tsx +2 -1
  109. package/src/components/Avatar/index.tsx +6 -2
  110. package/src/components/Badge/Badge.module.scss +17 -11
  111. package/src/components/Button/Button.module.scss +6 -5
  112. package/src/components/Button/index.tsx +60 -4
  113. package/src/components/Card/index.tsx +48 -3
  114. package/src/components/Checkbox/Checkbox.module.scss +16 -9
  115. package/src/components/ColorPicker/ColorPicker.module.scss +5 -13
  116. package/src/components/Combobox/Combobox.module.scss +30 -25
  117. package/src/components/DatePicker/DatePicker.module.scss +18 -28
  118. package/src/components/Editor/Editor.module.scss +23 -15
  119. package/src/components/Fieldset/Fieldset.module.scss +12 -6
  120. package/src/components/Fieldset/index.tsx +11 -1
  121. package/src/components/Header/Header.module.scss +99 -0
  122. package/src/components/Header/index.tsx +191 -10
  123. package/src/components/Input/Input.module.scss +97 -26
  124. package/src/components/Input/index.tsx +31 -12
  125. package/src/components/Listbox/Listbox.module.scss +9 -2
  126. package/src/components/RadioGroup/RadioGroup.module.scss +8 -6
  127. package/src/components/Select/Select.module.scss +10 -24
  128. package/src/components/Sidebar/Sidebar.module.scss +6 -4
  129. package/src/components/Slider/Slider.module.scss +12 -22
  130. package/src/components/Textarea/Textarea.module.scss +49 -18
  131. package/src/components/Textarea/index.tsx +43 -12
  132. package/src/components/Toast/Toast.module.scss +48 -8
  133. package/src/components/Toggle/Toggle.module.scss +21 -35
  134. package/src/components/Toggle/index.tsx +11 -3
  135. package/src/components/ToggleGroup/ToggleGroup.module.scss +23 -19
  136. package/src/index.ts +2 -0
  137. package/src/styles/globals.scss +4 -1
  138. package/src/tokens/_mixins.scss +57 -4
  139. package/src/tokens/_variables.scss +20 -1
@@ -27,6 +27,10 @@ export interface TextareaProps extends Omit<
27
27
  disabled?: boolean;
28
28
  /** Error state */
29
29
  error?: boolean;
30
+ /** Success state */
31
+ success?: boolean;
32
+ /** Show character count when maxLength is set */
33
+ showCharCount?: boolean;
30
34
  /** Label text above the textarea */
31
35
  label?: string;
32
36
  /** Helper text below the textarea */
@@ -36,9 +40,9 @@ export interface TextareaProps extends Omit<
36
40
  /** Alias for onChange (value-first callback) */
37
41
  onValueChange?: (value: string) => void;
38
42
  /** Called when textarea loses focus */
39
- onBlur?: () => void;
43
+ onBlur?: React.FocusEventHandler<HTMLTextAreaElement>;
40
44
  /** Called when textarea receives focus */
41
- onFocus?: () => void;
45
+ onFocus?: React.FocusEventHandler<HTMLTextAreaElement>;
42
46
  /** Form field name */
43
47
  name?: string;
44
48
  /** Maximum character length */
@@ -72,6 +76,8 @@ const TextareaRoot = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
72
76
  size = 'md',
73
77
  disabled = false,
74
78
  error = false,
79
+ success = false,
80
+ showCharCount = false,
75
81
  label,
76
82
  helperText,
77
83
  onChange,
@@ -100,16 +106,25 @@ const TextareaRoot = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
100
106
  const labelId = label ? `${textareaId}-label` : undefined;
101
107
  const helperId = `${textareaId}-helper`;
102
108
 
109
+ const [charCount, setCharCount] = React.useState(() =>
110
+ (value ?? defaultValue ?? '').length
111
+ );
112
+
103
113
  const textareaClasses = [
104
114
  styles.textarea,
105
115
  styles[size],
106
116
  error && styles.error,
117
+ success && styles.success,
107
118
  styles[`resize-${resize}`],
108
119
  ]
109
120
  .filter(Boolean)
110
121
  .join(' ');
111
122
 
112
- const helperClasses = [styles.helper, error && styles.helperError]
123
+ const helperClasses = [
124
+ styles.helper,
125
+ error && styles.helperError,
126
+ success && styles.helperSuccess,
127
+ ]
113
128
  .filter(Boolean)
114
129
  .join(' ');
115
130
 
@@ -125,6 +140,7 @@ const TextareaRoot = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
125
140
  return (
126
141
  <div
127
142
  {...rootProps}
143
+ data-success={success || undefined}
128
144
  className={[styles.wrapper, rootProps?.className, className].filter(Boolean).join(' ')}
129
145
  style={{ ...(rootProps?.style ?? {}), ...(wrapperStyle ?? {}) }}
130
146
  >
@@ -154,19 +170,34 @@ const TextareaRoot = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
154
170
  helperText ? helperId : undefined
155
171
  )}
156
172
  onChange={(e) => {
157
- onChange?.(e.target.value);
158
- onValueChange?.(e.target.value);
173
+ const val = e.target.value;
174
+ setCharCount(val.length);
175
+ onChange?.(val);
176
+ onValueChange?.(val);
159
177
  }}
160
- onBlur={() => onBlur?.()}
161
- onFocus={() => onFocus?.()}
178
+ onBlur={(e) => onBlur?.(e)}
179
+ onFocus={(e) => onFocus?.(e)}
162
180
  className={textareaClasses}
163
181
  style={Object.keys(textareaInlineStyle).length > 0 ? textareaInlineStyle : undefined}
164
182
  />
165
- {helperText && (
166
- <span id={helperId} className={helperClasses}>
167
- {helperText}
168
- </span>
169
- )}
183
+ <div className={styles.footer}>
184
+ {helperText && (
185
+ <span id={helperId} className={helperClasses}>
186
+ {helperText}
187
+ </span>
188
+ )}
189
+ {showCharCount && maxLength != null && (
190
+ <span
191
+ className={[
192
+ styles.charCount,
193
+ charCount > maxLength && styles.charCountOver,
194
+ ].filter(Boolean).join(' ')}
195
+ aria-live="polite"
196
+ >
197
+ {charCount}/{maxLength}
198
+ </span>
199
+ )}
200
+ </div>
170
201
  </div>
171
202
  );
172
203
  }
@@ -97,34 +97,74 @@
97
97
  }
98
98
 
99
99
  .success {
100
- border-left: 3px solid var(--fui-color-success, $fui-color-success);
100
+ background: color-mix(in srgb, var(--fui-color-success, #{$fui-color-success}) 15%, var(--fui-bg-elevated, #{$fui-bg-elevated}));
101
+ border-color: color-mix(in srgb, var(--fui-color-success, #{$fui-color-success}) 40%, transparent);
102
+
103
+ .title {
104
+ color: var(--fui-color-success-text, $fui-color-success-text);
105
+ }
106
+
107
+ .description {
108
+ color: var(--fui-color-success-text, $fui-color-success-text);
109
+ opacity: 0.8;
110
+ }
101
111
 
102
112
  .icon {
103
- color: var(--fui-color-success, $fui-color-success);
113
+ color: var(--fui-color-success-text, $fui-color-success-text);
104
114
  }
105
115
  }
106
116
 
107
117
  .error {
108
- border-left: 3px solid var(--fui-color-danger, $fui-color-danger);
118
+ background: color-mix(in srgb, var(--fui-color-danger, #{$fui-color-danger}) 15%, var(--fui-bg-elevated, #{$fui-bg-elevated}));
119
+ border-color: color-mix(in srgb, var(--fui-color-danger, #{$fui-color-danger}) 40%, transparent);
120
+
121
+ .title {
122
+ color: var(--fui-color-danger-text, $fui-color-danger-text);
123
+ }
124
+
125
+ .description {
126
+ color: var(--fui-color-danger-text, $fui-color-danger-text);
127
+ opacity: 0.8;
128
+ }
109
129
 
110
130
  .icon {
111
- color: var(--fui-color-danger, $fui-color-danger);
131
+ color: var(--fui-color-danger-text, $fui-color-danger-text);
112
132
  }
113
133
  }
114
134
 
115
135
  .warning {
116
- border-left: 3px solid var(--fui-color-warning, $fui-color-warning);
136
+ background: color-mix(in srgb, var(--fui-color-warning, #{$fui-color-warning}) 15%, var(--fui-bg-elevated, #{$fui-bg-elevated}));
137
+ border-color: color-mix(in srgb, var(--fui-color-warning, #{$fui-color-warning}) 40%, transparent);
138
+
139
+ .title {
140
+ color: var(--fui-color-warning-text, $fui-color-warning-text);
141
+ }
142
+
143
+ .description {
144
+ color: var(--fui-color-warning-text, $fui-color-warning-text);
145
+ opacity: 0.8;
146
+ }
117
147
 
118
148
  .icon {
119
- color: var(--fui-color-warning, $fui-color-warning);
149
+ color: var(--fui-color-warning-text, $fui-color-warning-text);
120
150
  }
121
151
  }
122
152
 
123
153
  .info {
124
- border-left: 3px solid var(--fui-color-info, $fui-color-info);
154
+ background: color-mix(in srgb, var(--fui-color-info, #{$fui-color-info}) 15%, var(--fui-bg-elevated, #{$fui-bg-elevated}));
155
+ border-color: color-mix(in srgb, var(--fui-color-info, #{$fui-color-info}) 40%, transparent);
156
+
157
+ .title {
158
+ color: var(--fui-color-info-text, $fui-color-info-text);
159
+ }
160
+
161
+ .description {
162
+ color: var(--fui-color-info-text, $fui-color-info-text);
163
+ opacity: 0.8;
164
+ }
125
165
 
126
166
  .icon {
127
- color: var(--fui-color-info, $fui-color-info);
167
+ color: var(--fui-color-info-text, $fui-color-info-text);
128
168
  }
129
169
  }
130
170
 
@@ -29,32 +29,31 @@
29
29
  display: inline-flex;
30
30
  align-items: center;
31
31
  flex-shrink: 0;
32
- box-sizing: content-box;
32
+ box-sizing: border-box;
33
33
  border-radius: var(--fui-radius-full, $fui-radius-full);
34
- border: none;
35
- // iOS unchecked: semi-transparent gray — visible in both light and dark
36
- background-color: var(--fui-bg-active, $fui-bg-active);
37
- overflow: hidden;
38
- transition: background-color var(--fui-transition-normal, $fui-transition-normal);
34
+ border: 1px solid var(--fui-field-border, $fui-border-strong);
35
+ background-color: var(--fui-field-bg, $fui-bg-elevated);
36
+ box-shadow: inset 0 1px 0 color-mix(in srgb, var(--fui-bg-primary, $fui-bg-primary) 45%, transparent);
37
+ transition:
38
+ background-color var(--fui-transition-normal, $fui-transition-normal),
39
+ border-color var(--fui-transition-normal, $fui-transition-normal),
40
+ box-shadow var(--fui-transition-normal, $fui-transition-normal);
39
41
 
40
- // Checked: accent fill
41
42
  .root[data-checked] & {
42
- background-color: var(--fui-color-accent, $fui-color-accent);
43
+ background-color: var(--fui-field-selection-bg, $fui-bg-hover);
44
+ border-color: var(--fui-field-selection-border, $fui-color-accent);
43
45
  }
44
46
 
45
- // Hover (unchecked): slightly more opaque
46
47
  .root:not([data-disabled]):not([data-checked]) &:hover {
47
- background-color: var(--fui-border, $fui-border);
48
+ border-color: var(--fui-field-border-hover, $fui-text-tertiary);
48
49
  }
49
50
 
50
- // Hover (checked): accent hover
51
51
  .root:not([data-disabled])[data-checked] &:hover {
52
- background-color: var(--fui-color-accent-hover, $fui-color-accent-hover);
52
+ background-color: var(--fui-field-selection-bg-hover, $fui-bg-hover);
53
53
  }
54
54
 
55
- // Focus ring (consistent box-shadow approach across all form controls)
56
55
  .root:focus-visible & {
57
- @include focus-ring;
56
+ @include field-shell-focus;
58
57
  }
59
58
  }
60
59
 
@@ -97,10 +96,6 @@
97
96
  );
98
97
  }
99
98
 
100
- // iOS-style thumb: always white, multi-layer shadow.
101
- // The thumb is intentionally always white (#fff) to match iOS toggle UX — this is
102
- // a hardcoded design decision (like CodeBlock/Tooltip dark surfaces). Override via
103
- // --fui-toggle-thumb-bg and --fui-toggle-thumb-shadow CSS custom properties.
104
99
  .thumb {
105
100
  position: absolute;
106
101
  top: var(--_toggle-inset, $fui-toggle-thumb-offset);
@@ -110,32 +105,23 @@
110
105
  height: var(--_toggle-thumb-size, $fui-toggle-thumb-md);
111
106
  border-radius: 50%;
112
107
  pointer-events: none;
113
- background-color: var(--fui-toggle-thumb-bg, #fff);
114
- box-shadow: var(
115
- --fui-toggle-thumb-shadow,
116
- 0 3px 8px rgba(0, 0, 0, 0.15),
117
- 0 1px 1px rgba(0, 0, 0, 0.06),
118
- 0 3px 1px rgba(0, 0, 0, 0.06)
119
- );
108
+ background-color: var(--fui-bg-primary, $fui-bg-primary);
109
+ border: 1px solid color-mix(in srgb, var(--fui-border-strong, $fui-border-strong) 55%, transparent);
110
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.12);
120
111
  transition:
121
112
  transform var(--fui-transition-normal, $fui-transition-normal),
122
113
  background-color var(--fui-transition-normal, $fui-transition-normal),
123
- width var(--fui-transition-fast, $fui-transition-fast);
114
+ border-color var(--fui-transition-normal, $fui-transition-normal),
115
+ box-shadow var(--fui-transition-fast, $fui-transition-fast);
124
116
 
125
- // Slide right when checked — inverse color for contrast against accent
126
117
  .root[data-checked] & {
127
118
  transform: translateX(var(--_toggle-translate, 0));
128
- background-color: var(--fui-text-inverse, $fui-text-inverse);
119
+ background-color: var(--fui-color-accent, $fui-color-accent);
120
+ border-color: color-mix(in srgb, var(--fui-color-accent, $fui-color-accent) 60%, transparent);
129
121
  }
130
122
 
131
- // Press: stretch thumb wider (iOS 17+)
132
123
  .root:active:not([data-disabled]) & {
133
- width: calc(var(--_toggle-thumb-size, #{$fui-toggle-thumb-md}) + 4px);
134
- }
135
-
136
- // Press + checked: compensate translate for wider thumb
137
- .root:active:not([data-disabled])[data-checked] & {
138
- transform: translateX(calc(var(--_toggle-translate, 0) - 4px));
124
+ box-shadow: 0 2px 7px rgba(0, 0, 0, 0.16);
139
125
  }
140
126
  }
141
127
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  import * as React from 'react';
4
4
  import { Switch as BaseSwitch } from '@base-ui/react/switch';
5
+ import { mergeAriaIds } from '../../utils/aria';
5
6
  import styles from './Toggle.module.scss';
6
7
 
7
8
  /**
@@ -56,6 +57,9 @@ const SwitchRoot = React.forwardRef<HTMLButtonElement, SwitchProps>(
56
57
  ref
57
58
  ) {
58
59
  const resolvedHelperText = helperText ?? description;
60
+ const generatedId = React.useId();
61
+ const resolvedId = id ?? `switch-${generatedId}`;
62
+ const helperId = resolvedHelperText ? `${resolvedId}-helper` : undefined;
59
63
  const trackClasses = [
60
64
  styles.track,
61
65
  size === 'sm' ? styles.trackSm : size === 'lg' ? styles.trackLg : styles.trackMd,
@@ -89,7 +93,7 @@ const SwitchRoot = React.forwardRef<HTMLButtonElement, SwitchProps>(
89
93
  return (
90
94
  <BaseSwitch.Root
91
95
  ref={ref}
92
- id={id}
96
+ id={resolvedId}
93
97
  checked={checked}
94
98
  defaultChecked={defaultChecked}
95
99
  onCheckedChange={onCheckedChange ?? onChange}
@@ -98,7 +102,7 @@ const SwitchRoot = React.forwardRef<HTMLButtonElement, SwitchProps>(
98
102
  className={rootClasses}
99
103
  aria-label={ariaLabel}
100
104
  aria-labelledby={ariaLabelledBy}
101
- aria-describedby={ariaDescribedBy}
105
+ aria-describedby={mergeAriaIds(ariaDescribedBy, helperId)}
102
106
  >
103
107
  <span className={trackClasses} aria-hidden="true">
104
108
  <BaseSwitch.Thumb className={thumbClasses} />
@@ -107,7 +111,11 @@ const SwitchRoot = React.forwardRef<HTMLButtonElement, SwitchProps>(
107
111
  {(label || resolvedHelperText) && (
108
112
  <div className={styles.content}>
109
113
  {label && <span className={labelClasses}>{label}</span>}
110
- {resolvedHelperText && <span className={helperClasses}>{resolvedHelperText}</span>}
114
+ {resolvedHelperText && (
115
+ <span id={helperId} className={helperClasses}>
116
+ {resolvedHelperText}
117
+ </span>
118
+ )}
111
119
  </div>
112
120
  )}
113
121
  </BaseSwitch.Root>
@@ -24,19 +24,20 @@
24
24
  // ============================================
25
25
 
26
26
  .default {
27
- background-color: var(--fui-bg-secondary, $fui-bg-secondary);
28
- border-radius: var(--fui-radius-md, $fui-radius-md);
27
+ @include form-group-surface;
28
+
29
29
  padding: calc(var(--fui-space-px, $fui-space-px) * 2);
30
30
  gap: calc(var(--fui-space-px, $fui-space-px) * 2);
31
31
 
32
32
  .item {
33
33
  background-color: transparent;
34
- border: none;
34
+ border: 1px solid transparent;
35
35
  border-radius: calc(var(--fui-radius-md, $fui-radius-md) - 2px);
36
36
 
37
37
  &.selected {
38
- background-color: var(--fui-bg-primary, $fui-bg-primary);
39
- box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
38
+ background-color: var(--fui-field-bg, $fui-bg-elevated);
39
+ border-color: var(--fui-field-selection-border, $fui-color-accent);
40
+ box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--fui-field-selection-border, $fui-color-accent) 55%, transparent);
40
41
  }
41
42
  }
42
43
  }
@@ -47,17 +48,17 @@
47
48
 
48
49
  .pills {
49
50
  .item {
50
- background-color: transparent;
51
- border: 1px solid transparent;
51
+ background-color: var(--fui-field-bg, $fui-bg-elevated);
52
+ border: 1px solid var(--fui-form-group-border, $fui-border);
52
53
  border-radius: var(--fui-radius-md, $fui-radius-md);
53
54
 
54
55
  &:hover:not(:disabled):not(.selected) {
55
- background-color: var(--fui-bg-secondary, $fui-bg-secondary);
56
+ background-color: var(--fui-bg-hover, $fui-bg-hover);
57
+ border-color: var(--fui-field-border-hover, $fui-text-tertiary);
56
58
  }
57
59
 
58
60
  &.selected {
59
- background-color: var(--fui-bg-secondary, $fui-bg-secondary);
60
- border-color: var(--fui-border, $fui-border);
61
+ @include popup-item-selected;
61
62
  }
62
63
  }
63
64
  }
@@ -69,17 +70,16 @@
69
70
  .outline {
70
71
  .item {
71
72
  background-color: transparent;
72
- border: 1px solid var(--fui-border, $fui-border);
73
+ border: 1px solid var(--fui-field-border, $fui-border);
73
74
  border-radius: var(--fui-radius-md, $fui-radius-md);
74
75
 
75
76
  &:hover:not(:disabled):not(.selected) {
76
- background-color: var(--fui-bg-secondary, $fui-bg-secondary);
77
+ background-color: var(--fui-bg-hover, $fui-bg-hover);
78
+ border-color: var(--fui-field-border-hover, $fui-text-tertiary);
77
79
  }
78
80
 
79
81
  &.selected {
80
- background-color: var(--fui-bg-secondary, $fui-bg-secondary);
81
- border-color: var(--fui-color-accent, $fui-color-accent);
82
- color: var(--fui-color-accent, $fui-color-accent);
82
+ @include popup-item-selected;
83
83
  }
84
84
  }
85
85
  }
@@ -101,7 +101,11 @@
101
101
  color: var(--fui-text-secondary, $fui-text-secondary);
102
102
  white-space: nowrap;
103
103
  user-select: none;
104
- transition: all 150ms ease;
104
+ transition:
105
+ background-color var(--fui-transition-fast, $fui-transition-fast),
106
+ border-color var(--fui-transition-fast, $fui-transition-fast),
107
+ box-shadow var(--fui-transition-fast, $fui-transition-fast),
108
+ color var(--fui-transition-fast, $fui-transition-fast);
105
109
 
106
110
  &.selected {
107
111
  color: var(--fui-text-primary, $fui-text-primary);
@@ -119,7 +123,7 @@
119
123
 
120
124
  .size-sm {
121
125
  .item {
122
- min-height: 28px;
126
+ min-height: var(--fui-button-height-sm, $fui-button-height-sm);
123
127
  padding: 0 var(--fui-space-2, $fui-space-2);
124
128
  font-size: var(--fui-font-size-xs, $fui-font-size-xs);
125
129
  }
@@ -127,7 +131,7 @@
127
131
 
128
132
  .size-md {
129
133
  .item {
130
- min-height: 32px;
134
+ min-height: var(--fui-button-height-md, $fui-button-height-md);
131
135
  padding: 0 var(--fui-space-3, $fui-space-3);
132
136
  font-size: var(--fui-font-size-sm, $fui-font-size-sm);
133
137
  }
@@ -135,7 +139,7 @@
135
139
 
136
140
  .size-lg {
137
141
  .item {
138
- min-height: 40px;
142
+ min-height: var(--fui-button-height-lg, $fui-button-height-lg);
139
143
  padding: 0 var(--fui-space-4, $fui-space-4);
140
144
  font-size: var(--fui-font-size-base, $fui-font-size-base);
141
145
  }
package/src/index.ts CHANGED
@@ -279,6 +279,7 @@ export {
279
279
  Fieldset,
280
280
  type FieldsetProps,
281
281
  type FieldsetLegendProps,
282
+ type FieldsetDescriptionProps,
282
283
  } from './components/Fieldset';
283
284
 
284
285
  // Form
@@ -334,6 +335,7 @@ export {
334
335
  type HeaderSearchProps,
335
336
  type HeaderActionsProps,
336
337
  type HeaderTriggerProps,
338
+ type HeaderMobileNavProps,
337
339
  } from './components/Header';
338
340
 
339
341
  // AppShell
@@ -148,7 +148,10 @@ $fui-info: #3b82f6 !default;
148
148
  }
149
149
 
150
150
  :where(:focus-visible) {
151
- outline: var(--fui-focus-ring-width, 2px) solid var(--fui-focus-ring-color, #18181b);
151
+ outline: 1px solid color-mix(in srgb, var(--fui-focus-ring-color, #18181b) 24%, transparent);
152
152
  outline-offset: var(--fui-focus-ring-offset, 2px);
153
+ box-shadow:
154
+ 0 0 0 calc(var(--fui-focus-ring-width, 2px) + 1px) color-mix(in srgb, var(--fui-focus-ring-color, #18181b) 10%, transparent),
155
+ 0 10px 24px -16px color-mix(in srgb, var(--fui-focus-ring-color, #18181b) 18%, transparent);
153
156
  }
154
157
  }
@@ -8,8 +8,9 @@
8
8
  @mixin focus-ring {
9
9
  outline: none;
10
10
  box-shadow:
11
- 0 0 0 var(--fui-focus-ring-offset, #{$fui-focus-ring-offset}) var(--fui-bg-primary, #{$fui-bg-primary}),
12
- 0 0 0 calc(var(--fui-focus-ring-offset, #{$fui-focus-ring-offset}) + var(--fui-focus-ring-width, #{$fui-focus-ring-width})) var(--fui-focus-ring-color, #{$fui-focus-ring-color});
11
+ 0 0 0 1px color-mix(in srgb, var(--fui-focus-ring-color, #{$fui-focus-ring-color}) 28%, transparent),
12
+ 0 0 0 calc(var(--fui-focus-ring-width, #{$fui-focus-ring-width}) + 1px) color-mix(in srgb, var(--fui-focus-ring-color, #{$fui-focus-ring-color}) 12%, transparent),
13
+ 0 10px 24px -16px color-mix(in srgb, var(--fui-focus-ring-color, #{$fui-focus-ring-color}) 32%, transparent);
13
14
  }
14
15
 
15
16
  // Reset button styles
@@ -139,6 +140,48 @@
139
140
  letter-spacing: 0.05em;
140
141
  }
141
142
 
143
+ // Shared shell for field-like controls.
144
+ @mixin field-shell {
145
+ background-color: var(--fui-field-bg, var(--fui-bg-elevated, #{$fui-bg-elevated}));
146
+ border: 1px solid var(--fui-field-border, var(--fui-border-strong, #{$fui-border-strong}));
147
+ border-radius: var(--fui-radius-md, #{$fui-radius-md});
148
+ transition:
149
+ background-color var(--fui-transition-fast, #{$fui-transition-fast}),
150
+ border-color var(--fui-transition-fast, #{$fui-transition-fast}),
151
+ box-shadow var(--fui-transition-fast, #{$fui-transition-fast}),
152
+ color var(--fui-transition-fast, #{$fui-transition-fast});
153
+
154
+ &:hover:not(:disabled):not([data-disabled]):not(:focus-visible) {
155
+ border-color: var(--fui-field-border-hover, var(--fui-text-tertiary, #{$fui-text-tertiary}));
156
+ }
157
+
158
+ &:disabled,
159
+ &[data-disabled] {
160
+ background-color: var(--fui-field-bg-disabled, var(--fui-bg-tertiary, #{$fui-bg-tertiary}));
161
+ color: var(--fui-text-tertiary, #{$fui-text-tertiary});
162
+ }
163
+ }
164
+
165
+ @mixin field-shell-focus {
166
+ @include focus-ring;
167
+ border-color: var(
168
+ --fui-field-border-focus,
169
+ color-mix(in srgb, var(--fui-focus-ring-color, #{$fui-focus-ring-color}) 20%, var(--fui-border-strong, #{$fui-border-strong}))
170
+ );
171
+ }
172
+
173
+ @mixin popup-item-selected {
174
+ background-color: var(--fui-field-selection-bg, var(--fui-bg-hover, #{$fui-bg-hover}));
175
+ color: var(--fui-field-selection-color, var(--fui-text-primary, #{$fui-text-primary}));
176
+ box-shadow: inset 0 0 0 1px var(--fui-field-selection-border, var(--fui-color-accent, #{$fui-color-accent}));
177
+ }
178
+
179
+ @mixin form-group-surface {
180
+ background-color: var(--fui-form-group-bg, var(--fui-bg-secondary, #{$fui-bg-secondary}));
181
+ border: 1px solid var(--fui-form-group-border, var(--fui-border, #{$fui-border}));
182
+ border-radius: var(--fui-radius-lg, #{$fui-radius-lg});
183
+ }
184
+
142
185
  // Responsive breakpoint mixins (mobile-first, min-width)
143
186
  @mixin breakpoint-sm { @media (min-width: $fui-breakpoint-sm) { @content; } }
144
187
  @mixin breakpoint-md { @media (min-width: $fui-breakpoint-md) { @content; } }
@@ -240,12 +283,22 @@
240
283
  }
241
284
  }
242
285
 
286
+ // Focus ring for success states (e.g., validated form fields)
287
+ @mixin focus-ring-success {
288
+ outline: none;
289
+ box-shadow:
290
+ 0 0 0 1px color-mix(in srgb, var(--fui-color-success, #{$fui-color-success}) 42%, transparent),
291
+ 0 0 0 calc(var(--fui-focus-ring-width, #{$fui-focus-ring-width}) + 1px) color-mix(in srgb, var(--fui-color-success, #{$fui-color-success}) 14%, transparent),
292
+ 0 10px 24px -16px color-mix(in srgb, var(--fui-color-success, #{$fui-color-success}) 28%, transparent);
293
+ }
294
+
243
295
  // Focus ring for error states (e.g., invalid form fields)
244
296
  @mixin focus-ring-error {
245
297
  outline: none;
246
298
  box-shadow:
247
- 0 0 0 var(--fui-focus-ring-offset, #{$fui-focus-ring-offset}) var(--fui-bg-primary, #{$fui-bg-primary}),
248
- 0 0 0 calc(var(--fui-focus-ring-offset, #{$fui-focus-ring-offset}) + var(--fui-focus-ring-width, #{$fui-focus-ring-width})) var(--fui-color-danger, #{$fui-color-danger});
299
+ 0 0 0 1px color-mix(in srgb, var(--fui-color-danger, #{$fui-color-danger}) 42%, transparent),
300
+ 0 0 0 calc(var(--fui-focus-ring-width, #{$fui-focus-ring-width}) + 1px) color-mix(in srgb, var(--fui-color-danger, #{$fui-color-danger}) 14%, transparent),
301
+ 0 10px 24px -16px color-mix(in srgb, var(--fui-color-danger, #{$fui-color-danger}) 28%, transparent);
249
302
  }
250
303
 
251
304
  // ============================================
@@ -505,6 +505,19 @@ $fui-dark-tooltip-shadow:
505
505
  --fui-border-default: light-dark(#{$fui-border-default}, #{$fui-dark-border-default});
506
506
  --fui-border-strong: light-dark(#{$fui-border-strong}, #{$fui-dark-border-strong});
507
507
 
508
+ // Form chrome (derived from theme surfaces + seed-driven accent)
509
+ --fui-field-bg: color-mix(in srgb, var(--fui-bg-elevated) 92%, var(--fui-bg-primary));
510
+ --fui-field-bg-disabled: color-mix(in srgb, var(--fui-bg-tertiary) 88%, var(--fui-bg-primary));
511
+ --fui-field-border: color-mix(in srgb, var(--fui-border-strong) 78%, var(--fui-bg-primary));
512
+ --fui-field-border-hover: color-mix(in srgb, var(--fui-text-tertiary) 58%, var(--fui-border-strong));
513
+ --fui-field-border-focus: color-mix(in srgb, var(--fui-focus-ring-color) 18%, var(--fui-border-strong));
514
+ --fui-field-selection-bg: color-mix(in srgb, var(--fui-color-accent) 12%, var(--fui-bg-elevated));
515
+ --fui-field-selection-bg-hover: color-mix(in srgb, var(--fui-color-accent) 16%, var(--fui-bg-hover));
516
+ --fui-field-selection-border: color-mix(in srgb, var(--fui-color-accent) 38%, var(--fui-border));
517
+ --fui-field-selection-color: var(--fui-text-primary);
518
+ --fui-form-group-bg: color-mix(in srgb, var(--fui-bg-secondary) 58%, var(--fui-bg-primary));
519
+ --fui-form-group-border: color-mix(in srgb, var(--fui-border) 82%, transparent);
520
+
508
521
  // Shadows (not <color> type — light-dark() can't be used, handled in dark overrides)
509
522
  --fui-shadow-sm: #{$fui-shadow-sm};
510
523
  --fui-shadow-md: #{$fui-shadow-md};
@@ -559,7 +572,8 @@ $fui-dark-tooltip-shadow:
559
572
  }
560
573
 
561
574
  // Light mode explicit override (for manual toggle back to light)
562
- :root[data-theme="light"] {
575
+ :root[data-theme="light"],
576
+ :root.light {
563
577
  color-scheme: light;
564
578
  }
565
579
 
@@ -694,6 +708,11 @@ $fui-dark-tooltip-shadow:
694
708
  // Ensure minimum contrast on interactive elements
695
709
  --fui-bg-hover: rgba(0, 0, 0, 0.12);
696
710
  --fui-bg-active: rgba(0, 0, 0, 0.18);
711
+ --fui-field-border: var(--fui-text-primary);
712
+ --fui-field-border-hover: var(--fui-text-primary);
713
+ --fui-field-border-focus: var(--fui-color-accent);
714
+ --fui-field-selection-border: var(--fui-color-accent);
715
+ --fui-form-group-border: var(--fui-text-primary);
697
716
  }
698
717
 
699
718
  // Private mixin for dark mode token values