orn-ui 0.1.0 → 0.2.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 +9 -2
- package/lib/commonjs/atoms/Avatar.js +1 -1
- package/lib/commonjs/atoms/Avatar.js.map +1 -1
- package/lib/commonjs/atoms/Badge.js +10 -8
- package/lib/commonjs/atoms/Badge.js.map +1 -1
- package/lib/commonjs/atoms/Button.js +8 -6
- package/lib/commonjs/atoms/Button.js.map +1 -1
- package/lib/commonjs/atoms/Checkbox.js +4 -1
- package/lib/commonjs/atoms/Checkbox.js.map +1 -1
- package/lib/commonjs/atoms/Fab.js +1 -1
- package/lib/commonjs/atoms/Fab.js.map +1 -1
- package/lib/commonjs/atoms/Image.js +2 -2
- package/lib/commonjs/atoms/Image.js.map +1 -1
- package/lib/commonjs/atoms/Input.js +2 -2
- package/lib/commonjs/atoms/Input.js.map +1 -1
- package/lib/commonjs/atoms/PressableScale.js +9 -10
- package/lib/commonjs/atoms/PressableScale.js.map +1 -1
- package/lib/commonjs/atoms/Skeleton.js +96 -0
- package/lib/commonjs/atoms/Skeleton.js.map +1 -0
- package/lib/commonjs/atoms/Transition.js +171 -0
- package/lib/commonjs/atoms/Transition.js.map +1 -0
- package/lib/commonjs/atoms/index.js +20 -0
- package/lib/commonjs/atoms/index.js.map +1 -1
- package/lib/commonjs/molecules/AvatarHeader.js +2 -2
- package/lib/commonjs/molecules/AvatarHeader.js.map +1 -1
- package/lib/commonjs/molecules/OptionCard.js +57 -24
- package/lib/commonjs/molecules/OptionCard.js.map +1 -1
- package/lib/commonjs/molecules/{ThemeToggle.js → SegmentedControl.js} +37 -37
- package/lib/commonjs/molecules/SegmentedControl.js.map +1 -0
- package/lib/commonjs/molecules/Stepper.js +124 -47
- package/lib/commonjs/molecules/Stepper.js.map +1 -1
- package/lib/commonjs/molecules/Steps.js +2 -2
- package/lib/commonjs/molecules/Steps.js.map +1 -1
- package/lib/commonjs/molecules/index.js +9 -3
- package/lib/commonjs/molecules/index.js.map +1 -1
- package/lib/commonjs/organisms/Alert.js +6 -5
- package/lib/commonjs/organisms/Alert.js.map +1 -1
- package/lib/commonjs/organisms/AlertProvider.js +26 -0
- package/lib/commonjs/organisms/AlertProvider.js.map +1 -1
- package/lib/commonjs/organisms/BottomSheet.js +118 -39
- package/lib/commonjs/organisms/BottomSheet.js.map +1 -1
- package/lib/commonjs/organisms/DateField.js +2 -2
- package/lib/commonjs/organisms/DateField.js.map +1 -1
- package/lib/commonjs/organisms/DatePicker.js +3 -3
- package/lib/commonjs/organisms/DatePicker.js.map +1 -1
- package/lib/commonjs/organisms/List.js +66 -44
- package/lib/commonjs/organisms/List.js.map +1 -1
- package/lib/commonjs/organisms/Modal.js +42 -87
- package/lib/commonjs/organisms/Modal.js.map +1 -1
- package/lib/commonjs/organisms/NavigationBar.js +205 -0
- package/lib/commonjs/organisms/NavigationBar.js.map +1 -0
- package/lib/commonjs/organisms/Screen.js +9 -2
- package/lib/commonjs/organisms/Screen.js.map +1 -1
- package/lib/commonjs/organisms/SearchList.js +99 -30
- package/lib/commonjs/organisms/SearchList.js.map +1 -1
- package/lib/commonjs/organisms/Select.js +226 -54
- package/lib/commonjs/organisms/Select.js.map +1 -1
- package/lib/commonjs/organisms/ThemeToggle.js +52 -0
- package/lib/commonjs/organisms/ThemeToggle.js.map +1 -0
- package/lib/commonjs/organisms/Toast.js +20 -25
- package/lib/commonjs/organisms/Toast.js.map +1 -1
- package/lib/commonjs/organisms/ToastProvider.js +24 -0
- package/lib/commonjs/organisms/ToastProvider.js.map +1 -1
- package/lib/commonjs/organisms/Wizard.js +22 -2
- package/lib/commonjs/organisms/Wizard.js.map +1 -1
- package/lib/commonjs/organisms/imperativeBridge.js +28 -0
- package/lib/commonjs/organisms/imperativeBridge.js.map +1 -0
- package/lib/commonjs/organisms/index.js +50 -0
- package/lib/commonjs/organisms/index.js.map +1 -1
- package/lib/commonjs/safe-area/SafeAreaUIProvider.js +65 -0
- package/lib/commonjs/safe-area/SafeAreaUIProvider.js.map +1 -0
- package/lib/commonjs/safe-area/index.js +13 -0
- package/lib/commonjs/safe-area/index.js.map +1 -0
- package/lib/commonjs/theme/UIProvider.js +11 -0
- package/lib/commonjs/theme/UIProvider.js.map +1 -1
- package/lib/commonjs/theme/colors.js +207 -0
- package/lib/commonjs/theme/colors.js.map +1 -0
- package/lib/commonjs/theme/createTheme.js +25 -2
- package/lib/commonjs/theme/createTheme.js.map +1 -1
- package/lib/commonjs/theme/index.js +25 -0
- package/lib/commonjs/theme/index.js.map +1 -1
- package/lib/commonjs/theme/palettes.js +73 -11
- package/lib/commonjs/theme/palettes.js.map +1 -1
- package/lib/module/atoms/Avatar.js +1 -1
- package/lib/module/atoms/Avatar.js.map +1 -1
- package/lib/module/atoms/Badge.js +10 -8
- package/lib/module/atoms/Badge.js.map +1 -1
- package/lib/module/atoms/Button.js +8 -6
- package/lib/module/atoms/Button.js.map +1 -1
- package/lib/module/atoms/Checkbox.js +4 -1
- package/lib/module/atoms/Checkbox.js.map +1 -1
- package/lib/module/atoms/Fab.js +1 -1
- package/lib/module/atoms/Fab.js.map +1 -1
- package/lib/module/atoms/Image.js +2 -2
- package/lib/module/atoms/Image.js.map +1 -1
- package/lib/module/atoms/Input.js +2 -2
- package/lib/module/atoms/Input.js.map +1 -1
- package/lib/module/atoms/PressableScale.js +9 -10
- package/lib/module/atoms/PressableScale.js.map +1 -1
- package/lib/module/atoms/Skeleton.js +91 -0
- package/lib/module/atoms/Skeleton.js.map +1 -0
- package/lib/module/atoms/Transition.js +165 -0
- package/lib/module/atoms/Transition.js.map +1 -0
- package/lib/module/atoms/index.js +2 -0
- package/lib/module/atoms/index.js.map +1 -1
- package/lib/module/molecules/AvatarHeader.js +2 -2
- package/lib/module/molecules/AvatarHeader.js.map +1 -1
- package/lib/module/molecules/OptionCard.js +58 -25
- package/lib/module/molecules/OptionCard.js.map +1 -1
- package/lib/module/molecules/{ThemeToggle.js → SegmentedControl.js} +37 -37
- package/lib/module/molecules/SegmentedControl.js.map +1 -0
- package/lib/module/molecules/Stepper.js +125 -49
- package/lib/module/molecules/Stepper.js.map +1 -1
- package/lib/module/molecules/Steps.js +2 -2
- package/lib/module/molecules/Steps.js.map +1 -1
- package/lib/module/molecules/index.js +2 -2
- package/lib/module/molecules/index.js.map +1 -1
- package/lib/module/organisms/Alert.js +6 -5
- package/lib/module/organisms/Alert.js.map +1 -1
- package/lib/module/organisms/AlertProvider.js +24 -1
- package/lib/module/organisms/AlertProvider.js.map +1 -1
- package/lib/module/organisms/BottomSheet.js +120 -41
- package/lib/module/organisms/BottomSheet.js.map +1 -1
- package/lib/module/organisms/DateField.js +2 -2
- package/lib/module/organisms/DateField.js.map +1 -1
- package/lib/module/organisms/DatePicker.js +3 -3
- package/lib/module/organisms/DatePicker.js.map +1 -1
- package/lib/module/organisms/List.js +68 -46
- package/lib/module/organisms/List.js.map +1 -1
- package/lib/module/organisms/Modal.js +44 -89
- package/lib/module/organisms/Modal.js.map +1 -1
- package/lib/module/organisms/NavigationBar.js +200 -0
- package/lib/module/organisms/NavigationBar.js.map +1 -0
- package/lib/module/organisms/Screen.js +9 -2
- package/lib/module/organisms/Screen.js.map +1 -1
- package/lib/module/organisms/SearchList.js +100 -31
- package/lib/module/organisms/SearchList.js.map +1 -1
- package/lib/module/organisms/Select.js +228 -56
- package/lib/module/organisms/Select.js.map +1 -1
- package/lib/module/organisms/ThemeToggle.js +47 -0
- package/lib/module/organisms/ThemeToggle.js.map +1 -0
- package/lib/module/organisms/Toast.js +22 -27
- package/lib/module/organisms/Toast.js.map +1 -1
- package/lib/module/organisms/ToastProvider.js +21 -0
- package/lib/module/organisms/ToastProvider.js.map +1 -1
- package/lib/module/organisms/Wizard.js +23 -3
- package/lib/module/organisms/Wizard.js.map +1 -1
- package/lib/module/organisms/imperativeBridge.js +24 -0
- package/lib/module/organisms/imperativeBridge.js.map +1 -0
- package/lib/module/organisms/index.js +4 -2
- package/lib/module/organisms/index.js.map +1 -1
- package/lib/module/safe-area/SafeAreaUIProvider.js +60 -0
- package/lib/module/safe-area/SafeAreaUIProvider.js.map +1 -0
- package/lib/module/safe-area/index.js +4 -0
- package/lib/module/safe-area/index.js.map +1 -0
- package/lib/module/theme/UIProvider.js +11 -0
- package/lib/module/theme/UIProvider.js.map +1 -1
- package/lib/module/theme/colors.js +194 -0
- package/lib/module/theme/colors.js.map +1 -0
- package/lib/module/theme/createTheme.js +25 -2
- package/lib/module/theme/createTheme.js.map +1 -1
- package/lib/module/theme/index.js +1 -0
- package/lib/module/theme/index.js.map +1 -1
- package/lib/module/theme/palettes.js +73 -11
- package/lib/module/theme/palettes.js.map +1 -1
- package/lib/typescript/atoms/Badge.d.ts.map +1 -1
- package/lib/typescript/atoms/Button.d.ts.map +1 -1
- package/lib/typescript/atoms/Checkbox.d.ts.map +1 -1
- package/lib/typescript/atoms/PressableScale.d.ts +3 -2
- package/lib/typescript/atoms/PressableScale.d.ts.map +1 -1
- package/lib/typescript/atoms/Skeleton.d.ts +29 -0
- package/lib/typescript/atoms/Skeleton.d.ts.map +1 -0
- package/lib/typescript/atoms/Transition.d.ts +51 -0
- package/lib/typescript/atoms/Transition.d.ts.map +1 -0
- package/lib/typescript/atoms/index.d.ts +4 -0
- package/lib/typescript/atoms/index.d.ts.map +1 -1
- package/lib/typescript/molecules/OptionCard.d.ts +11 -6
- package/lib/typescript/molecules/OptionCard.d.ts.map +1 -1
- package/lib/typescript/molecules/SegmentedControl.d.ts +24 -0
- package/lib/typescript/molecules/SegmentedControl.d.ts.map +1 -0
- package/lib/typescript/molecules/Stepper.d.ts +35 -3
- package/lib/typescript/molecules/Stepper.d.ts.map +1 -1
- package/lib/typescript/molecules/index.d.ts +4 -4
- package/lib/typescript/molecules/index.d.ts.map +1 -1
- package/lib/typescript/organisms/Alert.d.ts.map +1 -1
- package/lib/typescript/organisms/AlertProvider.d.ts +9 -0
- package/lib/typescript/organisms/AlertProvider.d.ts.map +1 -1
- package/lib/typescript/organisms/BottomSheet.d.ts +26 -4
- package/lib/typescript/organisms/BottomSheet.d.ts.map +1 -1
- package/lib/typescript/organisms/List.d.ts +9 -2
- package/lib/typescript/organisms/List.d.ts.map +1 -1
- package/lib/typescript/organisms/Modal.d.ts +3 -5
- package/lib/typescript/organisms/Modal.d.ts.map +1 -1
- package/lib/typescript/organisms/NavigationBar.d.ts +32 -0
- package/lib/typescript/organisms/NavigationBar.d.ts.map +1 -0
- package/lib/typescript/organisms/Screen.d.ts +2 -1
- package/lib/typescript/organisms/Screen.d.ts.map +1 -1
- package/lib/typescript/organisms/SearchList.d.ts +21 -5
- package/lib/typescript/organisms/SearchList.d.ts.map +1 -1
- package/lib/typescript/organisms/Select.d.ts +19 -4
- package/lib/typescript/organisms/Select.d.ts.map +1 -1
- package/lib/typescript/{molecules → organisms}/ThemeToggle.d.ts +4 -4
- package/lib/typescript/organisms/ThemeToggle.d.ts.map +1 -0
- package/lib/typescript/organisms/Toast.d.ts.map +1 -1
- package/lib/typescript/organisms/ToastProvider.d.ts +8 -0
- package/lib/typescript/organisms/ToastProvider.d.ts.map +1 -1
- package/lib/typescript/organisms/Wizard.d.ts +7 -1
- package/lib/typescript/organisms/Wizard.d.ts.map +1 -1
- package/lib/typescript/organisms/imperativeBridge.d.ts +12 -0
- package/lib/typescript/organisms/imperativeBridge.d.ts.map +1 -0
- package/lib/typescript/organisms/index.d.ts +7 -3
- package/lib/typescript/organisms/index.d.ts.map +1 -1
- package/lib/typescript/safe-area/SafeAreaUIProvider.d.ts +46 -0
- package/lib/typescript/safe-area/SafeAreaUIProvider.d.ts.map +1 -0
- package/lib/typescript/safe-area/index.d.ts +3 -0
- package/lib/typescript/safe-area/index.d.ts.map +1 -0
- package/lib/typescript/theme/UIProvider.d.ts.map +1 -1
- package/lib/typescript/theme/colors.d.ts +44 -0
- package/lib/typescript/theme/colors.d.ts.map +1 -0
- package/lib/typescript/theme/createTheme.d.ts +3 -0
- package/lib/typescript/theme/createTheme.d.ts.map +1 -1
- package/lib/typescript/theme/index.d.ts +3 -1
- package/lib/typescript/theme/index.d.ts.map +1 -1
- package/lib/typescript/theme/palettes.d.ts +41 -3
- package/lib/typescript/theme/palettes.d.ts.map +1 -1
- package/lib/typescript/theme/types.d.ts +9 -0
- package/lib/typescript/theme/types.d.ts.map +1 -1
- package/package.json +101 -18
- package/registry/alert-provider.json +9 -2
- package/registry/alert.json +1 -1
- package/registry/avatar-header.json +1 -1
- package/registry/avatar.json +1 -1
- package/registry/badge.json +1 -1
- package/registry/bottom-sheet.json +6 -2
- package/registry/button.json +1 -1
- package/registry/checkbox.json +1 -1
- package/registry/core.json +9 -5
- package/registry/date-field.json +2 -1
- package/registry/date-picker.json +1 -1
- package/registry/fab.json +1 -1
- package/registry/image.json +1 -1
- package/registry/index.json +108 -39
- package/registry/input.json +1 -1
- package/registry/list.json +2 -1
- package/registry/manifest.json +44 -6
- package/registry/modal.json +3 -2
- package/registry/navigation-bar.json +18 -0
- package/registry/option-card.json +3 -2
- package/registry/pressable-scale.json +1 -1
- package/registry/screen.json +1 -1
- package/registry/search-list.json +2 -1
- package/registry/segmented-control.json +18 -0
- package/registry/select.json +1 -1
- package/registry/skeleton.json +18 -0
- package/registry/stepper.json +3 -2
- package/registry/steps.json +1 -1
- package/registry/theme-toggle.json +5 -4
- package/registry/toast-provider.json +11 -3
- package/registry/toast.json +3 -2
- package/registry/transition.json +19 -0
- package/registry/wizard.json +3 -2
- package/src/atoms/Avatar.tsx +1 -1
- package/src/atoms/Badge.tsx +6 -4
- package/src/atoms/Button.tsx +8 -6
- package/src/atoms/Checkbox.tsx +4 -1
- package/src/atoms/Fab.tsx +1 -1
- package/src/atoms/Image.tsx +2 -2
- package/src/atoms/Input.tsx +2 -2
- package/src/atoms/PressableScale.tsx +16 -6
- package/src/atoms/Skeleton.tsx +114 -0
- package/src/atoms/Transition.tsx +189 -0
- package/src/atoms/index.ts +4 -0
- package/src/molecules/AvatarHeader.tsx +2 -2
- package/src/molecules/OptionCard.tsx +64 -27
- package/src/molecules/SegmentedControl.tsx +84 -0
- package/src/molecules/Stepper.tsx +147 -32
- package/src/molecules/Steps.tsx +2 -2
- package/src/molecules/index.ts +4 -4
- package/src/organisms/Alert.tsx +6 -5
- package/src/organisms/AlertProvider.tsx +26 -1
- package/src/organisms/BottomSheet.tsx +214 -62
- package/src/organisms/DateField.tsx +2 -2
- package/src/organisms/DatePicker.tsx +3 -3
- package/src/organisms/List.tsx +73 -41
- package/src/organisms/Modal.tsx +49 -61
- package/src/organisms/NavigationBar.tsx +223 -0
- package/src/organisms/Screen.tsx +11 -3
- package/src/organisms/SearchList.tsx +134 -38
- package/src/organisms/Select.tsx +227 -49
- package/src/organisms/ThemeToggle.tsx +30 -0
- package/src/organisms/Toast.tsx +19 -24
- package/src/organisms/ToastProvider.tsx +24 -0
- package/src/organisms/Wizard.tsx +36 -3
- package/src/organisms/imperativeBridge.ts +28 -0
- package/src/organisms/index.ts +7 -3
- package/src/safe-area/SafeAreaUIProvider.tsx +67 -0
- package/src/safe-area/index.ts +2 -0
- package/src/theme/UIProvider.tsx +15 -0
- package/src/theme/colors.ts +175 -0
- package/src/theme/createTheme.ts +38 -4
- package/src/theme/index.ts +3 -1
- package/src/theme/palettes.ts +108 -11
- package/src/theme/types.ts +10 -0
- package/lib/commonjs/molecules/ThemeToggle.js.map +0 -1
- package/lib/module/molecules/ThemeToggle.js.map +0 -1
- package/lib/typescript/molecules/ThemeToggle.d.ts.map +0 -1
- package/src/molecules/ThemeToggle.tsx +0 -67
package/src/organisms/Toast.tsx
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import React, { memo
|
|
2
|
-
import {
|
|
1
|
+
import React, { memo } from 'react';
|
|
2
|
+
import { Pressable, Text, View, type StyleProp, type ViewStyle } from 'react-native';
|
|
3
3
|
import { createStyles } from '../theme/createStyles';
|
|
4
4
|
import { useAllowFontScaling, useColors } from '../theme/UIProvider';
|
|
5
|
+
import { Transition } from '../atoms/Transition';
|
|
5
6
|
import { Icon } from '../icons/Icon';
|
|
6
7
|
import type { IconName } from '../icons/types';
|
|
7
8
|
|
|
@@ -70,17 +71,25 @@ export const Toast = memo(
|
|
|
70
71
|
const styles = useStyles();
|
|
71
72
|
const allowFontScaling = useAllowFontScaling();
|
|
72
73
|
|
|
73
|
-
|
|
74
|
+
// La barra lateral es relleno (acento pleno); el ícono es contenido
|
|
75
|
+
// sobre la superficie del toast y necesita el rol `*Text`.
|
|
76
|
+
const barColor: Record<ToastVariant, string> = {
|
|
74
77
|
success: colors.success,
|
|
75
78
|
error: colors.error,
|
|
76
79
|
warning: colors.warning,
|
|
77
80
|
info: colors.secondary,
|
|
78
81
|
};
|
|
82
|
+
const iconColor: Record<ToastVariant, string> = {
|
|
83
|
+
success: colors.successText,
|
|
84
|
+
error: colors.errorText,
|
|
85
|
+
warning: colors.warningText,
|
|
86
|
+
info: colors.secondaryText,
|
|
87
|
+
};
|
|
79
88
|
|
|
80
89
|
const body = (
|
|
81
90
|
<View style={[styles.container, style]} testID={testID} accessibilityRole="alert">
|
|
82
|
-
<View style={[styles.accent, { backgroundColor:
|
|
83
|
-
<Icon name={ICON_BY_VARIANT[variant]} size={22} color={
|
|
91
|
+
<View style={[styles.accent, { backgroundColor: barColor[variant] }]} />
|
|
92
|
+
<Icon name={ICON_BY_VARIANT[variant]} size={22} color={iconColor[variant]} />
|
|
84
93
|
<View style={styles.text}>
|
|
85
94
|
<Text allowFontScaling={allowFontScaling} style={styles.title}>
|
|
86
95
|
{title}
|
|
@@ -112,24 +121,10 @@ Toast.displayName = 'Toast';
|
|
|
112
121
|
|
|
113
122
|
/** Envuelve un Toast con su animación de entrada/salida. Uso interno del provider. */
|
|
114
123
|
export const AnimatedToast = memo(
|
|
115
|
-
({ children, onHeight }: { children: React.ReactNode; onHeight?: (h: number) => void }) =>
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}, [progress]);
|
|
121
|
-
|
|
122
|
-
return (
|
|
123
|
-
<Animated.View
|
|
124
|
-
onLayout={(e) => onHeight?.(e.nativeEvent.layout.height)}
|
|
125
|
-
style={{
|
|
126
|
-
opacity: progress,
|
|
127
|
-
transform: [{ translateY: progress.interpolate({ inputRange: [0, 1], outputRange: [-16, 0] }) }],
|
|
128
|
-
}}
|
|
129
|
-
>
|
|
130
|
-
{children}
|
|
131
|
-
</Animated.View>
|
|
132
|
-
);
|
|
133
|
-
}
|
|
124
|
+
({ children, onHeight }: { children: React.ReactNode; onHeight?: (h: number) => void }) => (
|
|
125
|
+
<Transition preset={['fade', 'slide-down']} distance={16} spring>
|
|
126
|
+
<View onLayout={(e) => onHeight?.(e.nativeEvent.layout.height)}>{children}</View>
|
|
127
|
+
</Transition>
|
|
128
|
+
)
|
|
134
129
|
);
|
|
135
130
|
AnimatedToast.displayName = 'AnimatedToast';
|
|
@@ -3,6 +3,7 @@ import { View } from 'react-native';
|
|
|
3
3
|
import { AnimatedToast, Toast, type ToastVariant } from './Toast';
|
|
4
4
|
import { useInsets } from '../theme/UIProvider';
|
|
5
5
|
import { createStyles } from '../theme/createStyles';
|
|
6
|
+
import { createImperativeBridge } from './imperativeBridge';
|
|
6
7
|
|
|
7
8
|
export interface ToastOptions {
|
|
8
9
|
title: string;
|
|
@@ -26,6 +27,24 @@ interface ToastContextValue {
|
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
const ToastContext = createContext<ToastContextValue | undefined>(undefined);
|
|
30
|
+
const bridge = createImperativeBridge<ToastContextValue>('ToastProvider');
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Muestra un toast desde fuera del árbol de React —un servicio, un
|
|
34
|
+
* interceptor— sin pasar por el hook. Devuelve el id, o '' si no hay
|
|
35
|
+
* <ToastProvider> montado.
|
|
36
|
+
*/
|
|
37
|
+
export function showToast(options: ToastOptions): string {
|
|
38
|
+
return bridge.get()?.show(options) ?? '';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function hideToast(id: string): void {
|
|
42
|
+
bridge.get()?.hide(id);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function hideAllToasts(): void {
|
|
46
|
+
bridge.get()?.hideAll();
|
|
47
|
+
}
|
|
29
48
|
|
|
30
49
|
const useStyles = createStyles((theme) => ({
|
|
31
50
|
host: { position: 'absolute', left: 0, right: 0, paddingHorizontal: theme.tokens.spacing.lg, gap: theme.tokens.spacing.sm },
|
|
@@ -122,6 +141,11 @@ export function ToastProvider({
|
|
|
122
141
|
|
|
123
142
|
const value = useMemo<ToastContextValue>(() => ({ show, hide, hideAll }), [show, hide, hideAll]);
|
|
124
143
|
|
|
144
|
+
useEffect(() => {
|
|
145
|
+
bridge.set(value);
|
|
146
|
+
return () => bridge.set(null);
|
|
147
|
+
}, [value]);
|
|
148
|
+
|
|
125
149
|
const hostPosition =
|
|
126
150
|
position === 'top'
|
|
127
151
|
? { top: insets.top + 8 }
|
package/src/organisms/Wizard.tsx
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import React, { useCallback, useMemo, useState } from 'react';
|
|
1
|
+
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
2
|
import { ScrollView, View, type StyleProp, type ViewStyle } from 'react-native';
|
|
3
3
|
import { createStyles } from '../theme/createStyles';
|
|
4
4
|
import { Steps, type StepItem } from '../molecules/Steps';
|
|
5
5
|
import { Button } from '../atoms/Button';
|
|
6
|
+
import { Transition } from '../atoms/Transition';
|
|
6
7
|
|
|
7
8
|
export interface WizardStep extends StepItem {
|
|
8
9
|
content: React.ReactNode;
|
|
@@ -31,6 +32,12 @@ export interface WizardProps {
|
|
|
31
32
|
* @default true
|
|
32
33
|
*/
|
|
33
34
|
scrollableContent?: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* El contenido del paso entra deslizándose en el sentido de la navegación.
|
|
37
|
+
* Sólo opacity y transform, así que la animación corre en el hilo nativo.
|
|
38
|
+
* @default true
|
|
39
|
+
*/
|
|
40
|
+
animated?: boolean;
|
|
34
41
|
style?: StyleProp<ViewStyle>;
|
|
35
42
|
}
|
|
36
43
|
|
|
@@ -61,6 +68,7 @@ export function Wizard({
|
|
|
61
68
|
nextLabel = 'Next',
|
|
62
69
|
finishLabel = 'Finish',
|
|
63
70
|
scrollableContent = true,
|
|
71
|
+
animated = true,
|
|
64
72
|
style,
|
|
65
73
|
}: WizardProps) {
|
|
66
74
|
const styles = useStyles();
|
|
@@ -82,6 +90,15 @@ export function Wizard({
|
|
|
82
90
|
[steps]
|
|
83
91
|
);
|
|
84
92
|
|
|
93
|
+
// El ref todavía guarda el paso anterior durante el render que trae el nuevo.
|
|
94
|
+
const previous = useRef(current);
|
|
95
|
+
const forward = current >= previous.current;
|
|
96
|
+
const entered = useRef(false);
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
previous.current = current;
|
|
99
|
+
entered.current = true;
|
|
100
|
+
}, [current]);
|
|
101
|
+
|
|
85
102
|
const activeStep = steps[current];
|
|
86
103
|
const isFirst = current === 0;
|
|
87
104
|
const isLast = current === steps.length - 1;
|
|
@@ -92,6 +109,22 @@ export function Wizard({
|
|
|
92
109
|
else goTo(current + 1);
|
|
93
110
|
};
|
|
94
111
|
|
|
112
|
+
// El paso saliente no se anima: mantener los dos montados a la vez duplicaría
|
|
113
|
+
// los formularios y su estado.
|
|
114
|
+
const renderContent = () =>
|
|
115
|
+
animated ? (
|
|
116
|
+
<Transition
|
|
117
|
+
key={current}
|
|
118
|
+
testID="wizard-step"
|
|
119
|
+
preset={['fade', forward ? 'slide-left' : 'slide-right']}
|
|
120
|
+
appear={entered.current}
|
|
121
|
+
>
|
|
122
|
+
{activeStep?.content}
|
|
123
|
+
</Transition>
|
|
124
|
+
) : (
|
|
125
|
+
activeStep?.content
|
|
126
|
+
);
|
|
127
|
+
|
|
95
128
|
return (
|
|
96
129
|
<View style={[styles.container, style]}>
|
|
97
130
|
<Steps
|
|
@@ -115,10 +148,10 @@ export function Wizard({
|
|
|
115
148
|
automaticallyAdjustKeyboardInsets
|
|
116
149
|
showsVerticalScrollIndicator={false}
|
|
117
150
|
>
|
|
118
|
-
{
|
|
151
|
+
{renderContent()}
|
|
119
152
|
</ScrollView>
|
|
120
153
|
) : (
|
|
121
|
-
<View style={styles.content}>{
|
|
154
|
+
<View style={styles.content}>{renderContent()}</View>
|
|
122
155
|
)}
|
|
123
156
|
|
|
124
157
|
<View style={styles.footer}>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Puente entre el proveedor montado y el código que no es un componente
|
|
3
|
+
* (servicios, capa de API, interceptores). El proveedor se registra al montar y
|
|
4
|
+
* se borra al desmontar, así que fuera del árbol nunca queda una referencia
|
|
5
|
+
* viva a un contexto muerto.
|
|
6
|
+
*/
|
|
7
|
+
export interface ImperativeBridge<T> {
|
|
8
|
+
set: (value: T | null) => void;
|
|
9
|
+
get: () => T | null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function createImperativeBridge<T>(providerName: string): ImperativeBridge<T> {
|
|
13
|
+
let current: T | null = null;
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
set: (value) => {
|
|
17
|
+
current = value;
|
|
18
|
+
},
|
|
19
|
+
get: () => {
|
|
20
|
+
if (!current && __DEV__) {
|
|
21
|
+
console.warn(
|
|
22
|
+
`orn-ui: se llamó a la API imperativa sin un <${providerName}> montado. La llamada se ignoró.`
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
return current;
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
}
|
package/src/organisms/index.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
export { Modal } from './Modal';
|
|
2
2
|
export type { ModalProps, ModalVariant } from './Modal';
|
|
3
3
|
export { BottomSheet } from './BottomSheet';
|
|
4
|
-
export type { BottomSheetProps } from './BottomSheet';
|
|
4
|
+
export type { BottomSheetProps, FooterPlacement } from './BottomSheet';
|
|
5
5
|
export { Select } from './Select';
|
|
6
6
|
export type { SelectProps, SelectOption } from './Select';
|
|
7
7
|
export { Alert } from './Alert';
|
|
8
8
|
export type { AlertProps, AlertButton, AlertType } from './Alert';
|
|
9
|
-
export { AlertProvider, useAlert } from './AlertProvider';
|
|
9
|
+
export { AlertProvider, useAlert, showAlert, showConfirm, hideAlert } from './AlertProvider';
|
|
10
10
|
export type { AlertOptions, ConfirmOptions } from './AlertProvider';
|
|
11
11
|
export { Screen } from './Screen';
|
|
12
12
|
export type { ScreenProps, ScreenEdge } from './Screen';
|
|
@@ -16,7 +16,7 @@ export { SearchList } from './SearchList';
|
|
|
16
16
|
export type { SearchListProps } from './SearchList';
|
|
17
17
|
export { Toast } from './Toast';
|
|
18
18
|
export type { ToastProps, ToastVariant } from './Toast';
|
|
19
|
-
export { ToastProvider, useToast } from './ToastProvider';
|
|
19
|
+
export { ToastProvider, useToast, showToast, hideToast, hideAllToasts } from './ToastProvider';
|
|
20
20
|
export type { ToastProviderProps, ToastOptions } from './ToastProvider';
|
|
21
21
|
export { DatePicker, buildMonthGrid, DEFAULT_MONTH_NAMES, DEFAULT_WEEKDAY_NAMES } from './DatePicker';
|
|
22
22
|
export type { DatePickerProps, DateRange } from './DatePicker';
|
|
@@ -24,3 +24,7 @@ export { DateField } from './DateField';
|
|
|
24
24
|
export type { DateFieldProps } from './DateField';
|
|
25
25
|
export { Wizard } from './Wizard';
|
|
26
26
|
export type { WizardProps, WizardStep } from './Wizard';
|
|
27
|
+
export { ThemeToggle } from './ThemeToggle';
|
|
28
|
+
export type { ThemeToggleProps } from './ThemeToggle';
|
|
29
|
+
export { NavigationBar } from './NavigationBar';
|
|
30
|
+
export type { NavigationBarProps, NavigationBarItem } from './NavigationBar';
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { SafeAreaProvider, useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
3
|
+
import { UIProvider, type UIProviderProps } from '../theme/UIProvider';
|
|
4
|
+
|
|
5
|
+
export interface SafeAreaUIProviderProps extends Omit<UIProviderProps, 'insets'> {
|
|
6
|
+
/**
|
|
7
|
+
* Insets explícitos. Ganan sobre los medidos — útil para tests, storybook o
|
|
8
|
+
* una pantalla embebida que no ocupa la ventana entera.
|
|
9
|
+
*/
|
|
10
|
+
insets?: UIProviderProps['insets'];
|
|
11
|
+
/**
|
|
12
|
+
* Montar también el `<SafeAreaProvider>`. Ponelo en false si tu app ya tiene
|
|
13
|
+
* uno arriba (React Navigation lo monta solo en algunas plantillas):
|
|
14
|
+
* un provider anidado mide el frame de *su* View, no el de la ventana, así
|
|
15
|
+
* que ahí adentro los insets darían cero.
|
|
16
|
+
* @default true
|
|
17
|
+
*/
|
|
18
|
+
mountSafeAreaProvider?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function Measured({ insets: override, children, ...rest }: SafeAreaUIProviderProps) {
|
|
22
|
+
const measured = useSafeAreaInsets();
|
|
23
|
+
return (
|
|
24
|
+
<UIProvider insets={override ?? measured} {...rest}>
|
|
25
|
+
{children}
|
|
26
|
+
</UIProvider>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* SafeAreaUIProvider
|
|
32
|
+
* `UIProvider` con los insets ya cableados desde `react-native-safe-area-context`.
|
|
33
|
+
*
|
|
34
|
+
* Vive en su propio subpath (`orn-ui/safe-area`) a propósito: es el único
|
|
35
|
+
* archivo de la librería que importa un paquete de terceros. Nada del entry
|
|
36
|
+
* principal lo referencia, así que quien hace `import { Button } from 'orn-ui'`
|
|
37
|
+
* nunca lo resuelve y el bundle sigue sin dependencias de runtime.
|
|
38
|
+
*
|
|
39
|
+
* ```tsx
|
|
40
|
+
* import { SafeAreaUIProvider } from 'orn-ui/safe-area';
|
|
41
|
+
*
|
|
42
|
+
* export default function App() {
|
|
43
|
+
* return (
|
|
44
|
+
* <SafeAreaUIProvider defaultMode="system">
|
|
45
|
+
* <RootNavigator />
|
|
46
|
+
* </SafeAreaUIProvider>
|
|
47
|
+
* );
|
|
48
|
+
* }
|
|
49
|
+
* ```
|
|
50
|
+
*
|
|
51
|
+
* El equivalente a mano — pasarle `insets={useSafeAreaInsets()}` a `UIProvider`
|
|
52
|
+
* desde un componente que ya esté dentro de `<SafeAreaProvider>` — sigue
|
|
53
|
+
* siendo válido. Esto sólo evita el error silencioso de olvidarlo: `insets`
|
|
54
|
+
* por defecto es `{0,0,0,0}`, y con ceros el bottom sheet queda pegado contra
|
|
55
|
+
* la barra de gestos y el modal `full` se mete abajo del notch.
|
|
56
|
+
*/
|
|
57
|
+
export function SafeAreaUIProvider({
|
|
58
|
+
mountSafeAreaProvider = true,
|
|
59
|
+
...props
|
|
60
|
+
}: SafeAreaUIProviderProps) {
|
|
61
|
+
if (!mountSafeAreaProvider) return <Measured {...props} />;
|
|
62
|
+
return (
|
|
63
|
+
<SafeAreaProvider>
|
|
64
|
+
<Measured {...props} />
|
|
65
|
+
</SafeAreaProvider>
|
|
66
|
+
);
|
|
67
|
+
}
|
package/src/theme/UIProvider.tsx
CHANGED
|
@@ -21,6 +21,9 @@ interface UIContextValue {
|
|
|
21
21
|
|
|
22
22
|
const UIContext = createContext<UIContextValue | undefined>(undefined);
|
|
23
23
|
|
|
24
|
+
/** Una sola vez por sesión: en un re-render el aviso no aporta nada nuevo. */
|
|
25
|
+
let warnedAboutInsets = false;
|
|
26
|
+
|
|
24
27
|
export interface UIProviderProps {
|
|
25
28
|
children: React.ReactNode;
|
|
26
29
|
/** Par light/dark. Por defecto el theme de marca de la librería (créalo con createTheme()). */
|
|
@@ -70,6 +73,18 @@ export function UIProvider({
|
|
|
70
73
|
[isControlled, onModeChange]
|
|
71
74
|
);
|
|
72
75
|
|
|
76
|
+
// Olvidar `insets` no rompe nada visible al montar: el default de ceros
|
|
77
|
+
// recién se nota cuando abrís un Modal `full` (se mete abajo del notch) o un
|
|
78
|
+
// BottomSheet (queda pegado a la barra de gestos). El aviso llega antes.
|
|
79
|
+
if (__DEV__ && insets === zeroInsets && !warnedAboutInsets) {
|
|
80
|
+
warnedAboutInsets = true;
|
|
81
|
+
console.warn(
|
|
82
|
+
'orn-ui: <UIProvider> sin `insets`, se usan {top:0,bottom:0,left:0,right:0}. ' +
|
|
83
|
+
'Los modales y el Screen quedan sin safe area. Pasá `insets={useSafeAreaInsets()}` ' +
|
|
84
|
+
"o usá <SafeAreaUIProvider> de 'orn-ui/safe-area', que lo cablea solo."
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
73
88
|
const scheme: 'light' | 'dark' =
|
|
74
89
|
mode === 'system' ? (systemScheme === 'dark' ? 'dark' : 'light') : mode;
|
|
75
90
|
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utilidades de color sin dependencias, usadas para derivar una familia de
|
|
3
|
+
* roles (relleno / encima / tenue / legible) a partir de un solo hex de marca.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** Contraste mínimo AA para texto normal. */
|
|
7
|
+
export const AA_CONTRAST = 4.5;
|
|
8
|
+
|
|
9
|
+
export interface Rgb {
|
|
10
|
+
r: number;
|
|
11
|
+
g: number;
|
|
12
|
+
b: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface Hsl {
|
|
16
|
+
h: number;
|
|
17
|
+
s: number;
|
|
18
|
+
l: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Roles que necesita un acento del theme, en un esquema concreto. */
|
|
22
|
+
export interface AccentRoles {
|
|
23
|
+
/** Relleno pleno: fondo de botón, círculo del paso actual. */
|
|
24
|
+
base: string;
|
|
25
|
+
/** Texto/ícono que va encima del relleno. */
|
|
26
|
+
on: string;
|
|
27
|
+
/** Fondo tenue: badges, pill del indicador activo. */
|
|
28
|
+
soft: string;
|
|
29
|
+
/** El acento legible sobre `surface`: labels, links. */
|
|
30
|
+
text: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const HEX = /^#?([0-9a-f]{3}|[0-9a-f]{6})$/i;
|
|
34
|
+
|
|
35
|
+
export function hexToRgb(hex: string): Rgb {
|
|
36
|
+
const match = HEX.exec(hex.trim());
|
|
37
|
+
if (!match) {
|
|
38
|
+
throw new Error(`orn-ui: se esperaba un color hex como "#7c3aed", se recibió "${hex}".`);
|
|
39
|
+
}
|
|
40
|
+
const digits = match[1]!;
|
|
41
|
+
const full = digits.length === 3 ? digits.replace(/./g, (c) => c + c) : digits;
|
|
42
|
+
return {
|
|
43
|
+
r: parseInt(full.slice(0, 2), 16),
|
|
44
|
+
g: parseInt(full.slice(2, 4), 16),
|
|
45
|
+
b: parseInt(full.slice(4, 6), 16),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const toHexPair = (value: number) => Math.round(clamp(value, 0, 255)).toString(16).padStart(2, '0');
|
|
50
|
+
|
|
51
|
+
export function rgbToHex({ r, g, b }: Rgb): string {
|
|
52
|
+
return `#${toHexPair(r)}${toHexPair(g)}${toHexPair(b)}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function clamp(value: number, min: number, max: number) {
|
|
56
|
+
return Math.min(max, Math.max(min, value));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function rgbToHsl({ r, g, b }: Rgb): Hsl {
|
|
60
|
+
const rn = r / 255;
|
|
61
|
+
const gn = g / 255;
|
|
62
|
+
const bn = b / 255;
|
|
63
|
+
const max = Math.max(rn, gn, bn);
|
|
64
|
+
const min = Math.min(rn, gn, bn);
|
|
65
|
+
const delta = max - min;
|
|
66
|
+
const l = (max + min) / 2;
|
|
67
|
+
|
|
68
|
+
if (delta === 0) return { h: 0, s: 0, l };
|
|
69
|
+
|
|
70
|
+
const s = delta / (1 - Math.abs(2 * l - 1));
|
|
71
|
+
let h: number;
|
|
72
|
+
if (max === rn) h = ((gn - bn) / delta) % 6;
|
|
73
|
+
else if (max === gn) h = (bn - rn) / delta + 2;
|
|
74
|
+
else h = (rn - gn) / delta + 4;
|
|
75
|
+
|
|
76
|
+
h *= 60;
|
|
77
|
+
return { h: h < 0 ? h + 360 : h, s, l };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function hslToRgb({ h, s, l }: Hsl): Rgb {
|
|
81
|
+
const c = (1 - Math.abs(2 * l - 1)) * s;
|
|
82
|
+
const hp = (((h % 360) + 360) % 360) / 60;
|
|
83
|
+
const x = c * (1 - Math.abs((hp % 2) - 1));
|
|
84
|
+
const [r1, g1, b1] =
|
|
85
|
+
hp < 1 ? [c, x, 0] : hp < 2 ? [x, c, 0] : hp < 3 ? [0, c, x] : hp < 4 ? [0, x, c] : hp < 5 ? [x, 0, c] : [c, 0, x];
|
|
86
|
+
const m = l - c / 2;
|
|
87
|
+
return { r: (r1 + m) * 255, g: (g1 + m) * 255, b: (b1 + m) * 255 };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function toHsl(hex: string): Hsl {
|
|
91
|
+
return rgbToHsl(hexToRgb(hex));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function toHex(hsl: Hsl): string {
|
|
95
|
+
return rgbToHex(hslToRgb({ ...hsl, s: clamp(hsl.s, 0, 1), l: clamp(hsl.l, 0, 1) }));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function channelLuminance(value: number) {
|
|
99
|
+
const c = value / 255;
|
|
100
|
+
return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Luminancia relativa WCAG de un hex. */
|
|
104
|
+
export function luminance(hex: string): number {
|
|
105
|
+
const { r, g, b } = hexToRgb(hex);
|
|
106
|
+
return 0.2126 * channelLuminance(r) + 0.7152 * channelLuminance(g) + 0.0722 * channelLuminance(b);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Ratio de contraste WCAG entre dos hex: 1 (igual) a 21 (blanco sobre negro). */
|
|
110
|
+
export function contrast(a: string, b: string): number {
|
|
111
|
+
const la = luminance(a);
|
|
112
|
+
const lb = luminance(b);
|
|
113
|
+
return (Math.max(la, lb) + 0.05) / (Math.min(la, lb) + 0.05);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Blanco si alcanza el contraste AA —es lo que se espera encima de un relleno
|
|
118
|
+
* de color— y si no, el extremo que más contraste dé.
|
|
119
|
+
*/
|
|
120
|
+
function bestOnColor(background: string, light: string, dark: string): string {
|
|
121
|
+
if (contrast(background, light) >= AA_CONTRAST) return light;
|
|
122
|
+
return contrast(background, dark) >= contrast(background, light) ? dark : light;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Mueve la luminosidad del tono hasta alcanzar el contraste pedido contra
|
|
127
|
+
* `against`. Si ni el extremo llega —un amarillo puro sobre blanco, por
|
|
128
|
+
* ejemplo— devuelve el más contrastado que encontró en el camino.
|
|
129
|
+
*/
|
|
130
|
+
function reachContrast(hsl: Hsl, against: string, target: number, direction: 'darker' | 'lighter'): string {
|
|
131
|
+
const step = direction === 'darker' ? -0.02 : 0.02;
|
|
132
|
+
let best = toHex(hsl);
|
|
133
|
+
let bestRatio = contrast(best, against);
|
|
134
|
+
|
|
135
|
+
for (let l = hsl.l; l >= 0 && l <= 1; l += step) {
|
|
136
|
+
const candidate = toHex({ ...hsl, l });
|
|
137
|
+
const ratio = contrast(candidate, against);
|
|
138
|
+
if (ratio > bestRatio) {
|
|
139
|
+
best = candidate;
|
|
140
|
+
bestRatio = ratio;
|
|
141
|
+
}
|
|
142
|
+
if (ratio >= target) return candidate;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return best;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Deriva los cuatro roles de un acento a partir de un hex, para un esquema.
|
|
150
|
+
* `surface` es el fondo sobre el que tiene que leerse el rol `text`, así que
|
|
151
|
+
* cada esquema pasa el suyo.
|
|
152
|
+
*/
|
|
153
|
+
export function deriveAccent(hex: string, scheme: 'light' | 'dark', surface: string): AccentRoles {
|
|
154
|
+
const hsl = toHsl(hex);
|
|
155
|
+
|
|
156
|
+
if (scheme === 'light') {
|
|
157
|
+
const base = toHex(hsl);
|
|
158
|
+
return {
|
|
159
|
+
base,
|
|
160
|
+
on: bestOnColor(base, '#ffffff', '#1a1a1a'),
|
|
161
|
+
soft: toHex({ h: hsl.h, s: Math.min(hsl.s, 0.85), l: 0.94 }),
|
|
162
|
+
text: reachContrast({ ...hsl, l: Math.min(hsl.l, 0.38) }, surface, AA_CONTRAST, 'darker'),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// En oscuro el acento sube de luminosidad y lo que va encima baja: un
|
|
167
|
+
// #004cef sobre #1e1e1e no se distingue del fondo.
|
|
168
|
+
const base = toHex({ ...hsl, l: 0.72 });
|
|
169
|
+
return {
|
|
170
|
+
base,
|
|
171
|
+
on: bestOnColor(base, '#ffffff', toHex({ ...hsl, s: Math.min(hsl.s, 0.9), l: 0.16 })),
|
|
172
|
+
soft: toHex({ h: hsl.h, s: Math.min(hsl.s, 0.35), l: 0.18 }),
|
|
173
|
+
text: reachContrast({ ...hsl, l: Math.max(hsl.l, 0.74) }, surface, AA_CONTRAST, 'lighter'),
|
|
174
|
+
};
|
|
175
|
+
}
|
package/src/theme/createTheme.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { lightPalette, darkPalette } from './palettes';
|
|
1
|
+
import { lightPalette, darkPalette, type ThemeColors } from './palettes';
|
|
2
2
|
import { tokens as defaultTokens } from './tokens';
|
|
3
|
-
import
|
|
3
|
+
import { deriveAccent } from './colors';
|
|
4
|
+
import type { BrandAccent, Theme, ThemeOverrides, ThemePair } from './types';
|
|
4
5
|
|
|
5
6
|
function mergeDeep<T extends Record<string, any>>(base: T, patch?: Partial<T>): T {
|
|
6
7
|
if (!patch) return base;
|
|
@@ -23,22 +24,55 @@ function mergeDeep<T extends Record<string, any>>(base: T, patch?: Partial<T>):
|
|
|
23
24
|
return out as T;
|
|
24
25
|
}
|
|
25
26
|
|
|
27
|
+
const capitalize = <T extends string>(name: T) => (name.charAt(0).toUpperCase() + name.slice(1)) as Capitalize<T>;
|
|
28
|
+
|
|
29
|
+
/** Expande cada hex de `brand` a los cuatro roles del acento en un esquema. */
|
|
30
|
+
function brandColors(
|
|
31
|
+
brand: ThemeOverrides['brand'],
|
|
32
|
+
scheme: 'light' | 'dark',
|
|
33
|
+
palette: ThemeColors
|
|
34
|
+
): Partial<ThemeColors> {
|
|
35
|
+
if (!brand) return {};
|
|
36
|
+
const accents = typeof brand === 'string' ? { primary: brand } : brand;
|
|
37
|
+
const out: Partial<ThemeColors> = {};
|
|
38
|
+
|
|
39
|
+
for (const [name, hex] of Object.entries(accents) as [BrandAccent, string | undefined][]) {
|
|
40
|
+
if (!hex) continue;
|
|
41
|
+
const roles = deriveAccent(hex, scheme, palette.surface);
|
|
42
|
+
out[name] = roles.base;
|
|
43
|
+
out[`on${capitalize(name)}`] = roles.on;
|
|
44
|
+
out[`${name}Soft`] = roles.soft;
|
|
45
|
+
out[`${name}Text`] = roles.text;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
|
|
26
51
|
/**
|
|
27
52
|
* Construye el par de themes light/dark que consume <UIProvider theme={...}>.
|
|
28
53
|
* Nunca muta los defaults: cada llamada produce objetos nuevos, seguros de
|
|
29
54
|
* compartir entre providers distintos (tests, multi-brand en el mismo árbol).
|
|
55
|
+
*
|
|
56
|
+
* `brand` se aplica antes que `colors`, así que un rol escrito a mano siempre
|
|
57
|
+
* gana sobre el derivado.
|
|
30
58
|
*/
|
|
31
59
|
export function createTheme(overrides?: ThemeOverrides): ThemePair {
|
|
32
60
|
const tokens = mergeDeep(defaultTokens, overrides?.tokens as any);
|
|
33
61
|
|
|
34
62
|
const light: Theme = {
|
|
35
|
-
colors: mergeDeep(
|
|
63
|
+
colors: mergeDeep(
|
|
64
|
+
mergeDeep(lightPalette, brandColors(overrides?.brand, 'light', lightPalette)),
|
|
65
|
+
overrides?.colors?.light
|
|
66
|
+
),
|
|
36
67
|
tokens,
|
|
37
68
|
scheme: 'light',
|
|
38
69
|
};
|
|
39
70
|
|
|
40
71
|
const dark: Theme = {
|
|
41
|
-
colors: mergeDeep(
|
|
72
|
+
colors: mergeDeep(
|
|
73
|
+
mergeDeep(darkPalette, brandColors(overrides?.brand, 'dark', darkPalette)),
|
|
74
|
+
overrides?.colors?.dark
|
|
75
|
+
),
|
|
42
76
|
tokens,
|
|
43
77
|
scheme: 'dark',
|
|
44
78
|
};
|
package/src/theme/index.ts
CHANGED
|
@@ -3,7 +3,9 @@ export type { Tokens } from './tokens';
|
|
|
3
3
|
export { lightPalette, darkPalette } from './palettes';
|
|
4
4
|
export type { ThemeColors } from './palettes';
|
|
5
5
|
export { createTheme, defaultTheme } from './createTheme';
|
|
6
|
-
export type { Theme, ThemePair, ThemeOverrides, DeepPartial } from './types';
|
|
6
|
+
export type { Theme, ThemePair, ThemeOverrides, BrandAccent, DeepPartial } from './types';
|
|
7
|
+
export { deriveAccent, contrast, luminance, AA_CONTRAST } from './colors';
|
|
8
|
+
export type { AccentRoles } from './colors';
|
|
7
9
|
export { defaultLabels } from './labels';
|
|
8
10
|
export type { Labels } from './labels';
|
|
9
11
|
export { zeroInsets } from './insets';
|