@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,15 @@
1
+ export * from './resolveControlSize';
2
+ export * from './resolveFieldPresentation';
3
+ export * from './resolveFieldState';
4
+ export * from './resolveFocusRingStyles';
5
+ export * from './resolveIconSize';
6
+ export * from './resolveIndicatorSize';
7
+ export * from './resolveInteractiveColors';
8
+ export * from './resolveInteractiveState';
9
+ export * from './resolveOverlayAnimation';
10
+ export * from './resolveOverlayZIndex';
11
+ export * from './resolveSelectionControlBehavior';
12
+ export * from './resolveSelectionControlColors';
13
+ export * from './resolveTextColor';
14
+ export * from './resolveTextStyles';
15
+ export * from './resolveTone';
@@ -0,0 +1,25 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+
3
+ import { createTheme } from '../../theme/createTheme';
4
+ import { resolveControlSize } from './resolveControlSize';
5
+
6
+ describe('resolveControlSize', () => {
7
+ it('returns a fixed control shape for each size', () => {
8
+ const theme = createTheme();
9
+ const small = resolveControlSize(theme, 's');
10
+ const medium = resolveControlSize(theme, 'm');
11
+ const large = resolveControlSize(theme, 'l');
12
+
13
+ expect(small).toEqual({
14
+ minHeight: 32,
15
+ paddingHorizontal: theme.spacing.s,
16
+ paddingVertical: 6,
17
+ borderRadius: theme.radii.m,
18
+ textVariant: 'bodySmall',
19
+ iconSize: 16,
20
+ });
21
+ expect(medium.minHeight).toBe(40);
22
+ expect(large.minHeight).toBe(48);
23
+ expect(large.iconSize).toBe(20);
24
+ });
25
+ });
@@ -0,0 +1,45 @@
1
+ import type { AnkhTheme } from '../../theme/types';
2
+
3
+ export type ControlSize = 's' | 'm' | 'l';
4
+
5
+ export interface ResolvedControlSize {
6
+ minHeight: number;
7
+ paddingHorizontal: number;
8
+ paddingVertical: number;
9
+ borderRadius: number;
10
+ textVariant: 'bodySmall' | 'body';
11
+ iconSize: number;
12
+ }
13
+
14
+ export function resolveControlSize(theme: AnkhTheme, size: ControlSize = 'm'): ResolvedControlSize {
15
+ switch (size) {
16
+ case 's':
17
+ return {
18
+ minHeight: 32,
19
+ paddingHorizontal: theme.spacing.s,
20
+ paddingVertical: 6,
21
+ borderRadius: theme.radii.m,
22
+ textVariant: 'bodySmall',
23
+ iconSize: 16,
24
+ };
25
+ case 'l':
26
+ return {
27
+ minHeight: 48,
28
+ paddingHorizontal: 20,
29
+ paddingVertical: 10,
30
+ borderRadius: theme.radii.l,
31
+ textVariant: 'body',
32
+ iconSize: 20,
33
+ };
34
+ case 'm':
35
+ default:
36
+ return {
37
+ minHeight: 40,
38
+ paddingHorizontal: theme.spacing.m,
39
+ paddingVertical: 8,
40
+ borderRadius: theme.radii.m,
41
+ textVariant: 'body',
42
+ iconSize: 18,
43
+ };
44
+ }
45
+ }
@@ -0,0 +1,31 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+
3
+ import { resolveFieldPresentation } from './resolveFieldPresentation';
4
+ import { resolveFieldState } from './resolveFieldState';
5
+
6
+ describe('resolveFieldPresentation', () => {
7
+ it('keeps label and helper tones aligned for default fields', () => {
8
+ expect(resolveFieldPresentation(resolveFieldState({}))).toEqual({
9
+ helperTone: 'default',
10
+ labelTone: 'default',
11
+ });
12
+ });
13
+
14
+ it('mutes both label and helper text for disabled and read-only fields', () => {
15
+ expect(resolveFieldPresentation(resolveFieldState({ disabled: true }))).toEqual({
16
+ helperTone: 'muted',
17
+ labelTone: 'muted',
18
+ });
19
+ expect(resolveFieldPresentation(resolveFieldState({ readOnly: true }))).toEqual({
20
+ helperTone: 'muted',
21
+ labelTone: 'muted',
22
+ });
23
+ });
24
+
25
+ it('elevates invalid fields to the danger tone', () => {
26
+ expect(resolveFieldPresentation(resolveFieldState({ invalid: true, readOnly: true }))).toEqual({
27
+ helperTone: 'danger',
28
+ labelTone: 'danger',
29
+ });
30
+ });
31
+ });
@@ -0,0 +1,30 @@
1
+ import type { FieldState } from './resolveFieldState';
2
+ import type { TextTone } from './resolveTextColor';
3
+
4
+ type FieldPresentationTone = Extract<TextTone, 'default' | 'muted' | 'danger'>;
5
+
6
+ export interface ResolvedFieldPresentation {
7
+ helperTone: FieldPresentationTone;
8
+ labelTone: FieldPresentationTone;
9
+ }
10
+
11
+ export function resolveFieldPresentation(fieldState: FieldState): ResolvedFieldPresentation {
12
+ if (fieldState.invalid) {
13
+ return {
14
+ helperTone: 'danger',
15
+ labelTone: 'danger',
16
+ };
17
+ }
18
+
19
+ if (fieldState.disabled || fieldState.readOnly) {
20
+ return {
21
+ helperTone: 'muted',
22
+ labelTone: 'muted',
23
+ };
24
+ }
25
+
26
+ return {
27
+ helperTone: 'default',
28
+ labelTone: 'default',
29
+ };
30
+ }
@@ -0,0 +1,22 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+
3
+ import { resolveFieldState } from './resolveFieldState';
4
+
5
+ describe('resolveFieldState', () => {
6
+ it('applies field-state precedence consistently', () => {
7
+ expect(resolveFieldState({})).toEqual({
8
+ name: 'default',
9
+ focused: false,
10
+ disabled: false,
11
+ invalid: false,
12
+ readOnly: false,
13
+ });
14
+
15
+ expect(resolveFieldState({ focused: true }).name).toBe('focused');
16
+ expect(resolveFieldState({ readOnly: true }).name).toBe('readOnly');
17
+ expect(resolveFieldState({ invalid: true, focused: true }).name).toBe('invalid');
18
+ expect(resolveFieldState({ disabled: true, invalid: true, focused: true }).name).toBe(
19
+ 'disabled',
20
+ );
21
+ });
22
+ });
@@ -0,0 +1,36 @@
1
+ export type FieldStateName = 'default' | 'focused' | 'disabled' | 'invalid' | 'readOnly';
2
+
3
+ export interface FieldState {
4
+ name: FieldStateName;
5
+ focused: boolean;
6
+ disabled: boolean;
7
+ invalid: boolean;
8
+ readOnly: boolean;
9
+ }
10
+
11
+ export function resolveFieldState({
12
+ focused = false,
13
+ disabled = false,
14
+ invalid = false,
15
+ readOnly = false,
16
+ }: Partial<Omit<FieldState, 'name'>>): FieldState {
17
+ let name: FieldStateName = 'default';
18
+
19
+ if (disabled) {
20
+ name = 'disabled';
21
+ } else if (invalid) {
22
+ name = 'invalid';
23
+ } else if (focused) {
24
+ name = 'focused';
25
+ } else if (readOnly) {
26
+ name = 'readOnly';
27
+ }
28
+
29
+ return {
30
+ name,
31
+ focused,
32
+ disabled,
33
+ invalid,
34
+ readOnly,
35
+ };
36
+ }
@@ -0,0 +1,14 @@
1
+ import type { ViewStyle } from 'react-native';
2
+
3
+ export function resolveFocusRingStyles(color: string, focused: boolean, isWeb: boolean): ViewStyle {
4
+ if (!focused || !isWeb) {
5
+ return {};
6
+ }
7
+
8
+ return {
9
+ shadowColor: color,
10
+ shadowOpacity: 0.22,
11
+ shadowRadius: 6,
12
+ shadowOffset: { width: 0, height: 0 },
13
+ };
14
+ }
@@ -0,0 +1,6 @@
1
+ import type { AnkhTheme } from '../../theme/types';
2
+ import { type ControlSize, resolveControlSize } from './resolveControlSize';
3
+
4
+ export function resolveIconSize(theme: AnkhTheme, size: ControlSize = 'm'): number {
5
+ return resolveControlSize(theme, size).iconSize;
6
+ }
@@ -0,0 +1,19 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+
3
+ import { resolveIndicatorSize } from './resolveIndicatorSize';
4
+
5
+ describe('resolveIndicatorSize', () => {
6
+ it('returns a stable size map for each control size', () => {
7
+ expect(resolveIndicatorSize('s')).toEqual({
8
+ checkbox: 16,
9
+ checkboxIndicator: 10,
10
+ radio: 16,
11
+ radioDot: 8,
12
+ switchWidth: 30,
13
+ switchHeight: 18,
14
+ switchThumb: 12,
15
+ });
16
+ expect(resolveIndicatorSize('m').switchWidth).toBe(34);
17
+ expect(resolveIndicatorSize('l').switchThumb).toBe(18);
18
+ });
19
+ });
@@ -0,0 +1,47 @@
1
+ import type { ControlSize } from './resolveControlSize';
2
+
3
+ export interface ResolvedIndicatorSize {
4
+ checkbox: number;
5
+ checkboxIndicator: number;
6
+ radio: number;
7
+ radioDot: number;
8
+ switchWidth: number;
9
+ switchHeight: number;
10
+ switchThumb: number;
11
+ }
12
+
13
+ export function resolveIndicatorSize(size: ControlSize = 'm'): ResolvedIndicatorSize {
14
+ switch (size) {
15
+ case 's':
16
+ return {
17
+ checkbox: 16,
18
+ checkboxIndicator: 10,
19
+ radio: 16,
20
+ radioDot: 8,
21
+ switchWidth: 30,
22
+ switchHeight: 18,
23
+ switchThumb: 12,
24
+ };
25
+ case 'l':
26
+ return {
27
+ checkbox: 22,
28
+ checkboxIndicator: 14,
29
+ radio: 22,
30
+ radioDot: 10,
31
+ switchWidth: 42,
32
+ switchHeight: 26,
33
+ switchThumb: 18,
34
+ };
35
+ case 'm':
36
+ default:
37
+ return {
38
+ checkbox: 18,
39
+ checkboxIndicator: 12,
40
+ radio: 18,
41
+ radioDot: 8,
42
+ switchWidth: 34,
43
+ switchHeight: 22,
44
+ switchThumb: 14,
45
+ };
46
+ }
47
+ }
@@ -0,0 +1,57 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+
3
+ import { createTheme } from '../../theme/createTheme';
4
+ import { resolveFieldState } from './resolveFieldState';
5
+ import { resolveButtonColors, resolveInputColors } from './resolveInteractiveColors';
6
+
7
+ describe('resolveInteractiveColors', () => {
8
+ it('maps button variant, tone, and state through semantic slots', () => {
9
+ const theme = createTheme();
10
+ const solid = resolveButtonColors(theme, {
11
+ variant: 'solid',
12
+ tone: 'primary',
13
+ state: {
14
+ disabled: false,
15
+ focused: false,
16
+ hovered: false,
17
+ pressed: false,
18
+ },
19
+ });
20
+ const outline = resolveButtonColors(theme, {
21
+ variant: 'outline',
22
+ tone: 'danger',
23
+ state: {
24
+ disabled: false,
25
+ focused: false,
26
+ hovered: true,
27
+ pressed: false,
28
+ },
29
+ });
30
+
31
+ expect(solid.backgroundColor).toBe(theme.semantics.action.primary.base);
32
+ expect(solid.contentColor).toBe(theme.semantics.action.primary.onSolidText);
33
+ expect(outline.borderColor).toBe(theme.semantics.action.danger.outline);
34
+ expect(outline.backgroundColor).toBe(theme.semantics.action.danger.softHover);
35
+ });
36
+
37
+ it('derives input colors from interaction and validation state', () => {
38
+ const theme = createTheme();
39
+ const focusedInvalid = resolveInputColors(
40
+ theme,
41
+ resolveFieldState({
42
+ focused: true,
43
+ invalid: true,
44
+ }),
45
+ );
46
+ const readOnly = resolveInputColors(
47
+ theme,
48
+ resolveFieldState({
49
+ readOnly: true,
50
+ }),
51
+ );
52
+
53
+ expect(focusedInvalid.borderColor).toBe(theme.semantics.danger.base);
54
+ expect(readOnly.backgroundColor).toBe(theme.semantics.surface.subtle);
55
+ expect(readOnly.placeholderColor).toBe(theme.semantics.content.muted);
56
+ });
57
+ });
@@ -0,0 +1,134 @@
1
+ import type { AnkhTheme } from '../../theme/types';
2
+ import type { FieldState } from './resolveFieldState';
3
+ import type { InteractionState } from './resolveInteractiveState';
4
+ import { type ComponentTone, resolveTone } from './resolveTone';
5
+
6
+ export type ButtonVariant = 'solid' | 'outline' | 'ghost' | 'soft';
7
+
8
+ export interface ResolvedInteractiveColors {
9
+ backgroundColor: string;
10
+ borderColor: string;
11
+ contentColor: string;
12
+ opacity?: number;
13
+ }
14
+
15
+ export function resolveButtonColors(
16
+ theme: AnkhTheme,
17
+ {
18
+ variant,
19
+ tone,
20
+ state,
21
+ }: {
22
+ variant: ButtonVariant;
23
+ tone: ComponentTone;
24
+ state: InteractionState;
25
+ },
26
+ ): ResolvedInteractiveColors {
27
+ if (state.disabled) {
28
+ return {
29
+ backgroundColor: theme.semantics.neutral.surface,
30
+ borderColor: theme.semantics.neutral.divider,
31
+ contentColor: theme.semantics.content.muted,
32
+ opacity: 0.72,
33
+ };
34
+ }
35
+
36
+ const semanticTone = resolveTone(theme, tone);
37
+
38
+ switch (variant) {
39
+ case 'outline':
40
+ return {
41
+ backgroundColor: state.pressed
42
+ ? semanticTone.softActive
43
+ : state.hovered
44
+ ? semanticTone.softHover
45
+ : 'transparent',
46
+ borderColor: semanticTone.outline,
47
+ contentColor: semanticTone.base,
48
+ };
49
+ case 'ghost':
50
+ return {
51
+ backgroundColor: state.pressed
52
+ ? semanticTone.softActive
53
+ : state.hovered
54
+ ? semanticTone.softHover
55
+ : 'transparent',
56
+ borderColor: 'transparent',
57
+ contentColor: semanticTone.base,
58
+ };
59
+ case 'soft':
60
+ return {
61
+ backgroundColor: state.pressed
62
+ ? semanticTone.softActive
63
+ : state.hovered
64
+ ? semanticTone.softHover
65
+ : semanticTone.softBg,
66
+ borderColor: 'transparent',
67
+ contentColor: semanticTone.base,
68
+ };
69
+ case 'solid':
70
+ default:
71
+ return {
72
+ backgroundColor: state.pressed
73
+ ? semanticTone.strong
74
+ : state.hovered
75
+ ? semanticTone.hover
76
+ : semanticTone.base,
77
+ borderColor: semanticTone.base,
78
+ contentColor: semanticTone.onSolidText,
79
+ };
80
+ }
81
+ }
82
+
83
+ export function resolveInputColors(
84
+ theme: AnkhTheme,
85
+ fieldState: FieldState,
86
+ ): ResolvedInteractiveColors & { placeholderColor: string } {
87
+ if (fieldState.disabled) {
88
+ return {
89
+ backgroundColor: theme.semantics.surface.subtle,
90
+ borderColor: theme.semantics.border.default,
91
+ contentColor: theme.semantics.content.muted,
92
+ placeholderColor: theme.semantics.content.subtle,
93
+ opacity: 0.72,
94
+ };
95
+ }
96
+
97
+ if (fieldState.invalid) {
98
+ return {
99
+ backgroundColor: fieldState.readOnly
100
+ ? theme.semantics.surface.subtle
101
+ : theme.semantics.surface.default,
102
+ borderColor: fieldState.focused
103
+ ? theme.semantics.danger.base
104
+ : theme.semantics.danger.outline,
105
+ contentColor: theme.semantics.content.default,
106
+ placeholderColor: theme.semantics.content.muted,
107
+ };
108
+ }
109
+
110
+ if (fieldState.focused) {
111
+ return {
112
+ backgroundColor: theme.semantics.surface.default,
113
+ borderColor: theme.semantics.border.focus,
114
+ contentColor: theme.semantics.content.default,
115
+ placeholderColor: theme.semantics.content.muted,
116
+ };
117
+ }
118
+
119
+ if (fieldState.readOnly) {
120
+ return {
121
+ backgroundColor: theme.semantics.surface.subtle,
122
+ borderColor: theme.semantics.border.default,
123
+ contentColor: theme.semantics.content.default,
124
+ placeholderColor: theme.semantics.content.muted,
125
+ };
126
+ }
127
+
128
+ return {
129
+ backgroundColor: theme.semantics.surface.default,
130
+ borderColor: theme.semantics.border.default,
131
+ contentColor: theme.semantics.content.default,
132
+ placeholderColor: theme.semantics.content.muted,
133
+ };
134
+ }
@@ -0,0 +1,14 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+
3
+ import { resolveInteractiveState } from './resolveInteractiveState';
4
+
5
+ describe('resolveInteractiveState', () => {
6
+ it('freezes the interaction state shape to booleans', () => {
7
+ expect(resolveInteractiveState({ hovered: true, pressed: 1 as never })).toEqual({
8
+ pressed: true,
9
+ hovered: true,
10
+ focused: false,
11
+ disabled: false,
12
+ });
13
+ });
14
+ });
@@ -0,0 +1,15 @@
1
+ export interface InteractionState {
2
+ pressed: boolean;
3
+ hovered: boolean;
4
+ focused: boolean;
5
+ disabled: boolean;
6
+ }
7
+
8
+ export function resolveInteractiveState(input: Partial<InteractionState>): InteractionState {
9
+ return {
10
+ pressed: Boolean(input.pressed),
11
+ hovered: Boolean(input.hovered),
12
+ focused: Boolean(input.focused),
13
+ disabled: Boolean(input.disabled),
14
+ };
15
+ }
@@ -0,0 +1,15 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+
3
+ import { resolveOverlayAnimation } from './resolveOverlayAnimation';
4
+
5
+ describe('resolveOverlayAnimation', () => {
6
+ it('returns stable transition values for each overlay layer', () => {
7
+ const modal = resolveOverlayAnimation('modal');
8
+ const drawer = resolveOverlayAnimation('drawer');
9
+ const tooltip = resolveOverlayAnimation('tooltip');
10
+
11
+ expect(modal.backdropOpacity).toBeGreaterThan(drawer.backdropOpacity);
12
+ expect(drawer.offset).toBeGreaterThan(tooltip.offset);
13
+ expect(tooltip.enterDuration).toBeLessThan(modal.enterDuration);
14
+ });
15
+ });
@@ -0,0 +1,24 @@
1
+ import type { OverlayLayer } from './resolveOverlayZIndex';
2
+
3
+ export interface OverlayAnimationValues {
4
+ enterDuration: number;
5
+ exitDuration: number;
6
+ backdropOpacity: number;
7
+ offset: number;
8
+ }
9
+
10
+ export function resolveOverlayAnimation(layer: OverlayLayer): OverlayAnimationValues {
11
+ switch (layer) {
12
+ case 'drawer':
13
+ return { enterDuration: 180, exitDuration: 140, backdropOpacity: 0.26, offset: 24 };
14
+ case 'tooltip':
15
+ return { enterDuration: 90, exitDuration: 90, backdropOpacity: 0, offset: 8 };
16
+ case 'toast':
17
+ return { enterDuration: 140, exitDuration: 120, backdropOpacity: 0, offset: 12 };
18
+ case 'menu':
19
+ return { enterDuration: 110, exitDuration: 110, backdropOpacity: 0.04, offset: 6 };
20
+ case 'modal':
21
+ default:
22
+ return { enterDuration: 160, exitDuration: 140, backdropOpacity: 0.32, offset: 12 };
23
+ }
24
+ }
@@ -0,0 +1,15 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+
3
+ import { resolveOverlayZIndex } from './resolveOverlayZIndex';
4
+
5
+ describe('resolveOverlayZIndex', () => {
6
+ it('assigns higher base layers to later overlay types', () => {
7
+ expect(resolveOverlayZIndex('modal')).toBeLessThan(resolveOverlayZIndex('menu'));
8
+ expect(resolveOverlayZIndex('menu')).toBeLessThan(resolveOverlayZIndex('tooltip'));
9
+ expect(resolveOverlayZIndex('tooltip')).toBeLessThan(resolveOverlayZIndex('toast'));
10
+ });
11
+
12
+ it('increments the z-index within the same layer stack', () => {
13
+ expect(resolveOverlayZIndex('modal', 1)).toBe(resolveOverlayZIndex('modal', 0) + 1);
14
+ });
15
+ });
@@ -0,0 +1,13 @@
1
+ export type OverlayLayer = 'modal' | 'drawer' | 'menu' | 'tooltip' | 'toast';
2
+
3
+ const BASE_Z_INDEX: Record<OverlayLayer, number> = {
4
+ modal: 1000,
5
+ drawer: 1100,
6
+ menu: 1200,
7
+ tooltip: 1300,
8
+ toast: 1400,
9
+ };
10
+
11
+ export function resolveOverlayZIndex(layer: OverlayLayer, stackIndex = 0): number {
12
+ return BASE_Z_INDEX[layer] + stackIndex;
13
+ }
@@ -0,0 +1,52 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+
3
+ import { resolveSelectionControlNextChecked } from './resolveSelectionControlBehavior';
4
+
5
+ describe('resolveSelectionControlNextChecked', () => {
6
+ it('toggles checkbox and switch values when interactive', () => {
7
+ expect(
8
+ resolveSelectionControlNextChecked({
9
+ checked: false,
10
+ kind: 'checkbox',
11
+ }),
12
+ ).toBe(true);
13
+ expect(
14
+ resolveSelectionControlNextChecked({
15
+ checked: true,
16
+ kind: 'switch',
17
+ }),
18
+ ).toBe(false);
19
+ });
20
+
21
+ it('allows radio selection once but not repeat activation', () => {
22
+ expect(
23
+ resolveSelectionControlNextChecked({
24
+ checked: false,
25
+ kind: 'radio',
26
+ }),
27
+ ).toBe(true);
28
+ expect(
29
+ resolveSelectionControlNextChecked({
30
+ checked: true,
31
+ kind: 'radio',
32
+ }),
33
+ ).toBeNull();
34
+ });
35
+
36
+ it('keeps read-only and disabled controls focusable but non-mutating', () => {
37
+ expect(
38
+ resolveSelectionControlNextChecked({
39
+ checked: false,
40
+ kind: 'checkbox',
41
+ readOnly: true,
42
+ }),
43
+ ).toBeNull();
44
+ expect(
45
+ resolveSelectionControlNextChecked({
46
+ checked: false,
47
+ disabled: true,
48
+ kind: 'switch',
49
+ }),
50
+ ).toBeNull();
51
+ });
52
+ });
@@ -0,0 +1,23 @@
1
+ export type SelectionControlKind = 'checkbox' | 'radio' | 'switch';
2
+
3
+ export function resolveSelectionControlNextChecked({
4
+ kind,
5
+ checked,
6
+ disabled = false,
7
+ readOnly = false,
8
+ }: {
9
+ kind: SelectionControlKind;
10
+ checked: boolean;
11
+ disabled?: boolean;
12
+ readOnly?: boolean;
13
+ }): boolean | null {
14
+ if (disabled || readOnly) {
15
+ return null;
16
+ }
17
+
18
+ if (kind === 'radio') {
19
+ return checked ? null : true;
20
+ }
21
+
22
+ return !checked;
23
+ }