@ankhorage/surface 0.1.5 → 0.1.6

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 (291) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/components/badge/Badge.js.map +1 -1
  3. package/dist/components/badge/index.js.map +1 -1
  4. package/dist/components/badge/types.js.map +1 -1
  5. package/dist/components/button/Button.js.map +1 -1
  6. package/dist/components/button/index.js.map +1 -1
  7. package/dist/components/button/types.js.map +1 -1
  8. package/dist/components/card/Card.js.map +1 -1
  9. package/dist/components/card/index.js.map +1 -1
  10. package/dist/components/card/types.js.map +1 -1
  11. package/dist/components/checkbox/Checkbox.js.map +1 -1
  12. package/dist/components/checkbox/index.js.map +1 -1
  13. package/dist/components/checkbox/types.js.map +1 -1
  14. package/dist/components/drawer/Drawer.js.map +1 -1
  15. package/dist/components/drawer/index.js.map +1 -1
  16. package/dist/components/drawer/types.js.map +1 -1
  17. package/dist/components/field/Field.js.map +1 -1
  18. package/dist/components/field/index.js.map +1 -1
  19. package/dist/components/field/types.js.map +1 -1
  20. package/dist/components/helper-text/HelperText.js.map +1 -1
  21. package/dist/components/helper-text/index.js.map +1 -1
  22. package/dist/components/helper-text/types.js.map +1 -1
  23. package/dist/components/icon-button/IconButton.js.map +1 -1
  24. package/dist/components/icon-button/index.js.map +1 -1
  25. package/dist/components/icon-button/types.js.map +1 -1
  26. package/dist/components/label/Label.js.map +1 -1
  27. package/dist/components/label/index.js.map +1 -1
  28. package/dist/components/label/types.js.map +1 -1
  29. package/dist/components/list-item/ListItem.js.map +1 -1
  30. package/dist/components/list-item/index.js.map +1 -1
  31. package/dist/components/list-item/types.js.map +1 -1
  32. package/dist/components/menu/Menu.js.map +1 -1
  33. package/dist/components/menu/index.js.map +1 -1
  34. package/dist/components/menu/navigation.js.map +1 -1
  35. package/dist/components/menu/types.js.map +1 -1
  36. package/dist/components/modal/Modal.js.map +1 -1
  37. package/dist/components/modal/index.js.map +1 -1
  38. package/dist/components/modal/types.js.map +1 -1
  39. package/dist/components/radio/Radio.js.map +1 -1
  40. package/dist/components/radio/index.js.map +1 -1
  41. package/dist/components/radio/types.js.map +1 -1
  42. package/dist/components/switch/Switch.js.map +1 -1
  43. package/dist/components/switch/index.js.map +1 -1
  44. package/dist/components/switch/types.js.map +1 -1
  45. package/dist/components/tabs/Tab.js.map +1 -1
  46. package/dist/components/tabs/TabList.js.map +1 -1
  47. package/dist/components/tabs/TabPanel.js.map +1 -1
  48. package/dist/components/tabs/Tabs.js.map +1 -1
  49. package/dist/components/tabs/a11y.js.map +1 -1
  50. package/dist/components/tabs/context.js.map +1 -1
  51. package/dist/components/tabs/index.js.map +1 -1
  52. package/dist/components/tabs/navigation.js.map +1 -1
  53. package/dist/components/tabs/types.js.map +1 -1
  54. package/dist/components/text-input/TextInput.js.map +1 -1
  55. package/dist/components/text-input/index.js.map +1 -1
  56. package/dist/components/text-input/types.js.map +1 -1
  57. package/dist/components/textarea/Textarea.js.map +1 -1
  58. package/dist/components/textarea/index.js.map +1 -1
  59. package/dist/components/textarea/types.js.map +1 -1
  60. package/dist/components/toast/Toast.js.map +1 -1
  61. package/dist/components/toast/ToastProvider.js.map +1 -1
  62. package/dist/components/toast/index.js.map +1 -1
  63. package/dist/components/toast/types.js.map +1 -1
  64. package/dist/components/tooltip/Tooltip.js.map +1 -1
  65. package/dist/components/tooltip/index.js.map +1 -1
  66. package/dist/components/tooltip/types.js.map +1 -1
  67. package/dist/context/FontContext.js.map +1 -1
  68. package/dist/context/TranslationContext.js.map +1 -1
  69. package/dist/core/responsive/ResponsiveProvider.js.map +1 -1
  70. package/dist/core/responsive/breakpoints.js.map +1 -1
  71. package/dist/core/responsive/getBreakpointFromWidth.js.map +1 -1
  72. package/dist/core/responsive/index.js.map +1 -1
  73. package/dist/core/responsive/resolve.js.map +1 -1
  74. package/dist/core/responsive/types.js.map +1 -1
  75. package/dist/core/responsive/useBreakpoint.js.map +1 -1
  76. package/dist/examples/DocsExamples.js.map +1 -1
  77. package/dist/index.js.map +1 -1
  78. package/dist/internal/focus/FocusScope.js.map +1 -1
  79. package/dist/internal/focus/useFocusManager.js.map +1 -1
  80. package/dist/internal/overlay/OverlayProvider.js.map +1 -1
  81. package/dist/internal/overlay/Portal.js.map +1 -1
  82. package/dist/internal/overlay/useOverlayStack.js.map +1 -1
  83. package/dist/internal/resolvers/index.js.map +1 -1
  84. package/dist/internal/resolvers/resolveControlSize.js.map +1 -1
  85. package/dist/internal/resolvers/resolveFieldPresentation.js.map +1 -1
  86. package/dist/internal/resolvers/resolveFieldState.js.map +1 -1
  87. package/dist/internal/resolvers/resolveFocusRingStyles.js.map +1 -1
  88. package/dist/internal/resolvers/resolveIconSize.js.map +1 -1
  89. package/dist/internal/resolvers/resolveIndicatorSize.js.map +1 -1
  90. package/dist/internal/resolvers/resolveInteractiveColors.js.map +1 -1
  91. package/dist/internal/resolvers/resolveInteractiveState.js.map +1 -1
  92. package/dist/internal/resolvers/resolveOverlayAnimation.js.map +1 -1
  93. package/dist/internal/resolvers/resolveOverlayZIndex.js.map +1 -1
  94. package/dist/internal/resolvers/resolveSelectionControlBehavior.js.map +1 -1
  95. package/dist/internal/resolvers/resolveSelectionControlColors.js.map +1 -1
  96. package/dist/internal/resolvers/resolveTextColor.js.map +1 -1
  97. package/dist/internal/resolvers/resolveTextStyles.js.map +1 -1
  98. package/dist/internal/resolvers/resolveTone.js.map +1 -1
  99. package/dist/internal/useControllableState.js.map +1 -1
  100. package/dist/layout/Box.js.map +1 -1
  101. package/dist/layout/Center.js.map +1 -1
  102. package/dist/layout/Container.js.map +1 -1
  103. package/dist/layout/Divider.js.map +1 -1
  104. package/dist/layout/Grid.js.map +1 -1
  105. package/dist/layout/Inline.js.map +1 -1
  106. package/dist/layout/Show.js.map +1 -1
  107. package/dist/layout/Spacer.js.map +1 -1
  108. package/dist/layout/Stack.js.map +1 -1
  109. package/dist/layout/Surface.js.map +1 -1
  110. package/dist/layout/Template.js.map +1 -1
  111. package/dist/layout/helpers.js.map +1 -1
  112. package/dist/layout/index.js.map +1 -1
  113. package/dist/primitives/button-base/ButtonBase.js.map +1 -1
  114. package/dist/primitives/button-base/index.js.map +1 -1
  115. package/dist/primitives/button-base/types.js.map +1 -1
  116. package/dist/primitives/heading/Heading.js.map +1 -1
  117. package/dist/primitives/heading/index.js.map +1 -1
  118. package/dist/primitives/heading/resolveHeadingStyle.js.map +1 -1
  119. package/dist/primitives/heading/types.js.map +1 -1
  120. package/dist/primitives/icon/Icon.js.map +1 -1
  121. package/dist/primitives/icon/index.js.map +1 -1
  122. package/dist/primitives/icon/resolveExpoIconComponent.js.map +1 -1
  123. package/dist/primitives/text/Text.js.map +1 -1
  124. package/dist/primitives/text/index.js.map +1 -1
  125. package/dist/primitives/text/types.js.map +1 -1
  126. package/dist/theme/ThemeContext.js.map +1 -1
  127. package/dist/theme/colorEngine.js.map +1 -1
  128. package/dist/theme/createTheme.js.map +1 -1
  129. package/dist/theme/index.js.map +1 -1
  130. package/dist/theme/resolveToken.js.map +1 -1
  131. package/dist/theme/types.js.map +1 -1
  132. package/dist/utils/deepEqual.js.map +1 -1
  133. package/dist/utils/deepMerge.js.map +1 -1
  134. package/package.json +4 -1
  135. package/src/components/badge/Badge.tsx +47 -0
  136. package/src/components/badge/index.ts +2 -0
  137. package/src/components/badge/types.ts +13 -0
  138. package/src/components/button/Button.tsx +104 -0
  139. package/src/components/button/index.ts +2 -0
  140. package/src/components/button/types.ts +26 -0
  141. package/src/components/card/Card.tsx +81 -0
  142. package/src/components/card/index.ts +2 -0
  143. package/src/components/card/types.ts +11 -0
  144. package/src/components/checkbox/Checkbox.tsx +111 -0
  145. package/src/components/checkbox/index.ts +2 -0
  146. package/src/components/checkbox/types.ts +19 -0
  147. package/src/components/drawer/Drawer.tsx +92 -0
  148. package/src/components/drawer/index.ts +2 -0
  149. package/src/components/drawer/types.ts +10 -0
  150. package/src/components/field/Field.tsx +43 -0
  151. package/src/components/field/index.ts +2 -0
  152. package/src/components/field/types.ts +13 -0
  153. package/src/components/helper-text/HelperText.tsx +12 -0
  154. package/src/components/helper-text/index.ts +2 -0
  155. package/src/components/helper-text/types.ts +9 -0
  156. package/src/components/icon-button/IconButton.tsx +60 -0
  157. package/src/components/icon-button/index.ts +2 -0
  158. package/src/components/icon-button/types.ts +19 -0
  159. package/src/components/label/Label.tsx +17 -0
  160. package/src/components/label/index.ts +2 -0
  161. package/src/components/label/types.ts +10 -0
  162. package/src/components/list-item/ListItem.tsx +72 -0
  163. package/src/components/list-item/index.ts +2 -0
  164. package/src/components/list-item/types.ts +11 -0
  165. package/src/components/menu/Menu.tsx +180 -0
  166. package/src/components/menu/index.ts +2 -0
  167. package/src/components/menu/navigation.test.ts +21 -0
  168. package/src/components/menu/navigation.ts +34 -0
  169. package/src/components/menu/types.ts +16 -0
  170. package/src/components/modal/Modal.tsx +87 -0
  171. package/src/components/modal/index.ts +2 -0
  172. package/src/components/modal/types.ts +9 -0
  173. package/src/components/radio/Radio.tsx +116 -0
  174. package/src/components/radio/index.ts +2 -0
  175. package/src/components/radio/types.ts +19 -0
  176. package/src/components/switch/Switch.tsx +116 -0
  177. package/src/components/switch/index.ts +2 -0
  178. package/src/components/switch/types.ts +19 -0
  179. package/src/components/tabs/Tab.tsx +82 -0
  180. package/src/components/tabs/TabList.tsx +51 -0
  181. package/src/components/tabs/TabPanel.tsx +29 -0
  182. package/src/components/tabs/Tabs.tsx +67 -0
  183. package/src/components/tabs/a11y.test.ts +15 -0
  184. package/src/components/tabs/a11y.ts +15 -0
  185. package/src/components/tabs/context.tsx +31 -0
  186. package/src/components/tabs/index.ts +5 -0
  187. package/src/components/tabs/navigation.test.ts +21 -0
  188. package/src/components/tabs/navigation.ts +32 -0
  189. package/src/components/tabs/types.ts +27 -0
  190. package/src/components/text-input/TextInput.tsx +116 -0
  191. package/src/components/text-input/index.ts +2 -0
  192. package/src/components/text-input/types.ts +32 -0
  193. package/src/components/textarea/Textarea.tsx +15 -0
  194. package/src/components/textarea/index.ts +2 -0
  195. package/src/components/textarea/types.ts +5 -0
  196. package/src/components/toast/Toast.tsx +54 -0
  197. package/src/components/toast/ToastProvider.tsx +114 -0
  198. package/src/components/toast/index.ts +3 -0
  199. package/src/components/toast/types.ts +16 -0
  200. package/src/components/tooltip/Tooltip.tsx +109 -0
  201. package/src/components/tooltip/index.ts +2 -0
  202. package/src/components/tooltip/types.ts +9 -0
  203. package/src/context/FontContext.tsx +59 -0
  204. package/src/context/TranslationContext.tsx +54 -0
  205. package/src/core/responsive/ResponsiveProvider.tsx +31 -0
  206. package/src/core/responsive/breakpoints.ts +9 -0
  207. package/src/core/responsive/getBreakpointFromWidth.test.ts +15 -0
  208. package/src/core/responsive/getBreakpointFromWidth.ts +10 -0
  209. package/src/core/responsive/index.ts +6 -0
  210. package/src/core/responsive/resolve.test.ts +25 -0
  211. package/src/core/responsive/resolve.ts +24 -0
  212. package/src/core/responsive/types.ts +10 -0
  213. package/src/core/responsive/useBreakpoint.ts +9 -0
  214. package/src/examples/DocsExamples.tsx +116 -0
  215. package/src/index.test.ts +64 -0
  216. package/src/index.ts +55 -0
  217. package/src/internal/focus/FocusScope.tsx +66 -0
  218. package/src/internal/focus/useFocusManager.test.ts +44 -0
  219. package/src/internal/focus/useFocusManager.ts +142 -0
  220. package/src/internal/overlay/OverlayProvider.tsx +74 -0
  221. package/src/internal/overlay/Portal.tsx +38 -0
  222. package/src/internal/overlay/useOverlayStack.test.ts +31 -0
  223. package/src/internal/overlay/useOverlayStack.ts +61 -0
  224. package/src/internal/resolvers/index.ts +15 -0
  225. package/src/internal/resolvers/resolveControlSize.test.ts +25 -0
  226. package/src/internal/resolvers/resolveControlSize.ts +45 -0
  227. package/src/internal/resolvers/resolveFieldPresentation.test.ts +31 -0
  228. package/src/internal/resolvers/resolveFieldPresentation.ts +30 -0
  229. package/src/internal/resolvers/resolveFieldState.test.ts +22 -0
  230. package/src/internal/resolvers/resolveFieldState.ts +36 -0
  231. package/src/internal/resolvers/resolveFocusRingStyles.ts +14 -0
  232. package/src/internal/resolvers/resolveIconSize.ts +6 -0
  233. package/src/internal/resolvers/resolveIndicatorSize.test.ts +19 -0
  234. package/src/internal/resolvers/resolveIndicatorSize.ts +47 -0
  235. package/src/internal/resolvers/resolveInteractiveColors.test.ts +57 -0
  236. package/src/internal/resolvers/resolveInteractiveColors.ts +134 -0
  237. package/src/internal/resolvers/resolveInteractiveState.test.ts +14 -0
  238. package/src/internal/resolvers/resolveInteractiveState.ts +15 -0
  239. package/src/internal/resolvers/resolveOverlayAnimation.test.ts +15 -0
  240. package/src/internal/resolvers/resolveOverlayAnimation.ts +24 -0
  241. package/src/internal/resolvers/resolveOverlayZIndex.test.ts +15 -0
  242. package/src/internal/resolvers/resolveOverlayZIndex.ts +13 -0
  243. package/src/internal/resolvers/resolveSelectionControlBehavior.test.ts +52 -0
  244. package/src/internal/resolvers/resolveSelectionControlBehavior.ts +23 -0
  245. package/src/internal/resolvers/resolveSelectionControlColors.test.ts +44 -0
  246. package/src/internal/resolvers/resolveSelectionControlColors.ts +81 -0
  247. package/src/internal/resolvers/resolveTextColor.test.ts +23 -0
  248. package/src/internal/resolvers/resolveTextColor.ts +40 -0
  249. package/src/internal/resolvers/resolveTextStyles.test.ts +27 -0
  250. package/src/internal/resolvers/resolveTextStyles.ts +95 -0
  251. package/src/internal/resolvers/resolveTone.ts +19 -0
  252. package/src/internal/useControllableState.ts +28 -0
  253. package/src/layout/Box.tsx +79 -0
  254. package/src/layout/Center.tsx +22 -0
  255. package/src/layout/Container.tsx +43 -0
  256. package/src/layout/Divider.tsx +26 -0
  257. package/src/layout/Grid.tsx +83 -0
  258. package/src/layout/Inline.tsx +9 -0
  259. package/src/layout/Show.tsx +15 -0
  260. package/src/layout/Spacer.tsx +22 -0
  261. package/src/layout/Stack.tsx +67 -0
  262. package/src/layout/Surface.tsx +70 -0
  263. package/src/layout/Template.tsx +85 -0
  264. package/src/layout/helpers.test.ts +71 -0
  265. package/src/layout/helpers.ts +208 -0
  266. package/src/layout/index.ts +22 -0
  267. package/src/primitives/button-base/ButtonBase.tsx +81 -0
  268. package/src/primitives/button-base/index.ts +2 -0
  269. package/src/primitives/button-base/types.ts +16 -0
  270. package/src/primitives/heading/Heading.tsx +60 -0
  271. package/src/primitives/heading/index.ts +2 -0
  272. package/src/primitives/heading/resolveHeadingStyle.test.ts +31 -0
  273. package/src/primitives/heading/resolveHeadingStyle.ts +17 -0
  274. package/src/primitives/heading/types.ts +13 -0
  275. package/src/primitives/icon/Icon.tsx +40 -0
  276. package/src/primitives/icon/index.ts +2 -0
  277. package/src/primitives/icon/resolveExpoIconComponent.test.ts +29 -0
  278. package/src/primitives/icon/resolveExpoIconComponent.ts +20 -0
  279. package/src/primitives/text/Text.tsx +66 -0
  280. package/src/primitives/text/index.ts +2 -0
  281. package/src/primitives/text/types.ts +18 -0
  282. package/src/theme/ThemeContext.tsx +95 -0
  283. package/src/theme/colorEngine.test.ts +114 -0
  284. package/src/theme/colorEngine.ts +480 -0
  285. package/src/theme/createTheme.ts +121 -0
  286. package/src/theme/index.ts +5 -0
  287. package/src/theme/resolveToken.ts +32 -0
  288. package/src/theme/types.ts +188 -0
  289. package/src/utils/deepEqual.ts +34 -0
  290. package/src/utils/deepMerge.test.ts +117 -0
  291. package/src/utils/deepMerge.ts +29 -0
