@thewhileloop/whileui 3.0.0 → 4.1.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 (59) hide show
  1. package/README.md +244 -80
  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/datepicker/index.d.ts +1 -0
  8. package/dist/blocks/datepicker/index.js +1 -0
  9. package/dist/blocks/datepicker/time-picker-modal.d.ts +43 -0
  10. package/dist/blocks/datepicker/time-picker-modal.js +187 -0
  11. package/dist/blocks/layout/smart-input.js +2 -1
  12. package/dist/code.d.ts +1 -0
  13. package/dist/code.js +1 -0
  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/fab/fab.d.ts +87 -0
  21. package/dist/components/fab/fab.js +59 -0
  22. package/dist/components/fab/index.d.ts +1 -0
  23. package/dist/components/fab/index.js +1 -0
  24. package/dist/components/form-field/form-field.d.ts +8 -0
  25. package/dist/components/form-field/form-field.js +3 -0
  26. package/dist/components/form-field/index.d.ts +1 -1
  27. package/dist/components/form-field/index.js +1 -1
  28. package/dist/components/input/input.d.ts +7 -0
  29. package/dist/components/input/input.js +26 -9
  30. package/dist/components/markdown/index.d.ts +1 -0
  31. package/dist/components/markdown/index.js +1 -0
  32. package/dist/components/markdown/markdown.d.ts +14 -0
  33. package/dist/components/markdown/markdown.js +94 -0
  34. package/dist/components/numeric-input/numeric-input.d.ts +1 -0
  35. package/dist/components/numeric-input/numeric-input.js +19 -6
  36. package/dist/components/select/select.d.ts +2 -1
  37. package/dist/components/select/select.js +18 -15
  38. package/dist/components/surface/index.d.ts +1 -0
  39. package/dist/components/surface/index.js +1 -0
  40. package/dist/components/surface/surface.d.ts +14 -0
  41. package/dist/components/surface/surface.js +35 -0
  42. package/dist/components/text/text.d.ts +42 -42
  43. package/dist/components/text/text.js +39 -17
  44. package/dist/components/textarea/textarea.d.ts +1 -0
  45. package/dist/components/textarea/textarea.js +8 -4
  46. package/dist/index.d.ts +4 -2
  47. package/dist/index.js +4 -2
  48. package/dist/lib/focus-ring.d.ts +10 -0
  49. package/dist/lib/focus-ring.js +10 -0
  50. package/dist/lib/interaction-tokens.d.ts +24 -1
  51. package/dist/lib/interaction-tokens.js +26 -44
  52. package/dist/lib/motion-policy.js +2 -2
  53. package/dist/lib/theme-presets.d.ts +26 -0
  54. package/dist/lib/theme-presets.js +28 -0
  55. package/dist/lib/web-autofill.d.ts +9 -0
  56. package/dist/lib/web-autofill.js +9 -0
  57. package/dist/markdown.d.ts +1 -0
  58. package/dist/markdown.js +1 -0
  59. package/package.json +16 -2
@@ -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;
@@ -2,12 +2,14 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import React, { useCallback, useMemo, useState } from 'react';
3
3
  import { Platform, Pressable, Text, TextInput, View, } from 'react-native';
4
4
  import { cn } from '../../lib/cn';
5
- import { focusRing } from '../../lib/focus-ring';
5
+ import { focusFieldWrapper } from '../../lib/focus-ring';
6
6
  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
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,17 +137,22 @@ 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, 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');
147
+ // The border is on the wrapper and the focus on the field inside it.
148
+ const [focused, setFocused] = useState(false);
142
149
  const model = useNumericInputModel({
143
150
  value,
144
151
  defaultValue,
145
152
  min,
146
153
  max,
147
154
  step,
148
- editable,
155
+ editable: effectiveEditable,
149
156
  onValueChange,
150
157
  onBlur,
151
158
  });
