@mrmeg/expo-ui 0.27.1 → 0.29.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 (155) hide show
  1. package/CHANGELOG.md +666 -0
  2. package/LLM_USAGE.md +241 -37
  3. package/README.md +591 -136
  4. package/bin/expo-ui-icons.mjs +509 -0
  5. package/dist/components/Accordion.js +12 -6
  6. package/dist/components/AnimatedView.d.ts +1 -1
  7. package/dist/components/AnimatedView.js +5 -2
  8. package/dist/components/Avatar.d.ts +1 -1
  9. package/dist/components/Avatar.js +2 -2
  10. package/dist/components/Badge.js +5 -1
  11. package/dist/components/BottomSheet.d.ts +29 -1
  12. package/dist/components/BottomSheet.js +107 -30
  13. package/dist/components/Button.d.ts +20 -4
  14. package/dist/components/Button.js +60 -43
  15. package/dist/components/Card.d.ts +1 -1
  16. package/dist/components/Card.js +24 -4
  17. package/dist/components/Carousel.js +2 -2
  18. package/dist/components/Checkbox.js +51 -44
  19. package/dist/components/Collapsible.js +7 -3
  20. package/dist/components/Dialog.js +87 -16
  21. package/dist/components/Drawer.d.ts +1 -1
  22. package/dist/components/Drawer.js +65 -55
  23. package/dist/components/DropdownMenu.js +11 -7
  24. package/dist/components/EmptyState.d.ts +2 -2
  25. package/dist/components/EmptyState.js +5 -2
  26. package/dist/components/Hydrated.d.ts +31 -0
  27. package/dist/components/Hydrated.js +30 -0
  28. package/dist/components/Icon.d.ts +71 -28
  29. package/dist/components/Icon.js +70 -28
  30. package/dist/components/IconGlyph.d.ts +15 -0
  31. package/dist/components/IconGlyph.js +32 -0
  32. package/dist/components/IconGlyph.native.d.ts +10 -0
  33. package/dist/components/IconGlyph.native.js +30 -0
  34. package/dist/components/InputOTP.js +5 -2
  35. package/dist/components/Item.d.ts +103 -4
  36. package/dist/components/Item.js +168 -15
  37. package/dist/components/KeyboardAvoidingView.d.ts +1 -1
  38. package/dist/components/KeyboardAvoidingView.js +4 -1
  39. package/dist/components/Label.js +3 -2
  40. package/dist/components/MaxWidthContainer.js +1 -1
  41. package/dist/components/Notification.js +45 -27
  42. package/dist/components/Popover.d.ts +18 -5
  43. package/dist/components/Popover.js +72 -8
  44. package/dist/components/Progress.js +8 -6
  45. package/dist/components/RadioGroup.js +52 -49
  46. package/dist/components/Screen.d.ts +61 -0
  47. package/dist/components/Screen.js +62 -0
  48. package/dist/components/SectionHeader.d.ts +1 -1
  49. package/dist/components/SegmentedControl.d.ts +12 -4
  50. package/dist/components/SegmentedControl.js +94 -4
  51. package/dist/components/Select.js +21 -26
  52. package/dist/components/Skeleton.js +24 -10
  53. package/dist/components/Slider.d.ts +11 -3
  54. package/dist/components/Slider.js +73 -6
  55. package/dist/components/StatCard.d.ts +4 -2
  56. package/dist/components/StatCard.js +3 -1
  57. package/dist/components/StyledText.d.ts +7 -1
  58. package/dist/components/StyledText.js +5 -3
  59. package/dist/components/Switch.d.ts +1 -1
  60. package/dist/components/Switch.js +20 -30
  61. package/dist/components/Tabs.d.ts +2 -2
  62. package/dist/components/Tabs.js +13 -24
  63. package/dist/components/TextInput.d.ts +1 -1
  64. package/dist/components/TextInput.js +71 -26
  65. package/dist/components/ThemeSelector.d.ts +52 -0
  66. package/dist/components/ThemeSelector.js +90 -0
  67. package/dist/components/Toggle.d.ts +5 -49
  68. package/dist/components/Toggle.js +53 -29
  69. package/dist/components/ToggleGroup.d.ts +4 -8
  70. package/dist/components/ToggleGroup.js +52 -34
  71. package/dist/components/Tooltip.d.ts +1 -1
  72. package/dist/components/Tooltip.js +9 -3
  73. package/dist/components/UIProvider.d.ts +22 -2
  74. package/dist/components/UIProvider.js +15 -3
  75. package/dist/components/bottomSheetDismiss.d.ts +19 -0
  76. package/dist/components/bottomSheetDismiss.js +44 -0
  77. package/dist/components/iconBuiltins.generated.d.ts +143 -0
  78. package/dist/components/iconBuiltins.generated.js +25 -0
  79. package/dist/components/iconTypes.d.ts +37 -0
  80. package/dist/components/iconTypes.js +1 -0
  81. package/dist/components/index.d.ts +47 -44
  82. package/dist/components/index.js +3 -0
  83. package/dist/components/keyboardDismiss.js +12 -4
  84. package/dist/components/nativeTextField.android.js +13 -9
  85. package/dist/constants/colors.d.ts +40 -5
  86. package/dist/constants/colors.js +26 -8
  87. package/dist/constants/fonts.d.ts +41 -2
  88. package/dist/constants/fonts.js +46 -7
  89. package/dist/constants/index.d.ts +5 -4
  90. package/dist/constants/index.js +1 -0
  91. package/dist/constants/interaction.d.ts +18 -0
  92. package/dist/constants/interaction.js +18 -0
  93. package/dist/constants/spacing.d.ts +2 -1
  94. package/dist/constants/spacing.js +2 -1
  95. package/dist/design-system.json +429 -132
  96. package/dist/hooks/index.d.ts +12 -7
  97. package/dist/hooks/index.js +5 -0
  98. package/dist/hooks/useDimensions.d.ts +13 -8
  99. package/dist/hooks/useDimensions.js +123 -48
  100. package/dist/hooks/useFocusVisible.d.ts +30 -0
  101. package/dist/hooks/useFocusVisible.js +45 -0
  102. package/dist/hooks/useFontStyle.d.ts +2 -2
  103. package/dist/hooks/useFontStyle.js +3 -2
  104. package/dist/hooks/useHydrated.d.ts +13 -0
  105. package/dist/hooks/useHydrated.js +19 -0
  106. package/dist/hooks/useNotificationOffset.d.ts +20 -0
  107. package/dist/hooks/useNotificationOffset.js +34 -0
  108. package/dist/hooks/useResources.d.ts +21 -20
  109. package/dist/hooks/useResources.js +72 -68
  110. package/dist/hooks/useScalePress.d.ts +8 -5
  111. package/dist/hooks/useScalePress.js +16 -9
  112. package/dist/hooks/useShape.d.ts +24 -0
  113. package/dist/hooks/useShape.js +24 -0
  114. package/dist/hooks/useStaggeredEntrance.js +9 -7
  115. package/dist/hooks/useTheme.d.ts +3 -3
  116. package/dist/hooks/useTheme.js +177 -66
  117. package/dist/hooks/useWindowInsets.d.ts +13 -0
  118. package/dist/hooks/useWindowInsets.js +23 -0
  119. package/dist/icons/LICENSE.lucide +43 -0
  120. package/dist/icons/lucide.json +2144 -0
  121. package/dist/index.d.ts +5 -5
  122. package/dist/lib/composeRefs.d.ts +8 -0
  123. package/dist/lib/composeRefs.js +20 -0
  124. package/dist/lib/haptics.d.ts +12 -0
  125. package/dist/lib/haptics.js +21 -0
  126. package/dist/lib/index.d.ts +6 -5
  127. package/dist/lib/index.js +1 -0
  128. package/dist/lib/interFonts.d.ts +15 -0
  129. package/dist/lib/interFonts.js +1 -0
  130. package/dist/lib/interFonts.native.d.ts +3 -0
  131. package/dist/lib/interFonts.native.js +20 -0
  132. package/dist/lib/popoverPlacement.d.ts +36 -0
  133. package/dist/lib/popoverPlacement.js +29 -0
  134. package/dist/lib/themedStyles.d.ts +1 -1
  135. package/dist/lib/useAnimatedValue.d.ts +15 -0
  136. package/dist/lib/useAnimatedValue.js +19 -0
  137. package/dist/lib/webHitTarget.d.ts +40 -0
  138. package/dist/lib/webHitTarget.js +37 -0
  139. package/dist/state/feedbackStore.d.ts +24 -0
  140. package/dist/state/feedbackStore.js +16 -0
  141. package/dist/state/globalUIStore.d.ts +27 -0
  142. package/dist/state/globalUIStore.js +23 -0
  143. package/dist/state/index.d.ts +7 -5
  144. package/dist/state/index.js +2 -0
  145. package/dist/state/initialScheme.d.ts +27 -0
  146. package/dist/state/initialScheme.js +29 -0
  147. package/dist/state/notify.d.ts +1 -1
  148. package/dist/state/themeColorScope.d.ts +1 -1
  149. package/dist/state/themeStore.d.ts +69 -16
  150. package/dist/state/themeStore.js +65 -38
  151. package/llms-full.md +59 -10
  152. package/llms.txt +10 -1
  153. package/package.json +79 -4
  154. package/dist/components/iconRegistry.generated.d.ts +0 -148
  155. package/dist/components/iconRegistry.generated.js +0 -296
