@thewhileloop/whileui 4.2.3 → 4.4.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 (67) hide show
  1. package/README.md +1271 -663
  2. package/dist/agent/catalog.json +1737 -0
  3. package/dist/agent/llms-components.txt +2808 -0
  4. package/dist/agent/llms-full.txt +2998 -0
  5. package/dist/agent/llms.txt +152 -0
  6. package/dist/blocks/chat/chat.d.ts +10 -2
  7. package/dist/blocks/chat/chat.js +1 -1
  8. package/dist/blocks/chat/index.d.ts +1 -1
  9. package/dist/blocks/commerce/feature-gate.js +2 -2
  10. package/dist/blocks/commerce/metric-card.js +1 -1
  11. package/dist/blocks/commerce/subscription-card.d.ts +7 -0
  12. package/dist/blocks/commerce/usage-bar.d.ts +4 -0
  13. package/dist/blocks/layout/app-shell.d.ts +4 -0
  14. package/dist/blocks/layout/empty-state.js +1 -1
  15. package/dist/blocks/layout/error-state.js +1 -1
  16. package/dist/blocks/layout/page-skeleton.d.ts +1 -0
  17. package/dist/blocks/layout/pull-to-refresh-scroll-view.d.ts +1 -0
  18. package/dist/blocks/layout/screen-skeleton.d.ts +5 -0
  19. package/dist/blocks/layout/smart-input.js +2 -2
  20. package/dist/blocks/lists/list-item.js +1 -1
  21. package/dist/blocks/lists/notification-item.js +1 -1
  22. package/dist/blocks/media/smart-image.js +4 -2
  23. package/dist/blocks/navigation/bottom-nav.js +1 -1
  24. package/dist/blocks/navigation/drawer-menu.js +1 -1
  25. package/dist/blocks/navigation/floating-bottom-nav.js +3 -1
  26. package/dist/blocks/navigation/tab-bar.js +1 -1
  27. package/dist/components/alert/alert.d.ts +7 -0
  28. package/dist/components/alert/alert.js +23 -4
  29. package/dist/components/button/button.d.ts +3 -0
  30. package/dist/components/button/button.js +2 -1
  31. package/dist/components/fab/fab.d.ts +12 -0
  32. package/dist/components/fab/fab.js +14 -2
  33. package/dist/components/fab/index.d.ts +1 -1
  34. package/dist/components/fab/index.js +1 -1
  35. package/dist/components/form-field/form-field.d.ts +18 -0
  36. package/dist/components/form-field/form-field.js +25 -4
  37. package/dist/components/input/input.d.ts +5 -0
  38. package/dist/components/input/input.js +4 -4
  39. package/dist/components/numeric-input/numeric-input.d.ts +3 -0
  40. package/dist/components/numeric-input/numeric-input.js +4 -4
  41. package/dist/components/otp-input/otp-input.d.ts +8 -0
  42. package/dist/components/popover/popover.js +9 -1
  43. package/dist/components/pressable/pressable.d.ts +1 -0
  44. package/dist/components/segmented-control/segmented-control.d.ts +39 -1
  45. package/dist/components/segmented-control/segmented-control.js +29 -5
  46. package/dist/components/select/select.js +7 -1
  47. package/dist/components/surface/surface.d.ts +5 -0
  48. package/dist/components/textarea/textarea.js +3 -3
  49. package/dist/components/toast/toast.d.ts +10 -10
  50. package/dist/components/toast/toast.js +53 -35
  51. package/dist/index.d.ts +2 -1
  52. package/dist/index.js +2 -1
  53. package/dist/lib/dismiss-keyboard-on-open.d.ts +16 -0
  54. package/dist/lib/dismiss-keyboard-on-open.js +22 -0
  55. package/dist/lib/frosted-surface.d.ts +3 -0
  56. package/dist/lib/recipes.d.ts +20 -0
  57. package/dist/lib/recipes.js +19 -2
  58. package/dist/lib/theme-bridge.d.ts +9 -0
  59. package/dist/lib/theme-bridge.js +3 -1
  60. package/dist/lib/theme-contract.js +12 -12
  61. package/dist/lib/theme-presets.js +9 -9
  62. package/dist/lib/theme-seeds.d.ts +40 -0
  63. package/dist/lib/theme-seeds.js +143 -0
  64. package/dist/metro.cjs +5 -4
  65. package/dist/tokens.d.ts +1 -0
  66. package/dist/tokens.js +1 -0
  67. package/package.json +3 -2
@@ -51,8 +51,26 @@ export interface FormFieldState {
51
51
  invalid: boolean;
52
52
  disabled: boolean;
53
53
  required: boolean;
54
+ /** Set inside a FormField: `FormLabel` takes it as its id and controls point to it. */
55
+ labelId?: string;
54
56
  }