@@ -154,7 +161,7 @@ const NumericInput = React.forwardRef(({ className, inputClassName, prefix, suff
154
161
  const stepInset = Math.max(3, Math.round(controlHeight * 0.1));
155
162
  const stepSize = controlHeight - stepInset * 2;
156
163
  const stepRadius = Math.max(3, Math.min(visual.radiusMd, Math.round(stepSize / 3)));
157
- return (_jsxs(View, { className: cn(numericInputVariants({ variant, size }), unstyled && 'bg-transparent', className), style: [
164
+ return (_jsxs(View, { className: cn(numericInputVariants({ variant: effectiveVariant, size }), unstyled && 'bg-transparent', focused && !unstyled && focusFieldWrapper, className), style: [
158
165
  {
159
166
  // Definite height (not minHeight) so inner elements can center reliably.
160
167
  height: controlHeight,
@@ -163,8 +170,14 @@ const NumericInput = React.forwardRef(({ className, inputClassName, prefix, suff
163
170
  paddingLeft: !unstyled && prefix ? visual.controlPaddingXDefault : 0,
164
171
  paddingRight: unstyled ? 0 : showSteppers ? stepInset : 0,
165
172
  },
166
- !editable ? { opacity: interaction.disabledOpacity } : undefined,
167
- ], 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', focusRing, inputClassName), style: numericInputTextStyle(visual, size, unstyled, styleProp), value: model.textValue, onChangeText: model.handleTextChange, onBlur: model.handleBlur, 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 })] }));
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) => {
175
+ setFocused(true);
176
+ onFocus?.(event);
177
+ }, onBlur: (event) => {
178
+ setFocused(false);
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 })] }));
168
181
  });
169
182
  NumericInput.displayName = 'NumericInput';
170
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,19 +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),
22
- // Pass an array unless the caller actually gave a callback. On web this primitive
23
- // is wrapped in a Slot that merges `style` by object spread, which turns a
24
- // function into `{}` and silently drops every style — the trigger rendered 24px
25
- // tall with no padding, border or radius. A callback is still honoured on native,
26
- // where dropping it was the original bug.
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),
27
28
  // Flattened, not an array. On web this primitive is merged through a Slot that
28
29
  // spreads `style`, and spreading an array yields { 0: …, 1: … } — numeric keys
29
30
  // that reach the DOM as indexed style properties and throw. The Select rendered
@@ -31,8 +32,8 @@ const SelectTrigger = React.forwardRef(({ className, children, style: styleProp,
31
32
  style: StyleSheet.flatten([
32
33
  fieldRecipe(visual, 'default'),
33
34
  styleProp,
34
- props.disabled ? { opacity: interaction.disabledOpacity } : null,
35
- ]), ...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" })] }));
36
37
  });
37
38
  SelectTrigger.displayName = 'SelectTrigger';
38
39
  function SelectValue({ className, placeholder = 'Select...' }) {
@@ -44,7 +45,9 @@ function SelectValue({ className, placeholder = 'Select...' }) {
44
45
  // user-agent default black — 1.2:1 on a dark surface. Rendering the label ourselves
45
46
  // instead would fix the colour and lose the value: Radix owns the selection on web,
46
47
  // and `useRootContext().value` is not populated there.
47
- 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 }) }));
48
51
  }
