@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.
- package/README.md +186 -20
- package/dist/blocks/chat/chat-conversation.d.ts +8 -0
- package/dist/blocks/chat/chat-conversation.js +36 -0
- package/dist/blocks/chat/chat.js +5 -11
- package/dist/blocks/chat/index.d.ts +1 -0
- package/dist/blocks/chat/index.js +1 -0
- package/dist/blocks/layout/smart-input.js +2 -1
- package/dist/blocks/media/smart-image.js +5 -7
- package/dist/code.d.ts +1 -0
- package/dist/code.js +1 -0
- package/dist/components/avatar/avatar.d.ts +3 -2
- package/dist/components/avatar/avatar.js +19 -13
- package/dist/components/avatar/index.d.ts +1 -1
- package/dist/components/card/card.d.ts +1 -1
- package/dist/components/card/card.js +4 -25
- package/dist/components/code-block/code-block.d.ts +12 -0
- package/dist/components/code-block/code-block.js +13 -0
- package/dist/components/code-block/index.d.ts +1 -0
- package/dist/components/code-block/index.js +1 -0
- package/dist/components/form-field/form-field.d.ts +8 -0
- package/dist/components/form-field/form-field.js +3 -0
- package/dist/components/form-field/index.d.ts +1 -1
- package/dist/components/form-field/index.js +1 -1
- package/dist/components/input/input.d.ts +1 -0
- package/dist/components/input/input.js +11 -6
- package/dist/components/markdown/index.d.ts +1 -0
- package/dist/components/markdown/index.js +1 -0
- package/dist/components/markdown/markdown.d.ts +14 -0
- package/dist/components/markdown/markdown.js +94 -0
- package/dist/components/numeric-input/numeric-input.d.ts +1 -0
- package/dist/components/numeric-input/numeric-input.js +11 -6
- package/dist/components/select/select.d.ts +2 -1
- package/dist/components/select/select.js +44 -24
- package/dist/components/surface/index.d.ts +1 -0
- package/dist/components/surface/index.js +1 -0
- package/dist/components/surface/surface.d.ts +14 -0
- package/dist/components/surface/surface.js +35 -0
- package/dist/components/text/text.d.ts +42 -42
- package/dist/components/text/text.js +39 -17
- package/dist/components/textarea/textarea.d.ts +1 -0
- package/dist/components/textarea/textarea.js +7 -3
- package/dist/components/toast/toast.js +5 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -1
- package/dist/lib/theme-bridge.d.ts +1 -1
- package/dist/lib/theme-bridge.js +52 -18
- package/dist/lib/web-autofill.d.ts +9 -0
- package/dist/lib/web-autofill.js +9 -0
- package/dist/markdown.d.ts +1 -0
- package/dist/markdown.js +1 -0
- 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
|
|
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 =
|
|
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
|
-
!
|
|
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:
|
|
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
|
|
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
|
-
!
|
|
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:
|
|
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
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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:
|
|
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-
|
|
75
|
-
web: 'focus:bg-
|
|
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-
|
|
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: "
|
|
8
|
-
title: "
|
|
9
|
-
subtitle: "
|
|
10
|
-
body: "
|
|
11
|
-
caption: "text-
|
|
12
|
-
label: "
|
|
13
|
-
code: "font-mono
|
|
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: "
|
|
17
|
-
sm: "
|
|
18
|
-
base: "
|
|
19
|
-
lg: "
|
|
20
|
-
xl: "
|
|
21
|
-
'2xl': "
|
|
22
|
-
'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: "
|
|
28
|
-
title: "
|
|
29
|
-
subtitle: "
|
|
30
|
-
body: "
|
|
31
|
-
caption: "text-
|
|
32
|
-
label: "
|
|
33
|
-
code: "font-mono
|
|
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: "
|
|
37
|
-
sm: "
|
|
38
|
-
base: "
|
|
39
|
-
lg: "
|
|
40
|
-
xl: "
|
|
41
|
-
'2xl': "
|
|
42
|
-
'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: "
|
|
48
|
-
title: "
|
|
49
|
-
subtitle: "
|
|
50
|
-
body: "
|
|
51
|
-
caption: "text-
|
|
52
|
-
label: "
|
|
53
|
-
code: "font-mono
|
|
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: "
|
|
57
|
-
sm: "
|
|
58
|
-
base: "
|
|
59
|
-
lg: "
|
|
60
|
-
xl: "
|
|
61
|
-
'2xl': "
|
|
62
|
-
'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> {
|