@thewhileloop/whileui 4.2.1 → 4.3.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.
- package/README.md +1235 -663
- package/dist/agent/catalog.json +1737 -0
- package/dist/agent/llms-components.txt +2787 -0
- package/dist/agent/llms-full.txt +2962 -0
- package/dist/agent/llms.txt +152 -0
- package/dist/blocks/chat/chat.d.ts +10 -2
- package/dist/blocks/chat/chat.js +1 -1
- package/dist/blocks/chat/index.d.ts +1 -1
- package/dist/blocks/commerce/feature-gate.js +2 -2
- package/dist/blocks/commerce/metric-card.js +1 -1
- package/dist/blocks/commerce/subscription-card.d.ts +7 -0
- package/dist/blocks/commerce/usage-bar.d.ts +4 -0
- package/dist/blocks/layout/app-shell.d.ts +4 -0
- package/dist/blocks/layout/empty-state.js +1 -1
- package/dist/blocks/layout/error-state.js +1 -1
- package/dist/blocks/layout/page-skeleton.d.ts +1 -0
- package/dist/blocks/layout/pull-to-refresh-scroll-view.d.ts +1 -0
- package/dist/blocks/layout/screen-skeleton.d.ts +5 -0
- package/dist/blocks/layout/sheet.js +1 -1
- package/dist/blocks/lists/list-item.js +1 -1
- package/dist/blocks/lists/notification-item.js +1 -1
- package/dist/blocks/media/smart-image.js +4 -2
- package/dist/blocks/navigation/bottom-nav.js +1 -1
- package/dist/blocks/navigation/drawer-menu.js +2 -2
- package/dist/blocks/navigation/floating-bottom-nav.js +3 -1
- package/dist/blocks/navigation/tab-bar.js +1 -1
- package/dist/components/alert/alert.d.ts +7 -0
- package/dist/components/alert/alert.js +23 -4
- package/dist/components/button/button.d.ts +3 -0
- package/dist/components/button/button.js +2 -1
- package/dist/components/form-field/form-field.d.ts +18 -0
- package/dist/components/form-field/form-field.js +25 -4
- package/dist/components/input/input.js +2 -2
- package/dist/components/numeric-input/numeric-input.d.ts +3 -0
- package/dist/components/numeric-input/numeric-input.js +2 -2
- package/dist/components/otp-input/otp-input.d.ts +8 -0
- package/dist/components/pressable/pressable.d.ts +1 -0
- package/dist/components/segmented-control/segmented-control.d.ts +1 -0
- package/dist/components/surface/surface.d.ts +5 -0
- package/dist/components/textarea/textarea.js +2 -2
- package/dist/components/toast/toast.d.ts +10 -10
- package/dist/components/toast/toast.js +53 -35
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/lib/frosted-surface.d.ts +3 -0
- package/dist/lib/theme-colors.d.ts +4 -3
- package/dist/lib/theme-colors.js +26 -1
- package/dist/lib/theme-contract.js +12 -12
- package/dist/lib/theme-presets.js +9 -9
- package/dist/lib/theme-seeds.d.ts +40 -0
- package/dist/lib/theme-seeds.js +143 -0
- package/dist/lib/visual-tokens.js +13 -0
- package/dist/metro.cjs +12 -5
- package/dist/tokens.d.ts +1 -0
- package/dist/tokens.js +1 -0
- package/package.json +3 -2
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import React, { createContext, useContext, useMemo } from 'react';
|
|
2
|
+
import React, { createContext, useContext, useId, useMemo } from 'react';
|
|
3
3
|
import { Text, View, } from 'react-native';
|
|
4
4
|
import { cn } from '../../lib/cn';
|
|
5
5
|
import { tv } from '../../lib/tv';
|
|
@@ -44,16 +44,37 @@ const FormFieldContext = createContext({
|
|
|
44
44
|
export function useFormFieldState() {
|
|
45
45
|
return useContext(FormFieldContext);
|
|
46
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* Accessibility props that tie a text control to its FormField: named by the FormLabel
|
|
49
|
+
* unless the caller names or links it any of the four ways React Native allows, and
|
|
50
|
+
* required when the field is. Spread before the caller's props. iOS has no labelled-by;
|
|
51
|
+
* VoiceOver reads the visible label just before the field.
|
|
52
|
+
*/
|
|
53
|
+
export function fieldAccessibilityProps(field, props) {
|
|
54
|
+
const named = props.accessibilityLabel ??
|
|
55
|
+
props['aria-label'] ??
|
|
56
|
+
props['aria-labelledby'] ??
|
|
57
|
+
props.accessibilityLabelledBy;
|
|
58
|
+
return {
|
|
59
|
+
'aria-labelledby': named ? undefined : field.labelId,
|
|
60
|
+
'aria-required': field.required || undefined,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
47
63
|
const FormField = React.forwardRef(({ className, density = 'default', invalid = false, disabled = false, required = false, ...props }, ref) => {
|
|
48
64
|
const interaction = useInteractionTokens();
|
|
49
|
-
const
|
|
65
|
+
const labelId = useId();
|
|
66
|
+
const contextValue = useMemo(() => ({ density, invalid, disabled, required, labelId }), [density, disabled, invalid, labelId, required]);
|
|
50
67
|
return (_jsx(FormFieldContext.Provider, { value: contextValue, children: _jsx(View, { ref: ref, className: cn(formFieldVariants({ density }), className), style: disabled ? { opacity: interaction.disabledOpacitySoft } : undefined, ...props }) }));
|
|
51
68
|
});
|
|
52
69
|
FormField.displayName = 'FormField';
|
|
53
70
|
const FormLabel = React.forwardRef(({ className, showRequiredIndicator = true, children, style, ...props }, ref) => {
|
|
54
|
-
const { invalid, disabled, required } = useContext(FormFieldContext);
|
|
71
|
+
const { invalid, disabled, required, labelId } = useContext(FormFieldContext);
|
|
55
72
|
const visual = useVisualTokens();
|
|
56
|
-
|
|
73
|
+
const indicator = required && showRequiredIndicator;
|
|
74
|
+
return (_jsxs(Text, { ref: ref, nativeID: labelId,
|
|
75
|
+
// iOS reads nested Text as one string, so hiding the asterisk alone still says
|
|
76
|
+
// "star"; a plain-text label names it once. The control announces "required".
|
|
77
|
+
accessibilityLabel: indicator && typeof children === 'string' ? children : undefined, className: cn('font-medium text-foreground', invalid && 'text-destructive', disabled && 'text-muted-foreground', className), style: [typographyStyle(visual, 'label'), style], ...props, children: [children, indicator ? (_jsx(Text, { "aria-hidden": true, className: "text-destructive", children: ' *' })) : null] }));
|
|
57
78
|
});
|
|
58
79
|
FormLabel.displayName = 'FormLabel';
|
|
59
80
|
const FormControl = React.forwardRef(({ className, style, ...props }, ref) => {
|
|
@@ -9,7 +9,7 @@ import { useInteractionTokens } from '../../lib/interaction-tokens';
|
|
|
9
9
|
import { useVisualTokens } from '../../lib/visual-tokens';
|
|
10
10
|
import { typographyStyle } from '../../lib/recipes';
|
|
11
11
|
import { webAutofillField } from '../../lib/web-autofill';
|
|
12
|
-
import { useFormFieldState } from '../form-field/form-field';
|
|
12
|
+
import { fieldAccessibilityProps, useFormFieldState } from '../form-field/form-field';
|
|
13
13
|
const inputVariants = tv({
|
|
14
14
|
base: 'w-full border-border bg-muted text-foreground',
|
|
15
15
|
variants: {
|
|
@@ -43,7 +43,7 @@ const Input = React.forwardRef(({ className, prefix, suffix, inputClassName, var
|
|
|
43
43
|
paddingHorizontal: plain ? 0 : visual.controlPaddingXDefault,
|
|
44
44
|
},
|
|
45
45
|
!effectiveEditable ? { opacity: interaction.disabledOpacity } : undefined,
|
|
46
|
-
], children: [prefix && (_jsx(View, { style: { paddingRight: Math.max(8, Math.round(visual.controlPaddingXDefault * 0.5)) }, children: prefix })), _jsx(TextInput, { ref: ref, className: cn('flex-1 text-foreground placeholder:text-muted-foreground web:outline-none', !plain && webAutofillField, inputClassName), style: {
|
|
46
|
+
], children: [prefix && (_jsx(View, { style: { paddingRight: Math.max(8, Math.round(visual.controlPaddingXDefault * 0.5)) }, children: prefix })), _jsx(TextInput, { ref: ref, ...fieldAccessibilityProps(field, props), className: cn('flex-1 text-foreground placeholder:text-muted-foreground web:outline-none', !plain && webAutofillField, inputClassName), style: {
|
|
47
47
|
...typographyStyle(visual, 'label'),
|
|
48
48
|
minHeight: visual.controlHeightDefault - 2,
|
|
49
49
|
paddingVertical: visual.controlPaddingYDefault,
|
|
@@ -52,12 +52,15 @@ export interface NumericInputProps extends Omit<TextInputProps, 'value' | 'defau
|
|
|
52
52
|
inputClassName?: string;
|
|
53
53
|
prefix?: React.ReactNode;
|
|
54
54
|
suffix?: React.ReactNode;
|
|
55
|
+
/** Controlled numeric value. */
|
|
55
56
|
value?: number | null;
|
|
56
57
|
defaultValue?: number;
|
|
58
|
+
/** Numeric value change callback. */
|
|
57
59
|
onValueChange?: (value: number | null) => void;
|
|
58
60
|
min?: number;
|
|
59
61
|
max?: number;
|
|
60
62
|
step?: number;
|
|
63
|
+
/** Show decrement/increment controls. */
|
|
61
64
|
showSteppers?: boolean;
|
|
62
65
|
/** Drop the field shell (border, background, radius, padding) for embedding inside another field shell like LabeledField. */
|
|
63
66
|
unstyled?: boolean;
|
|
@@ -9,7 +9,7 @@ import { useInteractionTokens, withInteractivePressableStyle } from '../../lib/i
|
|
|
9
9
|
import { useVisualTokens } from '../../lib/visual-tokens';
|
|
10
10
|
import { typographyStyle } from '../../lib/recipes';
|
|
11
11
|
import { webAutofillField } from '../../lib/web-autofill';
|
|
12
|
-
import { useFormFieldState } from '../form-field/form-field';
|
|
12
|
+
import { fieldAccessibilityProps, useFormFieldState } from '../form-field/form-field';
|
|
13
13
|
const numericInputVariants = tv({
|
|
14
14
|
base: 'w-full flex-row items-center border-border bg-muted',
|
|
15
15
|
variants: {
|
|
@@ -171,7 +171,7 @@ const NumericInput = React.forwardRef(({ className, inputClassName, prefix, suff
|
|
|
171
171
|
paddingRight: unstyled ? 0 : showSteppers ? stepInset : 0,
|
|
172
172
|
},
|
|
173
173
|
!effectiveEditable ? { opacity: interaction.disabledOpacity } : undefined,
|
|
174
|
-
], children: [_jsx(NumericAffix, { side: "prefix", visual: visual, children: prefix }), _jsx(View, { className: "min-w-0 flex-1", style: Platform.OS === 'web' ? { minWidth: 0, flex: 1 } : undefined, children: _jsx(TextInput, { ref: ref, className: cn(numericInputTextVariants({ size }), 'w-full web:outline-none', !unstyled && webAutofillField, inputClassName), style: numericInputTextStyle(visual, size, unstyled, styleProp), value: model.textValue, onChangeText: model.handleTextChange, onFocus: (event) => {
|
|
174
|
+
], children: [_jsx(NumericAffix, { side: "prefix", visual: visual, children: prefix }), _jsx(View, { className: "min-w-0 flex-1", style: Platform.OS === 'web' ? { minWidth: 0, flex: 1 } : undefined, children: _jsx(TextInput, { ref: ref, ...fieldAccessibilityProps(field, props), className: cn(numericInputTextVariants({ size }), 'w-full web:outline-none', !unstyled && webAutofillField, inputClassName), style: numericInputTextStyle(visual, size, unstyled, styleProp), value: model.textValue, onChangeText: model.handleTextChange, onFocus: (event) => {
|
|
175
175
|
setFocused(true);
|
|
176
176
|
onFocus?.(event);
|
|
177
177
|
}, onBlur: (event) => {
|
|
@@ -34,14 +34,22 @@ declare const otpCellVariants: import("tailwind-variants").TVReturnType<{
|
|
|
34
34
|
};
|
|
35
35
|
}, undefined>>;
|
|
36
36
|
export interface OTPInputProps extends Omit<ViewProps, 'children'> {
|
|
37
|
+
/** Number of digit cells. */
|
|
37
38
|
length?: number;
|
|
39
|
+
/** Controlled value. */
|
|
38
40
|
value?: string;
|
|
41
|
+
/** Called on each digit change. */
|
|
39
42
|
onValueChange?: (value: string) => void;
|
|
43
|
+
/** Called when all digits are filled. */
|
|
40
44
|
onComplete?: (code: string) => void;
|
|
45
|
+
/** Error variant triggers shake. */
|
|
41
46
|
variant?: 'default' | 'error';
|
|
47
|
+
/** Cell size. */
|
|
42
48
|
size?: 'default' | 'compact';
|
|
49
|
+
/** Mask digits with dots. */
|
|
43
50
|
secure?: boolean;
|
|
44
51
|
disabled?: boolean;
|
|
52
|
+
/** Focus first cell on mount. */
|
|
45
53
|
autoFocus?: boolean;
|
|
46
54
|
className?: string;
|
|
47
55
|
}
|
|
@@ -2,6 +2,7 @@ import { type PressableInstance, type PressableProps as RNPressableProps } from
|
|
|
2
2
|
import { type DisabledVariant, type PressedVariant } from '../../lib/interaction-tokens';
|
|
3
3
|
import type { ForwardRefComponent } from '../../lib/ref-types';
|
|
4
4
|
export interface PressableProps extends RNPressableProps {
|
|
5
|
+
/** Additional Tailwind classes. */
|
|
5
6
|
className?: string;
|
|
6
7
|
/** Semantic press feedback. `none` preserves a plain React Native Pressable. */
|
|
7
8
|
feedback?: PressedVariant;
|
|
@@ -136,6 +136,7 @@ export interface SegmentedControlProps extends ViewProps {
|
|
|
136
136
|
value?: string;
|
|
137
137
|
defaultValue?: string;
|
|
138
138
|
onValueChange?: (value: string) => void;
|
|
139
|
+
/** Pill = rounded-full items. */
|
|
139
140
|
variant?: 'default' | 'pill';
|
|
140
141
|
wrap?: boolean | 'auto';
|
|
141
142
|
size?: 'default' | 'compact';
|
|
@@ -4,10 +4,15 @@ import { type ShadowTier, type SurfacePaddingSize } from '../../lib/recipes';
|
|
|
4
4
|
import type { ForwardRefComponent } from '../../lib/ref-types';
|
|
5
5
|
export type SurfaceLevel = 'base' | 'raised' | 'overlay';
|
|
6
6
|
export interface SurfaceProps extends ViewProps, FrostedSurfaceProps {
|
|
7
|
+
/** Semantic background and depth tier. */
|
|
7
8
|
level?: SurfaceLevel;
|
|
9
|
+
/** Token-backed interior spacing. */
|
|
8
10
|
padding?: SurfacePaddingSize;
|
|
11
|
+
/** Token-backed corner radius. */
|
|
9
12
|
radius?: 'sm' | 'md' | 'lg' | 'xl';
|
|
13
|
+
/** Draw the theme hairline border. */
|
|
10
14
|
bordered?: boolean;
|
|
15
|
+
/** Override the level's depth. */
|
|
11
16
|
shadow?: ShadowTier;
|
|
12
17
|
}
|
|
13
18
|
declare const Surface: ForwardRefComponent<ViewInstance, SurfaceProps>;
|
|
@@ -9,7 +9,7 @@ import { useInteractionTokens } from '../../lib/interaction-tokens';
|
|
|
9
9
|
import { useVisualTokens } from '../../lib/visual-tokens';
|
|
10
10
|
import { typographyStyle } from '../../lib/recipes';
|
|
11
11
|
import { webAutofillField } from '../../lib/web-autofill';
|
|
12
|
-
import { useFormFieldState } from '../form-field/form-field';
|
|
12
|
+
import { fieldAccessibilityProps, useFormFieldState } from '../form-field/form-field';
|
|
13
13
|
const textareaVariants = tv({
|
|
14
14
|
base: 'w-full border-border bg-muted text-foreground',
|
|
15
15
|
variants: {
|
|
@@ -48,7 +48,7 @@ const Textarea = React.forwardRef(({ className, size = 'default', placeholderTex
|
|
|
48
48
|
paddingVertical: visual.controlPaddingYDefault,
|
|
49
49
|
...(!effectiveEditable ? { opacity: interaction.disabledOpacity } : null),
|
|
50
50
|
};
|
|
51
|
-
return (_jsx(TextInput, { ref: ref, className: cn(textareaVariants({ size }), field.invalid && 'border-destructive', focusField, webAutofillField, className), style: style, multiline: true, textAlignVertical: "top", placeholderTextColor: placeholderTextColor ?? colors.placeholder, editable: effectiveEditable, "aria-invalid": ariaInvalid ?? field.invalid, ...props }));
|
|
51
|
+
return (_jsx(TextInput, { ref: ref, ...fieldAccessibilityProps(field, props), className: cn(textareaVariants({ size }), field.invalid && 'border-destructive', focusField, webAutofillField, className), style: style, multiline: true, textAlignVertical: "top", placeholderTextColor: placeholderTextColor ?? colors.placeholder, editable: effectiveEditable, "aria-invalid": ariaInvalid ?? field.invalid, ...props }));
|
|
52
52
|
});
|
|
53
53
|
Textarea.displayName = 'Textarea';
|
|
54
54
|
export { Textarea, textareaVariants };
|
|
@@ -25,21 +25,21 @@ export interface ToastProviderProps {
|
|
|
25
25
|
export declare function ToastProvider({ children }: ToastProviderProps): React.JSX.Element;
|
|
26
26
|
declare const toastVariants: import("tailwind-variants").TVReturnType<{
|
|
27
27
|
variant: {
|
|
28
|
-
default: "border-border
|
|
29
|
-
destructive: "border-destructive
|
|
30
|
-
success: "border-success
|
|
28
|
+
default: "border-border";
|
|
29
|
+
destructive: "border-destructive-soft-border";
|
|
30
|
+
success: "border-success-soft-border";
|
|
31
31
|
};
|
|
32
|
-
}, undefined, "w-full flex-row items-center
|
|
32
|
+
}, undefined, "w-full flex-row items-center gap-3 border bg-popover", {
|
|
33
33
|
variant: {
|
|
34
|
-
default: "border-border
|
|
35
|
-
destructive: "border-destructive
|
|
36
|
-
success: "border-success
|
|
34
|
+
default: "border-border";
|
|
35
|
+
destructive: "border-destructive-soft-border";
|
|
36
|
+
success: "border-success-soft-border";
|
|
37
37
|
};
|
|
38
38
|
}, undefined, import("tailwind-variants").TVReturnTypeLike<{
|
|
39
39
|
variant: {
|
|
40
|
-
default: "border-border
|
|
41
|
-
destructive: "border-destructive
|
|
42
|
-
success: "border-success
|
|
40
|
+
default: "border-border";
|
|
41
|
+
destructive: "border-destructive-soft-border";
|
|
42
|
+
success: "border-success-soft-border";
|
|
43
43
|
};
|
|
44
44
|
}, undefined>>;
|
|
45
45
|
export interface ToastProps extends ViewProps, VariantProps<typeof toastVariants> {
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
|
3
|
-
import { Pressable, View } from 'react-native';
|
|
4
|
-
import Animated, {
|
|
3
|
+
import { AccessibilityInfo, Platform, Pressable, View } from 'react-native';
|
|
4
|
+
import Animated, { FadeOut, SlideInUp } from 'react-native-reanimated';
|
|
5
5
|
import { Text } from '../text';
|
|
6
6
|
import { cn } from '../../lib/cn';
|
|
7
|
+
import { focusRing } from '../../lib/focus-ring';
|
|
8
|
+
import { useMotionPolicy } from '../../lib/motion-policy';
|
|
7
9
|
import { tv } from '../../lib/tv';
|
|
8
10
|
import { useVisualTokens } from '../../lib/visual-tokens';
|
|
9
11
|
import { useThemeColors } from '../../lib/theme-colors';
|
|
@@ -17,15 +19,11 @@ export function useToast() {
|
|
|
17
19
|
return context;
|
|
18
20
|
}
|
|
19
21
|
let toastId = 0;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const DESCRIPTION_COLOR_BY_VARIANT = {
|
|
26
|
-
default: 'text-muted-foreground',
|
|
27
|
-
destructive: 'text-destructive-foreground-muted',
|
|
28
|
-
success: 'text-success-foreground-muted',
|
|
22
|
+
/** Status variants carry a small dot, not a fill, so every toast sits on the theme's surface. */
|
|
23
|
+
const ACCENT_BY_VARIANT = {
|
|
24
|
+
default: undefined,
|
|
25
|
+
destructive: 'bg-destructive',
|
|
26
|
+
success: 'bg-success',
|
|
29
27
|
};
|
|
30
28
|
export function ToastProvider({ children }) {
|
|
31
29
|
const [toasts, setToasts] = useState([]);
|
|
@@ -44,12 +42,12 @@ export function ToastProvider({ children }) {
|
|
|
44
42
|
return _jsx(ToastContext.Provider, { value: contextValue, children: children });
|
|
45
43
|
}
|
|
46
44
|
const toastVariants = tv({
|
|
47
|
-
base: 'w-full flex-row items-center
|
|
45
|
+
base: 'w-full flex-row items-center gap-3 border bg-popover',
|
|
48
46
|
variants: {
|
|
49
47
|
variant: {
|
|
50
|
-
default: 'border-border
|
|
51
|
-
destructive: 'border-destructive
|
|
52
|
-
success: 'border-success
|
|
48
|
+
default: 'border-border',
|
|
49
|
+
destructive: 'border-destructive-soft-border',
|
|
50
|
+
success: 'border-success-soft-border',
|
|
53
51
|
},
|
|
54
52
|
},
|
|
55
53
|
defaultVariants: {
|
|
@@ -60,31 +58,51 @@ function Toast({ toast: toastData, onDismiss, className, style, ...props }) {
|
|
|
60
58
|
const { title, description, variant = 'default', duration = 4000, action } = toastData;
|
|
61
59
|
const visual = useVisualTokens();
|
|
62
60
|
const colors = useThemeColors();
|
|
61
|
+
const motion = useMotionPolicy();
|
|
62
|
+
const accent = ACCENT_BY_VARIANT[variant];
|
|
63
63
|
useEffect(() => {
|
|
64
64
|
if (duration > 0) {
|
|
65
65
|
const timer = setTimeout(onDismiss, duration);
|
|
66
66
|
return () => clearTimeout(timer);
|
|
67
67
|
}
|
|
68
68
|
}, [duration, onDismiss]);
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
69
|
+
// Android and the web announce through the live region below; iOS has no live
|
|
70
|
+
// regions, so it is told once, when the toast appears.
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
if (Platform.OS !== 'ios')
|
|
73
|
+
return;
|
|
74
|
+
const message = [title, description].filter(Boolean).join('. ');
|
|
75
|
+
if (message)
|
|
76
|
+
AccessibilityInfo.announceForAccessibility(message);
|
|
77
|
+
// Announce on appearance only; a re-render must not repeat it.
|
|
78
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- once per toast, by design
|
|
79
|
+
}, []);
|
|
80
|
+
return (_jsx(Animated.View, { entering: motion.animationsEnabled ? SlideInUp.duration(motion.durations.normal) : undefined, exiting: motion.animationsEnabled ? FadeOut.duration(motion.durations.fast) : undefined, children: _jsxs(View
|
|
81
|
+
// Announced where it appears, without moving focus. Set before the spread so a
|
|
82
|
+
// caller can still override.
|
|
83
|
+
, {
|
|
84
|
+
// Announced where it appears, without moving focus. Set before the spread so a
|
|
85
|
+
// caller can still override.
|
|
86
|
+
accessibilityRole: "alert", accessibilityLiveRegion: "polite", className: cn(toastVariants({ variant }), className), style: [
|
|
87
|
+
{
|
|
88
|
+
borderRadius: surfaceRadius(visual, 'lg'),
|
|
89
|
+
borderWidth: visual.borderWidthHairline,
|
|
90
|
+
padding: surfacePadding(visual, 'sm'),
|
|
91
|
+
},
|
|
92
|
+
shadowStyle(visual, colors, 'lg'),
|
|
93
|
+
style,
|
|
94
|
+
], ...props, children: [_jsxs(View, { className: "flex-1 gap-0.5", children: [title ? (_jsxs(View, { className: "flex-row items-center gap-2", children: [accent ? _jsx(View, { className: cn('h-2 w-2 rounded-full', accent) }) : null, _jsx(Text, { className: "font-semibold text-popover-foreground", style: typographyStyle(visual, 'label'), children: title })] })) : null, description ? (_jsx(Text, { className: "text-muted-foreground", style: typographyStyle(visual, 'label'), children: description })) : null] }), action ? (_jsx(Pressable, { accessibilityRole: "button", onPress: () => {
|
|
95
|
+
action.onPress();
|
|
96
|
+
onDismiss();
|
|
97
|
+
}, className: cn('border border-border active:bg-state-pressed', focusRing), style: {
|
|
98
|
+
borderRadius: visual.radiusMd,
|
|
99
|
+
borderWidth: visual.borderWidthControl,
|
|
100
|
+
paddingHorizontal: visual.controlPaddingXSm,
|
|
101
|
+
paddingVertical: visual.controlPaddingYSm,
|
|
102
|
+
}, children: _jsx(Text, { className: "font-medium text-popover-foreground", style: typographyStyle(visual, 'label'), children: action.label }) })) : null, _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: "Dismiss", onPress: onDismiss,
|
|
103
|
+
// 44 px everywhere, since hitSlop does nothing on the web; the negative margin
|
|
104
|
+
// keeps the toast as compact as the old 32 px button left it.
|
|
105
|
+
hitSlop: 2, className: cn('-my-1.5 -mr-1.5 h-11 w-11 items-center justify-center rounded-full active:bg-state-pressed', focusRing), children: _jsx(Text, { className: "text-muted-foreground", style: typographyStyle(visual, 'emphasis'), children: "\u00D7" }) })] }) }));
|
|
88
106
|
}
|
|
89
107
|
function ToastContainerItem({ toast, dismiss, }) {
|
|
90
108
|
const handleDismiss = useCallback(() => dismiss(toast.id), [dismiss, toast.id]);
|
|
@@ -94,6 +112,6 @@ export function ToastContainer({ position = 'top', className, style, ...props })
|
|
|
94
112
|
const { toasts, dismiss } = useToast();
|
|
95
113
|
const visual = useVisualTokens();
|
|
96
114
|
const positionClass = position === 'top' ? 'top-0' : 'bottom-0';
|
|
97
|
-
return (_jsx(View, { className: cn('absolute left-0 right-0 z-50 py-2', positionClass, className), style: [{ paddingHorizontal: visual.surfacePaddingSm }, style], pointerEvents: "box-none", ...props, children: toasts.map((toast) => (_jsx(ToastContainerItem, { toast: toast, dismiss: dismiss }, toast.id))) }));
|
|
115
|
+
return (_jsx(View, { className: cn('absolute left-0 right-0 z-50 py-2', positionClass, className), style: [{ paddingHorizontal: visual.surfacePaddingSm }, style], pointerEvents: "box-none", ...props, children: _jsx(View, { className: "w-full max-w-md self-center", pointerEvents: "box-none", children: toasts.map((toast) => (_jsx(ToastContainerItem, { toast: toast, dismiss: dismiss }, toast.id))) }) }));
|
|
98
116
|
}
|
|
99
117
|
export { Toast, toastVariants };
|
package/dist/index.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ export { registerFrostedBlurView, type FrostedSurfaceProps, type FrostedTintToke
|
|
|
15
15
|
export { controlRecipe, fieldRecipe, surfaceRecipe, surfacePadding, surfaceRadius, shadowStyle, typographyStyle, useControlRecipe, useFieldRecipe, useSurfaceRecipe, useShadowStyle, useTypography, type ControlSize, type SurfacePaddingSize, type SurfaceRecipeOptions, type ShadowTier, type TypographyRole, } from './lib/recipes';
|
|
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
|
+
export { checkThemeContrast, themeFromSeeds, type ContrastCheck, type ThemeSeeds, } from './lib/theme-seeds';
|
|
18
19
|
export { Text, textVariants, type TextProps } from './components/text';
|
|
19
20
|
export { Surface, type SurfaceLevel, type SurfaceProps } from './components/surface';
|
|
20
21
|
export { View, type ViewProps } from './components/view';
|
package/dist/index.js
CHANGED
|
@@ -15,6 +15,7 @@ export { registerFrostedBlurView, } from './lib/frosted-surface';
|
|
|
15
15
|
export { controlRecipe, fieldRecipe, surfaceRecipe, surfacePadding, surfaceRadius, shadowStyle, typographyStyle, useControlRecipe, useFieldRecipe, useSurfaceRecipe, useShadowStyle, useTypography, } from './lib/recipes';
|
|
16
16
|
export { WHILEUI_PRESET_SOFT, WHILEUI_PRESET_ATELIER, WHILEUI_THEME_PRESETS, themePresetToCssVariables, themePresetToCss, } from './lib/theme-presets';
|
|
17
17
|
export { deriveThemeColors, THEME_BASE_TOKEN_KEYS } from './lib/theme-derive';
|
|
18
|
+
export { checkThemeContrast, themeFromSeeds, } from './lib/theme-seeds';
|
|
18
19
|
export { Text, textVariants } from './components/text';
|
|
19
20
|
export { Surface } from './components/surface';
|
|
20
21
|
export { View } from './components/view';
|
|
@@ -3,8 +3,11 @@ import { type ViewStyle } from 'react-native';
|
|
|
3
3
|
export type FrostedTintToken = 'surfaceElevated' | 'surfaceTranslucent' | 'card' | 'popover';
|
|
4
4
|
export type FrostedBlurPreset = 'subtle' | 'medium' | 'strong';
|
|
5
5
|
export interface FrostedSurfaceProps {
|
|
6
|
+
/** Enable the optional frosted overlay. */
|
|
6
7
|
frosted?: boolean;
|
|
8
|
+
/** Override blur amount directly. */
|
|
7
9
|
blurIntensity?: number;
|
|
10
|
+
/** Tint token used for frosted mode. */
|
|
8
11
|
blurTintToken?: FrostedTintToken;
|
|
9
12
|
}
|
|
10
13
|
interface UseFrostedSurfaceOptions extends FrostedSurfaceProps {
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { type ThemeColors } from './theme-contract';
|
|
2
2
|
export type { ThemeColors } from './theme-contract';
|
|
3
3
|
export declare function useThemeColors(): ThemeColors;
|
|
4
|
-
|
|
5
|
-
export declare function useIconColors(): {
|
|
4
|
+
export interface IconColors {
|
|
6
5
|
foreground: string;
|
|
7
6
|
muted: string;
|
|
8
7
|
primary: string;
|
|
@@ -12,6 +11,8 @@ export declare function useIconColors(): {
|
|
|
12
11
|
success: string;
|
|
13
12
|
info: string;
|
|
14
13
|
destructive: string;
|
|
15
|
-
}
|
|
14
|
+
}
|
|
15
|
+
/** Icon colors derived from theme. For @expo/vector-icons which need hex values. */
|
|
16
|
+
export declare function useIconColors(): IconColors;
|
|
16
17
|
/** Alias for useThemeColors. Returns RN-safe color strings (hex/rgb/rgba) for RefreshControl, LinearGradient, charts. */
|
|
17
18
|
export declare const useThemeTokens: typeof useThemeColors;
|
package/dist/lib/theme-colors.js
CHANGED
|
@@ -41,10 +41,29 @@ function resolveThemeToken(cssValue, appValue) {
|
|
|
41
41
|
}
|
|
42
42
|
return parseThemeColor(cssValue) ?? parseThemeColor(appValue);
|
|
43
43
|
}
|
|
44
|
+
// Every themed component calls this hook on every render, and parsing 104 colour
|
|
45
|
+
// strings plus deriving the palette cost about 0.16 ms per call in Node, several times
|
|
46
|
+
// that under Hermes. The inputs only change with the theme, so the result is derived
|
|
47
|
+
// once per distinct snapshot and shared: the same object comes back until the CSS
|
|
48
|
+
// variables change, which also keeps it stable for downstream memoisation.
|
|
49
|
+
const resolvedThemeColors = new Map();
|
|
50
|
+
const RESOLVED_THEME_LIMIT = 8;
|
|
44
51
|
export function useThemeColors() {
|
|
45
52
|
const { theme } = useUniwind();
|
|
46
53
|
const values = useCSSVariable(THEME_TOKEN_VARIABLES);
|
|
47
54
|
const scheme = theme === 'dark' ? 'dark' : 'light';
|
|
55
|
+
const key = `${scheme}\u0001${values.join('\u0001')}`;
|
|
56
|
+
const cached = resolvedThemeColors.get(key);
|
|
57
|
+
if (cached)
|
|
58
|
+
return cached;
|
|
59
|
+
const resolved = resolveThemeColors(values, scheme);
|
|
60
|
+
if (resolvedThemeColors.size >= RESOLVED_THEME_LIMIT) {
|
|
61
|
+
resolvedThemeColors.delete(resolvedThemeColors.keys().next().value);
|
|
62
|
+
}
|
|
63
|
+
resolvedThemeColors.set(key, resolved);
|
|
64
|
+
return resolved;
|
|
65
|
+
}
|
|
66
|
+
function resolveThemeColors(values, scheme) {
|
|
48
67
|
const fallbackPalette = scheme === 'dark' ? WHILEUI_DARK_THEME_COLORS : WHILEUI_LIGHT_THEME_COLORS;
|
|
49
68
|
const defined = {};
|
|
50
69
|
let index = 0;
|
|
@@ -61,10 +80,14 @@ export function useThemeColors() {
|
|
|
61
80
|
const derived = deriveThemeColors(base, scheme);
|
|
62
81
|
return { ...derived, ...defined, placeholder: derived.placeholder };
|
|
63
82
|
}
|
|
83
|
+
const iconColorsByTheme = new WeakMap();
|
|
64
84
|
/** Icon colors derived from theme. For @expo/vector-icons which need hex values. */
|
|
65
85
|
export function useIconColors() {
|
|
66
86
|
const c = useThemeColors();
|
|
67
|
-
|
|
87
|
+
const cached = iconColorsByTheme.get(c);
|
|
88
|
+
if (cached)
|
|
89
|
+
return cached;
|
|
90
|
+
const icons = {
|
|
68
91
|
foreground: c.foreground,
|
|
69
92
|
muted: c.mutedForeground,
|
|
70
93
|
primary: c.primary,
|
|
@@ -75,6 +98,8 @@ export function useIconColors() {
|
|
|
75
98
|
info: c.info,
|
|
76
99
|
destructive: c.destructive,
|
|
77
100
|
};
|
|
101
|
+
iconColorsByTheme.set(c, icons);
|
|
102
|
+
return icons;
|
|
78
103
|
}
|
|
79
104
|
/** Alias for useThemeColors. Returns RN-safe color strings (hex/rgb/rgba) for RefreshControl, LinearGradient, charts. */
|
|
80
105
|
export const useThemeTokens = useThemeColors;
|
|
@@ -13,16 +13,16 @@ export const WHILEUI_LIGHT_THEME_COLORS = {
|
|
|
13
13
|
mutedForeground: '#645783',
|
|
14
14
|
border: 'rgba(68, 87, 255, 0.16)',
|
|
15
15
|
input: 'rgba(255, 255, 255, 0.84)',
|
|
16
|
-
ring: '
|
|
16
|
+
ring: '#3d8bff',
|
|
17
17
|
accent: '#a7f432',
|
|
18
18
|
accentForeground: '#163218',
|
|
19
|
-
destructive: '#
|
|
19
|
+
destructive: '#d03d31',
|
|
20
20
|
destructiveForeground: '#fff8f6',
|
|
21
|
-
success: '#
|
|
21
|
+
success: '#00864d',
|
|
22
22
|
successForeground: '#f5fff9',
|
|
23
23
|
warning: '#ffb224',
|
|
24
24
|
warningForeground: '#3d2200',
|
|
25
|
-
info: '#
|
|
25
|
+
info: '#2070e1',
|
|
26
26
|
infoForeground: '#f8fbff',
|
|
27
27
|
overlay: 'rgba(39, 31, 72, 0.14)',
|
|
28
28
|
overlayStrong: 'rgba(39, 31, 72, 0.22)',
|
|
@@ -37,19 +37,19 @@ export const WHILEUI_LIGHT_THEME_COLORS = {
|
|
|
37
37
|
shadow: '#271f48',
|
|
38
38
|
primaryActive: '#3346e6',
|
|
39
39
|
secondaryActive: 'rgba(255, 123, 92, 0.18)',
|
|
40
|
-
destructiveActive: '#
|
|
40
|
+
destructiveActive: '#bb261d',
|
|
41
41
|
primarySoft: 'rgba(68, 87, 255, 0.13)',
|
|
42
42
|
primarySoftSubtle: 'rgba(68, 87, 255, 0.07)',
|
|
43
43
|
secondarySoft: 'rgba(255, 123, 92, 0.16)',
|
|
44
44
|
mutedSoft: 'rgba(74, 55, 122, 0.05)',
|
|
45
|
-
destructiveSoft: 'rgba(
|
|
46
|
-
destructiveSoftBorder: 'rgba(
|
|
47
|
-
successSoft: 'rgba(
|
|
48
|
-
successSoftBorder: 'rgba(
|
|
45
|
+
destructiveSoft: 'rgba(208, 61, 49, 0.12)',
|
|
46
|
+
destructiveSoftBorder: 'rgba(208, 61, 49, 0.30)',
|
|
47
|
+
successSoft: 'rgba(0, 134, 77, 0.12)',
|
|
48
|
+
successSoftBorder: 'rgba(0, 134, 77, 0.30)',
|
|
49
49
|
warningSoft: 'rgba(255, 178, 36, 0.14)',
|
|
50
50
|
warningSoftBorder: 'rgba(255, 178, 36, 0.34)',
|
|
51
|
-
infoSoft: 'rgba(
|
|
52
|
-
infoSoftBorder: 'rgba(
|
|
51
|
+
infoSoft: 'rgba(32, 112, 225, 0.12)',
|
|
52
|
+
infoSoftBorder: 'rgba(32, 112, 225, 0.30)',
|
|
53
53
|
placeholder: '#645783',
|
|
54
54
|
};
|
|
55
55
|
export const WHILEUI_DARK_THEME_COLORS = {
|
|
@@ -67,7 +67,7 @@ export const WHILEUI_DARK_THEME_COLORS = {
|
|
|
67
67
|
mutedForeground: '#c5bad9',
|
|
68
68
|
border: 'rgba(255, 255, 255, 0.12)',
|
|
69
69
|
input: 'rgba(27, 18, 49, 0.86)',
|
|
70
|
-
ring: '
|
|
70
|
+
ring: '#7ff0c5',
|
|
71
71
|
accent: '#7ff0c5',
|
|
72
72
|
accentForeground: '#0f2d24',
|
|
73
73
|
destructive: '#ff6b57',
|
|
@@ -38,7 +38,7 @@ export const WHILEUI_PRESET_SOFT = {
|
|
|
38
38
|
};
|
|
39
39
|
// Compact geometry, hairline-plus borders, real drop shadows, warm ink.
|
|
40
40
|
const ATELIER_LIGHT = {
|
|
41
|
-
primary: '#
|
|
41
|
+
primary: '#af5028',
|
|
42
42
|
primaryForeground: '#fdf6ee',
|
|
43
43
|
foreground: '#2b2118',
|
|
44
44
|
background: '#f6efe4',
|
|
@@ -49,10 +49,10 @@ const ATELIER_LIGHT = {
|
|
|
49
49
|
secondary: 'rgba(43, 33, 24, 0.06)',
|
|
50
50
|
secondaryForeground: '#4a3a2a',
|
|
51
51
|
muted: 'rgba(43, 33, 24, 0.05)',
|
|
52
|
-
mutedForeground: '#
|
|
52
|
+
mutedForeground: '#716150',
|
|
53
53
|
border: 'rgba(43, 33, 24, 0.22)',
|
|
54
54
|
input: '#fffdf8',
|
|
55
|
-
ring: '
|
|
55
|
+
ring: '#af5028',
|
|
56
56
|
accent: '#1f6f54',
|
|
57
57
|
accentForeground: '#f0faf5',
|
|
58
58
|
destructive: '#b3261e',
|
|
@@ -70,15 +70,15 @@ const ATELIER_LIGHT = {
|
|
|
70
70
|
surfaceTranslucentBorder: 'rgba(43, 33, 24, 0.16)',
|
|
71
71
|
surfaceBorder: 'rgba(43, 33, 24, 0.22)',
|
|
72
72
|
surfaceHighlight: 'rgba(255, 255, 255, 0.65)',
|
|
73
|
-
stateHover: 'rgba(
|
|
74
|
-
statePressed: 'rgba(
|
|
73
|
+
stateHover: 'rgba(175, 80, 40, 0.07)',
|
|
74
|
+
statePressed: 'rgba(175, 80, 40, 0.14)',
|
|
75
75
|
stateDisabled: 'rgba(122, 106, 88, 0.30)',
|
|
76
76
|
shadow: '#3a2c1c',
|
|
77
77
|
primaryActive: '#96431f',
|
|
78
78
|
secondaryActive: 'rgba(43, 33, 24, 0.12)',
|
|
79
79
|
destructiveActive: '#8f1d17',
|
|
80
|
-
primarySoft: 'rgba(
|
|
81
|
-
primarySoftSubtle: 'rgba(
|
|
80
|
+
primarySoft: 'rgba(175, 80, 40, 0.12)',
|
|
81
|
+
primarySoftSubtle: 'rgba(175, 80, 40, 0.06)',
|
|
82
82
|
secondarySoft: 'rgba(31, 111, 84, 0.10)',
|
|
83
83
|
mutedSoft: 'rgba(43, 33, 24, 0.04)',
|
|
84
84
|
destructiveSoft: 'rgba(179, 38, 30, 0.10)',
|
|
@@ -89,7 +89,7 @@ const ATELIER_LIGHT = {
|
|
|
89
89
|
warningSoftBorder: 'rgba(192, 123, 18, 0.36)',
|
|
90
90
|
infoSoft: 'rgba(47, 93, 158, 0.10)',
|
|
91
91
|
infoSoftBorder: 'rgba(47, 93, 158, 0.32)',
|
|
92
|
-
placeholder: '#
|
|
92
|
+
placeholder: '#716150',
|
|
93
93
|
};
|
|
94
94
|
const ATELIER_DARK = {
|
|
95
95
|
primary: '#e8935f',
|
|
@@ -106,7 +106,7 @@ const ATELIER_DARK = {
|
|
|
106
106
|
mutedForeground: '#a3937d',
|
|
107
107
|
border: 'rgba(239, 230, 216, 0.18)',
|
|
108
108
|
input: '#211a14',
|
|
109
|
-
ring: '
|
|
109
|
+
ring: '#e8935f',
|
|
110
110
|
accent: '#6fd0a8',
|
|
111
111
|
accentForeground: '#0b2419',
|
|
112
112
|
destructive: '#e5695f',
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { ThemeColors } from './theme-contract';
|
|
2
|
+
import { type ThemeBaseColors } from './theme-derive';
|
|
3
|
+
import { type ThemePreset } from './theme-presets';
|
|
4
|
+
import type { VisualTokens } from './visual-token-contract';
|
|
5
|
+
export interface ThemeSeeds {
|
|
6
|
+
/** Page background. */
|
|
7
|
+
background: string;
|
|
8
|
+
/** Body text. */
|
|
9
|
+
foreground: string;
|
|
10
|
+
/** Brand colour: primary buttons, links, focus ring. */
|
|
11
|
+
primary: string;
|
|
12
|
+
/** Second voice for highlights and selected states. Derived from `primary` if left out. */
|
|
13
|
+
accent?: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* A complete preset from seeds. `light` is required; `dark` defaults to a dark page with
|
|
17
|
+
* the same brand colours. `overrides` replaces any base colour before the rest is
|
|
18
|
+
* derived, so nothing is out of reach; derived tokens can be adjusted on the result.
|
|
19
|
+
*/
|
|
20
|
+
export declare function themeFromSeeds(input: {
|
|
21
|
+
name: string;
|
|
22
|
+
description: string;
|
|
23
|
+
light: ThemeSeeds;
|
|
24
|
+
dark?: ThemeSeeds;
|
|
25
|
+
overrides?: {
|
|
26
|
+
light?: Partial<ThemeBaseColors>;
|
|
27
|
+
dark?: Partial<ThemeBaseColors>;
|
|
28
|
+
};
|
|
29
|
+
visual?: Partial<VisualTokens>;
|
|
30
|
+
}): ThemePreset;
|
|
31
|
+
export interface ContrastCheck {
|
|
32
|
+
/** "foreground on background" */
|
|
33
|
+
pair: string;
|
|
34
|
+
ratio: number;
|
|
35
|
+
/** 4.5 for text, 3 for focus rings and other non-text UI (WCAG 1.4.3, 1.4.11). */
|
|
36
|
+
min: number;
|
|
37
|
+
pass: boolean;
|
|
38
|
+
}
|
|
39
|
+
/** Every text and focus-ring pairing the components render, checked against WCAG AA. */
|
|
40
|
+
export declare function checkThemeContrast(colors: ThemeColors): ContrastCheck[];
|