@thewhileloop/whileui 4.0.0 → 4.1.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 (51) hide show
  1. package/README.md +186 -20
  2. package/dist/blocks/chat/chat-conversation.d.ts +8 -0
  3. package/dist/blocks/chat/chat-conversation.js +36 -0
  4. package/dist/blocks/chat/chat.js +5 -11
  5. package/dist/blocks/chat/index.d.ts +1 -0
  6. package/dist/blocks/chat/index.js +1 -0
  7. package/dist/blocks/layout/smart-input.js +2 -1
  8. package/dist/blocks/media/smart-image.js +5 -7
  9. package/dist/code.d.ts +1 -0
  10. package/dist/code.js +1 -0
  11. package/dist/components/avatar/avatar.d.ts +3 -2
  12. package/dist/components/avatar/avatar.js +19 -13
  13. package/dist/components/avatar/index.d.ts +1 -1
  14. package/dist/components/card/card.d.ts +1 -1
  15. package/dist/components/card/card.js +4 -25
  16. package/dist/components/code-block/code-block.d.ts +12 -0
  17. package/dist/components/code-block/code-block.js +13 -0
  18. package/dist/components/code-block/index.d.ts +1 -0
  19. package/dist/components/code-block/index.js +1 -0
  20. package/dist/components/form-field/form-field.d.ts +8 -0
  21. package/dist/components/form-field/form-field.js +3 -0
  22. package/dist/components/form-field/index.d.ts +1 -1
  23. package/dist/components/form-field/index.js +1 -1
  24. package/dist/components/input/input.d.ts +1 -0
  25. package/dist/components/input/input.js +11 -6
  26. package/dist/components/markdown/index.d.ts +1 -0
  27. package/dist/components/markdown/index.js +1 -0
  28. package/dist/components/markdown/markdown.d.ts +14 -0
  29. package/dist/components/markdown/markdown.js +94 -0
  30. package/dist/components/numeric-input/numeric-input.d.ts +1 -0
  31. package/dist/components/numeric-input/numeric-input.js +11 -6
  32. package/dist/components/select/select.d.ts +2 -1
  33. package/dist/components/select/select.js +44 -24
  34. package/dist/components/surface/index.d.ts +1 -0
  35. package/dist/components/surface/index.js +1 -0
  36. package/dist/components/surface/surface.d.ts +14 -0
  37. package/dist/components/surface/surface.js +35 -0
  38. package/dist/components/text/text.d.ts +42 -42
  39. package/dist/components/text/text.js +39 -17
  40. package/dist/components/textarea/textarea.d.ts +1 -0
  41. package/dist/components/textarea/textarea.js +7 -3
  42. package/dist/components/toast/toast.js +5 -1
  43. package/dist/index.d.ts +3 -2
  44. package/dist/index.js +2 -1
  45. package/dist/lib/theme-bridge.d.ts +1 -1
  46. package/dist/lib/theme-bridge.js +52 -18
  47. package/dist/lib/web-autofill.d.ts +9 -0
  48. package/dist/lib/web-autofill.js +9 -0
  49. package/dist/markdown.d.ts +1 -0
  50. package/dist/markdown.js +1 -0
  51. package/package.json +16 -2
@@ -0,0 +1,12 @@
1
+ import React from 'react';
2
+ import { type ViewProps } from 'react-native';
3
+ export interface CodeBlockProps extends Omit<ViewProps, 'children'> {
4
+ code: string;
5
+ language?: string;
6
+ actions?: React.ReactNode;
7
+ }
8
+ declare function CodeBlock({ code, language, actions, className, ...props }: CodeBlockProps): React.JSX.Element;
9
+ declare namespace CodeBlock {
10
+ var displayName: string;
11
+ }
12
+ export { CodeBlock };
@@ -0,0 +1,13 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { ScrollView, View } from 'react-native';
3
+ import { Surface } from '../surface';
4
+ import { Text } from '../text';
5
+ import { cn } from '../../lib/cn';
6
+ import { useVisualTokens } from '../../lib/visual-tokens';
7
+ function CodeBlock({ code, language, actions, className, ...props }) {
8
+ const label = language ? `${language} code` : 'Code';
9
+ const visual = useVisualTokens();
10
+ return (_jsxs(Surface, { level: "base", padding: "none", radius: "lg", shadow: "none", className: cn('overflow-hidden bg-muted', className), ...props, children: [language || actions ? (_jsxs(View, { className: "flex-row items-center justify-between border-b border-border px-3 py-2", children: [_jsx(Text, { variant: "caption", children: language ?? 'Code' }), actions] })) : null, _jsx(ScrollView, { horizontal: true, showsHorizontalScrollIndicator: false, contentContainerStyle: { padding: visual.surfacePaddingSm }, children: _jsx(Text, { variant: "code", className: "bg-transparent px-0 py-0 text-foreground", accessibilityLabel: label, selectable: true, children: code }) })] }));
11
+ }
12
+ CodeBlock.displayName = 'CodeBlock';
13
+ export { CodeBlock };
@@ -0,0 +1 @@
1
+ export { CodeBlock, type CodeBlockProps } from './code-block';
@@ -0,0 +1 @@
1
+ export { CodeBlock } from './code-block';
@@ -45,6 +45,14 @@ declare const formControlVariants: import("tailwind-variants").TVReturnType<{
45
45
  false: "";
46
46
  };