@@ -1,10 +1,12 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useEffect, useCallback, useImperativeHandle, useMemo, useRef, useState, } from "react";
2
+ import { useEffect, useCallback, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, } from "react";
3
3
  import { StyleSheet, TextInput as RNTextInput, Platform, View, Pressable, } from "react-native";
4
4
  import { Host, useNativeState, } from "@expo/ui";
5
5
  import { NativeTextField } from "./nativeTextField";
6
6
  import { useTheme } from "../hooks/useTheme.js";
7
+ import { shapeRadius, useShape } from "../hooks/useShape.js";
7
8
  import { spacing } from "../constants/spacing.js";
9
+ import { interaction } from "../constants/interaction.js";
8
10
  import { useFontStyle } from "../hooks/useFontStyle.js";
9
11
  import { StyledText } from "./StyledText.js";
10
12
  import { Icon } from "./Icon.js";
@@ -15,6 +17,21 @@ import { palette } from "../constants/colors.js";
15
17
  import { clearKeyboardFocusedInput, setKeyboardFocusedInput, } from "./keyboardFocusRegistry.js";
16
18
  import { useTextInputSurfaceResponder } from "./keyboardDismiss.js";
17
19
  const NUMERIC_REGEX = /^[0-9]*$/;
20
+ /**
21
+ * Write the `@expo/ui` observable text buffer. `set()` is the React
22
+ * Compiler-friendly writer (`@expo/ui` >= 56.0.17); earlier releases in the
23
+ * peer range only have the `value` setter, which calls the same native
24
+ * `setValue`. Kept outside the component so the compiler doesn't see a hook
25
+ * result being mutated, which made it skip `NativeTextInput` entirely.
26
+ */
27
+ function writeNativeText(state, text) {
28
+ if (typeof state.set === "function") {
29
+ state.set(text);
30
+ }
31
+ else {
32
+ state.value = text;
33
+ }
34
+ }
18
35
  /** Platform touch-target guideline (pt); the compact fields sit below it. */
19
36
  const MIN_TOUCH_TARGET = 44;