@@ -0,0 +1,32 @@
1
+ import type React from 'react';
2
+ import type {
3
+ StyleProp,
4
+ TextInputProps as ReactNativeTextInputProps,
5
+ TextStyle,
6
+ } from 'react-native';
7
+
8
+ import type { ControlSize } from '../../internal/resolvers/resolveControlSize';
9
+
10
+ export interface TextInputProps extends Omit<
11
+ ReactNativeTextInputProps,
12
+ | 'defaultValue'
13
+ | 'editable'
14
+ | 'onChangeText'
15
+ | 'placeholderTextColor'
16
+ | 'style'
17
+ | 'testID'
18
+ | 'value'
19
+ > {
20
+ value?: string;
21
+ defaultValue?: string;
22
+ onChangeText?: ((text: string) => void) | undefined;
23
+ placeholder?: string;
24
+ size?: ControlSize;
25
+ disabled?: boolean;
26
+ readOnly?: boolean;
27
+ invalid?: boolean;
28
+ leadingAccessory?: React.ReactNode;
29
+ trailingAccessory?: React.ReactNode;
30
+ style?: StyleProp<TextStyle>;
31
+ testID?: string;
32
+ }
@@ -0,0 +1,15 @@
1
+ import React from 'react';
2
+
3
+ import { TextInput } from '../text-input';
4
+ import type { TextareaProps } from './types';
5
+
6
+ export function Textarea({ rows = 4, numberOfLines, style, ...props }: TextareaProps) {
7
+ return (
8
+ <TextInput
9
+ {...props}
10
+ multiline
11
+ numberOfLines={numberOfLines ?? rows}
12
+ style={[{ textAlignVertical: 'top' }, style]}
13
+ />
14
+ );
15
+ }
@@ -0,0 +1,2 @@
1
+ export { Textarea } from './Textarea';
2
+ export type { TextareaProps } from './types';
@@ -0,0 +1,5 @@
1
+ import type { TextInputProps } from '../text-input';
2
+
3
+ export interface TextareaProps extends Omit<TextInputProps, 'multiline'> {
4
+ rows?: number;
5
+ }
@@ -0,0 +1,54 @@
1
+ import React from 'react';
2
+ import { Pressable } from 'react-native';
3
+
4
+ import { Box, Inline, Surface } from '../../layout';
5
+ import { Text } from '../../primitives/text';
6
+ import { useTheme } from '../../theme/ThemeContext';
7
+ import type { ToastProps } from './types';
8
+
9
+ export function Toast({ title, description, tone = 'default', onDismiss, testID }: ToastProps) {
10
+ const { theme } = useTheme();
11
+ const toneColor =
12
+ tone === 'success'
13
+ ? theme.semantics.success.base
14
+ : tone === 'danger'
15
+ ? theme.semantics.danger.base
16
+ : theme.semantics.action.primary.base;
17
+
18
+ return (
19
+ <Surface
20
+ p="m"
21
+ style={{
22
+ borderLeftColor: toneColor,
23
+ borderLeftWidth: 3,
24
+ minWidth: 280,
25
+ shadowOpacity: 0.14,
26
+ shadowRadius: 12,
27
+ shadowOffset: { width: 0, height: 6 },
28
+ }}
29
+ testID={testID}
30
+ variant="raised"
31
+ >
32
+ <Inline align="center" justify="space-between">
33
+ <Box flex={1}>
34
+ {title ? (
35
+ <Text variant="label" weight="medium">
36
+ {title}
37
+ </Text>
38
+ ) : null}
39
+ {description ? <Text tone="muted">{description}</Text> : null}
40
+ </Box>
41
+ {onDismiss ? (
42
+ <Pressable
43
+ accessibilityLabel="Dismiss notification"
44
+ accessibilityRole="button"
45
+ onPress={onDismiss}
46
+ testID={testID ? `${testID}-dismiss` : undefined}
47
+ >
48
+ <Text color={toneColor}>×</Text>
49
+ </Pressable>
50
+ ) : null}
51
+ </Inline>
52
+ </Surface>
53
+ );
54
+ }
@@ -0,0 +1,114 @@
1
+ import React from 'react';
2
+
3
+ import { Portal } from '../../internal/overlay/Portal';
4
+ import { resolveOverlayAnimation } from '../../internal/resolvers';
5
+ import { Stack } from '../../layout';
6
+ import { Toast } from './Toast';
7
+ import type { ToastOptions } from './types';
8
+
9
+ interface ToastEntry extends ToastOptions {
10
+ id: string;
11
+ }
12
+
13
+ interface ToastContextValue {
14
+ dismissToast: (id: string) => void;
15
+ showToast: (options: ToastOptions) => string;
16
+ }
17
+
18
+ const ToastContext = React.createContext<ToastContextValue | null>(null);
19
+
20
+ let toastCounter = 0;
21
+
22
+ export function ToastProvider({
23
+ children,
24
+ defaultDuration = 4000,
25
+ }: {
26
+ children: React.ReactNode;
27
+ defaultDuration?: number;
28
+ }) {
29
+ const [toasts, setToasts] = React.useState<ToastEntry[]>([]);
30
+ const animation = resolveOverlayAnimation('toast');
31
+ const timersRef = React.useRef(new Map<string, ReturnType<typeof setTimeout>>());
32
+
33
+ const dismissToast = React.useCallback((id: string) => {
34
+ const timer = timersRef.current.get(id);
35
+ if (timer) {
36
+ clearTimeout(timer);
37
+ timersRef.current.delete(id);
38
+ }
39
+ setToasts((current) => current.filter((toast) => toast.id !== id));
40
+ }, []);
41
+
42
+ const showToast = React.useCallback((options: ToastOptions) => {
43
+ const id = options.id ?? `toast-${toastCounter++}`;
44
+ setToasts((current) => [...current, { ...options, id }]);
45
+ return id;
46
+ }, []);
47
+
48
+ React.useEffect(() => {
49
+ toasts.forEach((toast) => {
50
+ if (timersRef.current.has(toast.id)) {
51
+ return;
52
+ }
53
+
54
+ const timer = setTimeout(() => {
55
+ dismissToast(toast.id);
56
+ }, toast.duration ?? defaultDuration);
57
+
58
+ timersRef.current.set(toast.id, timer);
59
+ });
60
+
61
+ const activeToastIds = new Set(toasts.map((toast) => toast.id));
62
+ timersRef.current.forEach((timer, id) => {
63
+ if (!activeToastIds.has(id)) {
64
+ clearTimeout(timer);
65
+ timersRef.current.delete(id);
66
+ }
67
+ });
68
+ }, [defaultDuration, dismissToast, toasts]);
69
+
70
+ React.useEffect(() => {
71
+ return () => {
72
+ timersRef.current.forEach((timer) => clearTimeout(timer));
73
+ timersRef.current.clear();
74
+ };
75
+ }, []);
76
+
77
+ return (
78
+ <ToastContext.Provider value={{ dismissToast, showToast }}>
79
+ {children}
80
+ <Portal layer="toast" visible={toasts.length > 0}>
81
+ <Stack
82
+ gap="s"
83
+ pointerEvents="box-none"
84
+ style={{
85
+ alignItems: 'flex-end',
86
+ padding: 16,
87
+ paddingTop: 16 + animation.offset,
88
+ }}
89
+ >
90
+ {toasts.map((toast) => (
91
+ <Toast
92
+ description={toast.description}
93
+ key={toast.id}
94
+ onDismiss={() => dismissToast(toast.id)}
95
+ testID={toast.testID}
96
+ title={toast.title}
97
+ tone={toast.tone}
98
+ />
99
+ ))}
100
+ </Stack>
101
+ </Portal>
102
+ </ToastContext.Provider>
103
+ );
104
+ }
105
+
106
+ export function useToast() {
107
+ const context = React.useContext(ToastContext);
108
+
109
+ if (!context) {
110
+ throw new Error('useToast must be used within <ToastProvider>.');
111
+ }
112
+
113
+ return context;
114
+ }
@@ -0,0 +1,3 @@
1
+ export { Toast } from './Toast';
2
+ export { ToastProvider, useToast } from './ToastProvider';
3
+ export type { ToastOptions, ToastProps, ToastTone } from './types';
@@ -0,0 +1,16 @@
1
+ import type React from 'react';
2
+
3
+ export type ToastTone = 'default' | 'success' | 'danger';
4
+
5
+ export interface ToastProps {
6
+ title?: React.ReactNode;
7
+ description?: React.ReactNode;
8
+ tone?: ToastTone;
9
+ onDismiss?: (() => void) | undefined;
10
+ testID?: string;
11
+ }
12
+
13
+ export interface ToastOptions extends Omit<ToastProps, 'onDismiss'> {
14
+ duration?: number;
15
+ id?: string;
16
+ }
@@ -0,0 +1,109 @@
1
+ import React from 'react';
2
+ import { type LayoutRectangle, Platform, Pressable, View } from 'react-native';
3
+
4
+ import { Portal } from '../../internal/overlay/Portal';
5
+ import { resolveOverlayAnimation } from '../../internal/resolvers';
6
+ import { Surface } from '../../layout';
7
+ import { Text } from '../../primitives/text';
8
+ import { useTheme } from '../../theme/ThemeContext';
9
+ import type { TooltipProps } from './types';
10
+
11
+ interface MeasurableNode {
12
+ measureInWindow?: (
13
+ callback: (x: number, y: number, width: number, height: number) => void,
14
+ ) => void;
15
+ }
16
+
17
+ function measureNode(node: unknown, callback: (layout: LayoutRectangle) => void) {
18
+ const measurableNode = node as MeasurableNode | null;
19
+ measurableNode?.measureInWindow?.((x, y, width, height) => {
20
+ callback({ height, width, x, y });
21
+ });
22
+ }
23
+
24
+ export function Tooltip({
25
+ children,
26
+ content,
27
+ delay = 150,
28
+ placement = 'top',
29
+ testID,
30
+ }: TooltipProps) {
31
+ const { theme } = useTheme();
32
+ const animation = resolveOverlayAnimation('tooltip');
33
+ const anchorRef = React.useRef<View | null>(null);
34
+ const timeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
35
+ const [visible, setVisible] = React.useState(false);
36
+ const [layout, setLayout] = React.useState<LayoutRectangle | null>(null);
37
+
38
+ const show = React.useCallback(() => {
39
+ if (timeoutRef.current) {
40
+ clearTimeout(timeoutRef.current);
41
+ }
42
+
43
+ timeoutRef.current = setTimeout(() => {
44
+ measureNode(anchorRef.current, setLayout);
45
+ setVisible(true);
46
+ }, delay);
47
+ }, [delay]);
48
+
49
+ const hide = React.useCallback(() => {
50
+ if (timeoutRef.current) {
51
+ clearTimeout(timeoutRef.current);
52
+ timeoutRef.current = null;
53
+ }
54
+ setVisible(false);
55
+ }, []);
56
+
57
+ React.useEffect(
58
+ () => () => {
59
+ if (timeoutRef.current) {
60
+ clearTimeout(timeoutRef.current);
61
+ }
62
+ },
63
+ [],
64
+ );
65
+
66
+ const tooltipLeft = layout ? layout.x : 0;
67
+ const tooltipTop =
68
+ layout && placement === 'top'
69
+ ? layout.y - animation.offset
70
+ : layout
71
+ ? layout.y + layout.height + animation.offset
72
+ : 0;
73
+
74
+ return (
75
+ <View collapsable={false} ref={anchorRef} testID={testID ? `${testID}-anchor` : undefined}>
76
+ <Pressable
77
+ onBlur={hide}
78
+ onFocus={show}
79
+ onHoverIn={Platform.OS === 'web' ? show : undefined}
80
+ onHoverOut={Platform.OS === 'web' ? hide : undefined}
81
+ >
82
+ {children}
83
+ </Pressable>
84
+ <Portal layer="tooltip" visible={visible && Boolean(layout)}>
85
+ <View
86
+ pointerEvents="none"
87
+ style={{
88
+ left: tooltipLeft,
89
+ position: 'absolute',
90
+ top: tooltipTop,
91
+ }}
92
+ >
93
+ <Surface
94
+ p="s"
95
+ style={{
96
+ backgroundColor: theme.semantics.neutral.text,
97
+ }}
98
+ testID={testID}
99
+ variant="raised"
100
+ >
101
+ <Text color={theme.semantics.content.inverse} variant="caption">
102
+ {content}
103
+ </Text>
104
+ </Surface>
105
+ </View>
106
+ </Portal>
107
+ </View>
108
+ );
109
+ }
@@ -0,0 +1,2 @@
1
+ export { Tooltip } from './Tooltip';
2
+ export type { TooltipProps } from './types';
@@ -0,0 +1,9 @@
1
+ import type React from 'react';
2
+
3
+ export interface TooltipProps {
4
+ children?: React.ReactNode;
5
+ content?: React.ReactNode;
6
+ delay?: number;
7
+ placement?: 'top' | 'bottom';
8
+ testID?: string;
9
+ }
@@ -0,0 +1,59 @@
1
+ import React, { createContext, useContext, useMemo, useState } from 'react';
2
+
3
+ /**
4
+ * Runtime font state consumed by theme and text primitives.
5
+ */
6
+ export interface FontRuntime {
7
+ /** true when the active font assets have finished loading */
8
+ fontsLoaded: boolean;
9
+
10
+ /** The currently active font family id */
11
+ activeFontId: string | null;
12
+
13
+ /** Update the active font family id */
14
+ setActiveFontId: (id: string) => void;
15
+ }
16
+
17
+ const fallbackRuntime: FontRuntime = {
18
+ fontsLoaded: true,
19
+ activeFontId: null,
20
+ setActiveFontId: () => {
21
+ /* fallback */
22
+ },
23
+ };
24
+
25
+ const FontContext = createContext(fallbackRuntime);
26
+
27
+ export function FontProvider(props: {
28
+ fontsLoaded: boolean;
29
+ activeFontId?: string | null;
30
+ children: React.ReactNode;
31
+ onActiveFontChange?: (id: string) => void;
32
+ }) {
33
+ const {
34
+ fontsLoaded,
35
+ activeFontId: initialActiveFontId = null,
36
+ children,
37
+ onActiveFontChange,
38
+ } = props;
39
+
40
+ const [activeFontId, setActiveFontIdState] = useState(initialActiveFontId);
41
+
42
+ const value = useMemo<FontRuntime>(
43
+ () => ({
44
+ fontsLoaded,
45
+ activeFontId,
46
+ setActiveFontId: (id: string) => {
47
+ setActiveFontIdState(id);
48
+ if (onActiveFontChange) onActiveFontChange(id);
49
+ },
50
+ }),
51
+ [fontsLoaded, activeFontId, onActiveFontChange],
52
+ );
53
+
54
+ return <FontContext.Provider value={value}>{children}</FontContext.Provider>;
55
+ }
56
+
57
+ export function useFontContext(): FontRuntime {
58
+ return useContext(FontContext);
59
+ }
@@ -0,0 +1,54 @@
1
+ import React, { createContext, useContext, useMemo } from 'react';
2
+
3
+ /**
4
+ * Minimal translation runtime surface.
5
+ * Do not import i18next types here; keep Surface runtime-agnostic.
6
+ */
7
+ export interface I18nInstance {
8
+ changeLanguage: (lng: string) => Promise<unknown>;
9
+ language?: string;
10
+ t?: Translator;
11
+ }
12
+
13
+ export type Translator = (key: string, options?: Record<string, unknown>) => string;
14
+
15
+ export interface TranslationRuntime {
16
+ t: Translator;
17
+ i18n: I18nInstance | null;
18
+ }
19
+
20
+ const fallbackRuntime: TranslationRuntime = {
21
+ t: (key) => key,
22
+ i18n: null,
23
+ };
24
+
25
+ const TranslationContext = createContext(fallbackRuntime);
26
+
27
+ export function TranslationProvider(props: {
28
+ t: Translator;
29
+ i18n?: I18nInstance | null;
30
+ children: React.ReactNode;
31
+ }) {
32
+ const { t, i18n, children } = props;
33
+
34
+ const value = useMemo<TranslationRuntime>(
35
+ () => ({
36
+ t: (key: string, options?: Record<string, unknown>) => {
37
+ if (i18n?.t) {
38
+ const result = i18n.t(key, options);
39
+ // If translation returns the key, it's missing in the current dictionary
40
+ if (result !== key) return result;
41
+ }
42
+ return t(key, options);
43
+ },
44
+ i18n: i18n ?? null,
45
+ }),
46
+ [t, i18n, i18n?.language],
47
+ );
48
+
49
+ return <TranslationContext.Provider value={value}>{children}</TranslationContext.Provider>;
50
+ }
51
+
52
+ export function useTranslationContext(): TranslationRuntime {
53
+ return useContext(TranslationContext);
54
+ }
@@ -0,0 +1,31 @@
1
+ import React, { createContext, useContext, useMemo } from 'react';
2
+ import { useWindowDimensions } from 'react-native';
3
+
4
+ import { getBreakpointFromWidth } from './getBreakpointFromWidth';
5
+ import type { ResponsiveRuntime } from './types';
6
+
7
+ const ResponsiveContext = createContext<ResponsiveRuntime | null>(null);
8
+
9
+ export function ResponsiveProvider({ children }: { children: React.ReactNode }) {
10
+ const { width } = useWindowDimensions();
11
+
12
+ const value = useMemo<ResponsiveRuntime>(
13
+ () => ({
14
+ breakpoint: getBreakpointFromWidth(width),
15
+ width,
16
+ }),
17
+ [width],
18
+ );
19
+
20
+ return <ResponsiveContext.Provider value={value}>{children}</ResponsiveContext.Provider>;
21
+ }
22
+
23
+ export function useResponsiveRuntime(): ResponsiveRuntime {
24
+ const runtime = useContext(ResponsiveContext);
25
+
26
+ if (!runtime) {
27
+ throw new Error('useResponsiveRuntime must be used within a ResponsiveProvider');
28
+ }
29
+
30
+ return runtime;
31
+ }
@@ -0,0 +1,9 @@
1
+ export const BREAKPOINTS = {
2
+ base: 0,
3
+ sm: 480,
4
+ md: 768,
5
+ lg: 1024,
6
+ xl: 1280,
7
+ } as const;
8
+
9
+ export const BREAKPOINT_ORDER = ['base', 'sm', 'md', 'lg', 'xl'] as const;
@@ -0,0 +1,15 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+
3
+ import { getBreakpointFromWidth } from './getBreakpointFromWidth';
4
+
5
+ describe('getBreakpointFromWidth', () => {
6
+ test('resolves expected breakpoints', () => {
7
+ expect(getBreakpointFromWidth(0)).toBe('base');
8
+ expect(getBreakpointFromWidth(479)).toBe('base');
9
+ expect(getBreakpointFromWidth(480)).toBe('sm');
10
+ expect(getBreakpointFromWidth(767)).toBe('sm');
11
+ expect(getBreakpointFromWidth(768)).toBe('md');
12
+ expect(getBreakpointFromWidth(1024)).toBe('lg');
13
+ expect(getBreakpointFromWidth(1280)).toBe('xl');
14
+ });
15
+ });
@@ -0,0 +1,10 @@
1
+ import { BREAKPOINT_ORDER, BREAKPOINTS } from './breakpoints';
2
+ import type { Breakpoint } from './types';
3
+
4
+ export function getBreakpointFromWidth(width: number): Breakpoint {
5
+ let active: Breakpoint = 'base';
6
+ for (const key of BREAKPOINT_ORDER) {
7
+ if (width >= BREAKPOINTS[key]) active = key;
8
+ }
9
+ return active;
10
+ }
@@ -0,0 +1,6 @@
1
+ export { BREAKPOINT_ORDER, BREAKPOINTS } from './breakpoints';
2
+ export { getBreakpointFromWidth } from './getBreakpointFromWidth';
3
+ export { resolveResponsive } from './resolve';
4
+ export { ResponsiveProvider, useResponsiveRuntime } from './ResponsiveProvider';
5
+ export type { Breakpoint, Responsive, ResponsiveRuntime } from './types';
6
+ export { useBreakpoint } from './useBreakpoint';
@@ -0,0 +1,25 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+
3
+ import { resolveResponsive } from './resolve';
4
+
5
+ describe('resolveResponsive', () => {
6
+ test('returns plain value when non-responsive', () => {
7
+ expect(resolveResponsive(12, 'md')).toBe(12);
8
+ });
9
+
10
+ test('resolves value at current breakpoint', () => {
11
+ expect(resolveResponsive({ base: 8, md: 16 }, 'md')).toBe(16);
12
+ });
13
+
14
+ test('falls back to nearest lower breakpoint', () => {
15
+ expect(resolveResponsive({ base: 8, md: 16 }, 'lg')).toBe(16);
16
+ });
17
+
18
+ test('falls back to base when no lower explicit value exists', () => {
19
+ expect(resolveResponsive({ base: 8, xl: 24 }, 'md')).toBe(8);
20
+ });
21
+
22
+ test('returns undefined when value is undefined', () => {
23
+ expect(resolveResponsive(undefined, 'md')).toBeUndefined();
24
+ });
25
+ });
@@ -0,0 +1,24 @@
1
+ import { BREAKPOINT_ORDER } from './breakpoints';
2
+ import type { Breakpoint, Responsive } from './types';
3
+
4
+ function isResponsiveRecord<T>(value: Responsive<T>): value is Partial<Record<Breakpoint, T>> {
5
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
6
+ }
7
+
8
+ export function resolveResponsive<T>(
9
+ value: Responsive<T> | undefined,
10
+ breakpoint: Breakpoint,
11
+ ): T | undefined {
12
+ if (value === undefined) return undefined;
13
+ if (!isResponsiveRecord(value)) return value;
14
+
15
+ const activeIndex = BREAKPOINT_ORDER.indexOf(breakpoint);
16
+ for (let i = activeIndex; i >= 0; i -= 1) {
17
+ const key = BREAKPOINT_ORDER[i];
18
+ if (!key) continue;
19
+ const candidate = value[key];
20
+ if (candidate !== undefined) return candidate;
21
+ }
22
+
23
+ return undefined;
24
+ }
@@ -0,0 +1,10 @@
1
+ import type { BREAKPOINTS } from './breakpoints';
2
+
3
+ export type Breakpoint = keyof typeof BREAKPOINTS;
4
+
5
+ export type Responsive<T> = T | Partial<Record<Breakpoint, T>>;
6
+
7
+ export interface ResponsiveRuntime {
8
+ breakpoint: Breakpoint;
9
+ width: number;
10
+ }
@@ -0,0 +1,9 @@
1
+ import { useWindowDimensions } from 'react-native';
2
+
3
+ import { getBreakpointFromWidth } from './getBreakpointFromWidth';
4
+ import type { Breakpoint } from './types';
5
+
6
+ export function useBreakpoint(): Breakpoint {
7
+ const { width } = useWindowDimensions();
8
+ return getBreakpointFromWidth(width);
9
+ }