orn-ui 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/lib/commonjs/atoms/Avatar.js +1 -1
  2. package/lib/commonjs/atoms/Avatar.js.map +1 -1
  3. package/lib/commonjs/atoms/Badge.js +10 -8
  4. package/lib/commonjs/atoms/Badge.js.map +1 -1
  5. package/lib/commonjs/atoms/Button.js +8 -6
  6. package/lib/commonjs/atoms/Button.js.map +1 -1
  7. package/lib/commonjs/atoms/Checkbox.js +4 -1
  8. package/lib/commonjs/atoms/Checkbox.js.map +1 -1
  9. package/lib/commonjs/atoms/Fab.js +1 -1
  10. package/lib/commonjs/atoms/Fab.js.map +1 -1
  11. package/lib/commonjs/atoms/Image.js +2 -2
  12. package/lib/commonjs/atoms/Image.js.map +1 -1
  13. package/lib/commonjs/atoms/Input.js +2 -2
  14. package/lib/commonjs/atoms/Input.js.map +1 -1
  15. package/lib/commonjs/molecules/AvatarHeader.js +2 -2
  16. package/lib/commonjs/molecules/AvatarHeader.js.map +1 -1
  17. package/lib/commonjs/molecules/OptionCard.js +2 -2
  18. package/lib/commonjs/molecules/OptionCard.js.map +1 -1
  19. package/lib/commonjs/molecules/Steps.js +2 -2
  20. package/lib/commonjs/molecules/Steps.js.map +1 -1
  21. package/lib/commonjs/molecules/ThemeToggle.js +1 -1
  22. package/lib/commonjs/molecules/ThemeToggle.js.map +1 -1
  23. package/lib/commonjs/organisms/Alert.js +6 -5
  24. package/lib/commonjs/organisms/Alert.js.map +1 -1
  25. package/lib/commonjs/organisms/DateField.js +2 -2
  26. package/lib/commonjs/organisms/DateField.js.map +1 -1
  27. package/lib/commonjs/organisms/DatePicker.js +3 -3
  28. package/lib/commonjs/organisms/DatePicker.js.map +1 -1
  29. package/lib/commonjs/organisms/List.js +1 -1
  30. package/lib/commonjs/organisms/List.js.map +1 -1
  31. package/lib/commonjs/organisms/SearchList.js +2 -2
  32. package/lib/commonjs/organisms/SearchList.js.map +1 -1
  33. package/lib/commonjs/organisms/Select.js +4 -4
  34. package/lib/commonjs/organisms/Select.js.map +1 -1
  35. package/lib/commonjs/organisms/Toast.js +12 -3
  36. package/lib/commonjs/organisms/Toast.js.map +1 -1
  37. package/lib/commonjs/safe-area/SafeAreaUIProvider.js +65 -0
  38. package/lib/commonjs/safe-area/SafeAreaUIProvider.js.map +1 -0
  39. package/lib/commonjs/safe-area/index.js +13 -0
  40. package/lib/commonjs/safe-area/index.js.map +1 -0
  41. package/lib/commonjs/theme/UIProvider.js +11 -0
  42. package/lib/commonjs/theme/UIProvider.js.map +1 -1
  43. package/lib/commonjs/theme/palettes.js +70 -11
  44. package/lib/commonjs/theme/palettes.js.map +1 -1
  45. package/lib/module/atoms/Avatar.js +1 -1
  46. package/lib/module/atoms/Avatar.js.map +1 -1
  47. package/lib/module/atoms/Badge.js +10 -8
  48. package/lib/module/atoms/Badge.js.map +1 -1
  49. package/lib/module/atoms/Button.js +8 -6
  50. package/lib/module/atoms/Button.js.map +1 -1
  51. package/lib/module/atoms/Checkbox.js +4 -1
  52. package/lib/module/atoms/Checkbox.js.map +1 -1
  53. package/lib/module/atoms/Fab.js +1 -1
  54. package/lib/module/atoms/Fab.js.map +1 -1
  55. package/lib/module/atoms/Image.js +2 -2
  56. package/lib/module/atoms/Image.js.map +1 -1
  57. package/lib/module/atoms/Input.js +2 -2
  58. package/lib/module/atoms/Input.js.map +1 -1
  59. package/lib/module/molecules/AvatarHeader.js +2 -2
  60. package/lib/module/molecules/AvatarHeader.js.map +1 -1
  61. package/lib/module/molecules/OptionCard.js +2 -2
  62. package/lib/module/molecules/OptionCard.js.map +1 -1
  63. package/lib/module/molecules/Steps.js +2 -2
  64. package/lib/module/molecules/Steps.js.map +1 -1
  65. package/lib/module/molecules/ThemeToggle.js +1 -1
  66. package/lib/module/molecules/ThemeToggle.js.map +1 -1
  67. package/lib/module/organisms/Alert.js +6 -5
  68. package/lib/module/organisms/Alert.js.map +1 -1
  69. package/lib/module/organisms/DateField.js +2 -2
  70. package/lib/module/organisms/DateField.js.map +1 -1
  71. package/lib/module/organisms/DatePicker.js +3 -3
  72. package/lib/module/organisms/DatePicker.js.map +1 -1
  73. package/lib/module/organisms/List.js +1 -1
  74. package/lib/module/organisms/List.js.map +1 -1
  75. package/lib/module/organisms/SearchList.js +2 -2
  76. package/lib/module/organisms/SearchList.js.map +1 -1
  77. package/lib/module/organisms/Select.js +4 -4
  78. package/lib/module/organisms/Select.js.map +1 -1
  79. package/lib/module/organisms/Toast.js +12 -3
  80. package/lib/module/organisms/Toast.js.map +1 -1
  81. package/lib/module/safe-area/SafeAreaUIProvider.js +60 -0
  82. package/lib/module/safe-area/SafeAreaUIProvider.js.map +1 -0
  83. package/lib/module/safe-area/index.js +4 -0
  84. package/lib/module/safe-area/index.js.map +1 -0
  85. package/lib/module/theme/UIProvider.js +11 -0
  86. package/lib/module/theme/UIProvider.js.map +1 -1
  87. package/lib/module/theme/palettes.js +70 -11
  88. package/lib/module/theme/palettes.js.map +1 -1
  89. package/lib/typescript/atoms/Badge.d.ts.map +1 -1
  90. package/lib/typescript/atoms/Button.d.ts.map +1 -1
  91. package/lib/typescript/atoms/Checkbox.d.ts.map +1 -1
  92. package/lib/typescript/organisms/Alert.d.ts.map +1 -1
  93. package/lib/typescript/organisms/Toast.d.ts.map +1 -1
  94. package/lib/typescript/safe-area/SafeAreaUIProvider.d.ts +46 -0
  95. package/lib/typescript/safe-area/SafeAreaUIProvider.d.ts.map +1 -0
  96. package/lib/typescript/safe-area/index.d.ts +3 -0
  97. package/lib/typescript/safe-area/index.d.ts.map +1 -0
  98. package/lib/typescript/theme/UIProvider.d.ts.map +1 -1
  99. package/lib/typescript/theme/palettes.d.ts +41 -3
  100. package/lib/typescript/theme/palettes.d.ts.map +1 -1
  101. package/package.json +36 -14
  102. package/registry/alert.json +1 -1
  103. package/registry/avatar-header.json +1 -1
  104. package/registry/avatar.json +1 -1
  105. package/registry/badge.json +1 -1
  106. package/registry/button.json +1 -1
  107. package/registry/checkbox.json +1 -1
  108. package/registry/core.json +2 -2
  109. package/registry/date-field.json +1 -1
  110. package/registry/date-picker.json +1 -1
  111. package/registry/fab.json +1 -1
  112. package/registry/image.json +1 -1
  113. package/registry/index.json +19 -19
  114. package/registry/input.json +1 -1
  115. package/registry/list.json +1 -1
  116. package/registry/option-card.json +1 -1
  117. package/registry/search-list.json +1 -1
  118. package/registry/select.json +1 -1
  119. package/registry/steps.json +1 -1
  120. package/registry/theme-toggle.json +1 -1
  121. package/registry/toast.json +1 -1
  122. package/src/atoms/Avatar.tsx +1 -1
  123. package/src/atoms/Badge.tsx +6 -4
  124. package/src/atoms/Button.tsx +8 -6
  125. package/src/atoms/Checkbox.tsx +4 -1
  126. package/src/atoms/Fab.tsx +1 -1
  127. package/src/atoms/Image.tsx +2 -2
  128. package/src/atoms/Input.tsx +2 -2
  129. package/src/molecules/AvatarHeader.tsx +2 -2
  130. package/src/molecules/OptionCard.tsx +2 -2
  131. package/src/molecules/Steps.tsx +2 -2
  132. package/src/molecules/ThemeToggle.tsx +1 -1
  133. package/src/organisms/Alert.tsx +6 -5
  134. package/src/organisms/DateField.tsx +2 -2
  135. package/src/organisms/DatePicker.tsx +3 -3
  136. package/src/organisms/List.tsx +1 -1
  137. package/src/organisms/SearchList.tsx +2 -2
  138. package/src/organisms/Select.tsx +4 -4
  139. package/src/organisms/Toast.tsx +11 -3
  140. package/src/safe-area/SafeAreaUIProvider.tsx +67 -0
  141. package/src/safe-area/index.ts +2 -0
  142. package/src/theme/UIProvider.tsx +15 -0
  143. package/src/theme/palettes.ts +105 -11