49
52
  const SelectContent = React.forwardRef(({ className, children, portalHost, position = 'popper', insets, style: styleProp, frosted = false, blurIntensity, blurTintToken, ...props }, ref) => {
50
53
  const frostedSurface = useFrostedSurface({
@@ -76,8 +79,8 @@ SelectContent.displayName = 'SelectContent';
76
79
  const SelectItem = React.forwardRef(({ className, children, style: styleProp, ...props }, ref) => {
77
80
  const interaction = useInteractionTokens();
78
81
  const visual = useVisualTokens();
79
- 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({
80
- web: 'focus:bg-accent focus:text-accent-foreground cursor-default outline-none',
82
+ 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({
83
+ web: 'focus:bg-state-hover focus:text-foreground cursor-default outline-none',
81
84
  }), props.disabled && '', className), style: StyleSheet.flatten([
82
85
  {
83
86
  borderRadius: visual.radiusSm,
@@ -87,7 +90,7 @@ const SelectItem = React.forwardRef(({ className, children, style: styleProp, ..
87
90
  },
88
91
  styleProp,
89
92
  props.disabled ? { opacity: interaction.disabledOpacity } : null,
90
- ]), ...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') })] }));
93
+ ]), ...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: "text-foreground", style: typographyStyle(visual, 'body') })] }));
91
94
  });
92
95
  SelectItem.displayName = 'SelectItem';