55
57
  export declare function useFormFieldState(): FormFieldState;
58
+ interface CallerNaming {
59
+ accessibilityLabel?: string;
60
+ 'aria-label'?: string;
61
+ 'aria-labelledby'?: string;
62
+ accessibilityLabelledBy?: string | string[];
63
+ }
64
+ /**
65
+ * Accessibility props that tie a text control to its FormField: named by the FormLabel
66
+ * unless the caller names or links it any of the four ways React Native allows, and
67
+ * required when the field is. Spread before the caller's props. iOS has no labelled-by;
68
+ * VoiceOver reads the visible label just before the field.
69
+ */
70
+ export declare function fieldAccessibilityProps(field: FormFieldState, props: CallerNaming): {
71
+ 'aria-labelledby': string | undefined;
72
+ 'aria-required': true | undefined;
73
+ };
56
74
  export interface FormFieldProps extends ViewProps, VariantProps<typeof formFieldVariants> {
57
75
  invalid?: boolean;
58
76
  disabled?: boolean;
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import React, { createContext, useContext, useMemo } from 'react';
2
+ import React, { createContext, useContext, useId, useMemo } from 'react';
3
3
  import { Text, View, } from 'react-native';
4
4
  import { cn } from '../../lib/cn';
5
5
  import { tv } from '../../lib/tv';
@@ -44,16 +44,37 @@ const FormFieldContext = createContext({
44
44
  export function useFormFieldState() {
45
45
  return useContext(FormFieldContext);
46
46
  }
47
+ /**
48
+ * Accessibility props that tie a text control to its FormField: named by the FormLabel
49
+ * unless the caller names or links it any of the four ways React Native allows, and
50
+ * required when the field is. Spread before the caller's props. iOS has no labelled-by;
51
+ * VoiceOver reads the visible label just before the field.
52
+ */
53
+ export function fieldAccessibilityProps(field, props) {
54
+ const named = props.accessibilityLabel ??
55
+ props['aria-label'] ??
56
+ props['aria-labelledby'] ??
57
+ props.accessibilityLabelledBy;
58
+ return {
59
+ 'aria-labelledby': named ? undefined : field.labelId,
60
+ 'aria-required': field.required || undefined,
61
+ };
62
+ }
47
63
  const FormField = React.forwardRef(({ className, density = 'default', invalid = false, disabled = false, required = false, ...props }, ref) => {
48
64
  const interaction = useInteractionTokens();
49
- const contextValue = useMemo(() => ({ density, invalid, disabled, required }), [density, disabled, invalid, required]);
65
+ const labelId = useId();
66
+ const contextValue = useMemo(() => ({ density, invalid, disabled, required, labelId }), [density, disabled, invalid, labelId, required]);
50
67
  return (_jsx(FormFieldContext.Provider, { value: contextValue, children: _jsx(View, { ref: ref, className: cn(formFieldVariants({ density }), className), style: disabled ? { opacity: interaction.disabledOpacitySoft } : undefined, ...props }) }));
51
68
  });
52
69
  FormField.displayName = 'FormField';
53
70
  const FormLabel = React.forwardRef(({ className, showRequiredIndicator = true, children, style, ...props }, ref) => {
54
- const { invalid, disabled, required } = useContext(FormFieldContext);
71
+ const { invalid, disabled, required, labelId } = useContext(FormFieldContext);
55
72
  const visual = useVisualTokens();
56
- return (_jsxs(Text, { ref: ref, className: cn('font-medium text-foreground', invalid && 'text-destructive', disabled && 'text-muted-foreground', className), style: [typographyStyle(visual, 'label'), style], ...props, children: [children, required && showRequiredIndicator ? _jsx(Text, { className: "text-destructive", children: " *" }) : null] }));
73
+ const indicator = required && showRequiredIndicator;
74
+ return (_jsxs(Text, { ref: ref, nativeID: labelId,
75
+ // iOS reads nested Text as one string, so hiding the asterisk alone still says
76
+ // "star"; a plain-text label names it once. The control announces "required".
77
+ accessibilityLabel: indicator && typeof children === 'string' ? children : undefined, className: cn('font-medium text-foreground', invalid && 'text-destructive', disabled && 'text-muted-foreground', className), style: [typographyStyle(visual, 'label'), style], ...props, children: [children, indicator ? (_jsx(Text, { "aria-hidden": true, className: "text-destructive", children: ' *' })) : null] }));
57
78
  });
58
79
  FormLabel.displayName = 'FormLabel';
59
80
  const FormControl = React.forwardRef(({ className, style, ...props }, ref) => {
@@ -27,7 +27,12 @@ declare const inputVariants: import("tailwind-variants").TVReturnType<{
27
27
  export interface InputProps extends TextInputProps, VariantProps<typeof inputVariants> {
28
28
  'aria-invalid'?: boolean;
29
29
  className?: string;
30
+ /**
31
+ * Shown before the text. A decorative icon here needs `aria-hidden`; screen readers
32
+ * otherwise stop on it as a separate element and read out its glyph.
33
+ */
30
34
  prefix?: React.ReactNode;
35
+ /** Shown after the text: a unit, or a control such as a clear button. */
31
36
  suffix?: React.ReactNode;
32
37
  inputClassName?: string;
33
38
  }
@@ -7,9 +7,9 @@ import { tv } from '../../lib/tv';
7
7
  import { useThemeColors } from '../../lib/theme-colors';
8
8
  import { useInteractionTokens } from '../../lib/interaction-tokens';
9
9
  import { useVisualTokens } from '../../lib/visual-tokens';
10
- import { typographyStyle } from '../../lib/recipes';
10
+ import { textCursorColors, typographyStyle } from '../../lib/recipes';
11
11
  import { webAutofillField } from '../../lib/web-autofill';
12
- import { useFormFieldState } from '../form-field/form-field';
12
+ import { fieldAccessibilityProps, useFormFieldState } from '../form-field/form-field';
13
13
  const inputVariants = tv({
14
14
  base: 'w-full border-border bg-muted text-foreground',
15
15
  variants: {
@@ -43,11 +43,11 @@ const Input = React.forwardRef(({ className, prefix, suffix, inputClassName, var
43
43
  paddingHorizontal: plain ? 0 : visual.controlPaddingXDefault,
44
44
  },
45
45
  !effectiveEditable ? { opacity: interaction.disabledOpacity } : undefined,
46
- ], children: [prefix && (_jsx(View, { style: { paddingRight: Math.max(8, Math.round(visual.controlPaddingXDefault * 0.5)) }, children: prefix })), _jsx(TextInput, { ref: ref, className: cn('flex-1 text-foreground placeholder:text-muted-foreground web:outline-none', !plain && webAutofillField, inputClassName), style: {
46
+ ], children: [prefix && (_jsx(View, { style: { paddingRight: Math.max(8, Math.round(visual.controlPaddingXDefault * 0.5)) }, children: prefix })), _jsx(TextInput, { ref: ref, ...fieldAccessibilityProps(field, props), className: cn('flex-1 text-foreground placeholder:text-muted-foreground web:outline-none', !plain && webAutofillField, inputClassName), style: {
47
47
  ...typographyStyle(visual, 'label'),
48
48
  minHeight: visual.controlHeightDefault - 2,
49
49
  paddingVertical: visual.controlPaddingYDefault,
50
- }, editable: effectiveEditable, "aria-invalid": ariaInvalid ?? field.invalid, placeholderTextColor: placeholderTextColor ?? colors.placeholder, onFocus: (event) => {
50
+ }, editable: effectiveEditable, "aria-invalid": ariaInvalid ?? field.invalid, placeholderTextColor: placeholderTextColor ?? colors.placeholder, ...textCursorColors(colors), onFocus: (event) => {
51
51
  setFocused(true);
52
52
  onFocus?.(event);
53
53
  }, onBlur: (event) => {
@@ -52,12 +52,15 @@ export interface NumericInputProps extends Omit<TextInputProps, 'value' | 'defau
52
52
  inputClassName?: string;
53
53
  prefix?: React.ReactNode;
54
54
  suffix?: React.ReactNode;
55
+ /** Controlled numeric value. */
55
56
  value?: number | null;
56
57
  defaultValue?: number;
58
+ /** Numeric value change callback. */
57
59
  onValueChange?: (value: number | null) => void;
58
60
  min?: number;
59
61
  max?: number;
60
62
  step?: number;
63
+ /** Show decrement/increment controls. */
61
64
  showSteppers?: boolean;
62
65
  /** Drop the field shell (border, background, radius, padding) for embedding inside another field shell like LabeledField. */
63
66
  unstyled?: boolean;
@@ -7,9 +7,9 @@ import { tv } from '../../lib/tv';
7
7
  import { useThemeColors } from '../../lib/theme-colors';
8
8
  import { useInteractionTokens, withInteractivePressableStyle } from '../../lib/interaction-tokens';
9
9
  import { useVisualTokens } from '../../lib/visual-tokens';
10
- import { typographyStyle } from '../../lib/recipes';
10
+ import { textCursorColors, typographyStyle } from '../../lib/recipes';
11
11
  import { webAutofillField } from '../../lib/web-autofill';
12
- import { useFormFieldState } from '../form-field/form-field';
12
+ import { fieldAccessibilityProps, useFormFieldState } from '../form-field/form-field';
13
13
  const numericInputVariants = tv({
14
14
  base: 'w-full flex-row items-center border-border bg-muted',
15
15
  variants: {
@@ -171,13 +171,13 @@ const NumericInput = React.forwardRef(({ className, inputClassName, prefix, suff
171
171
  paddingRight: unstyled ? 0 : showSteppers ? stepInset : 0,
172
172
  },
173
173
  !effectiveEditable ? { opacity: interaction.disabledOpacity } : undefined,
174
- ], children: [_jsx(NumericAffix, { side: "prefix", visual: visual, children: prefix }), _jsx(View, { className: "min-w-0 flex-1", style: Platform.OS === 'web' ? { minWidth: 0, flex: 1 } : undefined, children: _jsx(TextInput, { ref: ref, className: cn(numericInputTextVariants({ size }), 'w-full web:outline-none', !unstyled && webAutofillField, inputClassName), style: numericInputTextStyle(visual, size, unstyled, styleProp), value: model.textValue, onChangeText: model.handleTextChange, onFocus: (event) => {
174
+ ], children: [_jsx(NumericAffix, { side: "prefix", visual: visual, children: prefix }), _jsx(View, { className: "min-w-0 flex-1", style: Platform.OS === 'web' ? { minWidth: 0, flex: 1 } : undefined, children: _jsx(TextInput, { ref: ref, ...fieldAccessibilityProps(field, props), className: cn(numericInputTextVariants({ size }), 'w-full web:outline-none', !unstyled && webAutofillField, inputClassName), style: numericInputTextStyle(visual, size, unstyled, styleProp), value: model.textValue, onChangeText: model.handleTextChange, onFocus: (event) => {
175
175
  setFocused(true);
176
176
  onFocus?.(event);
177
177
  }, onBlur: (event) => {
178
178
  setFocused(false);
179
179
  model.handleBlur(event);
180
- }, keyboardType: "decimal-pad", editable: effectiveEditable, "aria-invalid": ariaInvalid ?? field.invalid, placeholderTextColor: placeholderTextColor ?? colors.placeholder, ...props }) }), _jsx(NumericAffix, { side: "suffix", visual: visual, children: suffix }), _jsx(NumericSteppers, { visible: showSteppers, inset: stepInset, size: stepSize, radius: stepRadius, model: model, interaction: interaction, visual: visual })] }));
180
+ }, keyboardType: "decimal-pad", editable: effectiveEditable, "aria-invalid": ariaInvalid ?? field.invalid, placeholderTextColor: placeholderTextColor ?? colors.placeholder, ...textCursorColors(colors), ...props }) }), _jsx(NumericAffix, { side: "suffix", visual: visual, children: suffix }), _jsx(NumericSteppers, { visible: showSteppers, inset: stepInset, size: stepSize, radius: stepRadius, model: model, interaction: interaction, visual: visual })] }));
181
181
  });
182
182
  NumericInput.displayName = 'NumericInput';
183
183
  export { NumericInput, numericInputVariants, numericInputTextVariants };
@@ -34,14 +34,22 @@ declare const otpCellVariants: import("tailwind-variants").TVReturnType<{
34
34
  };
35
35
  }, undefined>>;
36
36
  export interface OTPInputProps extends Omit<ViewProps, 'children'> {
37
+ /** Number of digit cells. */
37
38
  length?: number;
39
+ /** Controlled value. */
38
40
  value?: string;
41
+ /** Called on each digit change. */
39
42
  onValueChange?: (value: string) => void;
43
+ /** Called when all digits are filled. */
40
44
  onComplete?: (code: string) => void;
45
+ /** Error variant triggers shake. */
41
46
  variant?: 'default' | 'error';
47
+ /** Cell size. */
42
48
  size?: 'default' | 'compact';
49
+ /** Mask digits with dots. */
43
50
  secure?: boolean;
44
51
  disabled?: boolean;
52
+ /** Focus first cell on mount. */
45
53
  autoFocus?: boolean;
46
54
  className?: string;
47
55
  }
@@ -1,8 +1,10 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import * as React from 'react';
3
3
  import { Platform, StyleSheet } from 'react-native';
4
+ import { useComposedRefs } from '@rn-primitives/hooks';
4
5
  import * as PopoverPrimitive from '@rn-primitives/popover';
5
6
  import { cn } from '../../lib/cn';
7
+ import { useDismissKeyboardOnOpen } from '../../lib/dismiss-keyboard-on-open';
6
8
  import { useFrostedSurface } from '../../lib/frosted-surface';
7
9
  import { useNestedOverlay, useOverlayPortalTarget } from '../../lib/overlay-host';
8
10
  import { useOverlayInsets } from '../../lib/overlay-insets';
@@ -12,7 +14,13 @@ import { shadowStyle, surfacePadding, surfaceRadius } from '../../lib/recipes';
12
14
  function Popover({ onOpenChange, children }) {
13
15
  return _jsx(PopoverPrimitive.Root, { onOpenChange: onOpenChange, children: children });
14
16
  }
15
- const PopoverTrigger = React.forwardRef(({ className, ...props }, ref) => (_jsx(PopoverPrimitive.Trigger, { ref: ref, className: cn(className), ...props })));
17
+ const PopoverTrigger = React.forwardRef(({ className, ...props }, ref) => {
18
+ const { open, setTriggerPosition } = PopoverPrimitive.useRootContext();
19
+ const triggerRef = React.useRef(null);
20
+ const composedRef = useComposedRefs(triggerRef, ref);
21
+ useDismissKeyboardOnOpen(open, triggerRef, setTriggerPosition);
22
+ return _jsx(PopoverPrimitive.Trigger, { ref: composedRef, className: cn(className), ...props });
23
+ });
16
24
  PopoverTrigger.displayName = 'PopoverTrigger';
17
25
  const PopoverContent = React.forwardRef(({ className, align = 'center', sideOffset = 4, insets, portalHost, children, style, frosted = false, blurIntensity, blurTintToken, ...props }, ref) => {
18
26
  const frostedSurface = useFrostedSurface({
@@ -2,6 +2,7 @@ import { type PressableInstance, type PressableProps as RNPressableProps } from
2
2
  import { type DisabledVariant, type PressedVariant } from '../../lib/interaction-tokens';
3
3
  import type { ForwardRefComponent } from '../../lib/ref-types';
4
4
  export interface PressableProps extends RNPressableProps {
5
+ /** Additional Tailwind classes. */
5
6
  className?: string;
6
7
  /** Semantic press feedback. `none` preserves a plain React Native Pressable. */
7
8
  feedback?: PressedVariant;
@@ -9,6 +9,11 @@ declare const segmentedControlVariants: import("tailwind-variants").TVReturnType
9
9
  true: "flex-wrap justify-center gap-1";
10
10
  false: "flex-nowrap gap-1";
11
11
  };
12
+ /** Sized to its segments inside a horizontal scroll view. */
13
+ scroll: {
14
+ true: "w-auto";
15
+ false: "";
16
+ };
12
17
  size: {
13
18
  default: "";
14
19
  compact: "";
@@ -22,6 +27,11 @@ declare const segmentedControlVariants: import("tailwind-variants").TVReturnType
22
27
  true: "flex-wrap justify-center gap-1";
23
28
  false: "flex-nowrap gap-1";
24
29
  };
30
+ /** Sized to its segments inside a horizontal scroll view. */
31
+ scroll: {
32
+ true: "w-auto";
33
+ false: "";
34
+ };
25
35
  size: {
26
36
  default: "";
27
37
  compact: "";
@@ -35,6 +45,11 @@ declare const segmentedControlVariants: import("tailwind-variants").TVReturnType
35
45
  true: "flex-wrap justify-center gap-1";
36
46
  false: "flex-nowrap gap-1";
37
47
  };
48
+ /** Sized to its segments inside a horizontal scroll view. */
49
+ scroll: {
50
+ true: "w-auto";
51
+ false: "";
52
+ };
38
53
  size: {
39
54
  default: "";
40
55
  compact: "";
@@ -61,6 +76,11 @@ declare const segmentedControlItemVariants: import("tailwind-variants").TVReturn
61
76
  true: "grow-0";
62
77
  false: "flex-1";
63
78
  };
79
+ /** As wide as its label, so segments overflow into the scroll view rather than shrink. */
80
+ scroll: {
81
+ true: "flex-none";
82
+ false: "";
83
+ };
64
84
  }, undefined, "min-w-0 flex-row items-center justify-center", {
65
85
  variant: {
66
86
  default: "";
@@ -82,6 +102,11 @@ declare const segmentedControlItemVariants: import("tailwind-variants").TVReturn
82
102
  true: "grow-0";
83
103
  false: "flex-1";
84
104
  };
105
+ /** As wide as its label, so segments overflow into the scroll view rather than shrink. */
106
+ scroll: {
107
+ true: "flex-none";
108
+ false: "";
109
+ };
85
110
  }, undefined, import("tailwind-variants").TVReturnTypeLike<{
86
111
  variant: {
87
112
  default: "";
@@ -103,6 +128,11 @@ declare const segmentedControlItemVariants: import("tailwind-variants").TVReturn
103
128
  true: "grow-0";
104
129
  false: "flex-1";
105
130
  };
131
+ /** As wide as its label, so segments overflow into the scroll view rather than shrink. */
132
+ scroll: {
133
+ true: "flex-none";
134
+ false: "";
135
+ };
106
136
  }, undefined>>;
107
137
  declare const segmentedControlItemTextVariants: import("tailwind-variants").TVReturnType<{
108
138
  selected: {
@@ -136,8 +166,16 @@ export interface SegmentedControlProps extends ViewProps {
136
166
  value?: string;
137
167
  defaultValue?: string;
138
168
  onValueChange?: (value: string) => void;
169
+ /** Pill = rounded-full items. */
139
170
  variant?: 'default' | 'pill';
140
- wrap?: boolean | 'auto';
171
+ /**
172
+ * `true` wraps segments onto more rows; `false` keeps one row and shares its width.
173
+ * `'scroll'` keeps one row with each segment as wide as its label and scrolls
174
+ * sideways when they do not fit — for labels a phone cannot show in full.
175
+ * `'auto'` wraps above three segments.
176
+ * @default 'auto'
177
+ */
178
+ wrap?: boolean | 'auto' | 'scroll';
141
179
  size?: 'default' | 'compact';
142
180
  disabled?: boolean;
143
181
  className?: string;
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import React, { createContext, useCallback, useContext, useMemo, useState } from 'react';
3
- import { Pressable, Text, View, } from 'react-native';
3
+ import { Pressable, ScrollView, Text, View, } from 'react-native';
4
4
  import { cn } from '../../lib/cn';
5
5
  import { focusRingInset } from '../../lib/focus-ring';
6
6
  import { tv } from '../../lib/tv';
@@ -19,6 +19,11 @@ const segmentedControlVariants = tv({
19
19
  true: 'flex-wrap justify-center gap-1',
20
20
  false: 'flex-nowrap gap-1',
21
21
  },
22
+ /** Sized to its segments inside a horizontal scroll view. */
23
+ scroll: {
24
+ true: 'w-auto',
25
+ false: '',
26
+ },
22
27
  size: {
23
28
  default: '',
24
29
  compact: '',
@@ -27,6 +32,7 @@ const segmentedControlVariants = tv({
27
32
  defaultVariants: {
28
33
  variant: 'default',
29
34
  wrap: true,
35
+ scroll: false,
30
36
  size: 'default',
31
37
  },
32
38
  });
@@ -53,6 +59,11 @@ const segmentedControlItemVariants = tv({
53
59
  true: 'grow-0',
54
60
  false: 'flex-1',
55
61
  },
62
+ /** As wide as its label, so segments overflow into the scroll view rather than shrink. */
63
+ scroll: {
64
+ true: 'flex-none',
65
+ false: '',
66
+ },
56
67
  },
57
68
  defaultVariants: {
58
69
  variant: 'default',
@@ -60,6 +71,7 @@ const segmentedControlItemVariants = tv({
60
71
  size: 'default',
61
72
  disabled: false,
62
73
  wrap: true,
74
+ scroll: false,
63
75
  },
64
76
  });
65
77
  const segmentedControlItemTextVariants = tv({
@@ -105,6 +117,7 @@ const SegmentedControlContext = createContext({
105
117
  variant: 'default',
106
118
  size: 'default',
107
119
  wrap: true,
120
+ scroll: false,
108
121
  disabled: false,
109
122
  });
110
123
  const SegmentedControlItemContext = createContext({
@@ -117,7 +130,8 @@ function SegmentedControl({ value: controlledValue, defaultValue = '', onValueCh
117
130
  const [internalValue, setInternalValue] = useState(defaultValue);
118
131
  const value = controlledValue ?? internalValue;
119
132
  const itemCount = useMemo(() => React.Children.toArray(children).filter((child) => React.isValidElement(child)).length, [children]);
120
- const shouldWrap = wrap === 'auto' ? itemCount > 3 : wrap;
133
+ const scroll = wrap === 'scroll';
134
+ const shouldWrap = wrap === 'auto' ? itemCount > 3 : wrap === true;
121
135
  const handleValueChange = useCallback((nextValue) => {
122
136
  if (disabled || value === nextValue) {
123
137
  return;
@@ -125,11 +139,20 @@ function SegmentedControl({ value: controlledValue, defaultValue = '', onValueCh
125
139
  setInternalValue(nextValue);
126
140
  onValueChange?.(nextValue);
127
141
  }, [disabled, onValueChange, value]);
128
- const contextValue = useMemo(() => ({ value, onValueChange: handleValueChange, variant, size, wrap: shouldWrap, disabled }), [disabled, handleValueChange, shouldWrap, size, value, variant]);
129
- return (_jsx(SegmentedControlContext.Provider, { value: contextValue, children: _jsx(View, { className: cn(segmentedControlVariants({ variant, wrap: shouldWrap, size }), className), style: [getSegmentedContainerStyle(visual, colors, variant), styleProp], accessibilityRole: "radiogroup", ...props, children: children }) }));
142
+ const contextValue = useMemo(() => ({
143
+ value,
144
+ onValueChange: handleValueChange,
145
+ variant,
146
+ size,
147
+ wrap: shouldWrap,
148
+ scroll,
149
+ disabled,
150
+ }), [disabled, handleValueChange, scroll, shouldWrap, size, value, variant]);
151
+ const group = (_jsx(View, { className: cn(segmentedControlVariants({ variant, wrap: shouldWrap, scroll, size }), className), style: [getSegmentedContainerStyle(visual, colors, variant), styleProp], accessibilityRole: "radiogroup", ...props, children: children }));
152
+ return (_jsx(SegmentedControlContext.Provider, { value: contextValue, children: scroll ? (_jsx(ScrollView, { horizontal: true, showsHorizontalScrollIndicator: false, children: group })) : (group) }));
130
153
  }
131
154
  function SegmentedControlItem({ value: itemValue, className, children, disabled: itemDisabled, style: styleProp, ...props }) {
132
- const { value, onValueChange, variant, size, wrap, disabled } = useContext(SegmentedControlContext);
155
+ const { value, onValueChange, variant, size, wrap, scroll, disabled } = useContext(SegmentedControlContext);
133
156
  const selected = value === itemValue;
134
157
  const finalDisabled = disabled || Boolean(itemDisabled);
135
158
  const interaction = useInteractionTokens();
@@ -150,6 +173,7 @@ function SegmentedControlItem({ value: itemValue, className, children, disabled:
150
173
  size,
151
174
  disabled: finalDisabled,
152
175
  wrap,
176
+ scroll,
153
177
  }), focusRingInset, className), onPress: () => onValueChange(itemValue), accessibilityRole: "radio",
154
178
  // A radio is checked, not selected. `selected` maps to `aria-selected`, which is
155
179
  // not a valid attribute on `role="radio"` — assistive technology could not tell
@@ -1,10 +1,12 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import * as React from 'react';
3
3
  import { Platform, ScrollView, StyleSheet, useWindowDimensions, View, } from 'react-native';
4
+ import { useComposedRefs } from '@rn-primitives/hooks';
4
5
  import * as SelectPrimitive from '@rn-primitives/select';
5
6
  import { FullWindowOverlay as RNFullWindowOverlay } from 'react-native-screens';
6
7
  import { Text } from '../text';
7
8
  import { cn } from '../../lib/cn';
9
+ import { useDismissKeyboardOnOpen } from '../../lib/dismiss-keyboard-on-open';
8
10
  import { focusRing } from '../../lib/focus-ring';
9
11
  import { useInteractionTokens } from '../../lib/interaction-tokens';
10
12
  import { useFrostedSurface } from '../../lib/frosted-surface';
@@ -26,7 +28,11 @@ const SelectTrigger = React.forwardRef(({ className, children, style: styleProp,
26
28
  const visual = useVisualTokens();
27
29
  const field = useFormFieldState();
28
30
  const effectiveDisabled = disabled ?? field.disabled;
29
- return (_jsxs(SelectPrimitive.Trigger, { ref: ref, className: cn('border-border bg-muted flex w-full flex-row items-center justify-between gap-2 shadow-sm active:bg-state-pressed', field.invalid && 'border-destructive', focusRing, effectiveDisabled && '', className),
31
+ const { open, setTriggerPosition } = SelectPrimitive.useRootContext();
32
+ const triggerRef = React.useRef(null);
33
+ const composedRef = useComposedRefs(triggerRef, ref);
34
+ useDismissKeyboardOnOpen(open, triggerRef, setTriggerPosition);
35
+ return (_jsxs(SelectPrimitive.Trigger, { ref: composedRef, className: cn('border-border bg-muted flex w-full flex-row items-center justify-between gap-2 shadow-sm active:bg-state-pressed', field.invalid && 'border-destructive', focusRing, effectiveDisabled && '', className),
30
36
  // Flattened, not an array. On web this primitive is merged through a Slot that
31
37
  // spreads `style`, and spreading an array yields { 0: …, 1: … } — numeric keys
32
38
  // that reach the DOM as indexed style properties and throw. The Select rendered
@@ -4,10 +4,15 @@ import { type ShadowTier, type SurfacePaddingSize } from '../../lib/recipes';
4
4
  import type { ForwardRefComponent } from '../../lib/ref-types';
5
5
  export type SurfaceLevel = 'base' | 'raised' | 'overlay';
6
6
  export interface SurfaceProps extends ViewProps, FrostedSurfaceProps {
7
+ /** Semantic background and depth tier. */
7
8
  level?: SurfaceLevel;
9
+ /** Token-backed interior spacing. */
8
10
  padding?: SurfacePaddingSize;
11
+ /** Token-backed corner radius. */
9
12
  radius?: 'sm' | 'md' | 'lg' | 'xl';
13
+ /** Draw the theme hairline border. */
10
14
  bordered?: boolean;
15
+ /** Override the level's depth. */
11
16
  shadow?: ShadowTier;
12
17
  }
13
18
  declare const Surface: ForwardRefComponent<ViewInstance, SurfaceProps>;
@@ -7,9 +7,9 @@ import { tv } from '../../lib/tv';
7
7
  import { useThemeColors } from '../../lib/theme-colors';
8
8
  import { useInteractionTokens } from '../../lib/interaction-tokens';
9
9
  import { useVisualTokens } from '../../lib/visual-tokens';
10
- import { typographyStyle } from '../../lib/recipes';
10
+ import { textCursorColors, typographyStyle } from '../../lib/recipes';
11
11
  import { webAutofillField } from '../../lib/web-autofill';
12
- import { useFormFieldState } from '../form-field/form-field';
12
+ import { fieldAccessibilityProps, useFormFieldState } from '../form-field/form-field';
13
13
  const textareaVariants = tv({
14
14
  base: 'w-full border-border bg-muted text-foreground',
15
15
  variants: {
@@ -48,7 +48,7 @@ const Textarea = React.forwardRef(({ className, size = 'default', placeholderTex
48
48
  paddingVertical: visual.controlPaddingYDefault,
49
49
  ...(!effectiveEditable ? { opacity: interaction.disabledOpacity } : null),
50
50
  };
51
- return (_jsx(TextInput, { ref: ref, className: cn(textareaVariants({ size }), field.invalid && 'border-destructive', focusField, webAutofillField, className), style: style, multiline: true, textAlignVertical: "top", placeholderTextColor: placeholderTextColor ?? colors.placeholder, editable: effectiveEditable, "aria-invalid": ariaInvalid ?? field.invalid, ...props }));
51
+ return (_jsx(TextInput, { ref: ref, ...fieldAccessibilityProps(field, props), className: cn(textareaVariants({ size }), field.invalid && 'border-destructive', focusField, webAutofillField, className), style: style, multiline: true, textAlignVertical: "top", placeholderTextColor: placeholderTextColor ?? colors.placeholder, ...textCursorColors(colors), editable: effectiveEditable, "aria-invalid": ariaInvalid ?? field.invalid, ...props }));
52
52
  });
53
53
  Textarea.displayName = 'Textarea';
54
54
  export { Textarea, textareaVariants };
@@ -25,21 +25,21 @@ export interface ToastProviderProps {
25
25
  export declare function ToastProvider({ children }: ToastProviderProps): React.JSX.Element;
26
26
  declare const toastVariants: import("tailwind-variants").TVReturnType<{
27
27
  variant: {
28
- default: "border-border bg-background";
29
- destructive: "border-destructive bg-destructive";
30
- success: "border-success bg-success";
28
+ default: "border-border";
29
+ destructive: "border-destructive-soft-border";
30
+ success: "border-success-soft-border";
31
31
  };
32
- }, undefined, "w-full flex-row items-center justify-between border", {
32
+ }, undefined, "w-full flex-row items-center gap-3 border bg-popover", {
33
33
  variant: {
34
- default: "border-border bg-background";
35
- destructive: "border-destructive bg-destructive";
36
- success: "border-success bg-success";
34
+ default: "border-border";
35
+ destructive: "border-destructive-soft-border";
36
+ success: "border-success-soft-border";
37
37
  };
38
38
  }, undefined, import("tailwind-variants").TVReturnTypeLike<{
39
39
  variant: {
40
- default: "border-border bg-background";
41
- destructive: "border-destructive bg-destructive";
42
- success: "border-success bg-success";
40
+ default: "border-border";
41
+ destructive: "border-destructive-soft-border";
42
+ success: "border-success-soft-border";
43
43
  };
44
44
  }, undefined>>;
45
45
  export interface ToastProps extends ViewProps, VariantProps<typeof toastVariants> {