@harkenapp/sdk-react-native 0.0.1-alpha.1 → 0.0.1-alpha.2

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 (278) hide show
  1. package/README.md +44 -7
  2. package/app.plugin.cjs +12 -17
  3. package/dist/__mocks__/async-storage.d.ts +16 -0
  4. package/dist/__mocks__/async-storage.d.ts.map +1 -0
  5. package/dist/__mocks__/async-storage.js +39 -0
  6. package/dist/__mocks__/async-storage.js.map +1 -0
  7. package/dist/__mocks__/expo-document-picker.d.ts +26 -0
  8. package/dist/__mocks__/expo-document-picker.d.ts.map +1 -0
  9. package/dist/__mocks__/expo-document-picker.js +25 -0
  10. package/dist/__mocks__/expo-document-picker.js.map +1 -0
  11. package/dist/__mocks__/expo-file-system.d.ts +42 -0
  12. package/dist/__mocks__/expo-file-system.d.ts.map +1 -0
  13. package/dist/__mocks__/expo-file-system.js +37 -0
  14. package/dist/__mocks__/expo-file-system.js.map +1 -0
  15. package/dist/__mocks__/expo-image-picker.d.ts +30 -0
  16. package/dist/__mocks__/expo-image-picker.d.ts.map +1 -0
  17. package/dist/__mocks__/expo-image-picker.js +30 -0
  18. package/dist/__mocks__/expo-image-picker.js.map +1 -0
  19. package/dist/__mocks__/expo-secure-store.d.ts +15 -0
  20. package/dist/__mocks__/expo-secure-store.d.ts.map +1 -0
  21. package/dist/__mocks__/expo-secure-store.js +30 -0
  22. package/dist/__mocks__/expo-secure-store.js.map +1 -0
  23. package/dist/__mocks__/react-native.d.ts +73 -0
  24. package/dist/__mocks__/react-native.d.ts.map +1 -0
  25. package/dist/__mocks__/react-native.js +45 -0
  26. package/dist/__mocks__/react-native.js.map +1 -0
  27. package/dist/api/client.d.ts +8 -8
  28. package/dist/api/client.d.ts.map +1 -1
  29. package/dist/api/client.js +17 -19
  30. package/dist/api/client.js.map +1 -1
  31. package/dist/api/client.test.d.ts +2 -0
  32. package/dist/api/client.test.d.ts.map +1 -0
  33. package/dist/api/client.test.js +417 -0
  34. package/dist/api/client.test.js.map +1 -0
  35. package/dist/api/errors.d.ts +3 -3
  36. package/dist/api/errors.d.ts.map +1 -1
  37. package/dist/api/errors.js +3 -3
  38. package/dist/api/errors.js.map +1 -1
  39. package/dist/api/errors.test.d.ts +2 -0
  40. package/dist/api/errors.test.d.ts.map +1 -0
  41. package/dist/api/errors.test.js +155 -0
  42. package/dist/api/errors.test.js.map +1 -0
  43. package/dist/api/index.d.ts +6 -6
  44. package/dist/api/index.d.ts.map +1 -1
  45. package/dist/api/index.js.map +1 -1
  46. package/dist/api/retry.d.ts +1 -1
  47. package/dist/api/retry.d.ts.map +1 -1
  48. package/dist/api/retry.js.map +1 -1
  49. package/dist/api/retry.test.d.ts +2 -0
  50. package/dist/api/retry.test.d.ts.map +1 -0
  51. package/dist/api/retry.test.js +193 -0
  52. package/dist/api/retry.test.js.map +1 -0
  53. package/dist/attachments/FeedbackSheet.d.ts +36 -13
  54. package/dist/attachments/FeedbackSheet.d.ts.map +1 -1
  55. package/dist/attachments/FeedbackSheet.js +50 -30
  56. package/dist/attachments/FeedbackSheet.js.map +1 -1
  57. package/dist/attachments/index.d.ts +2 -2
  58. package/dist/components/AttachmentGrid.d.ts +12 -4
  59. package/dist/components/AttachmentGrid.d.ts.map +1 -1
  60. package/dist/components/AttachmentGrid.js +44 -34
  61. package/dist/components/AttachmentGrid.js.map +1 -1
  62. package/dist/components/AttachmentPicker.d.ts +3 -3
  63. package/dist/components/AttachmentPicker.d.ts.map +1 -1
  64. package/dist/components/AttachmentPicker.js +34 -36
  65. package/dist/components/AttachmentPicker.js.map +1 -1
  66. package/dist/components/AttachmentPreview.d.ts +10 -4
  67. package/dist/components/AttachmentPreview.d.ts.map +1 -1
  68. package/dist/components/AttachmentPreview.js +48 -34
  69. package/dist/components/AttachmentPreview.js.map +1 -1
  70. package/dist/components/CategorySelector.d.ts +3 -3
  71. package/dist/components/CategorySelector.d.ts.map +1 -1
  72. package/dist/components/CategorySelector.js +21 -27
  73. package/dist/components/CategorySelector.js.map +1 -1
  74. package/dist/components/FeedbackForm.d.ts +3 -3
  75. package/dist/components/FeedbackForm.d.ts.map +1 -1
  76. package/dist/components/FeedbackForm.js +7 -8
  77. package/dist/components/FeedbackForm.js.map +1 -1
  78. package/dist/components/FeedbackSheet.d.ts +34 -11
  79. package/dist/components/FeedbackSheet.d.ts.map +1 -1
  80. package/dist/components/FeedbackSheet.js +46 -28
  81. package/dist/components/FeedbackSheet.js.map +1 -1
  82. package/dist/components/ThemedButton.d.ts +16 -5
  83. package/dist/components/ThemedButton.d.ts.map +1 -1
  84. package/dist/components/ThemedButton.js +38 -29
  85. package/dist/components/ThemedButton.js.map +1 -1
  86. package/dist/components/ThemedText.d.ts +3 -3
  87. package/dist/components/ThemedText.d.ts.map +1 -1
  88. package/dist/components/ThemedText.js +1 -1
  89. package/dist/components/ThemedText.js.map +1 -1
  90. package/dist/components/ThemedTextInput.d.ts +11 -2
  91. package/dist/components/ThemedTextInput.d.ts.map +1 -1
  92. package/dist/components/ThemedTextInput.js +19 -9
  93. package/dist/components/ThemedTextInput.js.map +1 -1
  94. package/dist/components/UploadStatusOverlay.d.ts +11 -3
  95. package/dist/components/UploadStatusOverlay.d.ts.map +1 -1
  96. package/dist/components/UploadStatusOverlay.js +59 -76
  97. package/dist/components/UploadStatusOverlay.js.map +1 -1
  98. package/dist/components/index.d.ts +18 -18
  99. package/dist/components/index.d.ts.map +1 -1
  100. package/dist/components/index.js.map +1 -1
  101. package/dist/context/HarkenContext.d.ts +20 -15
  102. package/dist/context/HarkenContext.d.ts.map +1 -1
  103. package/dist/context/HarkenContext.js +20 -17
  104. package/dist/context/HarkenContext.js.map +1 -1
  105. package/dist/context/index.d.ts +2 -2
  106. package/dist/domain/index.d.ts +2 -2
  107. package/dist/domain/index.d.ts.map +1 -1
  108. package/dist/domain/index.js.map +1 -1
  109. package/dist/hooks/index.d.ts +5 -5
  110. package/dist/hooks/useAnonymousId.js +1 -1
  111. package/dist/hooks/useAnonymousId.test.d.ts +2 -0
  112. package/dist/hooks/useAnonymousId.test.d.ts.map +1 -0
  113. package/dist/hooks/useAnonymousId.test.js +154 -0
  114. package/dist/hooks/useAnonymousId.test.js.map +1 -0
  115. package/dist/hooks/useAttachmentPicker.d.ts +3 -3
  116. package/dist/hooks/useAttachmentPicker.js +7 -7
  117. package/dist/hooks/useAttachmentStatus.d.ts +1 -1
  118. package/dist/hooks/useAttachmentStatus.d.ts.map +1 -1
  119. package/dist/hooks/useAttachmentStatus.js.map +1 -1
  120. package/dist/hooks/useAttachmentUpload.d.ts +2 -2
  121. package/dist/hooks/useAttachmentUpload.d.ts.map +1 -1
  122. package/dist/hooks/useAttachmentUpload.js +5 -5
  123. package/dist/hooks/useAttachmentUpload.js.map +1 -1
  124. package/dist/hooks/useAttachmentUpload.test.d.ts +2 -0
  125. package/dist/hooks/useAttachmentUpload.test.d.ts.map +1 -0
  126. package/dist/hooks/useAttachmentUpload.test.js +542 -0
  127. package/dist/hooks/useAttachmentUpload.test.js.map +1 -0
  128. package/dist/hooks/useFeedback.d.ts +4 -4
  129. package/dist/hooks/useFeedback.d.ts.map +1 -1
  130. package/dist/hooks/useFeedback.js +3 -5
  131. package/dist/hooks/useFeedback.js.map +1 -1
  132. package/dist/hooks/useFeedback.test.d.ts +2 -0
  133. package/dist/hooks/useFeedback.test.d.ts.map +1 -0
  134. package/dist/hooks/useFeedback.test.js +299 -0
  135. package/dist/hooks/useFeedback.test.js.map +1 -0
  136. package/dist/hooks/useHarkenContext.d.ts +1 -1
  137. package/dist/hooks/useHarkenContext.js +1 -1
  138. package/dist/hooks/useHarkenTheme.d.ts +27 -3
  139. package/dist/hooks/useHarkenTheme.d.ts.map +1 -1
  140. package/dist/hooks/useHarkenTheme.js +26 -2
  141. package/dist/hooks/useHarkenTheme.js.map +1 -1
  142. package/dist/index.d.ts +28 -28
  143. package/dist/index.d.ts.map +1 -1
  144. package/dist/index.js.map +1 -1
  145. package/dist/services/index.d.ts +3 -3
  146. package/dist/services/index.d.ts.map +1 -1
  147. package/dist/services/index.js.map +1 -1
  148. package/dist/services/uploadQueueService.d.ts +2 -2
  149. package/dist/services/uploadQueueService.d.ts.map +1 -1
  150. package/dist/services/uploadQueueService.js +16 -17
  151. package/dist/services/uploadQueueService.js.map +1 -1
  152. package/dist/services/uploadQueueService.test.d.ts +2 -0
  153. package/dist/services/uploadQueueService.test.d.ts.map +1 -0
  154. package/dist/services/uploadQueueService.test.js +426 -0
  155. package/dist/services/uploadQueueService.test.js.map +1 -0
  156. package/dist/services/uploadQueueStorage.d.ts +1 -1
  157. package/dist/services/uploadQueueStorage.d.ts.map +1 -1
  158. package/dist/services/uploadQueueStorage.js +4 -4
  159. package/dist/services/uploadQueueStorage.js.map +1 -1
  160. package/dist/services/uploadQueueStorage.test.d.ts +2 -0
  161. package/dist/services/uploadQueueStorage.test.d.ts.map +1 -0
  162. package/dist/services/uploadQueueStorage.test.js +200 -0
  163. package/dist/services/uploadQueueStorage.test.js.map +1 -0
  164. package/dist/storage/IdentityStore.d.ts +1 -1
  165. package/dist/storage/IdentityStore.d.ts.map +1 -1
  166. package/dist/storage/IdentityStore.js.map +1 -1
  167. package/dist/storage/IdentityStore.test.d.ts +2 -0
  168. package/dist/storage/IdentityStore.test.d.ts.map +1 -0
  169. package/dist/storage/IdentityStore.test.js +176 -0
  170. package/dist/storage/IdentityStore.test.js.map +1 -0
  171. package/dist/storage/SecureStoreAdapter.d.ts +1 -1
  172. package/dist/storage/SecureStoreAdapter.test.d.ts +2 -0
  173. package/dist/storage/SecureStoreAdapter.test.d.ts.map +1 -0
  174. package/dist/storage/SecureStoreAdapter.test.js +114 -0
  175. package/dist/storage/SecureStoreAdapter.test.js.map +1 -0
  176. package/dist/storage/defaultStorage.d.ts +1 -1
  177. package/dist/storage/defaultStorage.js +4 -4
  178. package/dist/storage/defaultStorage.test.d.ts +2 -0
  179. package/dist/storage/defaultStorage.test.d.ts.map +1 -0
  180. package/dist/storage/defaultStorage.test.js +159 -0
  181. package/dist/storage/defaultStorage.test.js.map +1 -0
  182. package/dist/storage/index.d.ts +5 -5
  183. package/dist/storage/types.js +1 -1
  184. package/dist/theme/defaults.d.ts +14 -3
  185. package/dist/theme/defaults.d.ts.map +1 -1
  186. package/dist/theme/defaults.js +58 -43
  187. package/dist/theme/defaults.js.map +1 -1
  188. package/dist/theme/index.d.ts +3 -2
  189. package/dist/theme/index.d.ts.map +1 -1
  190. package/dist/theme/index.js +4 -1
  191. package/dist/theme/index.js.map +1 -1
  192. package/dist/theme/resolver.d.ts +16 -0
  193. package/dist/theme/resolver.d.ts.map +1 -0
  194. package/dist/theme/resolver.js +375 -0
  195. package/dist/theme/resolver.js.map +1 -0
  196. package/dist/theme/resolver.test.d.ts +2 -0
  197. package/dist/theme/resolver.test.d.ts.map +1 -0
  198. package/dist/theme/resolver.test.js +344 -0
  199. package/dist/theme/resolver.test.js.map +1 -0
  200. package/dist/theme/types.d.ts +378 -5
  201. package/dist/theme/types.d.ts.map +1 -1
  202. package/dist/types/config.d.ts +4 -4
  203. package/dist/types/index.d.ts +2 -2
  204. package/dist/utils/index.d.ts +1 -1
  205. package/dist/utils/uuid.d.ts.map +1 -1
  206. package/dist/utils/uuid.js +4 -5
  207. package/dist/utils/uuid.js.map +1 -1
  208. package/dist/utils/uuid.test.d.ts +2 -0
  209. package/dist/utils/uuid.test.d.ts.map +1 -0
  210. package/dist/utils/uuid.test.js +78 -0
  211. package/dist/utils/uuid.test.js.map +1 -0
  212. package/package.json +21 -13
  213. package/src/@types/expo-file-system-legacy.d.ts +3 -3
  214. package/src/__mocks__/async-storage.ts +46 -0
  215. package/src/__mocks__/expo-document-picker.ts +41 -0
  216. package/src/__mocks__/expo-file-system.ts +62 -0
  217. package/src/__mocks__/expo-image-picker.ts +48 -0
  218. package/src/__mocks__/expo-secure-store.ts +29 -0
  219. package/src/__mocks__/react-native.ts +46 -0
  220. package/src/api/client.test.ts +515 -0
  221. package/src/api/client.ts +45 -64
  222. package/src/api/errors.test.ts +193 -0
  223. package/src/api/errors.ts +7 -11
  224. package/src/api/index.ts +6 -10
  225. package/src/api/retry.test.ts +251 -0
  226. package/src/api/retry.ts +3 -6
  227. package/src/attachments/FeedbackSheet.tsx +100 -80
  228. package/src/attachments/index.ts +2 -2
  229. package/src/components/AttachmentGrid.tsx +54 -45
  230. package/src/components/AttachmentPicker.tsx +43 -54
  231. package/src/components/AttachmentPreview.tsx +51 -47
  232. package/src/components/CategorySelector.tsx +29 -35
  233. package/src/components/FeedbackForm.tsx +23 -35
  234. package/src/components/FeedbackSheet.tsx +89 -68
  235. package/src/components/ThemedButton.tsx +49 -47
  236. package/src/components/ThemedText.tsx +7 -10
  237. package/src/components/ThemedTextInput.tsx +23 -13
  238. package/src/components/UploadStatusOverlay.tsx +66 -89
  239. package/src/components/index.ts +18 -21
  240. package/src/context/HarkenContext.tsx +29 -28
  241. package/src/context/index.ts +2 -2
  242. package/src/domain/index.ts +2 -5
  243. package/src/domain/upload-queue.ts +5 -5
  244. package/src/hooks/index.ts +5 -5
  245. package/src/hooks/useAnonymousId.test.ts +189 -0
  246. package/src/hooks/useAnonymousId.ts +3 -3
  247. package/src/hooks/useAttachmentPicker.ts +12 -12
  248. package/src/hooks/useAttachmentStatus.ts +12 -16
  249. package/src/hooks/useAttachmentUpload.test.ts +632 -0
  250. package/src/hooks/useAttachmentUpload.ts +45 -54
  251. package/src/hooks/useFeedback.test.ts +376 -0
  252. package/src/hooks/useFeedback.ts +12 -14
  253. package/src/hooks/useHarkenContext.ts +4 -4
  254. package/src/hooks/useHarkenTheme.ts +30 -6
  255. package/src/index.ts +28 -52
  256. package/src/services/index.ts +3 -9
  257. package/src/services/uploadQueueService.test.ts +489 -0
  258. package/src/services/uploadQueueService.ts +40 -56
  259. package/src/services/uploadQueueStorage.test.ts +243 -0
  260. package/src/services/uploadQueueStorage.ts +7 -9
  261. package/src/storage/IdentityStore.test.ts +173 -0
  262. package/src/storage/IdentityStore.ts +4 -5
  263. package/src/storage/SecureStoreAdapter.test.ts +147 -0
  264. package/src/storage/SecureStoreAdapter.ts +1 -1
  265. package/src/storage/defaultStorage.test.ts +159 -0
  266. package/src/storage/defaultStorage.ts +6 -6
  267. package/src/storage/index.ts +5 -5
  268. package/src/storage/types.ts +1 -1
  269. package/src/theme/defaults.ts +75 -46
  270. package/src/theme/index.ts +15 -2
  271. package/src/theme/resolver.test.ts +411 -0
  272. package/src/theme/resolver.ts +446 -0
  273. package/src/theme/types.ts +453 -15
  274. package/src/types/config.ts +4 -4
  275. package/src/types/index.ts +2 -2
  276. package/src/utils/index.ts +1 -1
  277. package/src/utils/uuid.test.ts +85 -0
  278. package/src/utils/uuid.ts +4 -7