93
96
  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> {
@@ -4,37 +4,59 @@ import { Text as RNText } from 'react-native';
4
4
  import { cn } from '../../lib/cn';
5
5
  import { tv } from '../../lib/tv';
6
6
  import { useResolveFontFamily } from '../../lib/font-context';
7
+ import { useTypography } from '../../lib/recipes';
7
8
  const textVariants = tv({
8
9
  base: 'text-foreground',
9
10
  variants: {
10
11
  variant: {
11
12
  default: 'font-normal',
12
- heading: 'text-2xl font-bold tracking-tight',
13
- title: 'text-xl font-semibold',
14
- subtitle: 'text-lg font-medium',
15
- body: 'text-base font-normal',
16
- caption: 'text-sm text-muted-foreground font-normal',
17
- label: 'text-sm font-medium leading-tight',
18
- code: 'font-mono text-sm bg-muted rounded px-1 py-0.5',
13
+ heading: 'font-bold tracking-tight',
14
+ title: 'font-semibold',
15
+ subtitle: 'font-medium',
16
+ body: 'font-normal',
17
+ caption: 'text-muted-foreground font-normal',
18
+ label: 'font-medium leading-tight',
19
+ code: 'font-mono bg-muted rounded px-1 py-0.5',
19
20
  },
20
21
  size: {
21
- xs: 'text-xs',
22
- sm: 'text-sm',
23
- base: 'text-base',
24
- lg: 'text-lg',
25
- xl: 'text-xl',
26
- '2xl': 'text-2xl',
27
- '3xl': 'text-3xl',
22
+ xs: '',
23
+ sm: '',
24
+ base: '',
25
+ lg: '',
26
+ xl: '',
27
+ '2xl': '',
28
+ '3xl': '',
28
29
  },
29
30
  },
30
31
  defaultVariants: {
31
32
  variant: 'default',
32
33
  },
33
34
  });
34
- const Text = React.forwardRef(({ className, variant, size, style, ...props }, ref) => {
35
- const resolved = cn(textVariants({ variant, size }), className);
35
+ const VARIANT_ROLES = {
36
+ default: 'body',
37
+ heading: 'headline',
38
+ title: 'title',
39
+ subtitle: 'emphasis',
40
+ body: 'body',
41
+ caption: 'caption',
42
+ label: 'label',
43
+ code: 'label',
44
+ };
45
+ const SIZE_ROLES = {
46
+ xs: 'caption',
47
+ sm: 'label',
48
+ base: 'body',
49
+ lg: 'emphasis',
50
+ xl: 'title',
51
+ '2xl': 'headline',
52
+ '3xl': 'headline',
53
+ };
54
+ const Text = React.forwardRef(({ className, variant, size, style, accessibilityRole, ...props }, ref) => {
55
+ const effectiveVariant = variant ?? 'default';
56
+ const resolved = cn(textVariants({ variant: effectiveVariant, size }), className);
36
57
  const font = useResolveFontFamily(resolved);
37
- return (_jsx(RNText, { ref: ref, className: font ? font.className : resolved, style: font ? [font.style, style] : style, ...props }));
58
+ const semanticStyle = useTypography(size ? SIZE_ROLES[size] : VARIANT_ROLES[effectiveVariant]);
59
+ return (_jsx(RNText, { ref: ref, className: font ? font.className : resolved, accessibilityRole: accessibilityRole ?? (effectiveVariant === 'heading' ? 'header' : undefined), style: [variant || size ? semanticStyle : undefined, font?.style, style], ...props }));
38
60
  });
39
61
  Text.displayName = 'Text';
40
62
  export { Text, textVariants };
@@ -21,6 +21,7 @@ declare const textareaVariants: import("tailwind-variants").TVReturnType<{
21
21
  };
22
22
  }, undefined>>;
23
23
  export interface TextareaProps extends Omit<TextInputProps, 'style'>, VariantProps<typeof textareaVariants> {
24
+ 'aria-invalid'?: boolean;
24
25
  className?: string;
25
26
  }
26
27
  declare const Textarea: ForwardRefComponent<TextInputInstance, TextareaProps>;
@@ -2,12 +2,14 @@ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import React from 'react';
3
3
  import { TextInput, } from 'react-native';
4
4
  import { cn } from '../../lib/cn';
5
- import { focusRing } from '../../lib/focus-ring';
5
+ import { focusField } from '../../lib/focus-ring';
6
6
  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
10
  import { typographyStyle } from '../../lib/recipes';
11
+ import { webAutofillField } from '../../lib/web-autofill';
12
+ import { useFormFieldState } from '../form-field/form-field';
11
13
  const textareaVariants = tv({
12
14
  base: 'w-full border-border bg-muted text-foreground',
13
15
  variants: {
@@ -26,10 +28,12 @@ const TEXTAREA_TEXT_ROLE = {
26
28
  sm: 'caption',
27
29
  lg: 'body',
28
30
  };
29
- const Textarea = React.forwardRef(({ className, size = 'default', placeholderTextColor, ...props }, ref) => {
31
+ const Textarea = React.forwardRef(({ className, size = 'default', placeholderTextColor, editable, 'aria-invalid': ariaInvalid, ...props }, ref) => {
30
32
  const colors = useThemeColors();
31
33
  const interaction = useInteractionTokens();
32
34
  const visual = useVisualTokens();
35
+ const field = useFormFieldState();
36
+ const effectiveEditable = editable ?? !field.disabled;
33
37
  const minHeight = size === 'sm'
34
38
  ? visual.textareaMinHeightSm
35
39
  : size === 'lg'
@@ -42,9 +46,9 @@ const Textarea = React.forwardRef(({ className, size = 'default', placeholderTex
42
46
  borderRadius: visual.radiusLg,
43
47
  paddingHorizontal: visual.controlPaddingXDefault,
44
48
  paddingVertical: visual.controlPaddingYDefault,
45
- ...(props.editable === false ? { opacity: interaction.disabledOpacity } : null),
49
+ ...(!effectiveEditable ? { opacity: interaction.disabledOpacity } : null),
46
50
  };
47
- return (_jsx(TextInput, { ref: ref, className: cn(textareaVariants({ size }), focusRing, className), style: style, multiline: true, textAlignVertical: "top", placeholderTextColor: placeholderTextColor ?? colors.placeholder, ...props }));
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 }));
48
52
  });
49
53
  Textarea.displayName = 'Textarea';
50
54
  export { Textarea, textareaVariants };
package/dist/index.d.ts CHANGED
@@ -7,7 +7,7 @@ export { PortalHost } from './lib/portal';
7
7
  export { ThemeBridge, applyThemeMode, resolveThemeMode, syncThemeMode, useThemeBridge, type ThemeMode, type ThemeBridgeAdapter, type ThemeBridgeProps, type UseThemeBridgeOptions, type UseThemeBridgeResult, } from './lib/theme-bridge';
8
8
  export { useThemeColors, useThemeTokens, useIconColors, type ThemeColors, } from './lib/theme-colors';
9
9
  export { THEME_TOKEN_SPECS, THEME_TOKEN_GROUPS, WHILEUI_LIGHT_THEME_COLORS, WHILEUI_DARK_THEME_COLORS, type ThemeTokenGroup, type ThemeTokenKey, type ThemeTokenSpec, } from './lib/theme-contract';
10
- export { useInteractionTokens, withInteractivePressableStyle, type InteractionTokens, type InteractivePressableOptions, type PressedVariant, type DisabledVariant, } from './lib/interaction-tokens';
10
+ export { INTERACTION_TOKENS, useInteractionTokens, withInteractivePressableStyle, type InteractionTokens, type InteractivePressableOptions, type PressedVariant, type DisabledVariant, } from './lib/interaction-tokens';
11
11
  export { MotionProvider, useMotionPolicy, type MotionMode, type MotionDuration, type MotionSpring, type MotionSpringConfig, type MotionPolicy, type MotionProviderProps, } from './lib/motion-policy';
12
12
  export { useVisualTokens, type VisualTokens } from './lib/visual-tokens';
13
13
  export { VISUAL_TOKEN_SPECS, VISUAL_TOKEN_GROUPS, type VisualTokenGroup, type VisualTokenKey, type VisualTokenKind, type VisualTokenSpec, } from './lib/visual-token-contract';
@@ -16,6 +16,7 @@ export { controlRecipe, fieldRecipe, surfaceRecipe, surfacePadding, surfaceRadiu
16
16
  export { WHILEUI_PRESET_SOFT, WHILEUI_PRESET_ATELIER, WHILEUI_THEME_PRESETS, themePresetToCssVariables, themePresetToCss, type ThemePreset, } from './lib/theme-presets';
17
17
  export { deriveThemeColors, THEME_BASE_TOKEN_KEYS, type ThemeBaseColors } from './lib/theme-derive';
18
18
  export { Text, textVariants, type TextProps } from './components/text';
19
+ export { Surface, type SurfaceLevel, type SurfaceProps } from './components/surface';
19
20
  export { View, type ViewProps } from './components/view';
20
21
  export { Pressable, type PressableProps } from './components/pressable';
21
22
  export { Stack, stackVariants, type StackProps } from './components/stack';
@@ -25,9 +26,10 @@ export { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter,
25
26
  export { Input, inputVariants, type InputProps } from './components/input';
26
27
  export { NumericInput, numericInputVariants, numericInputTextVariants, type NumericInputProps, } from './components/numeric-input';
27
28
  export { OTPInput, otpCellVariants, type OTPInputProps } from './components/otp-input';
28
- export { FormField, FormLabel, FormControl, FormHint, FormMessage, formFieldVariants, formControlVariants, type FormFieldProps, type FormLabelProps, type FormControlProps, type FormHintProps, type FormMessageProps, } from './components/form-field';
29
+ export { FormField, FormLabel, FormControl, FormHint, FormMessage, useFormFieldState, formFieldVariants, formControlVariants, type FormFieldProps, type FormLabelProps, type FormControlProps, type FormHintProps, type FormMessageProps, type FormFieldState, } from './components/form-field';
29
30
  export { LabeledField, LabeledFieldControl, type LabeledFieldProps, type LabeledFieldControlProps, } from './components/labeled-field';
30
31
  export { Badge, BadgeText, badgeVariants, badgeTextVariants, type BadgeProps, type BadgeTextProps, } from './components/badge';
32
+ export { FAB, FABGroup, fabVariants, type FabProps, type FabGroupProps } from './components/fab';
31
33
  export { Alert, AlertTitle, AlertDescription, alertVariants, type AlertProps, type AlertTitleProps, type AlertDescriptionProps, } from './components/alert';
32
34
  export { Switch, type SwitchProps } from './components/switch';
33
35
  export { Checkbox, type CheckboxProps } from './components/checkbox';