47
47
  }, undefined>>;
48
+ type FormFieldDensity = NonNullable<VariantProps<typeof formFieldVariants>['density']>;
49
+ export interface FormFieldState {
50
+ density: FormFieldDensity;
51
+ invalid: boolean;
52
+ disabled: boolean;
53
+ required: boolean;
54
+ }
55
+ export declare function useFormFieldState(): FormFieldState;
48
56
  export interface FormFieldProps extends ViewProps, VariantProps<typeof formFieldVariants> {
49
57
  invalid?: boolean;
50
58
  disabled?: boolean;
@@ -41,6 +41,9 @@ const FormFieldContext = createContext({
41
41
  disabled: false,
42
42
  required: false,
43
43
  });
44
+ export function useFormFieldState() {
45
+ return useContext(FormFieldContext);
46
+ }
44
47
  const FormField = React.forwardRef(({ className, density = 'default', invalid = false, disabled = false, required = false, ...props }, ref) => {
45
48
  const interaction = useInteractionTokens();
46
49
  const contextValue = useMemo(() => ({ density, invalid, disabled, required }), [density, disabled, invalid, required]);
@@ -1 +1 @@
1
- export { FormField, FormLabel, FormControl, FormHint, FormMessage, formFieldVariants, formControlVariants, type FormFieldProps, type FormLabelProps, type FormControlProps, type FormHintProps, type FormMessageProps, } from './form-field';
1
+ export { FormField, FormLabel, FormControl, FormHint, FormMessage, useFormFieldState, formFieldVariants, formControlVariants, type FormFieldProps, type FormLabelProps, type FormControlProps, type FormHintProps, type FormMessageProps, type FormFieldState, } from './form-field';
@@ -1 +1 @@
1
- export { FormField, FormLabel, FormControl, FormHint, FormMessage, formFieldVariants, formControlVariants, } from './form-field';
1
+ export { FormField, FormLabel, FormControl, FormHint, FormMessage, useFormFieldState, formFieldVariants, formControlVariants, } from './form-field';
@@ -25,6 +25,7 @@ declare const inputVariants: import("tailwind-variants").TVReturnType<{
25
25
  };
26
26
  }, undefined>>;
27
27
  export interface InputProps extends TextInputProps, VariantProps<typeof inputVariants> {
28
+ 'aria-invalid'?: boolean;
28
29
  className?: string;
29
30
  prefix?: React.ReactNode;
30
31
  suffix?: React.ReactNode;
@@ -8,6 +8,8 @@ import { useThemeColors } from '../../lib/theme-colors';
8
8
  import { useInteractionTokens } from '../../lib/interaction-tokens';
9
9
  import { useVisualTokens } from '../../lib/visual-tokens';
10
10
  import { typographyStyle } from '../../lib/recipes';
11
+ import { webAutofillField } from '../../lib/web-autofill';
12
+ import { useFormFieldState } from '../form-field/form-field';
11
13
  const inputVariants = tv({
12
14
  base: 'w-full border-border bg-muted text-foreground',
13
15
  variants: {
@@ -22,27 +24,30 @@ const inputVariants = tv({
22
24
  variant: 'default',
23
25
  },
24
26
  });
25
- const Input = React.forwardRef(({ className, prefix, suffix, inputClassName, variant, editable = true, placeholderTextColor, onFocus, onBlur, ...props }, ref) => {
27
+ const Input = React.forwardRef(({ className, prefix, suffix, inputClassName, variant, editable, 'aria-invalid': ariaInvalid, placeholderTextColor, onFocus, onBlur, ...props }, ref) => {
26
28
  const colors = useThemeColors();
27
29
  const interaction = useInteractionTokens();
28
30
  const visual = useVisualTokens();
31
+ const field = useFormFieldState();
32
+ const effectiveEditable = editable ?? !field.disabled;
33
+ const effectiveVariant = variant ?? (field.invalid ? 'error' : 'default');
29
34
  // The border is on this wrapper and the focus is on the TextInput inside it, so the
30
35
  // wrapper learns about focus from the events rather than a pseudo-class.
31
36
  const [focused, setFocused] = useState(false);
32
- const plain = variant === 'plain';
33
- return (_jsxs(View, { className: cn('flex-row items-center border-border bg-muted', inputVariants({ variant }), focused && !plain && focusFieldWrapper, className), style: [
37
+ const plain = effectiveVariant === 'plain';
38
+ return (_jsxs(View, { className: cn('flex-row items-center border-border bg-muted', inputVariants({ variant: effectiveVariant }), focused && !plain && focusFieldWrapper, className), style: [
34
39
  {
35
40
  minHeight: visual.controlHeightDefault,
36
41
  borderWidth: plain ? 0 : visual.borderWidthControl,
37
42
  borderRadius: visual.radiusLg,
38
43
  paddingHorizontal: plain ? 0 : visual.controlPaddingXDefault,
39
44
  },
40
- !editable ? { opacity: interaction.disabledOpacity } : undefined,
41
- ], 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', inputClassName), style: {
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: {
42
47
  ...typographyStyle(visual, 'label'),
43
48
  minHeight: visual.controlHeightDefault - 2,
44
49
  paddingVertical: visual.controlPaddingYDefault,
45
- }, editable: editable, placeholderTextColor: placeholderTextColor ?? colors.placeholder, onFocus: (event) => {
50
+ }, editable: effectiveEditable, "aria-invalid": ariaInvalid ?? field.invalid, placeholderTextColor: placeholderTextColor ?? colors.placeholder, onFocus: (event) => {
46
51
  setFocused(true);
47
52
  onFocus?.(event);
48
53
  }, onBlur: (event) => {
@@ -0,0 +1 @@
1
+ export { Markdown, type MarkdownProps } from './markdown';
@@ -0,0 +1 @@
1
+ export { Markdown } from './markdown';
@@ -0,0 +1,14 @@
1
+ import React from 'react';
2
+ import { type StyleProp, type TextStyle, type ViewProps } from 'react-native';
3
+ export interface MarkdownProps extends Omit<ViewProps, 'children'> {
4
+ children: string;
5
+ onLinkPress?: (url: string) => void;
6
+ codeActions?: (code: string, language: string | undefined) => React.ReactNode;
7
+ textStyle?: StyleProp<TextStyle>;
8
+ selectable?: boolean;
9
+ }
10
+ declare function Markdown({ children, onLinkPress, codeActions, textStyle, selectable, className, ...props }: MarkdownProps): React.JSX.Element;
11
+ declare namespace Markdown {
12
+ var displayName: string;
13
+ }
14
+ export { Markdown };
@@ -0,0 +1,94 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useMemo } from 'react';
3
+ import { View } from 'react-native';
4
+ import MarkdownIt from 'markdown-it';
5
+ import { CodeBlock } from '../code-block';
6
+ import { Text } from '../text';
7
+ import { cn } from '../../lib/cn';
8
+ import { useVisualTokens } from '../../lib/visual-tokens';
9
+ const parser = new MarkdownIt({ html: false, linkify: true, typographer: true });
10
+ function toTree(tokens) {
11
+ const root = [];
12
+ const stack = [root];
13
+ for (const token of tokens) {
14
+ if (token.nesting === -1) {
15
+ stack.pop();
16
+ continue;
17
+ }
18
+ const node = { token, children: [] };
19
+ stack[stack.length - 1]?.push(node);
20
+ if (token.nesting === 1)
21
+ stack.push(node.children);
22
+ }
23
+ return root;
24
+ }
25
+ function getAttribute(token, name) {
26
+ return token.attrGet(name) ?? undefined;
27
+ }
28
+ function Markdown({ children, onLinkPress, codeActions, textStyle, selectable = true, className, ...props }) {
29
+ const visual = useVisualTokens();
30
+ const nodes = useMemo(() => toTree(parser.parse(children, {})), [children]);
31
+ const renderInline = (node, key) => {
32
+ const { token } = node;
33
+ if (token.type === 'text')
34
+ return token.content;
35
+ if (token.type === 'softbreak' || token.type === 'hardbreak')
36
+ return '\n';
37
+ if (token.type === 'code_inline') {
38
+ return (_jsx(Text, { variant: "code", selectable: selectable, children: token.content }, key));
39
+ }
40
+ if (token.type === 'image')
41
+ return token.content;
42
+ const content = node.children.map((child, index) => renderInline(child, `${key}-${index}`));
43
+ if (token.type === 'strong_open')
44
+ return (_jsx(Text, { className: "font-bold", children: content }, key));
45
+ if (token.type === 'em_open')
46
+ return (_jsx(Text, { style: { fontStyle: 'italic' }, children: content }, key));
47
+ if (token.type === 's_open')
48
+ return (_jsx(Text, { style: { textDecorationLine: 'line-through' }, children: content }, key));
49
+ if (token.type === 'link_open') {
50
+ const href = getAttribute(token, 'href');
51
+ return (_jsx(Text, { className: "text-primary underline", accessibilityRole: onLinkPress ? 'link' : undefined, onPress: href && onLinkPress ? () => onLinkPress(href) : undefined, children: content }, key));
52
+ }
53
+ return content;
54
+ };
55
+ const renderNode = (node, key, listIndex) => {
56
+ const { token } = node;
57
+ if (token.type === 'inline') {
58
+ return toTree(token.children ?? []).map((child, index) => renderInline(child, `${key}-${index}`));
59
+ }
60
+ if (token.type === 'paragraph_open') {
61
+ return (_jsx(Text, { variant: "body", style: textStyle, selectable: selectable, children: node.children.map((child, index) => renderNode(child, `${key}-${index}`)) }, key));
62
+ }
63
+ if (token.type === 'heading_open') {
64
+ const level = Number(token.tag.slice(1));
65
+ const variant = level === 1 ? 'heading' : level === 2 ? 'title' : 'subtitle';
66
+ return (_jsx(Text, { variant: variant, accessibilityRole: "header", style: textStyle, selectable: selectable, children: node.children.map((child, index) => renderNode(child, `${key}-${index}`)) }, key));
67
+ }
68
+ if (token.type === 'fence' || token.type === 'code_block') {
69
+ const language = token.info.trim().split(/\s+/)[0] || undefined;
70
+ return (_jsx(CodeBlock, { code: token.content.replace(/\n$/, ''), language: language, actions: codeActions?.(token.content, language) }, key));
71
+ }
72
+ if (token.type === 'bullet_list_open' || token.type === 'ordered_list_open') {
73
+ const ordered = token.type === 'ordered_list_open';
74
+ const start = Number(getAttribute(token, 'start') ?? 1);
75
+ return (_jsx(View, { className: "gap-2", children: node.children.map((child, index) => renderNode(child, `${key}-${index}`, ordered ? start + index : undefined)) }, key));
76
+ }
77
+ if (token.type === 'list_item_open') {
78
+ return (_jsxs(View, { className: "flex-row items-start gap-2", children: [_jsx(Text, { variant: "body", children: listIndex === undefined ? '•' : `${listIndex}.` }), _jsx(View, { className: "min-w-0 flex-1 gap-1", children: node.children.map((child, index) => renderNode(child, `${key}-${index}`)) })] }, key));
79
+ }
80
+ if (token.type === 'blockquote_open') {
81
+ return (_jsx(View, { className: "border-l border-border bg-muted", style: {
82
+ borderLeftWidth: visual.borderWidthEmphasis,
83
+ paddingHorizontal: visual.surfacePaddingSm,
84
+ paddingVertical: visual.controlPaddingYSm,
85
+ }, children: node.children.map((child, index) => renderNode(child, `${key}-${index}`)) }, key));
86
+ }
87
+ if (token.type === 'hr')
88
+ return _jsx(View, { className: "h-px bg-border" }, key);
89
+ return node.children.map((child, index) => renderNode(child, `${key}-${index}`));
90
+ };
91
+ return (_jsx(View, { className: cn('gap-3', className), ...props, children: nodes.map((node, index) => renderNode(node, String(index))) }));
92
+ }
93
+ Markdown.displayName = 'Markdown';
94
+ export { Markdown };
@@ -47,6 +47,7 @@ declare const numericInputTextVariants: import("tailwind-variants").TVReturnType
47
47
  };
48
48
  }, undefined>>;
49
49
  export interface NumericInputProps extends Omit<TextInputProps, 'value' | 'defaultValue' | 'onChangeText' | 'keyboardType'>, VariantProps<typeof numericInputVariants> {
50
+ 'aria-invalid'?: boolean;
50
51
  className?: string;
51
52
  inputClassName?: string;
52
53
  prefix?: React.ReactNode;
@@ -8,6 +8,8 @@ import { useThemeColors } from '../../lib/theme-colors';
8
8
  import { useInteractionTokens, withInteractivePressableStyle } from '../../lib/interaction-tokens';
9
9
  import { useVisualTokens } from '../../lib/visual-tokens';
10
10
  import { typographyStyle } from '../../lib/recipes';
11
+ import { webAutofillField } from '../../lib/web-autofill';
12
+ import { useFormFieldState } from '../form-field/form-field';
11
13
  const numericInputVariants = tv({
12
14
  base: 'w-full flex-row items-center border-border bg-muted',
13
15
  variants: {
@@ -135,10 +137,13 @@ function NumericSteppers({ visible, inset, size, radius, model, interaction, vis
135
137
  return null;
136
138
  return (_jsxs(View, { className: "shrink-0 flex-row items-center", style: { gap: inset, paddingLeft: Math.max(6, inset) }, children: [_jsx(NumericStepperButton, { delta: -1, enabled: model.canDecrease, label: "Decrease value", symbol: "\u2212", size: size, radius: radius, model: model, interaction: interaction, visual: visual }), _jsx(NumericStepperButton, { delta: 1, enabled: model.canIncrease, label: "Increase value", symbol: "+", size: size, radius: radius, model: model, interaction: interaction, visual: visual })] }));
137
139
  }
138
- const NumericInput = React.forwardRef(({ className, inputClassName, prefix, suffix, value, defaultValue, onValueChange, min, max, step = 1, showSteppers = false, unstyled = false, variant = 'default', size = 'default', editable = true, placeholderTextColor, onFocus, onBlur, style: styleProp, ...props }, ref) => {
140
+ const NumericInput = React.forwardRef(({ className, inputClassName, prefix, suffix, value, defaultValue, onValueChange, min, max, step = 1, showSteppers = false, unstyled = false, variant, size = 'default', editable, 'aria-invalid': ariaInvalid, placeholderTextColor, onFocus, onBlur, style: styleProp, ...props }, ref) => {
139
141
  const colors = useThemeColors();
140
142
  const interaction = useInteractionTokens();
141
143
  const visual = useVisualTokens();
144
+ const field = useFormFieldState();
145
+ const effectiveEditable = editable ?? !field.disabled;
146
+ const effectiveVariant = variant ?? (field.invalid ? 'error' : 'default');
142
147
  // The border is on the wrapper and the focus on the field inside it.
143
148
  const [focused, setFocused] = useState(false);
144
149
  const model = useNumericInputModel({
@@ -147,7 +152,7 @@ const NumericInput = React.forwardRef(({ className, inputClassName, prefix, suff
147
152
  min,
148
153
  max,
149
154
  step,
150
- editable,
155
+ editable: effectiveEditable,
151
156
  onValueChange,
152
157
  onBlur,
153
158
  });
@@ -156,7 +161,7 @@ const NumericInput = React.forwardRef(({ className, inputClassName, prefix, suff
156
161
  const stepInset = Math.max(3, Math.round(controlHeight * 0.1));
157
162
  const stepSize = controlHeight - stepInset * 2;
158
163
  const stepRadius = Math.max(3, Math.min(visual.radiusMd, Math.round(stepSize / 3)));
159
- return (_jsxs(View, { className: cn(numericInputVariants({ variant, size }), unstyled && 'bg-transparent', focused && !unstyled && focusFieldWrapper, className), style: [
164
+ return (_jsxs(View, { className: cn(numericInputVariants({ variant: effectiveVariant, size }), unstyled && 'bg-transparent', focused && !unstyled && focusFieldWrapper, className), style: [
160
165
  {
161
166
  // Definite height (not minHeight) so inner elements can center reliably.
162
167
  height: controlHeight,
@@ -165,14 +170,14 @@ const NumericInput = React.forwardRef(({ className, inputClassName, prefix, suff
165
170
  paddingLeft: !unstyled && prefix ? visual.controlPaddingXDefault : 0,
166
171
  paddingRight: unstyled ? 0 : showSteppers ? stepInset : 0,
167
172
  },
168
- !editable ? { opacity: interaction.disabledOpacity } : undefined,
169
- ], 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', inputClassName), style: numericInputTextStyle(visual, size, unstyled, styleProp), value: model.textValue, onChangeText: model.handleTextChange, onFocus: (event) => {
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) => {
170
175
  setFocused(true);
171
176
  onFocus?.(event);
172
177
  }, onBlur: (event) => {
173
178
  setFocused(false);
174
179
  model.handleBlur(event);
175
- }, keyboardType: "decimal-pad", editable: editable, 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, ...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 })] }));
176
181
  });
177
182
  NumericInput.displayName = 'NumericInput';
178
183
  export { NumericInput, numericInputVariants, numericInputTextVariants };
@@ -15,6 +15,7 @@ export interface SelectProps {
15
15
  }
16
16
  export interface SelectTriggerProps extends Omit<React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>, 'style'> {
17
17
  className?: string;
18
+ 'aria-invalid'?: boolean;
18
19
  /**
19
20
  * Object form only. On web this primitive is merged through a Slot that spreads
20
21
  * `style`, which cannot carry a state callback — a function silently becomes `{}` and
@@ -56,7 +57,7 @@ export interface SelectLabelProps extends React.ComponentPropsWithoutRef<typeof
56
57
  export interface SelectSeparatorProps extends React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator> {
57
58
  className?: string;
58
59
  }
59
- declare const Select: typeof SelectPrimitive.Root;
60
+ declare function Select({ disabled, ...props }: SelectProps): React.JSX.Element;
60
61
  declare const SelectGroup: typeof SelectPrimitive.Group;
61
62
  declare const SelectTrigger: ForwardRefComponent<SelectTriggerRef, SelectTriggerProps>;
62
63
  declare function SelectValue({ className, placeholder }: SelectValueProps): React.JSX.Element;
@@ -1,4 +1,4 @@
1
- import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
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, View, } from 'react-native';
4
4
  import * as SelectPrimitive from '@rn-primitives/select';
@@ -11,14 +11,20 @@ import { useFrostedSurface } from '../../lib/frosted-surface';
11
11
  import { useVisualTokens } from '../../lib/visual-tokens';
12
12
  import { useThemeColors } from '../../lib/theme-colors';
13
13
  import { fieldRecipe, shadowStyle, surfaceRadius, typographyStyle } from '../../lib/recipes';
14
+ import { useFormFieldState } from '../form-field/form-field';
14
15
  // iOS needs FullWindowOverlay to render above everything
15
16
  const FullWindowOverlay = Platform.OS === 'ios' ? RNFullWindowOverlay : React.Fragment;
16
- const Select = SelectPrimitive.Root;
17
+ function Select({ disabled, ...props }) {
18
+ const field = useFormFieldState();
19
+ return _jsx(SelectPrimitive.Root, { disabled: disabled ?? field.disabled, ...props });
20
+ }
17
21
  const SelectGroup = SelectPrimitive.Group;
18
- const SelectTrigger = React.forwardRef(({ className, children, style: styleProp, ...props }, ref) => {
22
+ const SelectTrigger = React.forwardRef(({ className, children, style: styleProp, disabled, 'aria-invalid': ariaInvalid, ...props }, ref) => {
19
23
  const interaction = useInteractionTokens();
20
24
  const visual = useVisualTokens();
21
- 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', focusRing, props.disabled && '', className),
25
+ const field = useFormFieldState();
26
+ const effectiveDisabled = disabled ?? field.disabled;
27
+ 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),
22
28
  // Flattened, not an array. On web this primitive is merged through a Slot that
23
29
  // spreads `style`, and spreading an array yields { 0: …, 1: … } — numeric keys
24
30
  // that reach the DOM as indexed style properties and throw. The Select rendered
@@ -26,8 +32,8 @@ const SelectTrigger = React.forwardRef(({ className, children, style: styleProp,
26
32
  style: StyleSheet.flatten([
27
33
  fieldRecipe(visual, 'default'),
28
34
  styleProp,
29
- props.disabled ? { opacity: interaction.disabledOpacity } : null,
30
- ]), ...props, children: [_jsx(_Fragment, { children: children }), _jsx(View, { className: "border-muted-foreground h-2 w-2 rotate-45 border-b-2 border-r-2" })] }));
35
+ effectiveDisabled ? { opacity: interaction.disabledOpacity } : null,
36
+ ]), disabled: effectiveDisabled, "aria-invalid": ariaInvalid ?? field.invalid, ...props, children: [_jsx(_Fragment, { children: children }), _jsx(View, { testID: "select-chevron", className: "border-muted-foreground h-2 w-2 shrink-0 rotate-45 border-b-2 border-r-2" })] }));
31
37
  });
32
38
  SelectTrigger.displayName = 'SelectTrigger';
33
39
  function SelectValue({ className, placeholder = 'Select...' }) {
@@ -39,7 +45,9 @@ function SelectValue({ className, placeholder = 'Select...' }) {
39
45
  // user-agent default black — 1.2:1 on a dark surface. Rendering the label ourselves
40
46
  // instead would fix the colour and lose the value: Radix owns the selection on web,
41
47
  // and `useRootContext().value` is not populated there.
42
- return (_jsx(Text, { className: cn('text-foreground', !value && 'text-muted-foreground', className), style: typographyStyle(visual, 'body'), numberOfLines: 1, children: _jsx(SelectPrimitive.Value, { placeholder: placeholder }) }));
48
+ // Radix renders the selected ItemText as a nested RNW div, which resets white-space.
49
+ // The descendant utilities keep that generated node on the same truncated line.
50
+ return (_jsx(Text, { testID: "select-value", className: cn('min-w-0 flex-1 text-foreground web:overflow-hidden web:text-ellipsis web:whitespace-nowrap web:[&_*]:max-w-full web:[&_*]:overflow-hidden web:[&_*]:text-ellipsis web:[&_*]:whitespace-nowrap', !value && 'text-muted-foreground', className), style: typographyStyle(visual, 'body'), numberOfLines: 1, children: _jsx(SelectPrimitive.Value, { placeholder: placeholder }) }));
43
51
  }
44
52
  const SelectContent = React.forwardRef(({ className, children, portalHost, position = 'popper', insets, style: styleProp, frosted = false, blurIntensity, blurTintToken, ...props }, ref) => {
45
53
  const frostedSurface = useFrostedSurface({
@@ -51,28 +59,40 @@ const SelectContent = React.forwardRef(({ className, children, portalHost, posit
51
59
  });
52
60
  const visual = useVisualTokens();
53
61
  const colors = useThemeColors();
54
- return (_jsx(SelectPrimitive.Portal, { hostName: portalHost, children: _jsx(FullWindowOverlay, { children: _jsx(SelectPrimitive.Overlay, { style: Platform.select({ native: StyleSheet.absoluteFill }), children: _jsxs(SelectPrimitive.Content, { ref: ref, position: position, insets: insets, style: StyleSheet.flatten([
55
- {
56
- borderRadius: surfaceRadius(visual, 'lg'),
57
- borderWidth: visual.borderWidthHairline,
58
- },
59
- shadowStyle(visual, colors, 'md'),
60
- frostedSurface.surfaceStyle,
61
- styleProp,
62
- ]), className: cn('border-border relative z-50 min-w-32 overflow-hidden', frosted ? 'bg-transparent' : 'bg-popover', Platform.select({
63
- web: cn('max-h-96 overflow-y-auto overflow-x-hidden', props.side === 'bottom' && 'translate-y-1', props.side === 'top' && '-translate-y-1'),
62
+ // Radix applies its collision variables to the listbox wrapper, while
63
+ // @rn-primitives renders our visual surface one level inside it. Keep the sizing
64
+ // on that surface as inline web CSS: the Viewport's own `width: 100%` otherwise
65
+ // wins class merging and lets the longest option size the outer popper.
66
+ const webSizingStyle = Platform.OS === 'web' && position === 'popper'
67
+ ? {
68
+ boxSizing: 'border-box',
69
+ width: 'var(--radix-select-trigger-width)',
70
+ maxWidth: 'var(--radix-select-content-available-width)',
71
+ maxHeight: 'var(--radix-select-content-available-height)',
72
+ }
73
+ : undefined;
74
+ return (_jsx(SelectPrimitive.Portal, { hostName: portalHost, children: _jsx(FullWindowOverlay, { children: _jsx(SelectPrimitive.Overlay, { style: Platform.select({ native: StyleSheet.absoluteFill }), children: _jsxs(SelectPrimitive.Content, { ref: ref, position: position, insets: insets, style: {
75
+ ...webSizingStyle,
76
+ ...StyleSheet.flatten([
77
+ {
78
+ borderRadius: surfaceRadius(visual, 'lg'),
79
+ borderWidth: visual.borderWidthHairline,
80
+ },
81
+ shadowStyle(visual, colors, 'md'),
82
+ frostedSurface.surfaceStyle,
83
+ styleProp,
84
+ ]),
85
+ }, className: cn('border-border relative z-50 overflow-hidden', frosted ? 'bg-transparent' : 'bg-popover', Platform.select({
86
+ web: cn('overflow-y-auto overflow-x-hidden', props.side === 'bottom' && 'translate-y-1', props.side === 'top' && '-translate-y-1'),
64
87
  native: 'p-1',
65
- }), className), ...props, children: [frostedSurface.overlay, _jsx(SelectPrimitive.Viewport, { className: cn('p-1', position === 'popper' &&
66
- Platform.select({
67
- web: 'h-(--radix-select-trigger-height) w-full min-w-(--radix-select-trigger-width)',
68
- })), children: children })] }) }) }) }));
88
+ }), className), ...props, children: [frostedSurface.overlay, _jsx(SelectPrimitive.Viewport, { className: "p-1", children: children })] }) }) }) }));
69
89
  });
70
90
  SelectContent.displayName = 'SelectContent';
71
91
  const SelectItem = React.forwardRef(({ className, children, style: styleProp, ...props }, ref) => {
72
92
  const interaction = useInteractionTokens();
73
93
  const visual = useVisualTokens();
74
- return (_jsxs(SelectPrimitive.Item, { ref: ref, className: cn('active:bg-accent group relative flex w-full flex-row items-center gap-2 pr-8', Platform.select({
75
- web: 'focus:bg-accent focus:text-accent-foreground cursor-default outline-none',
94
+ return (_jsxs(SelectPrimitive.Item, { ref: ref, className: cn('active:bg-state-pressed group relative flex w-full flex-row items-center gap-2 pr-8', Platform.select({
95
+ web: 'focus:bg-state-hover focus:text-foreground cursor-default outline-none',
76
96
  }), props.disabled && '', className), style: StyleSheet.flatten([
77
97
  {
78
98
  borderRadius: visual.radiusSm,
@@ -82,7 +102,7 @@ const SelectItem = React.forwardRef(({ className, children, style: styleProp, ..
82
102
  },
83
103
  styleProp,
84
104
  props.disabled ? { opacity: interaction.disabledOpacity } : null,
85
- ]), ...props, children: [_jsx(View, { className: "absolute right-3 flex h-4 w-4 items-center justify-center", children: _jsx(SelectPrimitive.ItemIndicator, { children: _jsx(View, { className: "border-foreground h-2.5 w-1.5 rotate-45 border-b-2 border-r-2 -mt-0.5" }) }) }), children, _jsx(SelectPrimitive.ItemText, { className: "text-foreground group-active:text-accent-foreground", style: typographyStyle(visual, 'body') })] }));
105
+ ]), ...props, children: [_jsx(View, { className: "absolute right-3 flex h-4 w-4 items-center justify-center", children: _jsx(SelectPrimitive.ItemIndicator, { children: _jsx(View, { className: "border-primary h-2.5 w-1.5 rotate-45 border-b-2 border-r-2 -mt-0.5" }) }) }), children, _jsx(SelectPrimitive.ItemText, { className: "min-w-0 flex-1 text-foreground web:whitespace-normal web:break-words", style: typographyStyle(visual, 'body') })] }));
86
106
  });
87
107
  SelectItem.displayName = 'SelectItem';
88
108
  function SelectItemIcon({ className, children }) {
@@ -0,0 +1 @@
1
+ export { Surface, type SurfaceLevel, type SurfaceProps } from './surface';
@@ -0,0 +1 @@
1
+ export { Surface } from './surface';
@@ -0,0 +1,14 @@
1
+ import { type ViewInstance, type ViewProps } from 'react-native';
2
+ import { type FrostedSurfaceProps } from '../../lib/frosted-surface';
3
+ import { type ShadowTier, type SurfacePaddingSize } from '../../lib/recipes';
4
+ import type { ForwardRefComponent } from '../../lib/ref-types';
5
+ export type SurfaceLevel = 'base' | 'raised' | 'overlay';
6
+ export interface SurfaceProps extends ViewProps, FrostedSurfaceProps {
7
+ level?: SurfaceLevel;
8
+ padding?: SurfacePaddingSize;
9
+ radius?: 'sm' | 'md' | 'lg' | 'xl';
10
+ bordered?: boolean;
11
+ shadow?: ShadowTier;
12
+ }
13
+ declare const Surface: ForwardRefComponent<ViewInstance, SurfaceProps>;
14
+ export { Surface };
@@ -0,0 +1,35 @@
1
+ import { jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React from 'react';
3
+ import { View } from 'react-native';
4
+ import { cn } from '../../lib/cn';
5
+ import { useFrostedSurface } from '../../lib/frosted-surface';
6
+ import { useShadowStyle, useSurfaceRecipe, } from '../../lib/recipes';
7
+ const LEVEL_CLASS = {
8
+ base: 'bg-card',
9
+ raised: 'bg-surface-elevated',
10
+ overlay: 'bg-popover',
11
+ };
12
+ const LEVEL_SHADOW = {
13
+ base: 'none',
14
+ raised: 'sm',
15
+ overlay: 'lg',
16
+ };
17
+ const LEVEL_TINT = {
18
+ base: 'card',
19
+ raised: 'surfaceElevated',
20
+ overlay: 'popover',
21
+ };
22
+ const Surface = React.forwardRef(({ level = 'base', padding = 'default', radius = 'xl', bordered = true, shadow, frosted = false, blurIntensity, blurTintToken, className, style, children, ...props }, ref) => {
23
+ const recipe = useSurfaceRecipe({ padding, radius, bordered });
24
+ const depth = useShadowStyle(shadow ?? LEVEL_SHADOW[level]);
25
+ const frost = useFrostedSurface({
26
+ frosted,
27
+ blurIntensity,
28
+ blurTintToken,
29
+ defaultTintToken: LEVEL_TINT[level],
30
+ defaultBlurPreset: 'subtle',
31
+ });
32
+ return (_jsxs(View, { ref: ref, className: cn('border-border', LEVEL_CLASS[level], frosted && 'relative overflow-hidden bg-transparent', className), style: [recipe, depth, frost.surfaceStyle, style], ...props, children: [frost.overlay, children] }));
33
+ });
34
+ Surface.displayName = 'Surface';
35
+ export { Surface };
@@ -4,62 +4,62 @@ import type { ForwardRefComponent } from '../../lib/ref-types';
4
4
  declare const textVariants: import("tailwind-variants").TVReturnType<{
5
5
  variant: {
6
6
  default: "font-normal";
7
- heading: "text-2xl font-bold tracking-tight";
8
- title: "text-xl font-semibold";
9
- subtitle: "text-lg font-medium";
10
- body: "text-base font-normal";
11
- caption: "text-sm text-muted-foreground font-normal";
12
- label: "text-sm font-medium leading-tight";
13
- code: "font-mono text-sm bg-muted rounded px-1 py-0.5";
7
+ heading: "font-bold tracking-tight";
8
+ title: "font-semibold";
9
+ subtitle: "font-medium";
10
+ body: "font-normal";
11
+ caption: "text-muted-foreground font-normal";
12
+ label: "font-medium leading-tight";
13
+ code: "font-mono bg-muted rounded px-1 py-0.5";
14
14
  };
15
15
  size: {
16
- xs: "text-xs";
17
- sm: "text-sm";
18
- base: "text-base";
19
- lg: "text-lg";
20
- xl: "text-xl";
21
- '2xl': "text-2xl";
22
- '3xl': "text-3xl";
16
+ xs: "";
17
+ sm: "";
18
+ base: "";
19
+ lg: "";
20
+ xl: "";
21
+ '2xl': "";
22
+ '3xl': "";
23
23
  };
24
24
  }, undefined, "text-foreground", {
25
25
  variant: {
26
26
  default: "font-normal";
27
- heading: "text-2xl font-bold tracking-tight";
28
- title: "text-xl font-semibold";
29
- subtitle: "text-lg font-medium";
30
- body: "text-base font-normal";
31
- caption: "text-sm text-muted-foreground font-normal";
32
- label: "text-sm font-medium leading-tight";
33
- code: "font-mono text-sm bg-muted rounded px-1 py-0.5";
27
+ heading: "font-bold tracking-tight";
28
+ title: "font-semibold";
29
+ subtitle: "font-medium";
30
+ body: "font-normal";
31
+ caption: "text-muted-foreground font-normal";
32
+ label: "font-medium leading-tight";
33
+ code: "font-mono bg-muted rounded px-1 py-0.5";
34
34
  };
35
35
  size: {
36
- xs: "text-xs";
37
- sm: "text-sm";
38
- base: "text-base";
39
- lg: "text-lg";
40
- xl: "text-xl";
41
- '2xl': "text-2xl";
42
- '3xl': "text-3xl";
36
+ xs: "";
37
+ sm: "";
38
+ base: "";
39
+ lg: "";
40
+ xl: "";
41
+ '2xl': "";
42
+ '3xl': "";
43
43
  };
44
44
  }, undefined, import("tailwind-variants").TVReturnTypeLike<{
45
45
  variant: {
46
46
  default: "font-normal";
47
- heading: "text-2xl font-bold tracking-tight";
48
- title: "text-xl font-semibold";
49
- subtitle: "text-lg font-medium";
50
- body: "text-base font-normal";
51
- caption: "text-sm text-muted-foreground font-normal";
52
- label: "text-sm font-medium leading-tight";
53
- code: "font-mono text-sm bg-muted rounded px-1 py-0.5";
47
+ heading: "font-bold tracking-tight";
48
+ title: "font-semibold";
49
+ subtitle: "font-medium";
50
+ body: "font-normal";
51
+ caption: "text-muted-foreground font-normal";
52
+ label: "font-medium leading-tight";
53
+ code: "font-mono bg-muted rounded px-1 py-0.5";
54
54
  };
55
55
  size: {
56
- xs: "text-xs";
57
- sm: "text-sm";
58
- base: "text-base";
59
- lg: "text-lg";
60
- xl: "text-xl";
61
- '2xl': "text-2xl";
62
- '3xl': "text-3xl";
56
+ xs: "";
57
+ sm: "";
58
+ base: "";
59
+ lg: "";
60
+ xl: "";
61
+ '2xl': "";
62
+ '3xl': "";
63
63
  };
64
64
  }, undefined>>;
65
65
  export interface TextProps extends RNTextProps, VariantProps<typeof textVariants> {