@ankhorage/surface 0.1.4 → 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 (292) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +23 -184
  3. package/dist/components/badge/Badge.js.map +1 -1
  4. package/dist/components/badge/index.js.map +1 -1
  5. package/dist/components/badge/types.js.map +1 -1
  6. package/dist/components/button/Button.js.map +1 -1
  7. package/dist/components/button/index.js.map +1 -1
  8. package/dist/components/button/types.js.map +1 -1
  9. package/dist/components/card/Card.js.map +1 -1
  10. package/dist/components/card/index.js.map +1 -1
  11. package/dist/components/card/types.js.map +1 -1
  12. package/dist/components/checkbox/Checkbox.js.map +1 -1
  13. package/dist/components/checkbox/index.js.map +1 -1
  14. package/dist/components/checkbox/types.js.map +1 -1
  15. package/dist/components/drawer/Drawer.js.map +1 -1
  16. package/dist/components/drawer/index.js.map +1 -1
  17. package/dist/components/drawer/types.js.map +1 -1
  18. package/dist/components/field/Field.js.map +1 -1
  19. package/dist/components/field/index.js.map +1 -1
  20. package/dist/components/field/types.js.map +1 -1
  21. package/dist/components/helper-text/HelperText.js.map +1 -1
  22. package/dist/components/helper-text/index.js.map +1 -1
  23. package/dist/components/helper-text/types.js.map +1 -1
  24. package/dist/components/icon-button/IconButton.js.map +1 -1
  25. package/dist/components/icon-button/index.js.map +1 -1
  26. package/dist/components/icon-button/types.js.map +1 -1
  27. package/dist/components/label/Label.js.map +1 -1
  28. package/dist/components/label/index.js.map +1 -1
  29. package/dist/components/label/types.js.map +1 -1
  30. package/dist/components/list-item/ListItem.js.map +1 -1
  31. package/dist/components/list-item/index.js.map +1 -1
  32. package/dist/components/list-item/types.js.map +1 -1
  33. package/dist/components/menu/Menu.js.map +1 -1
  34. package/dist/components/menu/index.js.map +1 -1
  35. package/dist/components/menu/navigation.js.map +1 -1
  36. package/dist/components/menu/types.js.map +1 -1
  37. package/dist/components/modal/Modal.js.map +1 -1
  38. package/dist/components/modal/index.js.map +1 -1
  39. package/dist/components/modal/types.js.map +1 -1
  40. package/dist/components/radio/Radio.js.map +1 -1
  41. package/dist/components/radio/index.js.map +1 -1
  42. package/dist/components/radio/types.js.map +1 -1
  43. package/dist/components/switch/Switch.js.map +1 -1
  44. package/dist/components/switch/index.js.map +1 -1
  45. package/dist/components/switch/types.js.map +1 -1
  46. package/dist/components/tabs/Tab.js.map +1 -1
  47. package/dist/components/tabs/TabList.js.map +1 -1
  48. package/dist/components/tabs/TabPanel.js.map +1 -1
  49. package/dist/components/tabs/Tabs.js.map +1 -1
  50. package/dist/components/tabs/a11y.js.map +1 -1
  51. package/dist/components/tabs/context.js.map +1 -1
  52. package/dist/components/tabs/index.js.map +1 -1
  53. package/dist/components/tabs/navigation.js.map +1 -1
  54. package/dist/components/tabs/types.js.map +1 -1
  55. package/dist/components/text-input/TextInput.js.map +1 -1
  56. package/dist/components/text-input/index.js.map +1 -1
  57. package/dist/components/text-input/types.js.map +1 -1
  58. package/dist/components/textarea/Textarea.js.map +1 -1
  59. package/dist/components/textarea/index.js.map +1 -1
  60. package/dist/components/textarea/types.js.map +1 -1
  61. package/dist/components/toast/Toast.js.map +1 -1
  62. package/dist/components/toast/ToastProvider.js.map +1 -1
  63. package/dist/components/toast/index.js.map +1 -1
  64. package/dist/components/toast/types.js.map +1 -1
  65. package/dist/components/tooltip/Tooltip.js.map +1 -1
  66. package/dist/components/tooltip/index.js.map +1 -1
  67. package/dist/components/tooltip/types.js.map +1 -1
  68. package/dist/context/FontContext.js.map +1 -1
  69. package/dist/context/TranslationContext.js.map +1 -1
  70. package/dist/core/responsive/ResponsiveProvider.js.map +1 -1
  71. package/dist/core/responsive/breakpoints.js.map +1 -1
  72. package/dist/core/responsive/getBreakpointFromWidth.js.map +1 -1
  73. package/dist/core/responsive/index.js.map +1 -1
  74. package/dist/core/responsive/resolve.js.map +1 -1
  75. package/dist/core/responsive/types.js.map +1 -1
  76. package/dist/core/responsive/useBreakpoint.js.map +1 -1
  77. package/dist/examples/DocsExamples.js.map +1 -1
  78. package/dist/index.js.map +1 -1
  79. package/dist/internal/focus/FocusScope.js.map +1 -1
  80. package/dist/internal/focus/useFocusManager.js.map +1 -1
  81. package/dist/internal/overlay/OverlayProvider.js.map +1 -1
  82. package/dist/internal/overlay/Portal.js.map +1 -1
  83. package/dist/internal/overlay/useOverlayStack.js.map +1 -1
  84. package/dist/internal/resolvers/index.js.map +1 -1
  85. package/dist/internal/resolvers/resolveControlSize.js.map +1 -1
  86. package/dist/internal/resolvers/resolveFieldPresentation.js.map +1 -1
  87. package/dist/internal/resolvers/resolveFieldState.js.map +1 -1
  88. package/dist/internal/resolvers/resolveFocusRingStyles.js.map +1 -1
  89. package/dist/internal/resolvers/resolveIconSize.js.map +1 -1
  90. package/dist/internal/resolvers/resolveIndicatorSize.js.map +1 -1
  91. package/dist/internal/resolvers/resolveInteractiveColors.js.map +1 -1
  92. package/dist/internal/resolvers/resolveInteractiveState.js.map +1 -1
  93. package/dist/internal/resolvers/resolveOverlayAnimation.js.map +1 -1
  94. package/dist/internal/resolvers/resolveOverlayZIndex.js.map +1 -1
  95. package/dist/internal/resolvers/resolveSelectionControlBehavior.js.map +1 -1
  96. package/dist/internal/resolvers/resolveSelectionControlColors.js.map +1 -1
  97. package/dist/internal/resolvers/resolveTextColor.js.map +1 -1
  98. package/dist/internal/resolvers/resolveTextStyles.js.map +1 -1
  99. package/dist/internal/resolvers/resolveTone.js.map +1 -1
  100. package/dist/internal/useControllableState.js.map +1 -1
  101. package/dist/layout/Box.js.map +1 -1
  102. package/dist/layout/Center.js.map +1 -1
  103. package/dist/layout/Container.js.map +1 -1
  104. package/dist/layout/Divider.js.map +1 -1
  105. package/dist/layout/Grid.js.map +1 -1
  106. package/dist/layout/Inline.js.map +1 -1
  107. package/dist/layout/Show.js.map +1 -1
  108. package/dist/layout/Spacer.js.map +1 -1
  109. package/dist/layout/Stack.js.map +1 -1
  110. package/dist/layout/Surface.js.map +1 -1
  111. package/dist/layout/Template.js.map +1 -1
  112. package/dist/layout/helpers.js.map +1 -1
  113. package/dist/layout/index.js.map +1 -1
  114. package/dist/primitives/button-base/ButtonBase.js.map +1 -1
  115. package/dist/primitives/button-base/index.js.map +1 -1
  116. package/dist/primitives/button-base/types.js.map +1 -1
  117. package/dist/primitives/heading/Heading.js.map +1 -1
  118. package/dist/primitives/heading/index.js.map +1 -1
  119. package/dist/primitives/heading/resolveHeadingStyle.js.map +1 -1
  120. package/dist/primitives/heading/types.js.map +1 -1
  121. package/dist/primitives/icon/Icon.js.map +1 -1
  122. package/dist/primitives/icon/index.js.map +1 -1
  123. package/dist/primitives/icon/resolveExpoIconComponent.js.map +1 -1
  124. package/dist/primitives/text/Text.js.map +1 -1
  125. package/dist/primitives/text/index.js.map +1 -1
  126. package/dist/primitives/text/types.js.map +1 -1
  127. package/dist/theme/ThemeContext.js.map +1 -1
  128. package/dist/theme/colorEngine.js.map +1 -1
  129. package/dist/theme/createTheme.js.map +1 -1
  130. package/dist/theme/index.js.map +1 -1
  131. package/dist/theme/resolveToken.js.map +1 -1
  132. package/dist/theme/types.js.map +1 -1
  133. package/dist/utils/deepEqual.js.map +1 -1
  134. package/dist/utils/deepMerge.js.map +1 -1
  135. package/package.json +4 -1
  136. package/src/components/badge/Badge.tsx +47 -0
  137. package/src/components/badge/index.ts +2 -0
  138. package/src/components/badge/types.ts +13 -0
  139. package/src/components/button/Button.tsx +104 -0
  140. package/src/components/button/index.ts +2 -0
  141. package/src/components/button/types.ts +26 -0
  142. package/src/components/card/Card.tsx +81 -0
  143. package/src/components/card/index.ts +2 -0
  144. package/src/components/card/types.ts +11 -0
  145. package/src/components/checkbox/Checkbox.tsx +111 -0
  146. package/src/components/checkbox/index.ts +2 -0
  147. package/src/components/checkbox/types.ts +19 -0
  148. package/src/components/drawer/Drawer.tsx +92 -0
  149. package/src/components/drawer/index.ts +2 -0
  150. package/src/components/drawer/types.ts +10 -0
  151. package/src/components/field/Field.tsx +43 -0
  152. package/src/components/field/index.ts +2 -0
  153. package/src/components/field/types.ts +13 -0
  154. package/src/components/helper-text/HelperText.tsx +12 -0
  155. package/src/components/helper-text/index.ts +2 -0
  156. package/src/components/helper-text/types.ts +9 -0
  157. package/src/components/icon-button/IconButton.tsx +60 -0
  158. package/src/components/icon-button/index.ts +2 -0
  159. package/src/components/icon-button/types.ts +19 -0
  160. package/src/components/label/Label.tsx +17 -0
  161. package/src/components/label/index.ts +2 -0
  162. package/src/components/label/types.ts +10 -0
  163. package/src/components/list-item/ListItem.tsx +72 -0
  164. package/src/components/list-item/index.ts +2 -0
  165. package/src/components/list-item/types.ts +11 -0
  166. package/src/components/menu/Menu.tsx +180 -0
  167. package/src/components/menu/index.ts +2 -0
  168. package/src/components/menu/navigation.test.ts +21 -0
  169. package/src/components/menu/navigation.ts +34 -0
  170. package/src/components/menu/types.ts +16 -0
  171. package/src/components/modal/Modal.tsx +87 -0
  172. package/src/components/modal/index.ts +2 -0
  173. package/src/components/modal/types.ts +9 -0
  174. package/src/components/radio/Radio.tsx +116 -0
  175. package/src/components/radio/index.ts +2 -0
  176. package/src/components/radio/types.ts +19 -0
  177. package/src/components/switch/Switch.tsx +116 -0
  178. package/src/components/switch/index.ts +2 -0
  179. package/src/components/switch/types.ts +19 -0
  180. package/src/components/tabs/Tab.tsx +82 -0
  181. package/src/components/tabs/TabList.tsx +51 -0
  182. package/src/components/tabs/TabPanel.tsx +29 -0
  183. package/src/components/tabs/Tabs.tsx +67 -0
  184. package/src/components/tabs/a11y.test.ts +15 -0
  185. package/src/components/tabs/a11y.ts +15 -0
  186. package/src/components/tabs/context.tsx +31 -0
  187. package/src/components/tabs/index.ts +5 -0
  188. package/src/components/tabs/navigation.test.ts +21 -0
  189. package/src/components/tabs/navigation.ts +32 -0
  190. package/src/components/tabs/types.ts +27 -0
  191. package/src/components/text-input/TextInput.tsx +116 -0
  192. package/src/components/text-input/index.ts +2 -0
  193. package/src/components/text-input/types.ts +32 -0
  194. package/src/components/textarea/Textarea.tsx +15 -0
  195. package/src/components/textarea/index.ts +2 -0
  196. package/src/components/textarea/types.ts +5 -0
  197. package/src/components/toast/Toast.tsx +54 -0
  198. package/src/components/toast/ToastProvider.tsx +114 -0
  199. package/src/components/toast/index.ts +3 -0
  200. package/src/components/toast/types.ts +16 -0
  201. package/src/components/tooltip/Tooltip.tsx +109 -0
  202. package/src/components/tooltip/index.ts +2 -0
  203. package/src/components/tooltip/types.ts +9 -0
  204. package/src/context/FontContext.tsx +59 -0
  205. package/src/context/TranslationContext.tsx +54 -0
  206. package/src/core/responsive/ResponsiveProvider.tsx +31 -0
  207. package/src/core/responsive/breakpoints.ts +9 -0
  208. package/src/core/responsive/getBreakpointFromWidth.test.ts +15 -0
  209. package/src/core/responsive/getBreakpointFromWidth.ts +10 -0
  210. package/src/core/responsive/index.ts +6 -0
  211. package/src/core/responsive/resolve.test.ts +25 -0
  212. package/src/core/responsive/resolve.ts +24 -0
  213. package/src/core/responsive/types.ts +10 -0
  214. package/src/core/responsive/useBreakpoint.ts +9 -0
  215. package/src/examples/DocsExamples.tsx +116 -0
  216. package/src/index.test.ts +64 -0
  217. package/src/index.ts +55 -0
  218. package/src/internal/focus/FocusScope.tsx +66 -0
  219. package/src/internal/focus/useFocusManager.test.ts +44 -0
  220. package/src/internal/focus/useFocusManager.ts +142 -0
  221. package/src/internal/overlay/OverlayProvider.tsx +74 -0
  222. package/src/internal/overlay/Portal.tsx +38 -0
  223. package/src/internal/overlay/useOverlayStack.test.ts +31 -0
  224. package/src/internal/overlay/useOverlayStack.ts +61 -0
  225. package/src/internal/resolvers/index.ts +15 -0
  226. package/src/internal/resolvers/resolveControlSize.test.ts +25 -0
  227. package/src/internal/resolvers/resolveControlSize.ts +45 -0
  228. package/src/internal/resolvers/resolveFieldPresentation.test.ts +31 -0
  229. package/src/internal/resolvers/resolveFieldPresentation.ts +30 -0
  230. package/src/internal/resolvers/resolveFieldState.test.ts +22 -0
  231. package/src/internal/resolvers/resolveFieldState.ts +36 -0
  232. package/src/internal/resolvers/resolveFocusRingStyles.ts +14 -0
  233. package/src/internal/resolvers/resolveIconSize.ts +6 -0
  234. package/src/internal/resolvers/resolveIndicatorSize.test.ts +19 -0
  235. package/src/internal/resolvers/resolveIndicatorSize.ts +47 -0
  236. package/src/internal/resolvers/resolveInteractiveColors.test.ts +57 -0
  237. package/src/internal/resolvers/resolveInteractiveColors.ts +134 -0
  238. package/src/internal/resolvers/resolveInteractiveState.test.ts +14 -0
  239. package/src/internal/resolvers/resolveInteractiveState.ts +15 -0
  240. package/src/internal/resolvers/resolveOverlayAnimation.test.ts +15 -0
  241. package/src/internal/resolvers/resolveOverlayAnimation.ts +24 -0
  242. package/src/internal/resolvers/resolveOverlayZIndex.test.ts +15 -0
  243. package/src/internal/resolvers/resolveOverlayZIndex.ts +13 -0
  244. package/src/internal/resolvers/resolveSelectionControlBehavior.test.ts +52 -0
  245. package/src/internal/resolvers/resolveSelectionControlBehavior.ts +23 -0
  246. package/src/internal/resolvers/resolveSelectionControlColors.test.ts +44 -0
  247. package/src/internal/resolvers/resolveSelectionControlColors.ts +81 -0
  248. package/src/internal/resolvers/resolveTextColor.test.ts +23 -0
  249. package/src/internal/resolvers/resolveTextColor.ts +40 -0
  250. package/src/internal/resolvers/resolveTextStyles.test.ts +27 -0
  251. package/src/internal/resolvers/resolveTextStyles.ts +95 -0
  252. package/src/internal/resolvers/resolveTone.ts +19 -0
  253. package/src/internal/useControllableState.ts +28 -0
  254. package/src/layout/Box.tsx +79 -0
  255. package/src/layout/Center.tsx +22 -0
  256. package/src/layout/Container.tsx +43 -0
  257. package/src/layout/Divider.tsx +26 -0
  258. package/src/layout/Grid.tsx +83 -0
  259. package/src/layout/Inline.tsx +9 -0
  260. package/src/layout/Show.tsx +15 -0
  261. package/src/layout/Spacer.tsx +22 -0
  262. package/src/layout/Stack.tsx +67 -0
  263. package/src/layout/Surface.tsx +70 -0
  264. package/src/layout/Template.tsx +85 -0
  265. package/src/layout/helpers.test.ts +71 -0
  266. package/src/layout/helpers.ts +208 -0
  267. package/src/layout/index.ts +22 -0
  268. package/src/primitives/button-base/ButtonBase.tsx +81 -0
  269. package/src/primitives/button-base/index.ts +2 -0
  270. package/src/primitives/button-base/types.ts +16 -0
  271. package/src/primitives/heading/Heading.tsx +60 -0
  272. package/src/primitives/heading/index.ts +2 -0
  273. package/src/primitives/heading/resolveHeadingStyle.test.ts +31 -0
  274. package/src/primitives/heading/resolveHeadingStyle.ts +17 -0
  275. package/src/primitives/heading/types.ts +13 -0
  276. package/src/primitives/icon/Icon.tsx +40 -0
  277. package/src/primitives/icon/index.ts +2 -0
  278. package/src/primitives/icon/resolveExpoIconComponent.test.ts +29 -0
  279. package/src/primitives/icon/resolveExpoIconComponent.ts +20 -0
  280. package/src/primitives/text/Text.tsx +66 -0
  281. package/src/primitives/text/index.ts +2 -0
  282. package/src/primitives/text/types.ts +18 -0
  283. package/src/theme/ThemeContext.tsx +95 -0
  284. package/src/theme/colorEngine.test.ts +114 -0
  285. package/src/theme/colorEngine.ts +480 -0
  286. package/src/theme/createTheme.ts +121 -0
  287. package/src/theme/index.ts +5 -0
  288. package/src/theme/resolveToken.ts +32 -0
  289. package/src/theme/types.ts +188 -0
  290. package/src/utils/deepEqual.ts +34 -0
  291. package/src/utils/deepMerge.test.ts +117 -0
  292. package/src/utils/deepMerge.ts +29 -0
