@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,116 @@
1
+ import React from 'react';
2
+
3
+ import {
4
+ Badge,
5
+ Box,
6
+ Button,
7
+ Checkbox,
8
+ Drawer,
9
+ Field,
10
+ HelperText,
11
+ Label,
12
+ Menu,
13
+ Modal,
14
+ Radio,
15
+ Stack,
16
+ Switch,
17
+ Tab,
18
+ TabList,
19
+ TabPanel,
20
+ Tabs,
21
+ Text,
22
+ Textarea,
23
+ TextInput,
24
+ ThemeProvider,
25
+ } from '../index';
26
+
27
+ const docsThemeConfig = {
28
+ id: 'docs-example',
29
+ name: 'Docs Example',
30
+ light: {
31
+ harmony: 'monochromatic' as const,
32
+ primaryColor: '#2563eb',
33
+ systemTone: 'neutral' as const,
34
+ },
35
+ dark: {
36
+ harmony: 'monochromatic' as const,
37
+ primaryColor: '#2563eb',
38
+ systemTone: 'neutral' as const,
39
+ },
40
+ };
41
+
42
+ export function ProviderExample() {
43
+ return (
44
+ <ThemeProvider initialConfig={docsThemeConfig}>
45
+ <Stack gap="m" p="l">
46
+ <Text variant="body">Surface starter</Text>
47
+ <Badge content="Foundation" tone="success" />
48
+ <Button tone="warning" variant="soft">
49
+ Continue
50
+ </Button>
51
+ </Stack>
52
+ </ThemeProvider>
53
+ );
54
+ }
55
+
56
+ export function FormAndOverlayExample() {
57
+ return (
58
+ <ThemeProvider initialConfig={docsThemeConfig}>
59
+ <Stack gap="m" p="l">
60
+ <Field helperText="We only use this for sign-in." label="Email">
61
+ <TextInput placeholder="name@example.com" />
62
+ </Field>
63
+ <Field errorText="Bio is required." invalid label="Bio">
64
+ <Textarea placeholder="Tell us a little about yourself" />
65
+ </Field>
66
+ <Stack gap="s">
67
+ <Label required>Preferences</Label>
68
+ <Checkbox defaultChecked>Weekly updates</Checkbox>
69
+ <Radio>Product announcements</Radio>
70
+ <Switch readOnly>Read-only setting</Switch>
71
+ <HelperText tone="danger">Invalid fields should reuse the same danger tone.</HelperText>
72
+ </Stack>
73
+ <Modal visible={false}>
74
+ <Text>Modal content</Text>
75
+ </Modal>
76
+ <Drawer visible={false}>
77
+ <Text>Drawer content</Text>
78
+ </Drawer>
79
+ </Stack>
80
+ </ThemeProvider>
81
+ );
82
+ }
83
+
84
+ export function NavigationExample() {
85
+ return (
86
+ <ThemeProvider initialConfig={docsThemeConfig}>
87
+ <Stack gap="m" p="l">
88
+ <Tabs defaultValue="account">
89
+ <TabList>
90
+ <Tab value="account">Account</Tab>
91
+ <Tab disabled value="security">
92
+ Security
93
+ </Tab>
94
+ <Tab value="billing">Billing</Tab>
95
+ </TabList>
96
+ <TabPanel value="account">
97
+ <Text>Account settings</Text>
98
+ </TabPanel>
99
+ <TabPanel value="billing">
100
+ <Text>Billing settings</Text>
101
+ </TabPanel>
102
+ </Tabs>
103
+ <Menu
104
+ items={[
105
+ { id: 'edit', label: 'Edit' },
106
+ { disabled: true, id: 'archive', label: 'Archive' },
107
+ ]}
108
+ trigger={<Text>Open menu</Text>}
109
+ />
110
+ <Box>
111
+ <Text tone="success">Theme overrides can stay semantic.</Text>
112
+ </Box>
113
+ </Stack>
114
+ </ThemeProvider>
115
+ );
116
+ }
@@ -0,0 +1,64 @@
1
+ import { readFileSync } from 'node:fs';
2
+
3
+ import { describe, expect, it } from 'bun:test';
4
+
5
+ const indexSource = readFileSync(new URL('./index.ts', import.meta.url), 'utf8');
6
+ const packageJson = JSON.parse(
7
+ readFileSync(new URL('../package.json', import.meta.url), 'utf8'),
8
+ ) as {
9
+ exports: Record<string, unknown>;
10
+ files: string[];
11
+ };
12
+
13
+ const expectedRootExports = [
14
+ "export { Badge } from './components/badge';",
15
+ "export { Button } from './components/button';",
16
+ "export { Card } from './components/card';",
17
+ "export { Checkbox } from './components/checkbox';",
18
+ "export { Drawer } from './components/drawer';",
19
+ "export { Field } from './components/field';",
20
+ "export { HelperText } from './components/helper-text';",
21
+ "export { IconButton } from './components/icon-button';",
22
+ "export { Label } from './components/label';",
23
+ "export { ListItem } from './components/list-item';",
24
+ "export { Menu } from './components/menu';",
25
+ "export { Modal } from './components/modal';",
26
+ "export { Radio } from './components/radio';",
27
+ "export { Switch } from './components/switch';",
28
+ "export { Tab, TabList, TabPanel, Tabs } from './components/tabs';",
29
+ "export { TextInput } from './components/text-input';",
30
+ "export { Textarea } from './components/textarea';",
31
+ "export { Toast, ToastProvider, useToast } from './components/toast';",
32
+ "export { Tooltip } from './components/tooltip';",
33
+ "export * from './core/responsive';",
34
+ "export * from './layout';",
35
+ "export * from './theme';",
36
+ ] as const;
37
+
38
+ describe('public package contract', () => {
39
+ it('keeps the intended package surface on the root barrel', () => {
40
+ expectedRootExports.forEach((line) => {
41
+ expect(indexSource).toContain(line);
42
+ });
43
+ });
44
+
45
+ it('keeps internal infrastructure off the public barrel', () => {
46
+ expect(indexSource).not.toContain("'./internal/");
47
+ expect(indexSource).not.toContain('resolveSelectionControlNextChecked');
48
+ expect(indexSource).not.toContain('resolveFieldPresentation');
49
+ expect(indexSource).not.toContain('FocusScope');
50
+ expect(indexSource).not.toContain('useFocusManager');
51
+ });
52
+
53
+ it('keeps package metadata aligned with the single-entry public surface', () => {
54
+ expect(packageJson.files).toEqual(['dist', 'README.md', 'CHANGELOG.md', 'LICENSE']);
55
+ expect(packageJson.exports).toEqual({
56
+ '.': {
57
+ default: './dist/index.js',
58
+ import: './dist/index.js',
59
+ types: './dist/index.d.ts',
60
+ },
61
+ './package.json': './package.json',
62
+ });
63
+ });
64
+ });
package/src/index.ts ADDED
@@ -0,0 +1,55 @@
1
+ export type { BadgeProps } from './components/badge';
2
+ export { Badge } from './components/badge';
3
+ export type { ButtonIconSpec, ButtonProps } from './components/button';
4
+ export { Button } from './components/button';
5
+ export type { CardProps } from './components/card';
6
+ export { Card } from './components/card';
7
+ export type { CheckboxProps } from './components/checkbox';
8
+ export { Checkbox } from './components/checkbox';
9
+ export type { DrawerProps } from './components/drawer';
10
+ export { Drawer } from './components/drawer';
11
+ export type { FieldProps } from './components/field';
12
+ export { Field } from './components/field';
13
+ export type { HelperTextProps } from './components/helper-text';
14
+ export { HelperText } from './components/helper-text';
15
+ export type { IconButtonProps } from './components/icon-button';
16
+ export { IconButton } from './components/icon-button';
17
+ export type { LabelProps } from './components/label';
18
+ export { Label } from './components/label';
19
+ export type { ListItemProps } from './components/list-item';
20
+ export { ListItem } from './components/list-item';
21
+ export type { MenuItem, MenuProps } from './components/menu';
22
+ export { Menu } from './components/menu';
23
+ export type { ModalProps } from './components/modal';
24
+ export { Modal } from './components/modal';
25
+ export type { RadioProps } from './components/radio';
26
+ export { Radio } from './components/radio';
27
+ export type { SwitchProps } from './components/switch';
28
+ export { Switch } from './components/switch';
29
+ export type { TabListProps, TabPanelProps, TabProps, TabsProps } from './components/tabs';
30
+ export { Tab, TabList, TabPanel, Tabs } from './components/tabs';
31
+ export type { TextInputProps } from './components/text-input';
32
+ export { TextInput } from './components/text-input';
33
+ export type { TextareaProps } from './components/textarea';
34
+ export { Textarea } from './components/textarea';
35
+ export type { ToastOptions, ToastProps, ToastTone } from './components/toast';
36
+ export { Toast, ToastProvider, useToast } from './components/toast';
37
+ export type { TooltipProps } from './components/tooltip';
38
+ export { Tooltip } from './components/tooltip';
39
+ export type { FontRuntime } from './context/FontContext';
40
+ export { FontProvider, useFontContext } from './context/FontContext';
41
+ export type { I18nInstance, TranslationRuntime, Translator } from './context/TranslationContext';
42
+ export { TranslationProvider, useTranslationContext } from './context/TranslationContext';
43
+ export * from './core/responsive';
44
+ export * from './layout';
45
+ export type { ButtonBaseProps } from './primitives/button-base';
46
+ export { ButtonBase } from './primitives/button-base';
47
+ export type { HeadingLevel, HeadingProps } from './primitives/heading';
48
+ export { Heading } from './primitives/heading';
49
+ export type { IconProps, IconProvider } from './primitives/icon';
50
+ export { Icon } from './primitives/icon';
51
+ export type { TextProps } from './primitives/text';
52
+ export { Text } from './primitives/text';
53
+ export * from './theme';
54
+ export { isDeepEqual } from './utils/deepEqual';
55
+ export { deepMerge } from './utils/deepMerge';
@@ -0,0 +1,66 @@
1
+ import React from 'react';
2
+ import { View } from 'react-native';
3
+
4
+ import { useFocusManager } from './useFocusManager';
5
+
6
+ export interface FocusScopeProps {
7
+ active: boolean;
8
+ children?: React.ReactNode;
9
+ onEscape?: (() => void) | undefined;
10
+ testID?: string;
11
+ }
12
+
13
+ export function FocusScope({ active, children, onEscape, testID }: FocusScopeProps) {
14
+ const containerRef = React.useRef<View | null>(null);
15
+ const { bindKeydown, capturePreviousFocus, cycleFocus, focusFirst, restorePreviousFocus } =
16
+ useFocusManager();
17
+
18
+ React.useEffect(() => {
19
+ if (!active) {
20
+ return undefined;
21
+ }
22
+
23
+ capturePreviousFocus();
24
+ const timeoutId = setTimeout(() => {
25
+ focusFirst(containerRef.current);
26
+ }, 0);
27
+
28
+ const unbind = bindKeydown((event) => {
29
+ if (event.key === 'Tab') {
30
+ event.preventDefault();
31
+ cycleFocus(containerRef.current, Boolean(event.shiftKey));
32
+ }
33
+
34
+ if (event.key === 'Escape') {
35
+ onEscape?.();
36
+ }
37
+ });
38
+
39
+ return () => {
40
+ clearTimeout(timeoutId);
41
+ unbind();
42
+ restorePreviousFocus();
43
+ };
44
+ }, [
45
+ active,
46
+ bindKeydown,
47
+ capturePreviousFocus,
48
+ cycleFocus,
49
+ focusFirst,
50
+ onEscape,
51
+ restorePreviousFocus,
52
+ ]);
53
+
54
+ return (
55
+ <View
56
+ collapsable={false}
57
+ ref={containerRef}
58
+ testID={testID}
59
+ style={{
60
+ flex: 1,
61
+ }}
62
+ >
63
+ {children}
64
+ </View>
65
+ );
66
+ }
@@ -0,0 +1,44 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+
3
+ import { getFocusableElements } from './useFocusManager';
4
+
5
+ interface MockFocusableTarget {
6
+ disabled?: boolean;
7
+ focus: () => void;
8
+ getAttribute?: (name: string) => string | null;
9
+ hidden?: boolean;
10
+ tabIndex?: number;
11
+ }
12
+
13
+ describe('getFocusableElements', () => {
14
+ it('filters disabled, hidden, and aria-hidden nodes from focus traps', () => {
15
+ const active: MockFocusableTarget = {
16
+ focus: () => undefined,
17
+ getAttribute: () => null,
18
+ };
19
+ const disabled: MockFocusableTarget = {
20
+ disabled: true,
21
+ focus: () => undefined,
22
+ getAttribute: () => null,
23
+ };
24
+ const ariaHidden: MockFocusableTarget = {
25
+ focus: () => undefined,
26
+ getAttribute: (name) => (name === 'aria-hidden' ? 'true' : null),
27
+ };
28
+ const untabbable: MockFocusableTarget = {
29
+ focus: () => undefined,
30
+ getAttribute: (name) => (name === 'tabindex' ? '-1' : null),
31
+ };
32
+
33
+ const container = {
34
+ querySelectorAll: () => [active, disabled, ariaHidden, untabbable],
35
+ };
36
+
37
+ expect(getFocusableElements(container)).toEqual([active]);
38
+ });
39
+
40
+ it('returns an empty list when no DOM-like query API is available', () => {
41
+ expect(getFocusableElements(null)).toEqual([]);
42
+ expect(getFocusableElements({})).toEqual([]);
43
+ });
44
+ });
@@ -0,0 +1,142 @@
1
+ import React from 'react';
2
+
3
+ interface FocusableTarget {
4
+ focus: () => void;
5
+ disabled?: boolean;
6
+ hidden?: boolean;
7
+ tabIndex?: number;
8
+ getAttribute?: (name: string) => string | null;
9
+ }
10
+
11
+ interface FocusContainer {
12
+ contains: (node: unknown) => boolean;
13
+ querySelectorAll: (selector: string) => ArrayLike<FocusableTarget>;
14
+ }
15
+
16
+ interface WebKeyboardEventLike {
17
+ key?: string;
18
+ preventDefault: () => void;
19
+ shiftKey?: boolean;
20
+ }
21
+
22
+ interface WebDocumentLike {
23
+ activeElement: unknown;
24
+ addEventListener: (type: 'keydown', listener: (event: WebKeyboardEventLike) => void) => void;
25
+ removeEventListener: (type: 'keydown', listener: (event: WebKeyboardEventLike) => void) => void;
26
+ }
27
+
28
+ const FOCUSABLE_SELECTOR = 'button,[href],input,select,textarea,[tabindex]:not([tabindex="-1"])';
29
+
30
+ function getWebDocument(): WebDocumentLike | null {
31
+ const maybeDocument = (globalThis as { document?: unknown }).document;
32
+
33
+ if (!maybeDocument || typeof maybeDocument !== 'object') {
34
+ return null;
35
+ }
36
+
37
+ return maybeDocument as WebDocumentLike;
38
+ }
39
+
40
+ function isFocusableTarget(target: FocusableTarget): boolean {
41
+ if (target.disabled || target.hidden) {
42
+ return false;
43
+ }
44
+
45
+ if (typeof target.tabIndex === 'number' && target.tabIndex < 0) {
46
+ return false;
47
+ }
48
+
49
+ const ariaHidden = target.getAttribute?.('aria-hidden');
50
+ if (ariaHidden === 'true') {
51
+ return false;
52
+ }
53
+
54
+ const tabIndexAttribute = target.getAttribute?.('tabindex');
55
+ if (tabIndexAttribute === '-1') {
56
+ return false;
57
+ }
58
+
59
+ return true;
60
+ }
61
+
62
+ export function getFocusableElements(container: unknown): FocusableTarget[] {
63
+ if (!container || typeof container !== 'object') {
64
+ return [];
65
+ }
66
+
67
+ const focusContainer = container as FocusContainer;
68
+ if (typeof focusContainer.querySelectorAll !== 'function') {
69
+ return [];
70
+ }
71
+
72
+ return Array.from(focusContainer.querySelectorAll(FOCUSABLE_SELECTOR)).filter(isFocusableTarget);
73
+ }
74
+
75
+ export function useFocusManager() {
76
+ const previousFocusRef = React.useRef<FocusableTarget | null>(null);
77
+
78
+ const capturePreviousFocus = React.useCallback(() => {
79
+ const documentRef = getWebDocument();
80
+ const activeElement = documentRef?.activeElement;
81
+
82
+ previousFocusRef.current =
83
+ activeElement && typeof activeElement === 'object' && 'focus' in activeElement
84
+ ? (activeElement as FocusableTarget)
85
+ : null;
86
+ }, []);
87
+
88
+ const focusFirst = React.useCallback((container: unknown) => {
89
+ const [firstFocusable] = getFocusableElements(container);
90
+
91
+ firstFocusable?.focus();
92
+ }, []);
93
+
94
+ const restorePreviousFocus = React.useCallback(() => {
95
+ previousFocusRef.current?.focus();
96
+ }, []);
97
+
98
+ const cycleFocus = React.useCallback((container: unknown, reverse = false) => {
99
+ const focusableElements = getFocusableElements(container);
100
+ if (focusableElements.length === 0) {
101
+ return;
102
+ }
103
+
104
+ const documentRef = getWebDocument();
105
+ const activeElement = documentRef?.activeElement;
106
+ const currentIndex = focusableElements.findIndex((element) => element === activeElement);
107
+
108
+ if (currentIndex === -1) {
109
+ (reverse ? focusableElements[focusableElements.length - 1] : focusableElements[0])?.focus();
110
+ return;
111
+ }
112
+
113
+ const nextIndex = reverse
114
+ ? (currentIndex - 1 + focusableElements.length) % focusableElements.length
115
+ : (currentIndex + 1) % focusableElements.length;
116
+
117
+ focusableElements[nextIndex]?.focus();
118
+ }, []);
119
+
120
+ const bindKeydown = React.useCallback((listener: (event: WebKeyboardEventLike) => void) => {
121
+ const documentRef = getWebDocument();
122
+ if (!documentRef) {
123
+ return () => {
124
+ /* no-op */
125
+ };
126
+ }
127
+
128
+ documentRef.addEventListener('keydown', listener);
129
+
130
+ return () => {
131
+ documentRef.removeEventListener('keydown', listener);
132
+ };
133
+ }, []);
134
+
135
+ return {
136
+ bindKeydown,
137
+ capturePreviousFocus,
138
+ cycleFocus,
139
+ focusFirst,
140
+ restorePreviousFocus,
141
+ };
142
+ }
@@ -0,0 +1,74 @@
1
+ import React from 'react';
2
+ import { View } from 'react-native';
3
+
4
+ import {
5
+ createOverlayEntry,
6
+ type OverlayDescriptor,
7
+ type OverlayEntry,
8
+ OverlayStackContext,
9
+ sortOverlayEntries,
10
+ } from './useOverlayStack';
11
+
12
+ export function OverlayProvider({ children }: { children: React.ReactNode }) {
13
+ const orderRef = React.useRef(0);
14
+ const [overlays, setOverlays] = React.useState<OverlayEntry[]>([]);
15
+
16
+ const setOverlay = React.useCallback((id: string, descriptor: OverlayDescriptor) => {
17
+ setOverlays((current) => {
18
+ const existing = current.find((entry) => entry.id === id);
19
+ const nextEntry = createOverlayEntry(id, existing?.order ?? orderRef.current++, descriptor);
20
+
21
+ if (!existing) {
22
+ return sortOverlayEntries([...current, nextEntry]);
23
+ }
24
+
25
+ return sortOverlayEntries(current.map((entry) => (entry.id === id ? nextEntry : entry)));
26
+ });
27
+ }, []);
28
+
29
+ const removeOverlay = React.useCallback((id: string) => {
30
+ setOverlays((current) => current.filter((entry) => entry.id !== id));
31
+ }, []);
32
+
33
+ const value = React.useMemo(
34
+ () => ({
35
+ overlays,
36
+ removeOverlay,
37
+ setOverlay,
38
+ }),
39
+ [overlays, removeOverlay, setOverlay],
40
+ );
41
+
42
+ return (
43
+ <OverlayStackContext.Provider value={value}>
44
+ {children}
45
+ <View
46
+ pointerEvents="box-none"
47
+ style={{
48
+ bottom: 0,
49
+ left: 0,
50
+ position: 'absolute',
51
+ right: 0,
52
+ top: 0,
53
+ }}
54
+ >
55
+ {overlays.map((overlay) => (
56
+ <View
57
+ key={overlay.id}
58
+ pointerEvents="box-none"
59
+ style={{
60
+ bottom: 0,
61
+ left: 0,
62
+ position: 'absolute',
63
+ right: 0,
64
+ top: 0,
65
+ zIndex: overlay.zIndex,
66
+ }}
67
+ >
68
+ {overlay.node}
69
+ </View>
70
+ ))}
71
+ </View>
72
+ </OverlayStackContext.Provider>
73
+ );
74
+ }
@@ -0,0 +1,38 @@
1
+ import React from 'react';
2
+
3
+ import type { OverlayLayer } from '../resolvers/resolveOverlayZIndex';
4
+ import { useOverlayStack } from './useOverlayStack';
5
+
6
+ let portalCounter = 0;
7
+
8
+ export interface PortalProps {
9
+ children?: React.ReactNode;
10
+ layer?: OverlayLayer;
11
+ visible?: boolean;
12
+ }
13
+
14
+ export function Portal({ children, layer = 'modal', visible = true }: PortalProps) {
15
+ const overlayStack = useOverlayStack();
16
+ const idRef = React.useRef(`surface-portal-${portalCounter++}`);
17
+
18
+ React.useEffect(() => {
19
+ if (!overlayStack || !visible || children === undefined || children === null) {
20
+ return undefined;
21
+ }
22
+
23
+ overlayStack.setOverlay(idRef.current, {
24
+ layer,
25
+ node: children,
26
+ });
27
+
28
+ return () => {
29
+ overlayStack.removeOverlay(idRef.current);
30
+ };
31
+ }, [children, layer, overlayStack, visible]);
32
+
33
+ if (!overlayStack) {
34
+ return visible ? <>{children}</> : null;
35
+ }
36
+
37
+ return null;
38
+ }
@@ -0,0 +1,31 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+
3
+ import { type OverlayEntry, sortOverlayEntries } from './useOverlayStack';
4
+
5
+ describe('sortOverlayEntries', () => {
6
+ it('sorts overlays by z-index then registration order', () => {
7
+ const overlays: OverlayEntry[] = [
8
+ { id: 'menu-1', layer: 'menu', node: null, order: 2, zIndex: 1200 },
9
+ { id: 'modal-1', layer: 'modal', node: null, order: 0, zIndex: 1000 },
10
+ { id: 'menu-0', layer: 'menu', node: null, order: 1, zIndex: 1200 },
11
+ ];
12
+
13
+ expect(sortOverlayEntries(overlays).map((entry) => entry.id)).toEqual([
14
+ 'modal-1',
15
+ 'menu-0',
16
+ 'menu-1',
17
+ ]);
18
+ });
19
+
20
+ it('recomputes per-layer stack indices to avoid z-index collisions after removals', () => {
21
+ const overlays: OverlayEntry[] = [
22
+ { id: 'menu-0', layer: 'menu', node: null, order: 1, zIndex: 1200 },
23
+ { id: 'menu-2', layer: 'menu', node: null, order: 3, zIndex: 1200 },
24
+ ];
25
+
26
+ const sorted = sortOverlayEntries(overlays);
27
+
28
+ expect(sorted[0]?.zIndex).toBe(1200);
29
+ expect(sorted[1]?.zIndex).toBe(1201);
30
+ });
31
+ });
@@ -0,0 +1,61 @@
1
+ import React from 'react';
2
+
3
+ import { type OverlayLayer, resolveOverlayZIndex } from '../resolvers/resolveOverlayZIndex';
4
+
5
+ export interface OverlayEntry {
6
+ id: string;
7
+ layer: OverlayLayer;
8
+ node: React.ReactNode;
9
+ order: number;
10
+ zIndex: number;
11
+ }
12
+
13
+ export interface OverlayDescriptor {
14
+ layer: OverlayLayer;
15
+ node: React.ReactNode;
16
+ }
17
+
18
+ export interface OverlayStackRuntime {
19
+ overlays: OverlayEntry[];
20
+ setOverlay: (id: string, overlay: OverlayDescriptor) => void;
21
+ removeOverlay: (id: string) => void;
22
+ }
23
+
24
+ export function sortOverlayEntries(entries: OverlayEntry[]): OverlayEntry[] {
25
+ const perLayerCounts: Partial<Record<OverlayLayer, number>> = {};
26
+ const normalizedEntries = [...entries]
27
+ .sort((left, right) => left.order - right.order)
28
+ .map((entry) => {
29
+ const stackIndex = perLayerCounts[entry.layer] ?? 0;
30
+ perLayerCounts[entry.layer] = stackIndex + 1;
31
+
32
+ return {
33
+ ...entry,
34
+ zIndex: resolveOverlayZIndex(entry.layer, stackIndex),
35
+ };
36
+ });
37
+
38
+ return normalizedEntries.sort((left, right) =>
39
+ left.zIndex === right.zIndex ? left.order - right.order : left.zIndex - right.zIndex,
40
+ );
41
+ }
42
+
43
+ export const OverlayStackContext = React.createContext<OverlayStackRuntime | null>(null);
44
+
45
+ export function useOverlayStack(): OverlayStackRuntime | null {
46
+ return React.useContext(OverlayStackContext);
47
+ }
48
+
49
+ export function createOverlayEntry(
50
+ id: string,
51
+ order: number,
52
+ descriptor: OverlayDescriptor,
53
+ ): OverlayEntry {
54
+ return {
55
+ id,
56
+ layer: descriptor.layer,
57
+ node: descriptor.node,
58
+ order,
59
+ zIndex: resolveOverlayZIndex(descriptor.layer, 0),
60
+ };
61
+ }