@@ -9,7 +9,7 @@
9
9
  "files": [
10
10
  {
11
11
  "path": "organisms/Toast.tsx",
12
- "content": "import React, { memo, useEffect, useRef } from 'react';\nimport { Animated, Pressable, Text, View, type StyleProp, type ViewStyle } from 'react-native';\nimport { createStyles } from '../theme/createStyles';\nimport { useAllowFontScaling, useColors } from '../theme/UIProvider';\nimport { Icon } from '../icons/Icon';\nimport type { IconName } from '../icons/types';\n\nexport type ToastVariant = 'success' | 'error' | 'warning' | 'info';\n\nexport interface ToastProps {\n title: string;\n message?: string;\n variant?: ToastVariant;\n /** Se dispara al tocar el toast (además de cerrarlo). */\n onPress?: () => void;\n onDismiss?: () => void;\n /** Oculta la X de cerrar. El toast igual se auto-descarta por tiempo. */\n hideCloseButton?: boolean;\n style?: StyleProp<ViewStyle>;\n testID?: string;\n}\n\nconst ICON_BY_VARIANT: Record<ToastVariant, IconName> = {\n success: 'check',\n error: 'alert',\n warning: 'warning',\n info: 'info',\n};\n\nconst useStyles = createStyles((theme) => ({\n container: {\n flexDirection: 'row',\n alignItems: 'center',\n gap: theme.tokens.spacing.md,\n backgroundColor: theme.colors.surface,\n borderRadius: theme.tokens.radius.lg,\n borderWidth: 1,\n borderColor: theme.colors.border,\n paddingVertical: theme.tokens.spacing.md,\n paddingHorizontal: theme.tokens.spacing.lg,\n ...theme.tokens.shadow.lg,\n },\n accent: {\n position: 'absolute',\n left: 0,\n top: 0,\n bottom: 0,\n width: 4,\n borderTopLeftRadius: theme.tokens.radius.lg,\n borderBottomLeftRadius: theme.tokens.radius.lg,\n },\n text: { flex: 1, gap: 2 },\n title: {\n fontSize: theme.tokens.fontSize.md,\n fontWeight: theme.tokens.fontWeight.semibold,\n color: theme.colors.text,\n },\n message: { fontSize: theme.tokens.fontSize.sm, color: theme.colors.textLight, lineHeight: theme.tokens.lineHeight.sm },\n}));\n\n/**\n * Toast\n * Notificación efímera. Normalmente no se usa directo: `ToastProvider` +\n * `useToast()` la montan y la descartan por vos. Se expone igual por si\n * querés colocarla vos mismo dentro de una pantalla concreta.\n */\nexport const Toast = memo(\n ({ title, message, variant = 'info', onPress, onDismiss, hideCloseButton, style, testID }: ToastProps) => {\n const colors = useColors();\n const styles = useStyles();\n const allowFontScaling = useAllowFontScaling();\n\n const accentColor: Record<ToastVariant, string> = {\n success: colors.success,\n error: colors.error,\n warning: colors.warning,\n info: colors.secondary,\n };\n\n const body = (\n <View style={[styles.container, style]} testID={testID} accessibilityRole=\"alert\">\n <View style={[styles.accent, { backgroundColor: accentColor[variant] }]} />\n <Icon name={ICON_BY_VARIANT[variant]} size={22} color={accentColor[variant]} />\n <View style={styles.text}>\n <Text allowFontScaling={allowFontScaling} style={styles.title}>\n {title}\n </Text>\n {!!message && (\n <Text allowFontScaling={allowFontScaling} style={styles.message}>\n {message}\n </Text>\n )}\n </View>\n {!hideCloseButton && !!onDismiss && (\n <Pressable onPress={onDismiss} accessibilityRole=\"button\" accessibilityLabel=\"Dismiss\" hitSlop={8}>\n <Icon name=\"close\" size={18} color={colors.textLight} />\n </Pressable>\n )}\n </View>\n );\n\n if (!onPress) return body;\n\n return (\n <Pressable onPress={onPress} accessibilityRole=\"button\" accessibilityLabel={title}>\n {body}\n </Pressable>\n );\n }\n);\nToast.displayName = 'Toast';\n\n/** Envuelve un Toast con su animación de entrada/salida. Uso interno del provider. */\nexport const AnimatedToast = memo(\n ({ children, onHeight }: { children: React.ReactNode; onHeight?: (h: number) => void }) => {\n const progress = useRef(new Animated.Value(0)).current;\n\n useEffect(() => {\n Animated.spring(progress, { toValue: 1, useNativeDriver: true, speed: 14, bounciness: 4 }).start();\n }, [progress]);\n\n return (\n <Animated.View\n onLayout={(e) => onHeight?.(e.nativeEvent.layout.height)}\n style={{\n opacity: progress,\n transform: [{ translateY: progress.interpolate({ inputRange: [0, 1], outputRange: [-16, 0] }) }],\n }}\n >\n {children}\n </Animated.View>\n );\n }\n);\nAnimatedToast.displayName = 'AnimatedToast';\n"
12
+ "content": "import React, { memo, useEffect, useRef } from 'react';\nimport { Animated, Pressable, Text, View, type StyleProp, type ViewStyle } from 'react-native';\nimport { createStyles } from '../theme/createStyles';\nimport { useAllowFontScaling, useColors } from '../theme/UIProvider';\nimport { Icon } from '../icons/Icon';\nimport type { IconName } from '../icons/types';\n\nexport type ToastVariant = 'success' | 'error' | 'warning' | 'info';\n\nexport interface ToastProps {\n title: string;\n message?: string;\n variant?: ToastVariant;\n /** Se dispara al tocar el toast (además de cerrarlo). */\n onPress?: () => void;\n onDismiss?: () => void;\n /** Oculta la X de cerrar. El toast igual se auto-descarta por tiempo. */\n hideCloseButton?: boolean;\n style?: StyleProp<ViewStyle>;\n testID?: string;\n}\n\nconst ICON_BY_VARIANT: Record<ToastVariant, IconName> = {\n success: 'check',\n error: 'alert',\n warning: 'warning',\n info: 'info',\n};\n\nconst useStyles = createStyles((theme) => ({\n container: {\n flexDirection: 'row',\n alignItems: 'center',\n gap: theme.tokens.spacing.md,\n backgroundColor: theme.colors.surface,\n borderRadius: theme.tokens.radius.lg,\n borderWidth: 1,\n borderColor: theme.colors.border,\n paddingVertical: theme.tokens.spacing.md,\n paddingHorizontal: theme.tokens.spacing.lg,\n ...theme.tokens.shadow.lg,\n },\n accent: {\n position: 'absolute',\n left: 0,\n top: 0,\n bottom: 0,\n width: 4,\n borderTopLeftRadius: theme.tokens.radius.lg,\n borderBottomLeftRadius: theme.tokens.radius.lg,\n },\n text: { flex: 1, gap: 2 },\n title: {\n fontSize: theme.tokens.fontSize.md,\n fontWeight: theme.tokens.fontWeight.semibold,\n color: theme.colors.text,\n },\n message: { fontSize: theme.tokens.fontSize.sm, color: theme.colors.textLight, lineHeight: theme.tokens.lineHeight.sm },\n}));\n\n/**\n * Toast\n * Notificación efímera. Normalmente no se usa directo: `ToastProvider` +\n * `useToast()` la montan y la descartan por vos. Se expone igual por si\n * querés colocarla vos mismo dentro de una pantalla concreta.\n */\nexport const Toast = memo(\n ({ title, message, variant = 'info', onPress, onDismiss, hideCloseButton, style, testID }: ToastProps) => {\n const colors = useColors();\n const styles = useStyles();\n const allowFontScaling = useAllowFontScaling();\n\n // La barra lateral es relleno (acento pleno); el ícono es contenido\n // sobre la superficie del toast y necesita el rol `*Text`.\n const barColor: Record<ToastVariant, string> = {\n success: colors.success,\n error: colors.error,\n warning: colors.warning,\n info: colors.secondary,\n };\n const iconColor: Record<ToastVariant, string> = {\n success: colors.successText,\n error: colors.errorText,\n warning: colors.warningText,\n info: colors.secondaryText,\n };\n\n const body = (\n <View style={[styles.container, style]} testID={testID} accessibilityRole=\"alert\">\n <View style={[styles.accent, { backgroundColor: barColor[variant] }]} />\n <Icon name={ICON_BY_VARIANT[variant]} size={22} color={iconColor[variant]} />\n <View style={styles.text}>\n <Text allowFontScaling={allowFontScaling} style={styles.title}>\n {title}\n </Text>\n {!!message && (\n <Text allowFontScaling={allowFontScaling} style={styles.message}>\n {message}\n </Text>\n )}\n </View>\n {!hideCloseButton && !!onDismiss && (\n <Pressable onPress={onDismiss} accessibilityRole=\"button\" accessibilityLabel=\"Dismiss\" hitSlop={8}>\n <Icon name=\"close\" size={18} color={colors.textLight} />\n </Pressable>\n )}\n </View>\n );\n\n if (!onPress) return body;\n\n return (\n <Pressable onPress={onPress} accessibilityRole=\"button\" accessibilityLabel={title}>\n {body}\n </Pressable>\n );\n }\n);\nToast.displayName = 'Toast';\n\n/** Envuelve un Toast con su animación de entrada/salida. Uso interno del provider. */\nexport const AnimatedToast = memo(\n ({ children, onHeight }: { children: React.ReactNode; onHeight?: (h: number) => void }) => {\n const progress = useRef(new Animated.Value(0)).current;\n\n useEffect(() => {\n Animated.spring(progress, { toValue: 1, useNativeDriver: true, speed: 14, bounciness: 4 }).start();\n }, [progress]);\n\n return (\n <Animated.View\n onLayout={(e) => onHeight?.(e.nativeEvent.layout.height)}\n style={{\n opacity: progress,\n transform: [{ translateY: progress.interpolate({ inputRange: [0, 1], outputRange: [-16, 0] }) }],\n }}\n >\n {children}\n </Animated.View>\n );\n }\n);\nAnimatedToast.displayName = 'AnimatedToast';\n"
13
13
  }
14
14
  ],
