@ankhorage/surface 0.1.5 → 0.1.7

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/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 +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,44 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+
3
+ import { createTheme } from '../../theme/createTheme';
4
+ import { resolveFieldState } from './resolveFieldState';
5
+ import { resolveSelectionControlColors } from './resolveSelectionControlColors';
6
+
7
+ describe('resolveSelectionControlColors', () => {
8
+ it('maps checked controls through the selected tone semantics', () => {
9
+ const theme = createTheme();
10
+ const colors = resolveSelectionControlColors(theme, {
11
+ checked: true,
12
+ fieldState: resolveFieldState({ focused: true }),
13
+ tone: 'primary',
14
+ });
15
+
16
+ expect(colors.borderColor).toBe(theme.semantics.action.primary.base);
17
+ expect(colors.indicatorColor).toBe(theme.semantics.action.primary.onSolidText);
18
+ expect(colors.labelColor).toBe(theme.semantics.content.default);
19
+ });
20
+
21
+ it('maps invalid unchecked controls through danger/border semantics', () => {
22
+ const theme = createTheme();
23
+ const colors = resolveSelectionControlColors(theme, {
24
+ checked: false,
25
+ fieldState: resolveFieldState({ invalid: true }),
26
+ tone: 'primary',
27
+ });
28
+
29
+ expect(colors.borderColor).toBe(theme.semantics.danger.outline);
30
+ expect(colors.trackColor).toBe(theme.semantics.danger.outline);
31
+ });
32
+
33
+ it('mutes disabled controls regardless of tone', () => {
34
+ const theme = createTheme();
35
+ const colors = resolveSelectionControlColors(theme, {
36
+ checked: true,
37
+ fieldState: resolveFieldState({ disabled: true }),
38
+ tone: 'danger',
39
+ });
40
+
41
+ expect(colors.labelColor).toBe(theme.semantics.content.muted);
42
+ expect(colors.opacity).toBe(0.72);
43
+ });
44
+ });
@@ -0,0 +1,81 @@
1
+ import type { AnkhTheme } from '../../theme/types';
2
+ import type { FieldState } from './resolveFieldState';
3
+ import { type ComponentTone, resolveTone } from './resolveTone';
4
+
5
+ export interface ResolvedSelectionControlColors {
6
+ backgroundColor: string;
7
+ borderColor: string;
8
+ indicatorColor: string;
9
+ labelColor: string;
10
+ trackColor: string;
11
+ thumbColor: string;
12
+ opacity?: number;
13
+ }
14
+
15
+ export function resolveSelectionControlColors(
16
+ theme: AnkhTheme,
17
+ {
18
+ tone = 'primary',
19
+ fieldState,
20
+ checked,
21
+ hovered = false,
22
+ pressed = false,
23
+ }: {
24
+ tone?: ComponentTone;
25
+ fieldState: FieldState;
26
+ checked: boolean;
27
+ hovered?: boolean;
28
+ pressed?: boolean;
29
+ },
30
+ ): ResolvedSelectionControlColors {
31
+ const semanticTone = resolveTone(theme, fieldState.invalid ? 'danger' : tone);
32
+ const isMuted = fieldState.disabled;
33
+ const isInteractiveReadOnly = fieldState.readOnly && !fieldState.disabled;
34
+
35
+ if (isMuted) {
36
+ return {
37
+ backgroundColor: checked
38
+ ? theme.semantics.neutral.surfaceActive
39
+ : theme.semantics.surface.subtle,
40
+ borderColor: theme.semantics.border.default,
41
+ indicatorColor: checked ? theme.semantics.content.muted : 'transparent',
42
+ labelColor: theme.semantics.content.muted,
43
+ trackColor: checked ? theme.semantics.neutral.surfaceActive : theme.semantics.neutral.border,
44
+ thumbColor: theme.semantics.surface.default,
45
+ opacity: 0.72,
46
+ };
47
+ }
48
+
49
+ const uncheckedBorderColor = fieldState.invalid
50
+ ? theme.semantics.danger.outline
51
+ : fieldState.focused
52
+ ? theme.semantics.border.focus
53
+ : theme.semantics.border.strong;
54
+ const uncheckedBackgroundColor = pressed
55
+ ? theme.semantics.neutral.surfaceActive
56
+ : hovered
57
+ ? theme.semantics.neutral.surfaceHover
58
+ : theme.semantics.surface.default;
59
+ const checkedBackgroundColor = pressed
60
+ ? semanticTone.strong
61
+ : hovered
62
+ ? semanticTone.hover
63
+ : semanticTone.base;
64
+ const checkedTrackColor = pressed
65
+ ? semanticTone.strong
66
+ : hovered
67
+ ? semanticTone.hover
68
+ : semanticTone.base;
69
+
70
+ return {
71
+ backgroundColor: checked ? checkedBackgroundColor : uncheckedBackgroundColor,
72
+ borderColor: checked ? semanticTone.base : uncheckedBorderColor,
73
+ indicatorColor: checked ? semanticTone.onSolidText : 'transparent',
74
+ labelColor: isInteractiveReadOnly
75
+ ? theme.semantics.content.muted
76
+ : theme.semantics.content.default,
77
+ trackColor: checked ? checkedTrackColor : uncheckedBorderColor,
78
+ thumbColor: checked ? semanticTone.onSolidText : theme.semantics.surface.default,
79
+ opacity: isInteractiveReadOnly ? 0.88 : 1,
80
+ };
81
+ }
@@ -0,0 +1,23 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+
3
+ import { createTheme } from '../../theme/createTheme';
4
+ import { resolveTextColor } from './resolveTextColor';
5
+
6
+ describe('resolveTextColor', () => {
7
+ it('maps semantic text tones through content aliases', () => {
8
+ const theme = createTheme();
9
+
10
+ expect(resolveTextColor(theme, 'default')).toBe(theme.semantics.content.default);
11
+ expect(resolveTextColor(theme, 'muted')).toBe(theme.semantics.content.muted);
12
+ expect(resolveTextColor(theme, 'subtle')).toBe(theme.semantics.content.subtle);
13
+ expect(resolveTextColor(theme, 'inverse')).toBe(theme.semantics.content.inverse);
14
+ });
15
+
16
+ it('supports semantic status tones for shared field and messaging copy', () => {
17
+ const theme = createTheme();
18
+
19
+ expect(resolveTextColor(theme, 'danger')).toBe(theme.semantics.danger.base);
20
+ expect(resolveTextColor(theme, 'success')).toBe(theme.semantics.success.base);
21
+ expect(resolveTextColor(theme, 'warning')).toBe(theme.semantics.warning.base);
22
+ });
23
+ });
@@ -0,0 +1,40 @@
1
+ import { resolveToken } from '../../theme/resolveToken';
2
+ import type { AnkhTheme } from '../../theme/types';
3
+
4
+ export type TextTone =
5
+ | 'default'
6
+ | 'muted'
7
+ | 'subtle'
8
+ | 'inverse'
9
+ | 'danger'
10
+ | 'success'
11
+ | 'warning';
12
+ export type TextColorValue = keyof AnkhTheme['colors'] | string;
13
+
14
+ export function resolveTextColor(
15
+ theme: AnkhTheme,
16
+ tone: TextTone = 'default',
17
+ color?: TextColorValue,
18
+ ): string {
19
+ if (color) {
20
+ return resolveToken(theme.colors, color);
21
+ }
22
+
23
+ switch (tone) {
24
+ case 'muted':
25
+ return theme.semantics.content.muted;
26
+ case 'subtle':
27
+ return theme.semantics.content.subtle;
28
+ case 'inverse':
29
+ return theme.semantics.content.inverse;
30
+ case 'danger':
31
+ return theme.semantics.danger.base;
32
+ case 'success':
33
+ return theme.semantics.success.base;
34
+ case 'warning':
35
+ return theme.semantics.warning.base;
36
+ case 'default':
37
+ default:
38
+ return theme.semantics.content.default;
39
+ }
40
+ }
@@ -0,0 +1,27 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+
3
+ import { createTheme } from '../../theme/createTheme';
4
+ import { resolveTextStyles } from './resolveTextStyles';
5
+
6
+ describe('resolveTextStyles', () => {
7
+ it('maps body and label variants to theme-driven typography', () => {
8
+ const theme = createTheme();
9
+
10
+ const body = resolveTextStyles(theme, { variant: 'body' });
11
+ const label = resolveTextStyles(theme, { variant: 'label' });
12
+
13
+ expect(body.fontSize).toBe(theme.typography.sizes.m);
14
+ expect(body.lineHeight).toBe(24);
15
+ expect(label.fontSize).toBe(theme.typography.sizes.s);
16
+ expect(label.fontWeight).toBe(theme.typography.weights.medium);
17
+ });
18
+
19
+ it('resolves heading levels through the shared text resolver', () => {
20
+ const theme = createTheme(undefined, 'light', 'space grotesk');
21
+ const style = resolveTextStyles(theme, { level: 1 });
22
+
23
+ expect(style.fontSize).toBe(theme.typography.headings[1].size);
24
+ expect(style.fontWeight).toBe(theme.typography.weights.bold);
25
+ expect(style.fontFamily).toBe('SpaceGrotesk_700Regular');
26
+ });
27
+ });
@@ -0,0 +1,95 @@
1
+ import type { TextStyle } from 'react-native';
2
+
3
+ import type { AnkhTheme, FontWeight } from '../../theme/types';
4
+
5
+ export type TextVariant = 'body' | 'bodySmall' | 'caption' | 'label' | 'mono';
6
+ export type TextWeight = keyof AnkhTheme['typography']['weights'] | FontWeight;
7
+
8
+ interface VariantStyle {
9
+ fontSize: number;
10
+ lineHeight: number;
11
+ weight: TextWeight;
12
+ }
13
+
14
+ export interface ResolveTextStyleOptions {
15
+ variant?: TextVariant;
16
+ align?: TextStyle['textAlign'];
17
+ weight?: TextWeight;
18
+ italic?: boolean;
19
+ level?: 1 | 2 | 3 | 4 | 5 | 6;
20
+ }
21
+
22
+ function resolveWeight(theme: AnkhTheme, value: TextWeight | undefined): FontWeight {
23
+ if (!value) return theme.typography.weights.regular;
24
+ if (value in theme.typography.weights) {
25
+ return theme.typography.weights[value as keyof AnkhTheme['typography']['weights']];
26
+ }
27
+ return value as FontWeight;
28
+ }
29
+
30
+ function getVariantStyle(theme: AnkhTheme, variant: TextVariant): VariantStyle {
31
+ switch (variant) {
32
+ case 'bodySmall':
33
+ return {
34
+ fontSize: theme.typography.sizes.s,
35
+ lineHeight: 20,
36
+ weight: 'regular',
37
+ };
38
+ case 'caption':
39
+ return {
40
+ fontSize: theme.typography.sizes.xs,
41
+ lineHeight: 16,
42
+ weight: 'regular',
43
+ };
44
+ case 'label':
45
+ return {
46
+ fontSize: theme.typography.sizes.s,
47
+ lineHeight: 18,
48
+ weight: 'medium',
49
+ };
50
+ case 'mono':
51
+ return {
52
+ fontSize: theme.typography.sizes.s,
53
+ lineHeight: 20,
54
+ weight: 'regular',
55
+ };
56
+ case 'body':
57
+ default:
58
+ return {
59
+ fontSize: theme.typography.sizes.m,
60
+ lineHeight: 24,
61
+ weight: 'regular',
62
+ };
63
+ }
64
+ }
65
+
66
+ export function resolveTextStyles(
67
+ theme: AnkhTheme,
68
+ options: ResolveTextStyleOptions = {},
69
+ ): TextStyle {
70
+ const { level, variant = 'body', align, weight, italic = false } = options;
71
+ const headingTypography = level ? theme.typography.headings[level] : null;
72
+ const variantStyle = headingTypography
73
+ ? {
74
+ fontSize: headingTypography.size,
75
+ lineHeight: headingTypography.lineHeight,
76
+ weight: headingTypography.weight,
77
+ }
78
+ : getVariantStyle(theme, variant);
79
+
80
+ const resolvedWeight = resolveWeight(theme, weight ?? variantStyle.weight);
81
+ const isMono = !level && variant === 'mono';
82
+ const fontFamily = isMono
83
+ ? 'monospace'
84
+ : theme.typography.fonts[italic ? 'italic' : 'normal'][resolvedWeight];
85
+
86
+ return {
87
+ fontSize: variantStyle.fontSize,
88
+ lineHeight: variantStyle.lineHeight,
89
+ fontWeight: resolvedWeight,
90
+ fontFamily,
91
+ fontStyle: italic ? 'italic' : 'normal',
92
+ textAlign: align,
93
+ elevation: 0,
94
+ };
95
+ }
@@ -0,0 +1,19 @@
1
+ import type { AnkhTheme, RoleSemantics } from '../../theme/types';
2
+
3
+ export type ComponentTone = 'primary' | 'neutral' | 'danger' | 'success' | 'warning';
4
+
5
+ export function resolveTone(theme: AnkhTheme, tone: ComponentTone = 'primary'): RoleSemantics {
6
+ switch (tone) {
7
+ case 'neutral':
8
+ return theme.semantics.action.neutral;
9
+ case 'danger':
10
+ return theme.semantics.action.danger;
11
+ case 'success':
12
+ return theme.semantics.success;
13
+ case 'warning':
14
+ return theme.semantics.warning;
15
+ case 'primary':
16
+ default:
17
+ return theme.semantics.action.primary;
18
+ }
19
+ }
@@ -0,0 +1,28 @@
1
+ import React from 'react';
2
+
3
+ export function useControllableState<T>({
4
+ value,
5
+ defaultValue,
6
+ onChange,
7
+ }: {
8
+ value: T | undefined;
9
+ defaultValue: T;
10
+ onChange?: ((nextValue: T) => void) | undefined;
11
+ }) {
12
+ const [uncontrolledValue, setUncontrolledValue] = React.useState(defaultValue);
13
+ const isControlled = value !== undefined;
14
+ const currentValue = isControlled ? value : uncontrolledValue;
15
+
16
+ const setValue = React.useCallback(
17
+ (nextValue: T) => {
18
+ if (!isControlled) {
19
+ setUncontrolledValue(nextValue);
20
+ }
21
+
22
+ onChange?.(nextValue);
23
+ },
24
+ [isControlled, onChange],
25
+ );
26
+
27
+ return [currentValue, setValue] as const;
28
+ }
@@ -0,0 +1,79 @@
1
+ import React from 'react';
2
+ import {
3
+ Platform,
4
+ type StyleProp,
5
+ View,
6
+ type ViewProps as ReactNativeViewProps,
7
+ type ViewStyle,
8
+ } from 'react-native';
9
+
10
+ import { useResponsiveRuntime } from '../core/responsive';
11
+ import { useTheme } from '../theme/ThemeContext';
12
+ import { type BoxStyleProps, resolveBoxStyles } from './helpers';
13
+
14
+ export interface BoxProps extends BoxStyleProps {
15
+ accessibilityLabel?: ReactNativeViewProps['accessibilityLabel'];
16
+ accessibilityRole?: ReactNativeViewProps['accessibilityRole'];
17
+ accessibilityState?: ReactNativeViewProps['accessibilityState'];
18
+ accessible?: ReactNativeViewProps['accessible'];
19
+ children?: React.ReactNode;
20
+ pointerEvents?: ReactNativeViewProps['pointerEvents'];
21
+ testID?: string;
22
+ }
23
+
24
+ function resolvePointerEventsStyle(
25
+ pointerEvents: ReactNativeViewProps['pointerEvents'] | undefined,
26
+ ): StyleProp<ViewStyle> | null {
27
+ if (Platform.OS !== 'web' || pointerEvents === undefined) return null;
28
+
29
+ if (pointerEvents === 'auto' || pointerEvents === 'none') {
30
+ return { pointerEvents };
31
+ }
32
+
33
+ return null;
34
+ }
35
+
36
+ function resolveViewPointerEvents(
37
+ pointerEvents: ReactNativeViewProps['pointerEvents'] | undefined,
38
+ ): ReactNativeViewProps['pointerEvents'] | undefined {
39
+ if (pointerEvents === undefined) return undefined;
40
+ if (Platform.OS !== 'web') return pointerEvents;
41
+
42
+ if (pointerEvents === 'box-none' || pointerEvents === 'box-only') {
43
+ return pointerEvents;
44
+ }
45
+
46
+ return undefined;
47
+ }
48
+
49
+ export function Box({
50
+ accessible,
51
+ accessibilityLabel,
52
+ accessibilityRole,
53
+ accessibilityState,
54
+ children,
55
+ pointerEvents,
56
+ style,
57
+ testID,
58
+ ...props
59
+ }: BoxProps) {
60
+ const { theme } = useTheme();
61
+ const { breakpoint } = useResponsiveRuntime();
62
+ const resolved = resolveBoxStyles(theme, breakpoint, props);
63
+ const pointerEventsStyle = resolvePointerEventsStyle(pointerEvents);
64
+ const viewPointerEvents = resolveViewPointerEvents(pointerEvents);
65
+
66
+ return (
67
+ <View
68
+ accessible={accessible}
69
+ accessibilityLabel={accessibilityLabel}
70
+ accessibilityRole={accessibilityRole}
71
+ accessibilityState={accessibilityState}
72
+ pointerEvents={viewPointerEvents}
73
+ testID={testID}
74
+ style={[resolved, pointerEventsStyle, style]}
75
+ >
76
+ {children}
77
+ </View>
78
+ );
79
+ }
@@ -0,0 +1,22 @@
1
+ import React from 'react';
2
+
3
+ import { Box, type BoxProps } from './Box';
4
+
5
+ export interface CenterProps extends BoxProps {
6
+ axis?: 'horizontal' | 'vertical' | 'both';
7
+ }
8
+
9
+ export function Center({ axis = 'both', style, ...props }: CenterProps) {
10
+ return (
11
+ <Box
12
+ {...props}
13
+ style={[
14
+ {
15
+ alignItems: axis === 'both' || axis === 'horizontal' ? 'center' : undefined,
16
+ justifyContent: axis === 'both' || axis === 'vertical' ? 'center' : undefined,
17
+ },
18
+ style,
19
+ ]}
20
+ />
21
+ );
22
+ }
@@ -0,0 +1,43 @@
1
+ import React from 'react';
2
+ import { View } from 'react-native';
3
+
4
+ import { resolveResponsive, type Responsive, useResponsiveRuntime } from '../core/responsive';
5
+ import { useTheme } from '../theme/ThemeContext';
6
+ import type { AnkhTheme } from '../theme/types';
7
+ import { Box, type BoxProps } from './Box';
8
+ import { resolveSpacing } from './helpers';
9
+
10
+ export interface ContainerProps extends Omit<BoxProps, 'children'> {
11
+ children?: React.ReactNode;
12
+ maxWidth?: Responsive<number>;
13
+ px?: Responsive<number | keyof AnkhTheme['spacing']>;
14
+ }
15
+
16
+ export function Container({
17
+ children,
18
+ maxWidth = { base: Number.MAX_SAFE_INTEGER, lg: 1120 },
19
+ px = { base: 16, md: 24, lg: 32 },
20
+ ...props
21
+ }: ContainerProps) {
22
+ const { theme } = useTheme();
23
+ const { breakpoint } = useResponsiveRuntime();
24
+
25
+ const activeMaxWidth = resolveResponsive(maxWidth, breakpoint);
26
+ const activePx = resolveSpacing(theme, resolveResponsive(px, breakpoint));
27
+
28
+ return (
29
+ <Box {...props} width="100%" style={[{ width: '100%' }, props.style]}>
30
+ <View
31
+ style={{
32
+ width: '100%',
33
+ maxWidth: activeMaxWidth,
34
+ alignSelf: 'center',
35
+ paddingLeft: activePx,
36
+ paddingRight: activePx,
37
+ }}
38
+ >
39
+ {children}
40
+ </View>
41
+ </Box>
42
+ );
43
+ }
@@ -0,0 +1,26 @@
1
+ import React from 'react';
2
+
3
+ import { Box, type BoxProps } from './Box';
4
+ import type { ColorValue } from './helpers';
5
+
6
+ export interface DividerProps extends Omit<BoxProps, 'bg' | 'height' | 'width'> {
7
+ orientation?: 'horizontal' | 'vertical';
8
+ color?: ColorValue;
9
+ thickness?: number;
10
+ }
11
+
12
+ export function Divider({
13
+ orientation = 'horizontal',
14
+ color = 'border',
15
+ thickness = 1,
16
+ ...props
17
+ }: DividerProps) {
18
+ return (
19
+ <Box
20
+ {...props}
21
+ bg={color}
22
+ height={orientation === 'horizontal' ? thickness : '100%'}
23
+ width={orientation === 'vertical' ? thickness : '100%'}
24
+ />
25
+ );
26
+ }
@@ -0,0 +1,83 @@
1
+ import React from 'react';
2
+ import { View, type ViewStyle } from 'react-native';
3
+
4
+ import { resolveResponsive, type Responsive, useResponsiveRuntime } from '../core/responsive';
5
+ import { useTheme } from '../theme/ThemeContext';
6
+ import { Box, type BoxProps } from './Box';
7
+ import { resolveSpacing, type SpaceValue } from './helpers';
8
+
9
+ export interface GridProps extends Omit<BoxProps, 'children'> {
10
+ children?: React.ReactNode;
11
+ cols: Responsive<number>;
12
+ gap?: Responsive<SpaceValue>;
13
+ rowGap?: Responsive<SpaceValue>;
14
+ colGap?: Responsive<SpaceValue>;
15
+ minItemWidth?: Responsive<number>;
16
+ }
17
+
18
+ export function Grid({
19
+ children,
20
+ cols,
21
+ gap = 0,
22
+ rowGap,
23
+ colGap,
24
+ minItemWidth,
25
+ ...props
26
+ }: GridProps) {
27
+ const { theme } = useTheme();
28
+ const { breakpoint } = useResponsiveRuntime();
29
+
30
+ const activeCols = Math.max(1, Math.floor(resolveResponsive(cols, breakpoint) ?? 1));
31
+ const defaultGap = resolveResponsive(gap, breakpoint) ?? 0;
32
+ const activeRowGap = resolveResponsive(rowGap, breakpoint) ?? defaultGap;
33
+ const activeColGap = resolveResponsive(colGap, breakpoint) ?? defaultGap;
34
+ const rowSpacing = Number(resolveSpacing(theme, activeRowGap) ?? 0);
35
+ const colSpacing = Number(resolveSpacing(theme, activeColGap) ?? 0);
36
+ const activeMinItemWidth = resolveResponsive(minItemWidth, breakpoint);
37
+
38
+ const basisPercent = `${100 / activeCols}%`;
39
+ const nodes = React.Children.toArray(children);
40
+
41
+ return (
42
+ <Box {...props}>
43
+ <View
44
+ style={{
45
+ flexDirection: 'row',
46
+ flexWrap: 'wrap',
47
+ marginTop: -rowSpacing / 2,
48
+ marginLeft: -colSpacing / 2,
49
+ marginRight: -colSpacing / 2,
50
+ }}
51
+ >
52
+ {nodes.map((node, index) => {
53
+ const itemStyle: ViewStyle =
54
+ activeMinItemWidth !== undefined
55
+ ? {
56
+ minWidth: activeMinItemWidth,
57
+ flexBasis: activeMinItemWidth,
58
+ flexGrow: 1,
59
+ }
60
+ : {
61
+ width: basisPercent as ViewStyle['width'],
62
+ flexBasis: basisPercent as ViewStyle['flexBasis'],
63
+ };
64
+
65
+ return (
66
+ <View
67
+ key={String(index)}
68
+ style={{
69
+ paddingTop: rowSpacing / 2,
70
+ paddingBottom: rowSpacing / 2,
71
+ paddingLeft: colSpacing / 2,
72
+ paddingRight: colSpacing / 2,
73
+ ...itemStyle,
74
+ }}
75
+ >
76
+ {node}
77
+ </View>
78
+ );
79
+ })}
80
+ </View>
81
+ </Box>
82
+ );
83
+ }
@@ -0,0 +1,9 @@
1
+ import React from 'react';
2
+
3
+ import { Stack, type StackProps } from './Stack';
4
+
5
+ export type InlineProps = Omit<StackProps, 'direction'>;
6
+
7
+ export function Inline({ wrap = 'wrap', align = 'center', ...props }: InlineProps) {
8
+ return <Stack {...props} align={align} direction="row" wrap={wrap} />;
9
+ }
@@ -0,0 +1,15 @@
1
+ import React from 'react';
2
+
3
+ import { resolveResponsive, type Responsive, useResponsiveRuntime } from '../core/responsive';
4
+
5
+ export interface ShowProps {
6
+ when: Responsive<boolean>;
7
+ children: React.ReactNode;
8
+ fallback?: React.ReactNode;
9
+ }
10
+
11
+ export function Show({ when, children, fallback = null }: ShowProps) {
12
+ const { breakpoint } = useResponsiveRuntime();
13
+ const visible = resolveResponsive(when, breakpoint) ?? false;
14
+ return <>{visible ? children : fallback}</>;
15
+ }
@@ -0,0 +1,22 @@
1
+ import React from 'react';
2
+
3
+ import { Box } from './Box';
4
+ import type { SpaceValue } from './helpers';
5
+
6
+ export interface SpacerProps {
7
+ size?: SpaceValue;
8
+ axis?: 'horizontal' | 'vertical' | 'both';
9
+ testID?: string;
10
+ }
11
+
12
+ export function Spacer({ size = 'm', axis = 'vertical', testID }: SpacerProps) {
13
+ if (axis === 'horizontal') {
14
+ return <Box testID={testID} width={size} />;
15
+ }
16
+
17
+ if (axis === 'both') {
18
+ return <Box testID={testID} height={size} width={size} />;
19
+ }
20
+
21
+ return <Box testID={testID} height={size} />;
22
+ }