@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,31 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+
3
+ import { createTheme } from '../../theme/createTheme';
4
+ import { resolveHeadingTextStyle } from './resolveHeadingStyle';
5
+
6
+ describe('resolveHeadingTextStyle', () => {
7
+ it('maps heading levels to theme-driven typography tokens', () => {
8
+ const theme = createTheme();
9
+ const style = resolveHeadingTextStyle(theme, 4, 'center');
10
+
11
+ expect(style.fontSize).toBe(18);
12
+ expect(style.lineHeight).toBe(24);
13
+ expect(style.fontWeight).toBe('600');
14
+ expect(style.color).toBe(theme.colors.text);
15
+ expect(style.textAlign).toBe('center');
16
+ });
17
+
18
+ it('uses the themed font family when one is configured', () => {
19
+ const theme = createTheme(undefined, 'light', 'space grotesk');
20
+ const style = resolveHeadingTextStyle(theme, 1);
21
+
22
+ expect(style.fontFamily).toBe('SpaceGrotesk_700Regular');
23
+ });
24
+
25
+ it('shares the default content tone with the text primitive', () => {
26
+ const theme = createTheme();
27
+ const style = resolveHeadingTextStyle(theme, 2);
28
+
29
+ expect(style.color).toBe(theme.semantics.content.default);
30
+ });
31
+ });
@@ -0,0 +1,17 @@
1
+ import type { TextStyle } from 'react-native';
2
+
3
+ import { resolveTextColor } from '../../internal/resolvers/resolveTextColor';
4
+ import { resolveTextStyles } from '../../internal/resolvers/resolveTextStyles';
5
+ import type { AnkhTheme } from '../../theme/types';
6
+ import type { HeadingLevel, HeadingProps } from './types';
7
+
8
+ export function resolveHeadingTextStyle(
9
+ theme: AnkhTheme,
10
+ level: HeadingLevel,
11
+ align?: HeadingProps['align'],
12
+ ): TextStyle {
13
+ return {
14
+ ...resolveTextStyles(theme, { align, level }),
15
+ color: resolveTextColor(theme, 'default'),
16
+ };
17
+ }
@@ -0,0 +1,13 @@
1
+ import type React from 'react';
2
+
3
+ export type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6;
4
+
5
+ export interface HeadingProps {
6
+ text?: string;
7
+ children?: React.ReactNode;
8
+ level?: HeadingLevel;
9
+ align?: 'auto' | 'left' | 'right' | 'center' | 'justify';
10
+ numberOfLines?: number;
11
+ i18nKey?: string;
12
+ testID?: string;
13
+ }
@@ -0,0 +1,40 @@
1
+ import React from 'react';
2
+ import { type StyleProp, type TextStyle } from 'react-native';
3
+
4
+ import { resolveToken } from '../../theme/resolveToken';
5
+ import { useTheme } from '../../theme/ThemeContext';
6
+ import type { AnkhTheme } from '../../theme/types';
7
+ import { resolveExpoIconComponent } from './resolveExpoIconComponent';
8
+
9
+ export type IconProvider = string;
10
+
11
+ export interface IconProps {
12
+ name: string;
13
+ provider?: IconProvider;
14
+ size?: keyof AnkhTheme['spacing'] | number;
15
+ color?: keyof AnkhTheme['colors'] | string;
16
+ style?: StyleProp<TextStyle>;
17
+ testID?: string;
18
+ }
19
+
20
+ export function Icon({
21
+ name,
22
+ provider = 'Ionicons',
23
+ size = 'm',
24
+ color = 'text',
25
+ style,
26
+ testID,
27
+ }: IconProps) {
28
+ const { theme } = useTheme();
29
+ const IconComponent = resolveExpoIconComponent(provider);
30
+ const resolvedSize = typeof size === 'number' ? size : resolveToken(theme.spacing, size);
31
+ const resolvedColor = resolveToken(theme.colors, color);
32
+
33
+ return React.createElement(IconComponent, {
34
+ color: resolvedColor,
35
+ name,
36
+ size: resolvedSize,
37
+ style,
38
+ testID,
39
+ });
40
+ }
@@ -0,0 +1,2 @@
1
+ export type { IconProps, IconProvider } from './Icon';
2
+ export { Icon } from './Icon';
@@ -0,0 +1,29 @@
1
+ import { describe, expect, mock, test } from 'bun:test';
2
+
3
+ const Ionicons = () => null;
4
+ const MaterialIcons = () => null;
5
+
6
+ describe('resolveExpoIconComponent', () => {
7
+ test('returns the requested Expo icon family when it exists', async () => {
8
+ await mock.module('@expo/vector-icons', () => ({
9
+ Ionicons,
10
+ MaterialIcons,
11
+ }));
12
+
13
+ const { resolveExpoIconComponent } = await import('./resolveExpoIconComponent');
14
+
15
+ expect(resolveExpoIconComponent('Ionicons')).toBe(Ionicons);
16
+ expect(resolveExpoIconComponent('MaterialIcons')).toBe(MaterialIcons);
17
+ });
18
+
19
+ test('falls back to Ionicons when the provider is unknown', async () => {
20
+ await mock.module('@expo/vector-icons', () => ({
21
+ Ionicons,
22
+ MaterialIcons,
23
+ }));
24
+
25
+ const { resolveExpoIconComponent } = await import('./resolveExpoIconComponent');
26
+
27
+ expect(resolveExpoIconComponent('MissingIconFamily')).toBe(Ionicons);
28
+ });
29
+ });
@@ -0,0 +1,20 @@
1
+ import * as ExpoIcons from '@expo/vector-icons';
2
+ import type React from 'react';
3
+ import { type StyleProp, type TextStyle } from 'react-native';
4
+
5
+ export type ExpoIconComponent = React.ElementType<{
6
+ color?: string;
7
+ name?: string;
8
+ size?: number;
9
+ style?: StyleProp<TextStyle>;
10
+ testID?: string;
11
+ }>;
12
+
13
+ export function resolveExpoIconComponent(provider: string): ExpoIconComponent {
14
+ const candidate = (ExpoIcons as Record<string, unknown>)[provider];
15
+ if (typeof candidate === 'function') {
16
+ return candidate as ExpoIconComponent;
17
+ }
18
+
19
+ return ExpoIcons.Ionicons as ExpoIconComponent;
20
+ }
@@ -0,0 +1,66 @@
1
+ import React from 'react';
2
+ import { Text as ReactNativeText } from 'react-native';
3
+
4
+ import { useTranslationContext } from '../../context/TranslationContext';
5
+ import { resolveTextColor, resolveTextStyles } from '../../internal/resolvers';
6
+ import { useTheme } from '../../theme/ThemeContext';
7
+ import type { TextProps } from './types';
8
+
9
+ function resolveTextContent(
10
+ children: TextProps['children'],
11
+ i18nKey: TextProps['i18nKey'],
12
+ translate: (key: string) => string,
13
+ ) {
14
+ if (children !== undefined) {
15
+ return children;
16
+ }
17
+
18
+ if (!i18nKey) {
19
+ return null;
20
+ }
21
+
22
+ try {
23
+ const translated = translate(i18nKey);
24
+ return translated && translated !== i18nKey ? translated : i18nKey;
25
+ } catch (error) {
26
+ console.warn('[Text] Translation error:', error);
27
+ return i18nKey;
28
+ }
29
+ }
30
+
31
+ export function Text({
32
+ children,
33
+ i18nKey,
34
+ variant = 'body',
35
+ tone = 'default',
36
+ color,
37
+ align,
38
+ weight,
39
+ italic = false,
40
+ numberOfLines,
41
+ testID,
42
+ }: TextProps) {
43
+ const { theme } = useTheme();
44
+ const { t } = useTranslationContext();
45
+ const content = resolveTextContent(children, i18nKey, t);
46
+
47
+ return (
48
+ <ReactNativeText
49
+ numberOfLines={numberOfLines}
50
+ testID={testID}
51
+ style={[
52
+ resolveTextStyles(theme, {
53
+ align,
54
+ italic,
55
+ variant,
56
+ weight,
57
+ }),
58
+ {
59
+ color: resolveTextColor(theme, tone, color),
60
+ },
61
+ ]}
62
+ >
63
+ {content}
64
+ </ReactNativeText>
65
+ );
66
+ }
@@ -0,0 +1,2 @@
1
+ export { Text } from './Text';
2
+ export type { TextProps } from './types';
@@ -0,0 +1,18 @@
1
+ import type React from 'react';
2
+ import type { TextStyle } from 'react-native';
3
+
4
+ import type { TextColorValue, TextTone } from '../../internal/resolvers/resolveTextColor';
5
+ import type { TextVariant, TextWeight } from '../../internal/resolvers/resolveTextStyles';
6
+
7
+ export interface TextProps {
8
+ children?: React.ReactNode;
9
+ i18nKey?: string;
10
+ variant?: TextVariant;
11
+ tone?: TextTone;
12
+ color?: TextColorValue;
13
+ align?: TextStyle['textAlign'];
14
+ weight?: TextWeight;
15
+ italic?: boolean;
16
+ numberOfLines?: number;
17
+ testID?: string;
18
+ }
@@ -0,0 +1,95 @@
1
+ import React, { createContext, useContext, useMemo } from 'react';
2
+
3
+ import { useFontContext } from '../context/FontContext';
4
+ import { ResponsiveProvider } from '../core/responsive/ResponsiveProvider';
5
+ import { OverlayProvider } from '../internal/overlay/OverlayProvider';
6
+ import { isDeepEqual } from '../utils/deepEqual';
7
+ import { deepMerge } from '../utils/deepMerge';
8
+ import { createTheme } from './createTheme';
9
+ import type { AnkhTheme, ThemeConfig } from './types';
10
+
11
+ const defaultTheme = createTheme();
12
+
13
+ export const ThemeContext = createContext<{
14
+ theme: AnkhTheme;
15
+ mode: 'light' | 'dark';
16
+ setThemeConfig: (config: Partial<ThemeConfig>) => void;
17
+ setMode: (mode: 'light' | 'dark') => void;
18
+ _hasProvider?: boolean;
19
+ }>({
20
+ theme: defaultTheme,
21
+ mode: 'light',
22
+ setThemeConfig: () => {
23
+ /* fallback */
24
+ },
25
+ setMode: () => {
26
+ /* fallback */
27
+ },
28
+ _hasProvider: false,
29
+ });
30
+
31
+ export const ThemeProvider = ({
32
+ children,
33
+ initialConfig,
34
+ initialMode = 'light',
35
+ }: {
36
+ children: React.ReactNode;
37
+ initialConfig?: Partial<ThemeConfig>;
38
+ initialMode?: 'light' | 'dark';
39
+ }) => {
40
+ const [config, setConfig] = React.useState<ThemeConfig>(() =>
41
+ initialConfig ? deepMerge(defaultTheme.config, initialConfig) : defaultTheme.config,
42
+ );
43
+ const [mode, setMode] = React.useState<'light' | 'dark'>(initialMode);
44
+ const { activeFontId } = useFontContext();
45
+
46
+ // Keep state in sync with prop for real-time Studio updates
47
+ React.useEffect(() => {
48
+ if (initialConfig) {
49
+ setConfig((prev) => {
50
+ const merged = deepMerge(prev, initialConfig);
51
+ if (isDeepEqual(prev, merged)) return prev;
52
+ return merged;
53
+ });
54
+ }
55
+ }, [initialConfig]);
56
+
57
+ const theme = useMemo(
58
+ () => createTheme(config, mode, activeFontId),
59
+ [config, mode, activeFontId],
60
+ );
61
+
62
+ const value = useMemo(
63
+ () => ({
64
+ theme,
65
+ mode,
66
+ setThemeConfig: (newConfig: Partial<ThemeConfig>) =>
67
+ setConfig((prev) => deepMerge(prev, newConfig)),
68
+ setMode,
69
+ _hasProvider: true,
70
+ }),
71
+ [theme, mode],
72
+ );
73
+
74
+ return (
75
+ <ResponsiveProvider>
76
+ <ThemeContext.Provider value={value}>
77
+ <OverlayProvider>{children}</OverlayProvider>
78
+ </ThemeContext.Provider>
79
+ </ResponsiveProvider>
80
+ );
81
+ };
82
+
83
+ export const useTheme = () => {
84
+ return useContext(ThemeContext);
85
+ };
86
+
87
+ export const useThemeConfig = () => {
88
+ const { setThemeConfig } = useTheme();
89
+ return setThemeConfig;
90
+ };
91
+
92
+ export const useThemeMode = () => {
93
+ const { mode, setMode } = useTheme();
94
+ return { mode, setMode };
95
+ };
@@ -0,0 +1,114 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+ import { oklch } from 'culori';
3
+
4
+ import { generatePalette } from './colorEngine';
5
+ import type { ThemeConfig } from './types';
6
+
7
+ const mockConfig: ThemeConfig = {
8
+ id: 'test',
9
+ name: 'Test Theme',
10
+ light: {
11
+ primaryColor: '#3B82F6',
12
+ harmony: 'triadic',
13
+ systemTone: 'neutral',
14
+ },
15
+ dark: {
16
+ primaryColor: '#3B82F6',
17
+ harmony: 'triadic',
18
+ systemTone: 'neutral',
19
+ },
20
+ };
21
+
22
+ describe('colorEngine', () => {
23
+ it('should generate a stable palette with deterministic chroma hierarchy', () => {
24
+ const { colors, semantics, scales } = generatePalette(mockConfig, 'light');
25
+
26
+ // Primary should be defined
27
+ expect(colors.primary).toBeDefined();
28
+
29
+ // Verify neutral surface chroma is strictly limited
30
+ const neutralBg = oklch(semantics.neutral.bg);
31
+ expect(neutralBg?.c).toBeLessThanOrEqual(0.021); // Small epsilon for float
32
+
33
+ // Verify presence of new tokens
34
+ expect(semantics.neutral.bgSubtle).toBeDefined();
35
+ expect(semantics.brand.onSolidText).toBeDefined();
36
+ expect(semantics.brand.softBg).toBeDefined();
37
+ expect(semantics.surface.default).toBe(semantics.neutral.surface);
38
+ expect(semantics.content.muted).toBe(semantics.neutral.textMuted);
39
+ expect(semantics.border.focus).toBe(semantics.brand.outline);
40
+ expect(semantics.action.primary.base).toBe(semantics.brand.base);
41
+ expect(semantics.action.danger.base).toBe(semantics.danger.base);
42
+
43
+ // Verify scale coverage
44
+ const primaryScale = scales.primary;
45
+ const neutralScale = scales.neutral;
46
+ expect(primaryScale).toBeDefined();
47
+ expect(neutralScale).toBeDefined();
48
+ if (!primaryScale || !neutralScale) throw new Error('Expected generated scales');
49
+ expect(Object.keys(primaryScale)).toHaveLength(11);
50
+ expect(neutralScale[50]).toBeDefined();
51
+ expect(neutralScale[950]).toBeDefined();
52
+ });
53
+
54
+ it('should respect triadic hue offsets (120 degrees)', () => {
55
+ const { colors } = generatePalette(mockConfig, 'light');
56
+
57
+ const p = oklch(colors.primary);
58
+ const s = oklch(colors.secondary);
59
+ const a = oklch(colors.accent);
60
+
61
+ if (p && s && a) {
62
+ const h1 = p.h ?? 0;
63
+ const h2 = s.h ?? 0;
64
+ const h3 = a.h ?? 0;
65
+
66
+ // Check distance (allowing for small float rounding and perceptual shift)
67
+ const diff1 = Math.abs((h2 - h1 + 360) % 360);
68
+ const diff2 = Math.abs((h3 - h1 + 360) % 360);
69
+
70
+ expect(diff1).toBeGreaterThan(115);
71
+ expect(diff1).toBeLessThan(125);
72
+ expect(diff2).toBeGreaterThan(235);
73
+ expect(diff2).toBeLessThan(245);
74
+ }
75
+ });
76
+
77
+ it('should handle monochromatic harmony (one hue)', () => {
78
+ const config = {
79
+ ...mockConfig,
80
+ light: { ...mockConfig.light, harmony: 'monochromatic' as const },
81
+ };
82
+ const { colors } = generatePalette(config, 'light');
83
+
84
+ const p = oklch(colors.primary);
85
+ const s = oklch(colors.secondary);
86
+
87
+ expect(p?.h).toBeCloseTo(s?.h ?? 0, 0);
88
+ });
89
+
90
+ it('should generate dark mode colors correctly', () => {
91
+ const { colors, semantics } = generatePalette(mockConfig, 'dark');
92
+
93
+ const bg = oklch(colors.background);
94
+ expect(bg?.l).toBeLessThan(0.2); // Should be dark
95
+ expect(colors.background).toBe(semantics.neutral.bg);
96
+ expect(semantics.content.inverse).toBe(semantics.brand.onSolidText);
97
+ });
98
+
99
+ it('should fall back to a default color if primaryColor is invalid', () => {
100
+ const config = {
101
+ ...mockConfig,
102
+ light: { ...mockConfig.light, primaryColor: 'invalid-color' },
103
+ };
104
+
105
+ // Should not throw
106
+ const { colors } = generatePalette(config, 'light');
107
+ expect(colors.primary).toBeDefined();
108
+ // Default fallback blue #3B82F6 in OKLCH
109
+ const p = oklch(colors.primary);
110
+ const fallbackBlue = oklch('#3B82F6');
111
+ expect(fallbackBlue).toBeDefined();
112
+ expect(p?.h).toBeCloseTo(fallbackBlue?.h ?? 0, 0);
113
+ });
114
+ });