@@ -0,0 +1,21 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+
3
+ import { resolveNextTabValue } from './navigation';
4
+
5
+ const tabs = [
6
+ { disabled: false, focus: () => undefined, value: 'overview' },
7
+ { disabled: true, focus: () => undefined, value: 'disabled' },
8
+ { disabled: false, focus: () => undefined, value: 'details' },
9
+ ];
10
+
11
+ describe('resolveNextTabValue', () => {
12
+ it('skips disabled tabs when cycling', () => {
13
+ expect(resolveNextTabValue(tabs, 'overview', 'ArrowRight')).toBe('details');
14
+ expect(resolveNextTabValue(tabs, 'details', 'ArrowRight')).toBe('overview');
15
+ });
16
+
17
+ it('supports home and end navigation', () => {
18
+ expect(resolveNextTabValue(tabs, 'details', 'Home')).toBe('overview');
19
+ expect(resolveNextTabValue(tabs, 'overview', 'End')).toBe('details');
20
+ });
21
+ });
@@ -0,0 +1,32 @@
1
+ import type { TabRegistration } from './context';
2
+
3
+ export function resolveNextTabValue(
4
+ tabs: TabRegistration[],
5
+ currentValue: string | undefined,
6
+ key: 'ArrowLeft' | 'ArrowRight' | 'ArrowUp' | 'ArrowDown' | 'Home' | 'End',
7
+ ): string | undefined {
8
+ const enabledTabs = tabs.filter((tab) => !tab.disabled);
9
+ if (enabledTabs.length === 0) {
10
+ return undefined;
11
+ }
12
+
13
+ if (key === 'Home') {
14
+ return enabledTabs[0]?.value;
15
+ }
16
+
17
+ if (key === 'End') {
18
+ return enabledTabs[enabledTabs.length - 1]?.value;
19
+ }
20
+
21
+ const currentIndex = enabledTabs.findIndex((tab) => tab.value === currentValue);
22
+ if (currentIndex === -1) {
23
+ return enabledTabs[0]?.value;
24
+ }
25
+
26
+ const nextIndex =
27
+ key === 'ArrowLeft' || key === 'ArrowUp'
28
+ ? (currentIndex - 1 + enabledTabs.length) % enabledTabs.length
29
+ : (currentIndex + 1) % enabledTabs.length;
30
+
31
+ return enabledTabs[nextIndex]?.value;
32
+ }
@@ -0,0 +1,27 @@
1
+ import type React from 'react';
2
+
3
+ export interface TabsProps {
4
+ children?: React.ReactNode;
5
+ value?: string;
6
+ defaultValue?: string;
7
+ onValueChange?: ((value: string) => void) | undefined;
8
+ testID?: string;
9
+ }
10
+
11
+ export interface TabListProps {
12
+ children?: React.ReactNode;
13
+ testID?: string;
14
+ }
15
+
16
+ export interface TabProps {
17
+ value: string;
18
+ children?: React.ReactNode;
19
+ disabled?: boolean;
20
+ testID?: string;
21
+ }
22
+
23
+ export interface TabPanelProps {
24
+ value: string;
25
+ children?: React.ReactNode;
26
+ testID?: string;
27
+ }
@@ -0,0 +1,116 @@
1
+ import React from 'react';
2
+ import { Platform, TextInput as ReactNativeTextInput, View } from 'react-native';
3
+
4
+ import {
5
+ resolveControlSize,
6
+ resolveFieldState,
7
+ resolveFocusRingStyles,
8
+ resolveInputColors,
9
+ resolveTextStyles,
10
+ } from '../../internal/resolvers';
11
+ import { useTheme } from '../../theme/ThemeContext';
12
+ import type { TextInputProps } from './types';
13
+
14
+ export function TextInput({
15
+ value,
16
+ defaultValue,
17
+ onChangeText,
18
+ placeholder,
19
+ size = 'm',
20
+ disabled = false,
21
+ readOnly = false,
22
+ invalid = false,
23
+ leadingAccessory,
24
+ trailingAccessory,
25
+ style,
26
+ testID,
27
+ onFocus,
28
+ onBlur,
29
+ ...props
30
+ }: TextInputProps) {
31
+ const { theme } = useTheme();
32
+ const controlSize = resolveControlSize(theme, size);
33
+ const [focused, setFocused] = React.useState(false);
34
+ const fieldState = resolveFieldState({
35
+ disabled,
36
+ focused,
37
+ invalid,
38
+ readOnly,
39
+ });
40
+ const colors = resolveInputColors(theme, fieldState);
41
+ const resolvedTextStyle = resolveTextStyles(theme, {
42
+ variant: controlSize.textVariant,
43
+ });
44
+ const resolvedLineHeight =
45
+ typeof resolvedTextStyle.lineHeight === 'number'
46
+ ? resolvedTextStyle.lineHeight
47
+ : controlSize.minHeight - controlSize.paddingVertical * 2;
48
+ const lineCount = Math.max(props.numberOfLines ?? 1, 1);
49
+ const inputMinHeight = props.multiline
50
+ ? resolvedLineHeight * lineCount
51
+ : controlSize.minHeight - controlSize.paddingVertical * 2;
52
+ const containerMinHeight = props.multiline
53
+ ? inputMinHeight + controlSize.paddingVertical * 2
54
+ : controlSize.minHeight;
55
+
56
+ const handleFocus: NonNullable<TextInputProps['onFocus']> = (event) => {
57
+ setFocused(true);
58
+ onFocus?.(event);
59
+ };
60
+
61
+ const handleBlur: NonNullable<TextInputProps['onBlur']> = (event) => {
62
+ setFocused(false);
63
+ onBlur?.(event);
64
+ };
65
+
66
+ return (
67
+ <View
68
+ style={[
69
+ {
70
+ minHeight: containerMinHeight,
71
+ paddingHorizontal: controlSize.paddingHorizontal,
72
+ paddingVertical: controlSize.paddingVertical,
73
+ borderRadius: controlSize.borderRadius,
74
+ borderWidth: 1,
75
+ borderColor: colors.borderColor,
76
+ backgroundColor: colors.backgroundColor,
77
+ flexDirection: 'row',
78
+ alignItems: props.multiline ? 'flex-start' : 'center',
79
+ opacity: colors.opacity,
80
+ },
81
+ resolveFocusRingStyles(theme.semantics.border.focus, focused, Platform.OS === 'web'),
82
+ ]}
83
+ >
84
+ {leadingAccessory ? (
85
+ <View style={{ marginRight: theme.spacing.s }}>{leadingAccessory}</View>
86
+ ) : null}
87
+ <ReactNativeTextInput
88
+ {...props}
89
+ defaultValue={defaultValue}
90
+ editable={!disabled && !readOnly}
91
+ numberOfLines={props.multiline ? props.numberOfLines : 1}
92
+ onBlur={handleBlur}
93
+ onChangeText={onChangeText}
94
+ onFocus={handleFocus}
95
+ placeholder={placeholder}
96
+ placeholderTextColor={colors.placeholderColor}
97
+ readOnly={readOnly}
98
+ style={[
99
+ {
100
+ flex: 1,
101
+ minHeight: inputMinHeight,
102
+ padding: 0,
103
+ color: colors.contentColor,
104
+ textAlignVertical: props.multiline ? 'top' : 'center',
105
+ },
106
+ style,
107
+ ]}
108
+ testID={testID}
109
+ value={value}
110
+ />
111
+ {trailingAccessory ? (
112
+ <View style={{ marginLeft: theme.spacing.s }}>{trailingAccessory}</View>
113
+ ) : null}
114
+ </View>
115
+ );
116
+ }
@@ -0,0 +1,2 @@
1
+ export { TextInput } from './TextInput';
2
+ export type { TextInputProps } from './types';
@@ -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
+ }