@@ -0,0 +1,411 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { resolveTheme } from "./resolver";
3
+ import { lightTheme, darkTheme, createTheme } from "./defaults";
4
+ import type { PartialHarkenTheme } from "./types";
5
+
6
+ describe("resolveTheme", () => {
7
+ describe("base theme resolution", () => {
8
+ it("returns all required color tokens from light theme", () => {
9
+ const resolved = resolveTheme(lightTheme);
10
+
11
+ expect(resolved.colors.primary).toBe(lightTheme.colors.primary);
12
+ expect(resolved.colors.background).toBe(lightTheme.colors.background);
13
+ expect(resolved.colors.text).toBe(lightTheme.colors.text);
14
+ expect(resolved.colors.surface).toBeDefined();
15
+ });
16
+
17
+ it("returns all required color tokens from dark theme", () => {
18
+ const resolved = resolveTheme(darkTheme);
19
+
20
+ expect(resolved.colors.primary).toBe(darkTheme.colors.primary);
21
+ expect(resolved.colors.background).toBe(darkTheme.colors.background);
22
+ expect(resolved.colors.text).toBe(darkTheme.colors.text);
23
+ expect(resolved.colors.surface).toBeDefined();
24
+ });
25
+
26
+ it("returns spacing, radii, sizing, and opacity", () => {
27
+ const resolved = resolveTheme(lightTheme);
28
+
29
+ expect(resolved.spacing.sm).toBe(lightTheme.spacing.sm);
30
+ expect(resolved.radii.md).toBe(lightTheme.radii.md);
31
+ expect(resolved.sizing.buttonMinHeight).toBeDefined();
32
+ expect(resolved.opacity.disabled).toBeDefined();
33
+ });
34
+ });
35
+
36
+ describe("component token fallbacks", () => {
37
+ describe("chip tokens", () => {
38
+ it("falls back chipBackground to surface", () => {
39
+ const resolved = resolveTheme(lightTheme);
40
+ expect(resolved.colors.chipBackground).toBe(resolved.colors.surface);
41
+ });
42
+
43
+ it("uses explicit chipBackground when provided", () => {
44
+ const overrides: PartialHarkenTheme = {
45
+ colors: { chipBackground: "#custom" },
46
+ };
47
+ const resolved = resolveTheme(lightTheme, overrides);
48
+ expect(resolved.colors.chipBackground).toBe("#custom");
49
+ });
50
+
51
+ it("uses custom surface when chipBackground not provided", () => {
52
+ const overrides: PartialHarkenTheme = {
53
+ colors: { surface: "#custom-surface" },
54
+ };
55
+ const resolved = resolveTheme(lightTheme, overrides);
56
+ expect(resolved.colors.chipBackground).toBe("#custom-surface");
57
+ });
58
+
59
+ it("falls back chipBorder to border", () => {
60
+ const resolved = resolveTheme(lightTheme);
61
+ expect(resolved.colors.chipBorder).toBe(resolved.colors.border);
62
+ });
63
+
64
+ it("falls back chipBackgroundSelected to primary", () => {
65
+ const resolved = resolveTheme(lightTheme);
66
+ expect(resolved.colors.chipBackgroundSelected).toBe(resolved.colors.primary);
67
+ });
68
+
69
+ it("falls back chipTextSelected to textOnPrimary", () => {
70
+ const resolved = resolveTheme(lightTheme);
71
+ expect(resolved.colors.chipTextSelected).toBe(resolved.colors.textOnPrimary);
72
+ });
73
+ });
74
+
75
+ describe("input tokens", () => {
76
+ it("falls back inputBackground to surface", () => {
77
+ const resolved = resolveTheme(lightTheme);
78
+ expect(resolved.colors.inputBackground).toBe(resolved.colors.surface);
79
+ });
80
+
81
+ it("falls back inputBorder to border", () => {
82
+ const resolved = resolveTheme(lightTheme);
83
+ expect(resolved.colors.inputBorder).toBe(resolved.colors.border);
84
+ });
85
+
86
+ it("falls back inputBorderFocused to borderFocused", () => {
87
+ const resolved = resolveTheme(lightTheme);
88
+ expect(resolved.colors.inputBorderFocused).toBe(resolved.colors.borderFocused);
89
+ });
90
+
91
+ it("falls back inputBorderError to error", () => {
92
+ const resolved = resolveTheme(lightTheme);
93
+ expect(resolved.colors.inputBorderError).toBe(resolved.colors.error);
94
+ });
95
+ });
96
+
97
+ describe("button tokens", () => {
98
+ it("falls back buttonPrimaryBackground to primary", () => {
99
+ const resolved = resolveTheme(lightTheme);
100
+ expect(resolved.colors.buttonPrimaryBackground).toBe(resolved.colors.primary);
101
+ });
102
+
103
+ it("falls back buttonSecondaryBackground to surface", () => {
104
+ const resolved = resolveTheme(lightTheme);
105
+ expect(resolved.colors.buttonSecondaryBackground).toBe(resolved.colors.surface);
106
+ });
107
+
108
+ it("falls back buttonGhostText to text", () => {
109
+ const resolved = resolveTheme(lightTheme);
110
+ expect(resolved.colors.buttonGhostText).toBe(resolved.colors.text);
111
+ });
112
+ });
113
+
114
+ describe("tile tokens", () => {
115
+ it("falls back tileBackground to surface", () => {
116
+ const resolved = resolveTheme(lightTheme);
117
+ expect(resolved.colors.tileBackground).toBe(resolved.colors.surface);
118
+ });
119
+
120
+ it("falls back tileBorder to border", () => {
121
+ const resolved = resolveTheme(lightTheme);
122
+ expect(resolved.colors.tileBorder).toBe(resolved.colors.border);
123
+ });
124
+ });
125
+
126
+ describe("upload tokens", () => {
127
+ it("falls back uploadProgressFill to primary", () => {
128
+ const resolved = resolveTheme(lightTheme);
129
+ expect(resolved.colors.uploadProgressFill).toBe(resolved.colors.primary);
130
+ });
131
+
132
+ it("falls back uploadBadgeSuccess to success", () => {
133
+ const resolved = resolveTheme(lightTheme);
134
+ expect(resolved.colors.uploadBadgeSuccess).toBe(resolved.colors.success);
135
+ });
136
+ });
137
+
138
+ describe("form tokens", () => {
139
+ it("defaults formBackground to background for backwards compatibility", () => {
140
+ const resolved = resolveTheme(lightTheme);
141
+ expect(resolved.colors.formBackground).toBe(resolved.colors.background);
142
+ });
143
+
144
+ it("uses explicit formBackground when provided", () => {
145
+ const overrides: PartialHarkenTheme = {
146
+ colors: { formBackground: "#333333" },
147
+ };
148
+ const resolved = resolveTheme(lightTheme, overrides);
149
+ expect(resolved.colors.formBackground).toBe("#333333");
150
+ });
151
+
152
+ it("can use transparent formBackground for modal embedding", () => {
153
+ const overrides: PartialHarkenTheme = {
154
+ colors: { formBackground: "transparent" },
155
+ };
156
+ const resolved = resolveTheme(lightTheme, overrides);
157
+ expect(resolved.colors.formBackground).toBe("transparent");
158
+ });
159
+ });
160
+ });
161
+
162
+ describe("spacing fallbacks", () => {
163
+ it("falls back chipPaddingVertical to sm", () => {
164
+ const resolved = resolveTheme(lightTheme);
165
+ expect(resolved.spacing.chipPaddingVertical).toBe(resolved.spacing.sm);
166
+ });
167
+
168
+ it("falls back chipPaddingHorizontal to md", () => {
169
+ const resolved = resolveTheme(lightTheme);
170
+ expect(resolved.spacing.chipPaddingHorizontal).toBe(resolved.spacing.md);
171
+ });
172
+
173
+ it("falls back formPadding to lg", () => {
174
+ const resolved = resolveTheme(lightTheme);
175
+ expect(resolved.spacing.formPadding).toBe(resolved.spacing.lg);
176
+ });
177
+
178
+ it("uses explicit spacing when provided", () => {
179
+ const overrides: PartialHarkenTheme = {
180
+ spacing: { chipPaddingVertical: 20 },
181
+ };
182
+ const resolved = resolveTheme(lightTheme, overrides);
183
+ expect(resolved.spacing.chipPaddingVertical).toBe(20);
184
+ });
185
+ });
186
+
187
+ describe("radii fallbacks", () => {
188
+ it("falls back chip radius to full", () => {
189
+ const resolved = resolveTheme(lightTheme);
190
+ expect(resolved.radii.chip).toBe(resolved.radii.full);
191
+ });
192
+
193
+ it("falls back input radius to md", () => {
194
+ const resolved = resolveTheme(lightTheme);
195
+ expect(resolved.radii.input).toBe(resolved.radii.md);
196
+ });
197
+
198
+ it("falls back button radius to md", () => {
199
+ const resolved = resolveTheme(lightTheme);
200
+ expect(resolved.radii.button).toBe(resolved.radii.md);
201
+ });
202
+
203
+ it("uses explicit radii when provided", () => {
204
+ const overrides: PartialHarkenTheme = {
205
+ radii: { chip: 4 },
206
+ };
207
+ const resolved = resolveTheme(lightTheme, overrides);
208
+ expect(resolved.radii.chip).toBe(4);
209
+ });
210
+ });
211
+
212
+ describe("sizing overrides", () => {
213
+ it("uses explicit sizing when provided", () => {
214
+ const overrides: PartialHarkenTheme = {
215
+ sizing: { tileSize: 100 },
216
+ };
217
+ const resolved = resolveTheme(lightTheme, overrides);
218
+ expect(resolved.sizing.tileSize).toBe(100);
219
+ });
220
+ });
221
+
222
+ describe("opacity overrides", () => {
223
+ it("uses explicit opacity when provided", () => {
224
+ const overrides: PartialHarkenTheme = {
225
+ opacity: { disabled: 0.5 },
226
+ };
227
+ const resolved = resolveTheme(lightTheme, overrides);
228
+ expect(resolved.opacity.disabled).toBe(0.5);
229
+ });
230
+ });
231
+
232
+ describe("component aliases", () => {
233
+ it("builds chip component aliases correctly", () => {
234
+ const resolved = resolveTheme(lightTheme);
235
+ const { chip } = resolved.components;
236
+
237
+ expect(chip.background).toBe(resolved.colors.chipBackground);
238
+ expect(chip.border).toBe(resolved.colors.chipBorder);
239
+ expect(chip.paddingVertical).toBe(resolved.spacing.chipPaddingVertical);
240
+ expect(chip.radius).toBe(resolved.radii.chip);
241
+ });
242
+
243
+ it("builds input component aliases correctly", () => {
244
+ const resolved = resolveTheme(lightTheme);
245
+ const { input } = resolved.components;
246
+
247
+ expect(input.background).toBe(resolved.colors.inputBackground);
248
+ expect(input.border).toBe(resolved.colors.inputBorder);
249
+ expect(input.padding).toBe(resolved.spacing.inputPadding);
250
+ expect(input.radius).toBe(resolved.radii.input);
251
+ expect(input.minHeight).toBe(resolved.sizing.inputMinHeight);
252
+ });
253
+
254
+ it("builds button component aliases correctly", () => {
255
+ const resolved = resolveTheme(lightTheme);
256
+ const { button } = resolved.components;
257
+
258
+ expect(button.primary.background).toBe(resolved.colors.buttonPrimaryBackground);
259
+ expect(button.secondary.border).toBe(resolved.colors.buttonSecondaryBorder);
260
+ expect(button.ghost.text).toBe(resolved.colors.buttonGhostText);
261
+ expect(button.radius).toBe(resolved.radii.button);
262
+ expect(button.minHeight).toBe(resolved.sizing.buttonMinHeight);
263
+ });
264
+
265
+ it("builds form component aliases correctly", () => {
266
+ const resolved = resolveTheme(lightTheme);
267
+ const { form } = resolved.components;
268
+
269
+ expect(form.background).toBe(resolved.colors.formBackground);
270
+ expect(form.padding).toBe(resolved.spacing.formPadding);
271
+ expect(form.sectionGap).toBe(resolved.spacing.sectionGap);
272
+ expect(form.radius).toBe(resolved.radii.form);
273
+ });
274
+ });
275
+
276
+ describe("modal embedding recipe", () => {
277
+ it("supports the minimal modal embedding theme with transparent background", () => {
278
+ const modalTheme: PartialHarkenTheme = {
279
+ colors: {
280
+ surface: "#2d2d2d",
281
+ chipBackground: "#3d3d3d",
282
+ chipBorder: "#4d4d4d",
283
+ formBackground: "transparent", // explicit transparent for modal embedding
284
+ text: "#ffffff",
285
+ textSecondary: "#a0a0a0",
286
+ textPlaceholder: "#666666",
287
+ primary: "#0066ff",
288
+ },
289
+ };
290
+
291
+ const resolved = resolveTheme(darkTheme, modalTheme);
292
+
293
+ // Verify explicit overrides
294
+ expect(resolved.colors.surface).toBe("#2d2d2d");
295
+ expect(resolved.colors.chipBackground).toBe("#3d3d3d");
296
+ expect(resolved.colors.chipBorder).toBe("#4d4d4d");
297
+ expect(resolved.colors.formBackground).toBe("transparent");
298
+ expect(resolved.colors.text).toBe("#ffffff");
299
+ expect(resolved.colors.primary).toBe("#0066ff");
300
+
301
+ // Verify component aliases reflect the overrides
302
+ expect(resolved.components.chip.background).toBe("#3d3d3d");
303
+ expect(resolved.components.chip.border).toBe("#4d4d4d");
304
+ expect(resolved.components.form.background).toBe("transparent");
305
+ });
306
+
307
+ it("correctly cascades surface to components when not explicitly set", () => {
308
+ const modalTheme: PartialHarkenTheme = {
309
+ colors: {
310
+ surface: "#2d2d2d",
311
+ // chipBackground not set - should fall back to surface
312
+ // inputBackground not set - should fall back to surface
313
+ },
314
+ };
315
+
316
+ const resolved = resolveTheme(darkTheme, modalTheme);
317
+
318
+ // Both should fall back to custom surface
319
+ expect(resolved.colors.chipBackground).toBe("#2d2d2d");
320
+ expect(resolved.colors.inputBackground).toBe("#2d2d2d");
321
+ expect(resolved.components.chip.background).toBe("#2d2d2d");
322
+ expect(resolved.components.input.background).toBe("#2d2d2d");
323
+ });
324
+ });
325
+
326
+ describe("backwards compatibility", () => {
327
+ it("treats backgroundSecondary as alias for surface", () => {
328
+ const overrides: PartialHarkenTheme = {
329
+ colors: {
330
+ backgroundSecondary: "#legacy-value",
331
+ },
332
+ };
333
+
334
+ const resolved = resolveTheme(lightTheme, overrides);
335
+
336
+ // Both surface and backgroundSecondary should have the legacy value
337
+ expect(resolved.colors.surface).toBe("#legacy-value");
338
+ expect(resolved.colors.backgroundSecondary).toBe("#legacy-value");
339
+ });
340
+
341
+ it("prefers surface over backgroundSecondary when both provided", () => {
342
+ const overrides: PartialHarkenTheme = {
343
+ colors: {
344
+ surface: "#new-value",
345
+ backgroundSecondary: "#legacy-value",
346
+ },
347
+ };
348
+
349
+ const resolved = resolveTheme(lightTheme, overrides);
350
+
351
+ // surface takes precedence
352
+ expect(resolved.colors.surface).toBe("#new-value");
353
+ });
354
+ });
355
+ });
356
+
357
+ describe("createTheme", () => {
358
+ describe("sizing and opacity merging", () => {
359
+ it("preserves base sizing when overrides omit sizing", () => {
360
+ const baseWithSizing = {
361
+ ...lightTheme,
362
+ sizing: { tileSize: 100, buttonMinHeight: 60 },
363
+ };
364
+
365
+ const result = createTheme(baseWithSizing, {
366
+ colors: { primary: "#ff0000" },
367
+ });
368
+
369
+ expect(result.sizing).toEqual({ tileSize: 100, buttonMinHeight: 60 });
370
+ });
371
+
372
+ it("preserves base opacity when overrides omit opacity", () => {
373
+ const baseWithOpacity = {
374
+ ...lightTheme,
375
+ opacity: { disabled: 0.4, pressed: 0.9 },
376
+ };
377
+
378
+ const result = createTheme(baseWithOpacity, {
379
+ colors: { primary: "#ff0000" },
380
+ });
381
+
382
+ expect(result.opacity).toEqual({ disabled: 0.4, pressed: 0.9 });
383
+ });
384
+
385
+ it("merges sizing from base and overrides", () => {
386
+ const baseWithSizing = {
387
+ ...lightTheme,
388
+ sizing: { tileSize: 100, buttonMinHeight: 60 },
389
+ };
390
+
391
+ const result = createTheme(baseWithSizing, {
392
+ sizing: { tileSize: 120 }, // Override one, keep the other
393
+ });
394
+
395
+ expect(result.sizing).toEqual({ tileSize: 120, buttonMinHeight: 60 });
396
+ });
397
+
398
+ it("merges opacity from base and overrides", () => {
399
+ const baseWithOpacity = {
400
+ ...lightTheme,
401
+ opacity: { disabled: 0.4, pressed: 0.9 },
402
+ };
403
+
404
+ const result = createTheme(baseWithOpacity, {
405
+ opacity: { disabled: 0.5 }, // Override one, keep the other
406
+ });
407
+
408
+ expect(result.opacity).toEqual({ disabled: 0.5, pressed: 0.9 });
409
+ });
410
+ });
411
+ });