15
15
  "registryDependencies": [
@@ -28,7 +28,7 @@ export const Avatar = memo(({ size = 44, backgroundColor, children, style }: Ava
28
28
  width: size,
29
29
  height: size,
30
30
  borderRadius: size / 2,
31
- backgroundColor: backgroundColor || colors.primary + '15',
31
+ backgroundColor: backgroundColor || colors.primarySoft,
32
32
  },
33
33
  style,
34
34
  ]}
@@ -37,10 +37,12 @@ export const Badge = memo(({ label, variant = 'neutral', backgroundColor, textCo
37
37
  const { colors } = theme;
38
38
 
39
39
  const palette: Record<BadgeVariant, { bg: string; fg: string }> = {
40
- success: { bg: colors.success + '20', fg: colors.success },
41
- error: { bg: colors.error + '20', fg: colors.error },
42
- warning: { bg: colors.warning + '20', fg: colors.warning },
43
- info: { bg: colors.secondary + '20', fg: colors.secondary },
40
+ // fg es el rol `*Text`, no el acento pleno: el acento sobre su propio
41
+ // tinte da ~2.2:1 en light — el label quedaba ilegible.
42
+ success: { bg: colors.successSoft, fg: colors.successText },
43
+ error: { bg: colors.errorSoft, fg: colors.errorText },
44
+ warning: { bg: colors.warningSoft, fg: colors.warningText },
45
+ info: { bg: colors.secondarySoft, fg: colors.secondaryText },
44
46
  neutral: { bg: colors.inputBackground, fg: colors.textLight },
45
47
  };
46
48
 
@@ -65,19 +65,21 @@ function resolveColors(variant: ButtonVariant, disabled: boolean, colors: Return
65
65
  return { background: colors.inputBackground, text: colors.textLight, border: 'transparent' };
66
66
  }
67
67
  switch (variant) {
68
+ // Las variantes sin relleno pintan texto sobre el fondo de la pantalla:
69
+ // van con el rol `*Text`. Las rellenas van con `on*` sobre el acento.
68
70
  case 'outline':
69
- return { background: 'transparent', text: colors.primary, border: colors.primary };
71
+ return { background: 'transparent', text: colors.primaryText, border: colors.primary };
70
72
  case 'ghost':
71
- return { background: 'transparent', text: colors.primary, border: 'transparent' };
73
+ return { background: 'transparent', text: colors.primaryText, border: 'transparent' };
72
74
  case 'link':
73
- return { background: 'transparent', text: colors.primary, border: 'transparent' };
75
+ return { background: 'transparent', text: colors.primaryText, border: 'transparent' };
74
76
  case 'secondary':
75
- return { background: colors.secondary, text: colors.white, border: 'transparent' };
77
+ return { background: colors.secondary, text: colors.onSecondary, border: 'transparent' };
76
78
  case 'destructive':
77
- return { background: colors.error, text: colors.white, border: 'transparent' };
79
+ return { background: colors.error, text: colors.onError, border: 'transparent' };
78
80
  case 'primary':
79
81
  default:
80
- return { background: colors.primary, text: colors.white, border: 'transparent' };
82
+ return { background: colors.primary, text: colors.onPrimary, border: 'transparent' };
81
83
  }
82
84
  }
83
85
 
@@ -32,6 +32,9 @@ export const Checkbox = memo(({ value, onValueChange, label, color, disabled, te
32
32
  const styles = useStyles();
33
33
  const allowFontScaling = useAllowFontScaling();
34
34
  const activeColor = color || colors.primary;
35
+ // El check va encima del relleno: con `color` propio no sabemos qué tono
36
+ // contrasta, así que ahí seguimos con blanco (decisión del consumidor).
37
+ const checkColor = color ? colors.white : colors.onPrimary;
35
38
  const progress = useRef(new Animated.Value(value ? 1 : 0)).current;
36
39
  const checkScale = useRef(new Animated.Value(value ? 1 : 0)).current;
37
40
 
@@ -61,7 +64,7 @@ export const Checkbox = memo(({ value, onValueChange, label, color, disabled, te
61
64
  >
62
65
  <Animated.View style={[styles.box, { backgroundColor, borderColor, opacity: disabled ? 0.5 : 1 }]}>
63
66
  <Animated.View style={{ transform: [{ scale: checkScale }] }}>
64
- <Icon name="check" size={16} color={colors.white} />
67
+ <Icon name="check" size={16} color={checkColor} />
65
68
  </Animated.View>
66
69
  </Animated.View>
67
70
  {label && (
package/src/atoms/Fab.tsx CHANGED
@@ -55,7 +55,7 @@ export const Fab = memo(
55
55
  accessibilityRole="button"
56
56
  accessibilityLabel={accessibilityLabel}
57
57
  >
58
- {icon ?? <Icon name={iconName} size={size * 0.5} color={colors.white} />}
58
+ {icon ?? <Icon name={iconName} size={size * 0.5} color={colors.onPrimary} />}
59
59
  </PressableScale>
60
60
  );
61
61
  }
@@ -40,9 +40,9 @@ export interface ImageProps {
40
40
  }
41
41
 
42
42
  const useStyles = createStyles((theme) => ({
43
- container: { overflow: 'hidden', backgroundColor: theme.colors.border + '30' },
43
+ container: { overflow: 'hidden', backgroundColor: theme.colors.skeleton },
44
44
  overlay: { alignItems: 'center', justifyContent: 'center' },
45
- fallback: { flex: 1, backgroundColor: theme.colors.border + '30' },
45
+ fallback: { flex: 1, backgroundColor: theme.colors.skeleton },
46
46
  }));
47
47
 
48
48
  function sourceUri(source: ImageSourcePropType): string | undefined {
@@ -37,7 +37,7 @@ const useStyles = createStyles((theme) => ({
37
37
  fontWeight: theme.tokens.fontWeight.medium,
38
38
  color: theme.colors.text,
39
39
  },
40
- required: { color: theme.colors.error },
40
+ required: { color: theme.colors.errorText },
41
41
  inputWrapper: {
42
42
  flexDirection: 'row',
43
43
  alignItems: 'center',
@@ -55,7 +55,7 @@ const useStyles = createStyles((theme) => ({
55
55
  },
56
56
  leftIcon: { marginLeft: theme.tokens.spacing.lg, marginRight: -theme.tokens.spacing.xs },
57
57
  rightIcon: { padding: theme.tokens.spacing.sm + 2, marginRight: theme.tokens.spacing.xs },
58
- errorText: { marginTop: theme.tokens.spacing.xs, fontSize: theme.tokens.fontSize.sm, color: theme.colors.error },
58
+ errorText: { marginTop: theme.tokens.spacing.xs, fontSize: theme.tokens.fontSize.sm, color: theme.colors.errorText },
59
59
  }));
60
60
 
61
61
  /**
@@ -22,12 +22,12 @@ const useStyles = createStyles((theme) => ({
22
22
  width: 80,
23
23
  height: 80,
24
24
  borderRadius: 40,
25
- backgroundColor: theme.colors.primary + '20',
25
+ backgroundColor: theme.colors.primarySoft,
26
26
  justifyContent: 'center',
27
27
  alignItems: 'center',
28
28
  marginBottom: theme.tokens.spacing.md,
29
29
  },
30
- initials: { fontSize: theme.tokens.fontSize.xxl, fontWeight: theme.tokens.fontWeight.bold, color: theme.colors.primary },
30
+ initials: { fontSize: theme.tokens.fontSize.xxl, fontWeight: theme.tokens.fontWeight.bold, color: theme.colors.primaryText },
31
31
  title: { fontSize: theme.tokens.fontSize.xl, fontWeight: theme.tokens.fontWeight.bold, textAlign: 'center', color: theme.colors.text, marginBottom: theme.tokens.spacing.xs },
32
32
  subtitle: { fontSize: theme.tokens.fontSize.md, color: theme.colors.textLight },
33
33
  }));
@@ -29,14 +29,14 @@ const useStyles = createStyles((theme) => ({
29
29
  cardVertical: { width: '31%', padding: theme.tokens.spacing.sm, gap: 5, minHeight: 100 },
30
30
  cardSelected: {
31
31
  borderColor: theme.colors.primary,
32
- backgroundColor: theme.colors.primary + '15',
32
+ backgroundColor: theme.colors.primarySoft,
33
33
  borderWidth: 1.5,
34
34
  elevation: 0,
35
35
  shadowOpacity: 0,
36
36
  },
37
37
  label: { color: theme.colors.textLight, fontWeight: theme.tokens.fontWeight.medium },
38
38
  labelVertical: { fontSize: theme.tokens.fontSize.xs, textAlign: 'center' },
39
- labelSelected: { color: theme.colors.primary },
39
+ labelSelected: { color: theme.colors.primaryText },
40
40
  }));
41
41
 
42
42
  /**
@@ -82,8 +82,8 @@ export const Steps = memo(
82
82
  const isVertical = orientation === 'vertical';
83
83
 
84
84
  const colorsFor = (status: StepStatus) => {
85
- if (status === 'completed') return { bg: colors.primary, border: colors.primary, fg: colors.white };
86
- if (status === 'current') return { bg: 'transparent', border: colors.primary, fg: colors.primary };
85
+ if (status === 'completed') return { bg: colors.primary, border: colors.primary, fg: colors.onPrimary };
86
+ if (status === 'current') return { bg: 'transparent', border: colors.primary, fg: colors.primaryText };
87
87
  return { bg: 'transparent', border: colors.border, fg: colors.textLight };
88
88
  };
89
89
 
@@ -26,7 +26,7 @@ const useStyles = createStyles((theme) => ({
26
26
  },
27
27
  segmentActive: { backgroundColor: theme.colors.surface, ...theme.tokens.shadow.sm },
28
28
  label: { fontSize: theme.tokens.fontSize.sm, fontWeight: theme.tokens.fontWeight.medium, color: theme.colors.textLight },
29
- labelActive: { color: theme.colors.primary, fontWeight: theme.tokens.fontWeight.semibold },
29
+ labelActive: { color: theme.colors.primaryText, fontWeight: theme.tokens.fontWeight.semibold },
30
30
  }));
31
31
 
32
32
  /**
@@ -91,11 +91,12 @@ export const Alert = memo((props: AlertProps) => {
91
91
  if (!visible) return null;
92
92
 
93
93
  const iconColor: Record<AlertType, string> = {
94
- success: colors.success,
95
- error: colors.error,
96
- warning: colors.warning,
97
- question: colors.primary,
98
- info: colors.secondary,
94
+ // Íconos sobre la superficie del diálogo: rol `*Text`, no el acento pleno.
95
+ success: colors.successText,
96
+ error: colors.errorText,
97
+ warning: colors.warningText,
98
+ question: colors.primaryText,
99
+ info: colors.secondaryText,
99
100
  };
100
101
 
101
102
  const effectiveButtons = resolveButtons(props, { confirm: labels.confirm, cancel: labels.cancel, close: labels.close });
@@ -30,7 +30,7 @@ function defaultFormat(date: Date, monthNames: string[]): string {
30
30
  const useStyles = createStyles((theme) => ({
31
31
  container: { marginBottom: theme.tokens.spacing.lg, width: '100%', gap: theme.tokens.spacing.sm },
32
32
  label: { fontSize: theme.tokens.fontSize.md, fontWeight: theme.tokens.fontWeight.medium, color: theme.colors.text },
33
- required: { color: theme.colors.error },
33
+ required: { color: theme.colors.errorText },
34
34
  field: {
35
35
  flexDirection: 'row',
36
36
  alignItems: 'center',
@@ -45,7 +45,7 @@ const useStyles = createStyles((theme) => ({
45
45
  fieldError: { borderColor: theme.colors.error },
46
46
  fieldText: { fontSize: theme.tokens.fontSize.lg, color: theme.colors.text },
47
47
  placeholder: { color: theme.colors.textLight },
48
- errorText: { fontSize: theme.tokens.fontSize.sm, color: theme.colors.error },
48
+ errorText: { fontSize: theme.tokens.fontSize.sm, color: theme.colors.errorText },
49
49
  }));
50
50
 
51
51
  /**
@@ -110,13 +110,13 @@ const useStyles = createStyles((theme) => ({
110
110
  dayButton: { width: 36, height: 36, borderRadius: 18, alignItems: 'center', justifyContent: 'center' },
111
111
  dayText: { fontSize: theme.tokens.fontSize.md, color: theme.colors.text },
112
112
  daySelected: { backgroundColor: theme.colors.primary },
113
- dayTextSelected: { color: theme.colors.white, fontWeight: theme.tokens.fontWeight.bold },
113
+ dayTextSelected: { color: theme.colors.onPrimary, fontWeight: theme.tokens.fontWeight.bold },
114
114
  // El resaltado del tramo intermedio va en la celda (cuadrada, sin gaps) y
115
115
  // no en el botón redondo, así la barra del rango se ve continua.
116
- cellInRange: { backgroundColor: theme.colors.primary + '1f' },
116
+ cellInRange: { backgroundColor: theme.colors.primarySoft },
117
117
  cellRangeStart: { borderTopLeftRadius: 18, borderBottomLeftRadius: 18 },
118
118
  cellRangeEnd: { borderTopRightRadius: 18, borderBottomRightRadius: 18 },
119
- dayTextInRange: { color: theme.colors.primary, fontWeight: theme.tokens.fontWeight.semibold },
119
+ dayTextInRange: { color: theme.colors.primaryText, fontWeight: theme.tokens.fontWeight.semibold },
120
120
  dayToday: { borderWidth: 1, borderColor: theme.colors.primary },
121
121
  dayTextDisabled: { color: theme.colors.textLight, opacity: 0.4 },
122
122
  }));
@@ -54,7 +54,7 @@ const useStyles = createStyles((theme) => ({
54
54
  left: 0,
55
55
  right: 0,
56
56
  bottom: 0,
57
- backgroundColor: theme.colors.background + 'CC',
57
+ backgroundColor: theme.colors.scrim,
58
58
  zIndex: 10,
59
59
  justifyContent: 'center',
60
60
  alignItems: 'center',
@@ -57,7 +57,7 @@ const useStyles = createStyles((theme) => ({
57
57
  left: 0,
58
58
  right: 0,
59
59
  bottom: 0,
60
- backgroundColor: theme.colors.surface + 'B3',
60
+ backgroundColor: theme.colors.scrim,
61
61
  zIndex: 10,
62
62
  justifyContent: 'center',
63
63
  alignItems: 'center',
@@ -147,7 +147,7 @@ export function SearchList<T>({
147
147
  <IconButton
148
148
  iconName="search"
149
149
  onPress={onScanPress}
150
- color={colors.white}
150
+ color={colors.onPrimary}
151
151
  style={styles.scanButton}
152
152
  accessibilityLabel={scanAccessibilityLabel}
153
153
  />
@@ -25,7 +25,7 @@ const useStyles = createStyles((theme) => ({
25
25
  container: { gap: theme.tokens.spacing.sm },
26
26
  containerOpen: { zIndex: 20, elevation: 20 },
27
27
  label: { fontWeight: theme.tokens.fontWeight.bold, fontSize: theme.tokens.fontSize.md, color: theme.colors.text },
28
- required: { color: theme.colors.error },
28
+ required: { color: theme.colors.errorText },
29
29
  field: {
30
30
  flexDirection: 'row',
31
31
  alignItems: 'center',
@@ -64,10 +64,10 @@ const useStyles = createStyles((theme) => ({
64
64
  borderBottomColor: theme.colors.border,
65
65
  },
66
66
  optionRowLast: { borderBottomWidth: 0 },
67
- optionRowSelected: { backgroundColor: theme.colors.primary + '15' },
67
+ optionRowSelected: { backgroundColor: theme.colors.primarySoft },
68
68
  optionText: { fontSize: theme.tokens.fontSize.lg - 1, color: theme.colors.text },
69
- optionTextSelected: { fontWeight: theme.tokens.fontWeight.semibold, color: theme.colors.primary },
70
- errorText: { fontSize: theme.tokens.fontSize.sm, color: theme.colors.error },
69
+ optionTextSelected: { fontWeight: theme.tokens.fontWeight.semibold, color: theme.colors.primaryText },
70
+ errorText: { fontSize: theme.tokens.fontSize.sm, color: theme.colors.errorText },
71
71
  }));
72
72
 
73
73
  /**
@@ -70,17 +70,25 @@ export const Toast = memo(
70
70
  const styles = useStyles();
71
71
  const allowFontScaling = useAllowFontScaling();
72
72
 
73
- const accentColor: Record<ToastVariant, string> = {
73
+ // La barra lateral es relleno (acento pleno); el ícono es contenido
74
+ // sobre la superficie del toast y necesita el rol `*Text`.
75
+ const barColor: Record<ToastVariant, string> = {
74
76
  success: colors.success,
75
77
  error: colors.error,
76
78
  warning: colors.warning,
77
79
  info: colors.secondary,
78
80
  };
81
+ const iconColor: Record<ToastVariant, string> = {
82
+ success: colors.successText,
83
+ error: colors.errorText,
84
+ warning: colors.warningText,
85
+ info: colors.secondaryText,
86
+ };
79
87
 
80
88
  const body = (
81
89
  <View style={[styles.container, style]} testID={testID} accessibilityRole="alert">
82
- <View style={[styles.accent, { backgroundColor: accentColor[variant] }]} />
83
- <Icon name={ICON_BY_VARIANT[variant]} size={22} color={accentColor[variant]} />
90
+ <View style={[styles.accent, { backgroundColor: barColor[variant] }]} />
91
+ <Icon name={ICON_BY_VARIANT[variant]} size={22} color={iconColor[variant]} />
84
92
  <View style={styles.text}>
85
93
  <Text allowFontScaling={allowFontScaling} style={styles.title}>
86
94
  {title}
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ export { SafeAreaUIProvider } from './SafeAreaUIProvider';
2
+ export type { SafeAreaUIProviderProps } from './SafeAreaUIProvider';
@@ -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
 
@@ -2,15 +2,49 @@
2
2
  * Cada color es `string`, no un literal (`as const`): light y dark tienen
3
3
  * valores distintos para la misma clave, y createTheme() debe poder
4
4
  * aceptar cualquier hex/rgba en los overrides — literal typing lo impediría.
5
+ *
6
+ * Cada acento viene en cuatro roles, y no son intercambiables:
7
+ *
8
+ * `primary` relleno sólido (fondo de botón, día seleccionado, FAB).
9
+ * `onPrimary` texto/ícono *encima* de ese relleno.
10
+ * `primarySoft` fondo teñido para estados seleccionados y pills.
11
+ * `primaryText` texto/ícono del acento sobre `surface`, `background`
12
+ * o sobre su propio `primarySoft`.
13
+ *
14
+ * El motivo es el contraste: un acento que se lee bien como relleno no se
15
+ * lee como texto. Antes los componentes usaban `colors.primary` para las dos
16
+ * cosas y lo teñían concatenando alpha al hex (`colors.primary + '15'`), lo
17
+ * que en dark daba 2.6:1 sobre la superficie — muy por debajo del 4.5:1 de
18
+ * WCAG AA. Todos los pares de esta paleta pasan AA en ambos esquemas.
5
19
  */
6
20
  export interface ThemeColors {
7
21
  primary: string;
22
+ onPrimary: string;
23
+ primarySoft: string;
24
+ primaryText: string;
25
+
8
26
  secondary: string;
9
- white: string;
10
- black: string;
11
- error: string;
27
+ onSecondary: string;
28
+ secondarySoft: string;
29
+ secondaryText: string;
30
+
12
31
  success: string;
32
+ onSuccess: string;
33
+ successSoft: string;
34
+ successText: string;
35
+
36
+ error: string;
37
+ onError: string;
38
+ errorSoft: string;
39
+ errorText: string;
40
+
13
41
  warning: string;
42
+ onWarning: string;
43
+ warningSoft: string;
44
+ warningText: string;
45
+
46
+ white: string;
47
+ black: string;
14
48
  background: string;
15
49
  surface: string;
16
50
  text: string;
@@ -18,20 +52,45 @@ export interface ThemeColors {
18
52
  border: string;
19
53
  inputBackground: string;
20
54
  overlay: string;
55
+ /** Velo semitransparente sobre contenido propio (footer de carga de List). */
56
+ scrim: string;
57
+ /** Fondo de placeholder mientras carga una imagen. */
58
+ skeleton: string;
21
59
  }
22
60
 
23
- const shared = {
24
- primary: '#004cef',
25
- secondary: '#00cae1',
61
+ const neutral = {
26
62
  white: '#ffffff',
27
63
  black: '#000000',
28
- error: '#ff3b30',
29
- success: '#34c759',
30
- warning: '#f59e0b',
31
64
  };
32
65
 
33
66
  export const lightPalette: ThemeColors = {
34
- ...shared,
67
+ ...neutral,
68
+
69
+ primary: '#004cef',
70
+ onPrimary: '#ffffff',
71
+ primarySoft: '#e6edfe',
72
+ primaryText: '#00369f',
73
+
74
+ secondary: '#00cae1',
75
+ onSecondary: '#00363d',
76
+ secondarySoft: '#dcf6f9',
77
+ secondaryText: '#00707d',
78
+
79
+ success: '#34c759',
80
+ onSuccess: '#00320f',
81
+ successSoft: '#e3f6e9',
82
+ successText: '#116a30',
83
+
84
+ error: '#ff3b30',
85
+ onError: '#3d0300',
86
+ errorSoft: '#fdeaea',
87
+ errorText: '#c62222',
88
+
89
+ warning: '#f59e0b',
90
+ onWarning: '#3d2600',
91
+ warningSoft: '#fdf1de',
92
+ warningText: '#8a4b00',
93
+
35
94
  background: '#f8f9fa',
36
95
  surface: '#ffffff',
37
96
  text: '#333333',
@@ -39,10 +98,43 @@ export const lightPalette: ThemeColors = {
39
98
  border: '#e5e7eb',
40
99
  inputBackground: '#f5f7fa',
41
100
  overlay: 'rgba(0,0,0,0.5)',
101
+ scrim: 'rgba(248,249,250,0.8)',
102
+ skeleton: '#eceef1',
42
103
  };
43
104
 
105
+ /**
106
+ * Los acentos de dark NO son los de light: sobre `#1e1e1e` hay que subir la
107
+ * luminosidad del acento y bajar la del texto que va encima (por eso
108
+ * `onPrimary` es azul oscuro acá y blanco en light).
109
+ */
44
110
  export const darkPalette: ThemeColors = {
45
- ...shared,
111
+ ...neutral,
112
+
113
+ primary: '#7ba4ff',
114
+ onPrimary: '#00205c',
115
+ primarySoft: '#1b2740',
116
+ primaryText: '#a9c3ff',
117
+
118
+ secondary: '#4dd8e8',
119
+ onSecondary: '#00363d',
120
+ secondarySoft: '#12333a',
121
+ secondaryText: '#7fe6f2',
122
+
123
+ success: '#5ddb84',
124
+ onSuccess: '#003514',
125
+ successSoft: '#13301e',
126
+ successText: '#7fe6a1',
127
+
128
+ error: '#ff7b72',
129
+ onError: '#4d0500',
130
+ errorSoft: '#3a1c1a',
131
+ errorText: '#ffa9a3',
132
+
133
+ warning: '#f0b458',
134
+ onWarning: '#3d2600',
135
+ warningSoft: '#33260f',
136
+ warningText: '#f5c97e',
137
+
46
138
  background: '#121212',
47
139
  surface: '#1e1e1e',
48
140
  text: '#ecedee',
@@ -50,4 +142,6 @@ export const darkPalette: ThemeColors = {
50
142
  border: '#2a2a2e',
51
143
  inputBackground: '#232326',
52
144
  overlay: 'rgba(0,0,0,0.6)',
145
+ scrim: 'rgba(18,18,18,0.8)',
146
+ skeleton: '#26262a',
53
147
  };