20
37
  const SIZE_CONFIGS = {
@@ -77,7 +94,7 @@ const SIZE_CONFIGS = {
77
94
  * // With custom elements
78
95
  * <TextInput
79
96
  * label="Search"
80
- * leftElement={<Icon as={Search} size={20} />}
97
+ * leftElement={<Icon name="search" size={20} />}
81
98
  * />
82
99
  * ```
83
100
  */
@@ -97,6 +114,8 @@ export function TextInput(props) {
97
114
  */
98
115
  function WebTextInput({ variant = "outline", size = "md", label, helperText, errorText, error, required, rows, showSecureEntryToggle, leftElement, rightElement, clearable = false, wrapperStyle, focusedStyle, forceLight, secureTextEntry, inputMode, style, onChangeText, onFocus, onBlur, value, multiline, editable = true, ref, ...rest }) {
99
116
  const { theme, getContrastingColor, getFocusRingStyle } = useTheme();
117
+ // Host-app `setShape({ input })`; the underlined variant stays square.
118
+ const inputRadius = variant === "underlined" ? undefined : shapeRadius(useShape("input"));
100
119
  const styles = themedStyles(theme)[variant][size];
101
120
  const inputFont = useFontStyle("regular");
102
121
  const [focused, setFocused] = useState(false);
@@ -153,8 +172,9 @@ function WebTextInput({ variant = "outline", size = "md", label, helperText, err
153
172
  const togglePasswordVisible = () => {
154
173
  setPasswordVisible(v => !v);
155
174
  };
156
- return (_jsxs(View, { style: wrapperStyle, children: [!!label && (_jsx(View, { style: styles.labelContainer, children: _jsxs(StyledText, { selectable: false, size: "base", fontWeight: "medium", style: styles.label, children: [label, required && _jsx(StyledText, { selectable: false, fontWeight: "bold", style: styles.required, children: " *" })] }) })), _jsxs(View, { style: [styles.wrapper, focused && getFocusRingStyle()], children: [leftElement && _jsx(View, { style: styles.leftElement, children: leftElement }), _jsx(RNTextInput, { ref: ref, ...rest, editable: editable, inputMode: inputMode || "text", multiline: multiline, numberOfLines: rows, secureTextEntry: secureTextEntry && !passwordVisible, onChangeText: inputMode === "numeric" ? handleNumericChange : handleTextChange, onFocus: showInputFocusRing, onBlur: hideInputFocusRing, onContentSizeChange: (e) => setContentHeight(e.nativeEvent.contentSize.height), scrollEnabled: shouldScroll, placeholderTextColor: theme.colors.textDim, style: [
175
+ return (_jsxs(View, { style: wrapperStyle, children: [!!label && (_jsx(View, { style: styles.labelContainer, children: _jsxs(StyledText, { selectable: false, size: "base", fontWeight: "medium", style: styles.label, children: [label, required && _jsx(StyledText, { selectable: false, fontWeight: "bold", style: styles.required, children: " *" })] }) })), _jsxs(View, { style: [styles.wrapper, inputRadius, focused && getFocusRingStyle()], children: [leftElement && _jsx(View, { style: styles.leftElement, children: leftElement }), _jsx(RNTextInput, { ref: ref, ...rest, editable: editable, inputMode: inputMode || "text", multiline: multiline, numberOfLines: rows, secureTextEntry: secureTextEntry && !passwordVisible, onChangeText: inputMode === "numeric" ? handleNumericChange : handleTextChange, onFocus: showInputFocusRing, onBlur: hideInputFocusRing, onContentSizeChange: (e) => setContentHeight(e.nativeEvent.contentSize.height), scrollEnabled: shouldScroll, placeholderTextColor: theme.colors.textDim, style: [
157
176
  styles.input,
177
+ inputRadius,
158
178
  // Resolved through the theme store so `setFonts` overrides apply;
159
179
  // identical to the old hardcoded regular family by default.
160
180
  inputFont,
@@ -181,7 +201,7 @@ function WebTextInput({ variant = "outline", size = "md", label, helperText, err
181
201
  }, accessibilityLabel: "Clear input", accessibilityRole: "button", children: _jsx(Icon, { name: "x", size: spacing.iconSm, color: "textDim", decorative: true }) })), showClearButton && rightElement && !hasSecureToggle && (_jsxs(View, { style: styles.rightElements, children: [_jsx(Pressable, { onPress: () => {
182
202
  hapticLight();
183
203
  onChangeText?.("");
184
- }, accessibilityLabel: "Clear input", accessibilityRole: "button", children: _jsx(Icon, { name: "x", size: spacing.iconSm, color: "textDim", decorative: true }) }), rightElement] })), !showClearButton && rightElement && !hasSecureToggle && (_jsx(View, { style: styles.rightElement, children: rightElement })), secureTextEntry && showSecureEntryToggle && (_jsx(Pressable, { style: styles.passwordToggle, onPress: togglePasswordVisible, accessibilityLabel: passwordVisible ? "Hide password" : "Show password", accessibilityRole: "button", children: _jsx(Icon, { name: passwordVisible ? "eye-off" : "eye", size: spacing.iconSm + 4, color: "textDim" }) })), showErrorIcon && (_jsx(View, { style: [styles.errorIcon, { pointerEvents: "none" }], accessibilityLabel: "Error", children: _jsx(Icon, { name: "circle-alert", size: spacing.iconSm, color: "destructive", decorative: true }) }))] }), !!(helperText || errorText) && (_jsx(StyledText, { selectable: false, size: "sm", style: [
204
+ }, accessibilityLabel: "Clear input", accessibilityRole: "button", children: _jsx(Icon, { name: "x", size: spacing.iconSm, color: "textDim", decorative: true }) }), rightElement] })), !showClearButton && rightElement && !hasSecureToggle && (_jsx(View, { style: styles.rightElement, children: rightElement })), secureTextEntry && showSecureEntryToggle && (_jsx(Pressable, { style: styles.passwordToggle, onPress: togglePasswordVisible, accessibilityLabel: passwordVisible ? "Hide password" : "Show password", accessibilityRole: "button", children: _jsx(Icon, { name: passwordVisible ? "eye-off" : "eye", size: spacing.iconSm + 4, color: "textDim", decorative: true }) })), showErrorIcon && (_jsx(View, { style: [styles.errorIcon, { pointerEvents: "none" }], accessibilityLabel: "Error", children: _jsx(Icon, { name: "circle-alert", size: spacing.iconSm, color: "destructive", decorative: true }) }))] }), !!(helperText || errorText) && (_jsx(StyledText, { selectable: false, size: "sm", style: [
185
205
  styles.helperText,
186
206
  hasError && styles.errorText,
187
207
  ], children: errorText || helperText }))] }));
@@ -221,6 +241,7 @@ function NativeTextInput({ variant = "outline", size = "md", label, helperText,
221
241
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
222
242
  leftElement, rightElement, clearable, focusedStyle, style, ...rest }) {
223
243
  const { theme, getContrastingColor } = useTheme();
244
+ const inputShape = useShape("input");
224
245
  const styles = themedStyles(theme)[variant][size];
225
246
  const inputFont = useFontStyle("regular");
226
247
  const [passwordVisible, setPasswordVisible] = useState(false);
@@ -241,8 +262,13 @@ leftElement, rightElement, clearable, focusedStyle, style, ...rest }) {
241
262
  const [handoff, setHandoff] = useState(null);
242
263
  const handoffRef = useRef(null);
243
264
  const isFocusedRef = useRef(false);
265
+ // Latest flavour for the focus/blur/toggle handlers and the handoff timer,
266
+ // synced after each commit (before any native event can be dispatched)
267
+ // instead of written during render.
244
268
  const activeSecureRef = useRef(effectiveSecureTextEntry);
245
- activeSecureRef.current = effectiveSecureTextEntry;
269
+ useLayoutEffect(() => {
270
+ activeSecureRef.current = effectiveSecureTextEntry;
271
+ });
246
272
  // Taps that land while a handoff is in flight are counted and applied (by
247
273
  // parity) once it settles.
248
274
  const queuedTogglesRef = useRef(0);
@@ -250,7 +276,9 @@ leftElement, rightElement, clearable, focusedStyle, style, ...rest }) {
250
276
  // iOS-only per-flavour mount generation. Bumped whenever a flavour becomes
251
277
  // the incoming view so React mounts a FRESH native view (whose `autoFocus`
252
278
  // will run) instead of reusing one still unmounting from the previous handoff.
253
- const generationRef = useRef({ secure: 0, plain: 0 });
279
+ // State, not a ref: it keys the rendered Host, and the bump lands in the same
280
+ // batch as the `passwordVisible` flip that makes that flavour incoming.
281
+ const [generation, setGeneration] = useState({ secure: 0, plain: 0 });
254
282
  // Last text the JS side knows about; see handleChangeText.
255
283
  const lastTextRef = useRef(value ?? defaultValue ?? "");
256
284
  // Armed when the hide toggle hands focus to the SecureField (iOS); see
@@ -263,7 +291,7 @@ leftElement, rightElement, clearable, focusedStyle, style, ...rest }) {
263
291
  // programmatic sets); typing already updated `state` natively.
264
292
  useEffect(() => {
265
293
  if (value !== undefined && value !== state.value) {
266
- state.value = value;
294
+ writeNativeText(state, value);
267
295
  }
268
296
  if (value !== undefined)
269
297
  lastTextRef.current = value;
@@ -348,10 +376,9 @@ leftElement, rightElement, clearable, focusedStyle, style, ...rest }) {
348
376
  restoreAfterSecureHandoffRef.current = false;
349
377
  const outgoingSecure = activeSecureRef.current;
350
378
  if (Platform.OS === "ios") {
351
- if (outgoingSecure)
352
- generationRef.current.plain += 1;
353
- else
354
- generationRef.current.secure += 1;
379
+ setGeneration((current) => outgoingSecure
380
+ ? { ...current, plain: current.plain + 1 }
381
+ : { ...current, secure: current.secure + 1 });
355
382
  }
356
383
  if (Platform.OS === "ios" && isFocusedRef.current) {
357
384
  const timer = setTimeout(() => {
@@ -377,7 +404,11 @@ leftElement, rightElement, clearable, focusedStyle, style, ...rest }) {
377
404
  }
378
405
  setPasswordVisible((v) => !v);
379
406
  }, [activeInput, finishHandoff, focusRegistryToken, parentOnBlur]);
380
- toggleRef.current = togglePasswordVisible;
407
+ // `finishHandoff` replays queued taps through this ref (it is declared
408
+ // before the toggle it calls). Synced after commit, like `activeSecureRef`.
409
+ useLayoutEffect(() => {
410
+ toggleRef.current = togglePasswordVisible;
411
+ }, [togglePasswordVisible]);
381
412
  // iOS clears a secure field's existing text on the first keystroke after it
382
413
  // becomes first responder (text it did not see typed). Right after the hide
383
414
  // toggle hands focus to the SecureField that reads as "type one character,
@@ -396,7 +427,7 @@ leftElement, rightElement, clearable, focusedStyle, style, ...rest }) {
396
427
  const next = wipedByKey ? previous + text : wipedByBackspace ? previous.slice(0, -1) : text;
397
428
  lastTextRef.current = next;
398
429
  if (wiped)
399
- state.value = next;
430
+ writeNativeText(state, next);
400
431
  onChangeText?.(next);
401
432
  }, [onChangeText, state]);
402
433
  useEffect(() => {
@@ -419,13 +450,13 @@ leftElement, rightElement, clearable, focusedStyle, style, ...rest }) {
419
450
  focus: () => activeInput()?.focus(),
420
451
  blur: () => blurAll(),
421
452
  clear: () => {
422
- state.value = "";
453
+ writeNativeText(state, "");
423
454
  lastTextRef.current = "";
424
455
  },
425
456
  isFocused: () => activeInput()?.isFocused() ?? false,
426
457
  setNativeProps: (props) => {
427
458
  if (typeof props?.text === "string") {
428
- state.value = props.text;
459
+ writeNativeText(state, props.text);
429
460
  lastTextRef.current = props.text;
430
461
  }
431
462
  },
@@ -452,9 +483,9 @@ leftElement, rightElement, clearable, focusedStyle, style, ...rest }) {
452
483
  const surfaceStyle = {
453
484
  backgroundColor,
454
485
  borderColor,
455
- borderRadius: variant === "underlined" ? 0 : spacing.radiusMd,
486
+ borderRadius: variant === "underlined" ? 0 : inputShape?.borderRadius ?? spacing.radiusMd,
456
487
  borderWidth: variant === "outline" ? 1 : 0,
457
- opacity: editable === false ? 0.6 : 1,
488
+ opacity: editable === false ? interaction.disabledOpacity : 1,
458
489
  overflow: "hidden",
459
490
  };
460
491
  // Native field: transparent, padding only. The visible surface is drawn by
@@ -503,9 +534,9 @@ leftElement, rightElement, clearable, focusedStyle, style, ...rest }) {
503
534
  : styles.nativeHostHandoff, pointerEvents: inLayout ? "auto" : "none", accessibilityElementsHidden: !inLayout, importantForAccessibility: inLayout ? "auto" : "no-hide-descendants", children: _jsx(NativeTextField, { ...rest, autoFocus: rest.autoFocus || (handoff != null && active), ref: Platform.OS === "android" ? androidRef : secure ? secureRef : plainRef, value: state, defaultValue: defaultValue, onChangeText: handleChangeText, onFocus: () => handleFocus(secure), onBlur: () => handleBlur(secure), editable: editable, multiline: multiline, rows: rows, inputMode: inputMode, secureTextEntry: secure, placeholderTextColor: theme.colors.textDim, style: boxStyle, textStyle: textStyle }) }, Platform.OS === "android"
504
535
  ? "android"
505
536
  : secure
506
- ? `secure-${generationRef.current.secure}`
507
- : `plain-${generationRef.current.plain}`));
508
- }), hasSecureToggle && (_jsx(Pressable, { style: styles.nativePasswordToggle, onPress: togglePasswordVisible, accessibilityLabel: passwordVisible ? "Hide password" : "Show password", accessibilityRole: "button", children: _jsx(Icon, { name: passwordVisible ? "eye-off" : "eye", size: spacing.iconSm + 4, color: "textDim" }) }))] }), !!(helperText || errorText) && (_jsx(StyledText, { selectable: false, size: "sm", style: [styles.helperText, hasError && styles.errorText], children: errorText || helperText }))] }));
537
+ ? `secure-${generation.secure}`
538
+ : `plain-${generation.plain}`));
539
+ }), hasSecureToggle && (_jsx(Pressable, { style: styles.nativePasswordToggle, onPress: togglePasswordVisible, accessibilityLabel: passwordVisible ? "Hide password" : "Show password", accessibilityRole: "button", children: _jsx(Icon, { name: passwordVisible ? "eye-off" : "eye", size: spacing.iconSm + 4, color: "textDim", decorative: true }) }))] }), !!(helperText || errorText) && (_jsx(StyledText, { selectable: false, size: "sm", style: [styles.helperText, hasError && styles.errorText], children: errorText || helperText }))] }));
509
540
  }
510
541
  const createStyles = (theme, variant, size) => StyleSheet.create({
511
542
  nativeHost: {
@@ -530,6 +561,10 @@ const createStyles = (theme, variant, size) => StyleSheet.create({
530
561
  },
531
562
  nativePasswordToggle: {
532
563
  paddingHorizontal: spacing.xs,
564
+ minWidth: spacing.minTarget,
565
+ minHeight: spacing.minTarget,
566
+ alignItems: "center",
567
+ justifyContent: "center",
533
568
  },
534
569
  wrapper: {
535
570
  width: "100%",
@@ -555,7 +590,7 @@ const createStyles = (theme, variant, size) => StyleSheet.create({
555
590
  borderBottomWidth: 2,
556
591
  },
557
592
  disabled: {
558
- opacity: 0.6,
593
+ opacity: interaction.disabledOpacity,
559
594
  ...(Platform.OS === "web" && { cursor: "not-allowed" }),
560
595
  },
561
596
  error: {
@@ -597,11 +632,17 @@ const createStyles = (theme, variant, size) => StyleSheet.create({
597
632
  transform: [{ translateY: -10 }],
598
633
  zIndex: 1,
599
634
  },
635
+ // The eye/clear buttons draw a 16-20px glyph; the pressable itself is at
636
+ // least `spacing.minTarget` square (WCAG 2.5.8 — `hitSlop` is inert on web).
600
637
  passwordToggle: {
601
638
  position: "absolute",
602
- right: spacing.sm,
639
+ right: spacing.sm - (spacing.minTarget - 20) / 2,
603
640
  top: "50%",
604
- transform: [{ translateY: Platform.OS === "web" ? -10 : -12 }],
641
+ minWidth: spacing.minTarget,
642
+ minHeight: spacing.minTarget,
643
+ alignItems: "center",
644
+ justifyContent: "center",
645
+ transform: [{ translateY: -spacing.minTarget / 2 }],
605
646
  zIndex: 1,
606
647
  ...(Platform.OS === "web" && { cursor: "pointer" }),
607
648
  },
@@ -614,9 +655,13 @@ const createStyles = (theme, variant, size) => StyleSheet.create({
614
655
  },
615
656
  clearButton: {
616
657
  position: "absolute",
617
- right: spacing.sm,
658
+ right: spacing.sm - (spacing.minTarget - 20) / 2,
618
659
  top: "50%",
619
- transform: [{ translateY: Platform.OS === "web" ? -10 : -12 }],
660
+ minWidth: spacing.minTarget,
661
+ minHeight: spacing.minTarget,
662
+ alignItems: "center",
663
+ justifyContent: "center",
664
+ transform: [{ translateY: -spacing.minTarget / 2 }],
620
665
  zIndex: 1,
621
666
  ...(Platform.OS === "web" && { cursor: "pointer" }),
622
667
  },
@@ -633,7 +678,7 @@ const createStyles = (theme, variant, size) => StyleSheet.create({
633
678
  });
634
679
  const VARIANT_KEYS = ["outline", "filled", "underlined"];
635
680
  const SIZE_KEYS = ["sm", "md", "lg"];
636
- const themedStyles = createThemedStyles((theme) => Object.fromEntries(VARIANT_KEYS.map((variant) => [
681
+ const themedStyles = /*#__PURE__*/ createThemedStyles((theme) => Object.fromEntries(VARIANT_KEYS.map((variant) => [
637
682
  variant,
638
683
  Object.fromEntries(SIZE_KEYS.map((size) => [size, createStyles(theme, variant, size)])),
639
684
  ])));
@@ -0,0 +1,52 @@
1
+ import React from "react";
2
+ import { type StyleProp, type ViewStyle } from "react-native";
3
+ import { type ThemePreference } from "../state/themeStore.js";
4
+ /**
5
+ * ThemeSelector — a compact System / Light / Dark switch wired to the kit's
6
+ * theme store.
7
+ *
8
+ * Icon-only by default (about 116pt wide), sized to sit in an `Item` row's
9
+ * `ItemActions` so the whole appearance setting is one row instead of three.
10
+ * Pass `showLabels` for a full-width control with text under a group title.
11
+ *
12
+ * Reads and writes `useThemeStore` unless `value` / `onValueChange` are
13
+ * passed. The package has no i18n: pass translated `labels`, which become the
14
+ * visible text (with `showLabels`) and each segment's accessibility label.
15
+ *
16
+ * Drawn in RN on every platform (no native segmented control), so the icons
17
+ * render on iOS and Android too and the server render matches the client: the
18
+ * store boots `system` on both sides until the persisted preference loads.
19
+ *
20
+ * @example
21
+ * ```tsx
22
+ * <Item>
23
+ * <ItemContent>
24
+ * <ItemTitle>Theme</ItemTitle>
25
+ * </ItemContent>
26
+ * <ItemActions>
27
+ * <ThemeSelector />
28
+ * </ItemActions>
29
+ * </Item>
30
+ * ```
31
+ */
32
+ export type ThemeSelectorLabels = Record<ThemePreference, string>;
33
+ export interface ThemeSelectorProps {
34
+ /** Controlled preference. Defaults to the theme store's `userTheme`. */
35
+ value?: ThemePreference;
36
+ /** Called with the chosen preference. Defaults to the theme store's `setTheme`. */
37
+ onValueChange?: (value: ThemePreference) => void;
38
+ /** Segment labels, for screen readers and `showLabels`. @default English */
39
+ labels?: Partial<ThemeSelectorLabels>;
40
+ /** Show the label beside each icon and stretch to the parent's width. @default false */
41
+ showLabels?: boolean;
42
+ /** Disable interaction. @default false */
43
+ disabled?: boolean;
44
+ /** Names the group for screen readers. Inside an `Item`, defaults to the row title. */
45
+ accessibilityLabel?: string;
46
+ /** Style override for the track. */
47
+ style?: StyleProp<ViewStyle>;
48
+ /** Test id for the track; segments get `${testID}-${value}`. @default "theme-selector" */
49
+ testID?: string;
50
+ }
51
+ declare function ThemeSelector({ value, onValueChange, labels, showLabels, disabled, accessibilityLabel, style, testID, }: ThemeSelectorProps): React.JSX.Element;
52
+ export { ThemeSelector };
@@ -0,0 +1,90 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Platform, Pressable, StyleSheet, View } from "react-native";
3
+ import { useTheme } from "../hooks/useTheme.js";
4
+ import { useFocusVisible } from "../hooks/useFocusVisible.js";
5
+ import { useThemeStore } from "../state/themeStore.js";
6
+ import { hapticSelection } from "../lib/haptics.js";
7
+ import { spacing } from "../constants/spacing.js";
8
+ import { interaction } from "../constants/interaction.js";
9
+ import { Icon } from "./Icon.js";
10
+ import { StyledText } from "./StyledText.js";
11
+ import { useItemControlLabel } from "./Item.js";
12
+ const DEFAULT_LABELS = { system: "System", light: "Light", dark: "Dark" };
13
+ const OPTIONS = [
14
+ // A phone reads as "device" on native; web visitors are mostly on a desktop.
15
+ { value: "system", icon: Platform.OS === "web" ? "monitor" : "smartphone" },
16
+ { value: "light", icon: "sun" },
17
+ { value: "dark", icon: "moon" },
18
+ ];
19
+ const TRACK_HEIGHT = 32;
20
+ const TRACK_INSET = 2;
21
+ const ICON_SEGMENT_WIDTH = 36;
22
+ function ThemeSelector({ value, onValueChange, labels, showLabels = false, disabled = false, accessibilityLabel, style, testID = "theme-selector", }) {
23
+ const rowLabel = useItemControlLabel({ accessibilityLabel });
24
+ const { theme } = useTheme();
25
+ const userTheme = useThemeStore((state) => state.userTheme);
26
+ const setTheme = useThemeStore((state) => state.setTheme);
27
+ const selected = value ?? userTheme;
28
+ const text = { ...DEFAULT_LABELS, ...labels };
29
+ const select = (next) => {
30
+ if (next === selected)
31
+ return;
32
+ hapticSelection();
33
+ (onValueChange ?? setTheme)(next);
34
+ };
35
+ return (_jsx(View, { testID: testID, accessibilityRole: "radiogroup", accessibilityLabel: accessibilityLabel, ...rowLabel, accessibilityState: { disabled }, style: [
36
+ styles.track,
37
+ { backgroundColor: theme.colors.muted, opacity: disabled ? interaction.disabledOpacity : 1 },
38
+ showLabels ? styles.trackStretch : styles.trackCompact,
39
+ style,
40
+ ], children: OPTIONS.map((option) => (_jsx(Segment, { testID: `${testID}-${option.value}`, icon: option.icon, label: text[option.value], showLabel: showLabels, selected: option.value === selected, disabled: disabled, onPress: () => select(option.value) }, option.value))) }));
41
+ }
42
+ function Segment({ testID, icon, label, showLabel, selected, disabled, onPress }) {
43
+ const { theme, getFocusRingStyle } = useTheme();
44
+ const focus = useFocusVisible();
45
+ const color = selected ? theme.colors.foreground : theme.colors.mutedForeground;
46
+ return (_jsxs(Pressable, { testID: testID, accessibilityRole: "radio", accessibilityLabel: label, accessibilityState: { checked: selected, disabled }, disabled: disabled, onPress: onPress, onFocus: focus.onFocus, onBlur: focus.onBlur,
47
+ // The track is 32pt; reach the 44pt touch target vertically.
48
+ hitSlop: { top: 6, bottom: 6 }, style: ({ pressed }) => [
49
+ styles.segment,
50
+ showLabel ? styles.segmentStretch : styles.segmentCompact,
51
+ selected && { backgroundColor: theme.colors.background, borderColor: theme.colors.border },
52
+ pressed && !selected && { opacity: interaction.pressedOpacity },
53
+ Platform.OS === "web" && {
54
+ cursor: (disabled ? "not-allowed" : "pointer"),
55
+ outlineStyle: "none",
56
+ },
57
+ focus.focused && !disabled && getFocusRingStyle(),
58
+ ], children: [_jsx(Icon, { name: icon, size: 16, color: color, decorative: true }), showLabel && (_jsx(StyledText, { selectable: false, numberOfLines: 1, fontWeight: "medium", style: { fontSize: 13, lineHeight: 18, color }, children: label }))] }));
59
+ }
60
+ const styles = /*#__PURE__*/ StyleSheet.create({
61
+ track: {
62
+ flexDirection: "row",
63
+ height: TRACK_HEIGHT,
64
+ padding: TRACK_INSET,
65
+ borderRadius: spacing.radiusMd,
66
+ },
67
+ trackCompact: {
68
+ alignSelf: "flex-start",
69
+ },
70
+ trackStretch: {
71
+ alignSelf: "stretch",
72
+ },
73
+ segment: {
74
+ flexDirection: "row",
75
+ alignItems: "center",
76
+ justifyContent: "center",
77
+ gap: spacing.xs,
78
+ borderRadius: spacing.radiusSm,
79
+ borderWidth: StyleSheet.hairlineWidth,
80
+ borderColor: "transparent",
81
+ },
82
+ segmentCompact: {
83
+ width: ICON_SEGMENT_WIDTH,
84
+ },
85
+ segmentStretch: {
86
+ flex: 1,
87
+ paddingHorizontal: spacing.sm,
88
+ },
89
+ });
90
+ export { ThemeSelector };
@@ -1,7 +1,7 @@
1
1
  import React from "react";
2
2
  import * as TogglePrimitive from "@rn-primitives/toggle";
3
3
  import { ViewStyle, StyleProp } from "react-native";
4
- import type { IconName } from "./Icon";
4
+ import type { IconProps } from "./Icon.js";
5
5
  export type ToggleVariant = "default" | "outline";
6
6
  export type ToggleSize = "sm" | "default" | "lg";
7
7
  export type ToggleShape = "default" | "square" | "circle";
@@ -44,51 +44,12 @@ interface ToggleProps extends Omit<TogglePrimitive.RootProps, "style"> {
44
44
  */
45
45
  style?: StyleProp<ViewStyle>;
46
46
  }
47
- /**
48
- * Enhanced Toggle Component
49
- *
50
- * Features:
51
- * - 2 variants (default, outline)
52
- * - 3 sizes (sm, default, lg)
53
- * - 3 shapes (default, square, circle)
54
- * - Loading state with spinner
55
- * - Icon-only optimization
56
- * - Style prop support
57
- * - Full accessibility
58
- *
59
- * Usage:
60
- * ```tsx
61
- * // Basic
62
- * const [pressed, setPressed] = useState(false);
63
- * <Toggle pressed={pressed} onPressedChange={setPressed}>
64
- * <SansSerifText>Toggle Me</SansSerifText>
65
- * </Toggle>
66
- *
67
- * // With icon
68
- * <Toggle pressed={pressed} onPressedChange={setPressed} iconOnly>
69
- * <ToggleIcon name="bold" />
70
- * </Toggle>
71
- *
72
- * // Different shapes
73
- * <Toggle shape="square" pressed={pressed} onPressedChange={setPressed}>
74
- * <SansSerifText>Square</SansSerifText>
75
- * </Toggle>
76
- *
77
- * <Toggle shape="circle" iconOnly pressed={pressed} onPressedChange={setPressed}>
78
- * <ToggleIcon name="bold" />
79
- * </Toggle>
80
- *
81
- * // Loading state
82
- * <Toggle loading pressed={pressed} onPressedChange={setPressed}>
83
- * <SansSerifText>Loading...</SansSerifText>
84
- * </Toggle>
85
- * ```
86
- */
87
- declare function Toggle({ variant, size, shape, loading, iconOnly, style: styleOverride, ...props }: ToggleProps): React.JSX.Element;
47
+ declare function Toggle({ variant, size, shape, loading, iconOnly, style: styleOverride, onPressedChange, ...props }: ToggleProps): React.JSX.Element;
88
48
  /**
89
49
  * ToggleIcon Component
90
50
  * Icon wrapper for use inside Toggle buttons
91
- * Automatically inherits sizing from parent Toggle
51
+ * Takes `Icon`'s props; the color defaults to the Toggle's text color, and the
52
+ * icon is decorative (the control's label names it).
92
53
  *
93
54
  * Usage:
94
55
  * ```tsx
@@ -97,11 +58,6 @@ declare function Toggle({ variant, size, shape, loading, iconOnly, style: styleO
97
58
  * </Toggle>
98
59
  * ```
99
60
  */
100
- interface ToggleIconProps {
101
- name: IconName;
102
- size?: number;
103
- color?: string;
104
- }
105
- declare function ToggleIcon({ name, size, color }: ToggleIconProps): React.JSX.Element;
61
+ declare function ToggleIcon(props: IconProps): React.JSX.Element;
106
62
  export { Toggle, ToggleIcon };
107
63
  export type { ToggleProps };
@@ -1,10 +1,15 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import React, { useState } from "react";
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React from "react";
3
3
  import { Icon } from "./Icon.js";
4
- import { TextClassContext, TextColorContext, TextSelectabilityContext } from "./StyledText.context";
4
+ import { TextClassContext, TextColorContext, TextSelectabilityContext } from "./StyledText.context.js";
5
5
  import { useTheme } from "../hooks/useTheme.js";
6
+ import { useFocusVisible } from "../hooks/useFocusVisible.js";
6
7
  import { spacing } from "../constants/spacing.js";
8
+ import { getWebHitInsets, WebHitTarget } from "../lib/webHitTarget.js";
9
+ import { interaction } from "../constants/interaction.js";
10
+ import { hapticSelection } from "../lib/haptics.js";
7
11
  import { useScalePress } from "../hooks/useScalePress.js";
12
+ import { useItemControlLabel } from "./Item.js";
8
13
  import * as TogglePrimitive from "@rn-primitives/toggle";
9
14
  import { Platform, StyleSheet, ActivityIndicator, Animated } from "react-native";
10
15
  import { palette } from "../constants/colors.js";
@@ -73,11 +78,12 @@ const TOGGLE_SIZES = {
73
78
  * </Toggle>
74
79
  * ```
75
80
  */
76
- function Toggle({ variant = "default", size = "default", shape = "default", loading = false, iconOnly = false, style: styleOverride, ...props }) {
81
+ // Dev-only, once per session: the same unnamed toggle re-renders constantly.
82
+ let warnedIconOnly = false;
83
+ function Toggle({ variant = "default", size = "default", shape = "default", loading = false, iconOnly = false, style: styleOverride, onPressedChange, ...props }) {
77
84
  const { theme, getContrastingColor, getFocusRingStyle, withAlpha } = useTheme();
78
85
  const sizeConfig = TOGGLE_SIZES[size];
79
86
  const focusRingStyle = getFocusRingStyle();
80
- const [focused, setFocused] = useState(false);
81
87
  // Calculate text color based on state and variant
82
88
  const getTextColor = () => {
83
89
  if (props.pressed) {
@@ -106,28 +112,33 @@ function Toggle({ variant = "default", size = "default", shape = "default", load
106
112
  const children = props.children;
107
113
  const { animatedStyle: scaleStyle, pressHandlers } = useScalePress({
108
114
  disabled: !!isDisabled,
109
- scaleTo: 0.92,
115
+ scaleTo: interaction.controlPressedScale,
110
116
  haptic: false,
111
117
  });
112
- const showFocusRing = (event) => {
113
- let ringVisible = true;
114
- if (Platform.OS === "web") {
115
- const target = event?.nativeEvent?.target;
116
- if (target && typeof target.matches === "function") {
117
- try {
118
- ringVisible = target.matches(":focus-visible");
119
- }
120
- catch {
121
- ringVisible = true;
122
- }
123
- }
124
- }
125
- setFocused(ringVisible);
126
- };
127
- const hideFocusRing = () => {
128
- setFocused(false);
118
+ // A toggle is a selection control: its state change is the haptic moment,
119
+ // not the press, so it follows the `"selection"` setting like Switch does.
120
+ const handlePressedChange = (next) => {
121
+ hapticSelection();
122
+ onPressedChange?.(next);
129
123
  };
130
- return (_jsx(TextColorContext.Provider, { value: textColor, children: _jsx(TextClassContext.Provider, { value: "", children: _jsx(Animated.View, { style: scaleStyle, children: _jsx(TogglePrimitive.Root, { ...props, disabled: isDisabled, onPressIn: pressHandlers.onPressIn, onPressOut: pressHandlers.onPressOut, onFocus: showFocusRing, onBlur: hideFocusRing, style: {
124
+ const { focused, onFocus: showFocusRing, onBlur: hideFocusRing } = useFocusVisible();
125
+ const rowLabel = useItemControlLabel(props);
126
+ if (process.env.NODE_ENV !== "production" &&
127
+ iconOnly &&
128
+ !warnedIconOnly &&
129
+ props.accessibilityLabel === undefined &&
130
+ props["aria-label"] === undefined &&
131
+ props["aria-labelledby"] === undefined &&
132
+ rowLabel.accessibilityLabel === undefined &&
133
+ rowLabel["aria-labelledby"] === undefined) {
134
+ warnedIconOnly = true;
135
+ console.warn("Toggle: an icon-only toggle (`iconOnly`) has no accessible name. Pass `accessibilityLabel` " +
136
+ "(what the toggle turns on, e.g. \"Bold\") so screen readers announce more than \"button\".");
137
+ }
138
+ // Web pointer target: 44pt tall through a transparent child inside the drawn
139
+ // box (react-native-web ignores `hitSlop`); native keeps DEFAULT_HIT_SLOP.
140
+ const hitTarget = (_jsx(WebHitTarget, { insets: Platform.OS === "web" ? getWebHitInsets(undefined, sizeConfig.height) : null, border: styles.root.borderWidth }));
141
+ return (_jsx(TextColorContext.Provider, { value: textColor, children: _jsx(TextClassContext.Provider, { value: "", children: _jsx(Animated.View, { style: scaleStyle, children: _jsx(TogglePrimitive.Root, { ...rowLabel, ...props, disabled: isDisabled, onPressedChange: handlePressedChange, onPressIn: pressHandlers.onPressIn, onPressOut: pressHandlers.onPressOut, onFocus: showFocusRing, onBlur: hideFocusRing, style: {
131
142
  ...styles.root,
132
143
  height: sizeConfig.height,
133
144
  minWidth: sizeConfig.minWidth,
@@ -152,7 +163,7 @@ function Toggle({ variant = "default", size = "default", shape = "default", load
152
163
  borderColor: theme.colors.primary,
153
164
  }),
154
165
  // Disabled state
155
- opacity: isDisabled ? 0.5 : 1,
166
+ opacity: isDisabled ? interaction.disabledOpacity : 1,
156
167
  // Web-specific styles
157
168
  ...(Platform.OS === "web" && {
158
169
  cursor: isDisabled ? "not-allowed" : "pointer",
@@ -166,13 +177,26 @@ function Toggle({ variant = "default", size = "default", shape = "default", load
166
177
  selected: props.pressed,
167
178
  disabled: !!isDisabled,
168
179
  busy: loading,
169
- }, children: loading ? (_jsx(TextSelectabilityContext.Provider, { value: false, children: _jsx(ActivityIndicator, { size: "small", color: textColor }) })) : typeof children === "function" ? ((state) => (_jsx(TextSelectabilityContext.Provider, { value: false, children: children(state) }))) : (_jsx(TextSelectabilityContext.Provider, { value: false, children: children })) }) }) }) }));
180
+ }, children: loading ? (_jsxs(_Fragment, { children: [_jsx(TextSelectabilityContext.Provider, { value: false, children: _jsx(ActivityIndicator, { size: "small", color: textColor }) }), hitTarget] })) : typeof children === "function" ? ((state) => (_jsxs(_Fragment, { children: [_jsx(TextSelectabilityContext.Provider, { value: false, children: children(state) }), hitTarget] }))) : (_jsxs(_Fragment, { children: [_jsx(TextSelectabilityContext.Provider, { value: false, children: children }), hitTarget] })) }) }) }) }));
170
181
  }
171
- function ToggleIcon({ name, size, color }) {
182
+ /**
183
+ * ToggleIcon Component
184
+ * Icon wrapper for use inside Toggle buttons
185
+ * Takes `Icon`'s props; the color defaults to the Toggle's text color, and the
186
+ * icon is decorative (the control's label names it).
187
+ *
188
+ * Usage:
189
+ * ```tsx
190
+ * <Toggle pressed={pressed} onPressedChange={setPressed}>
191
+ * <ToggleIcon name="bold" />
192
+ * </Toggle>
193
+ * ```
194
+ */
195
+ function ToggleIcon(props) {
172
196
  const contextColor = React.use(TextColorContext);
173
- return _jsx(Icon, { name: name, size: size || spacing.iconMd, color: color || contextColor });
197
+ return _jsx(Icon, { decorative: true, color: contextColor, ...props });
174
198
  }
175
- const styles = StyleSheet.create({
199
+ const styles = /*#__PURE__*/ StyleSheet.create({
176
200
  root: {
177
201
  flexDirection: "row",
178
202
  alignItems: "center",
@@ -1,4 +1,4 @@
1
- import type { IconName } from "./Icon";
1
+ import type { IconProps } from "./Icon.js";
2
2
  import * as ToggleGroupPrimitive from "@rn-primitives/toggle-group";
3
3
  import * as React from "react";
4
4
  type ToggleGroupVariant = "default" | "outline";
@@ -62,7 +62,8 @@ declare function ToggleGroupItem({ isFirst, isLast, children, ...props }: Toggle
62
62
  /**
63
63
  * ToggleGroupIcon Component
64
64
  * Icon wrapper for use inside ToggleGroup items
65
- * Automatically inherits sizing from parent ToggleGroup
65
+ * Takes `Icon`'s props; the color defaults to the ToggleGroup's text color, and the
66
+ * icon is decorative (the control's label names it).
66
67
  *
67
68
  * Usage:
68
69
  * ```tsx
@@ -71,11 +72,6 @@ declare function ToggleGroupItem({ isFirst, isLast, children, ...props }: Toggle
71
72
  * </ToggleGroupItem>
72
73
  * ```
73
74
  */
74
- interface ToggleGroupIconProps {
75
- name: IconName;
76
- size?: number;
77
- color?: string;
78
- }
79
- declare function ToggleGroupIcon({ name, size, color }: ToggleGroupIconProps): React.JSX.Element;
75
+ declare function ToggleGroupIcon(props: IconProps): React.JSX.Element;
80
76
  export { ToggleGroup, ToggleGroupIcon, ToggleGroupItem };
81
77
  export type { ToggleGroupProps, ToggleGroupSize, ToggleGroupVariant };