@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,188 @@
1
+ import type { ThemeConfig as ContractsThemeConfig } from '@ankhorage/contracts';
2
+
3
+ export type { ColorHarmony, SystemTone, ThemeConfig, ThemeModeConfig } from '@ankhorage/contracts';
4
+
5
+ export type ColorTone = 'jewel' | 'earth' | 'fluorescent' | 'grayscale' | 'pastel';
6
+
7
+ export interface ColorScale {
8
+ [key: number]: string;
9
+ 50: string;
10
+ 100: string;
11
+ 200: string;
12
+ 300: string;
13
+ 400: string;
14
+ 500: string;
15
+ 600: string;
16
+ 700: string;
17
+ 800: string;
18
+ 900: string;
19
+ 950: string;
20
+ }
21
+
22
+ export interface NeutralSemantics {
23
+ bg: string;
24
+ bgSubtle: string;
25
+ surface: string;
26
+ surfaceHover: string;
27
+ surfaceActive: string;
28
+ border: string;
29
+ borderStrong: string;
30
+ divider: string;
31
+ text: string;
32
+ textMuted: string;
33
+ textSubtle: string;
34
+ }
35
+
36
+ export interface RoleSemantics {
37
+ base: string;
38
+ hover: string;
39
+ strong: string;
40
+ softBg: string;
41
+ softHover: string;
42
+ softActive: string;
43
+ outline: string;
44
+ onSolidText: string;
45
+ }
46
+
47
+ export interface SurfaceSemantics {
48
+ default: string;
49
+ subtle: string;
50
+ raised: string;
51
+ }
52
+
53
+ export interface ContentSemantics {
54
+ default: string;
55
+ muted: string;
56
+ subtle: string;
57
+ inverse: string;
58
+ }
59
+
60
+ export interface BorderSemantics {
61
+ default: string;
62
+ strong: string;
63
+ focus: string;
64
+ }
65
+
66
+ export interface ActionSemantics {
67
+ primary: RoleSemantics;
68
+ neutral: RoleSemantics;
69
+ danger: RoleSemantics;
70
+ }
71
+
72
+ export interface ThemeSemantics {
73
+ neutral: NeutralSemantics;
74
+ brand: RoleSemantics;
75
+ secondary: RoleSemantics;
76
+ accent: RoleSemantics;
77
+ highlight: RoleSemantics;
78
+ danger: RoleSemantics;
79
+ success: RoleSemantics;
80
+ warning: RoleSemantics;
81
+ surface: SurfaceSemantics;
82
+ content: ContentSemantics;
83
+ border: BorderSemantics;
84
+ action: ActionSemantics;
85
+ }
86
+
87
+ export type FontWeight =
88
+ | '100'
89
+ | '200'
90
+ | '300'
91
+ | '400'
92
+ | '500'
93
+ | '600'
94
+ | '700'
95
+ | '800'
96
+ | '900'
97
+ | 'bold'
98
+ | 'normal';
99
+
100
+ export interface ThemeTokens {
101
+ colors: {
102
+ primary: string;
103
+ secondary: string;
104
+ accent: string;
105
+ highlight: string;
106
+ background: string;
107
+ surface: string;
108
+ text: string;
109
+ textSecondary: string;
110
+ border: string;
111
+ error: string;
112
+ success: string;
113
+ warning: string;
114
+ [key: string]: string;
115
+ };
116
+ scales: Record<string, ColorScale>;
117
+ semantics: ThemeSemantics;
118
+ spacing: {
119
+ none: 0;
120
+ xs: number;
121
+ s: number;
122
+ m: number;
123
+ l: number;
124
+ xl: number;
125
+ xxl: number;
126
+ [key: string]: number;
127
+ };
128
+ radii: {
129
+ none: 0;
130
+ s: number;
131
+ m: number;
132
+ l: number;
133
+ full: number;
134
+ };
135
+ typography: {
136
+ headings: Record<
137
+ 1 | 2 | 3 | 4 | 5 | 6,
138
+ {
139
+ size: number;
140
+ lineHeight: number;
141
+ weight: 'regular' | 'medium' | 'semiBold' | 'bold';
142
+ }
143
+ >;
144
+ sizes: {
145
+ xs: number;
146
+ s: number;
147
+ m: number;
148
+ l: number;
149
+ xl: number;
150
+ xxl: number;
151
+ '3xl': number;
152
+ h1: number;
153
+ h2: number;
154
+ h3: number;
155
+ h4: number;
156
+ h5: number;
157
+ h6: number;
158
+ [key: string]: number;
159
+ };
160
+ weights: {
161
+ thin: FontWeight;
162
+ extraLight: FontWeight;
163
+ light: FontWeight;
164
+ regular: FontWeight;
165
+ medium: FontWeight;
166
+ semiBold: FontWeight;
167
+ bold: FontWeight;
168
+ extraBold: FontWeight;
169
+ black: FontWeight;
170
+ };
171
+ fonts: {
172
+ /** Map of weight -> fontFamily name for normal style */
173
+ normal: Record<FontWeight, string | undefined>;
174
+ /** Map of weight -> fontFamily name for italic style */
175
+ italic: Record<FontWeight, string | undefined>;
176
+ };
177
+ };
178
+ shadows: {
179
+ soft: number;
180
+ medium: number;
181
+ hard: number;
182
+ [key: string]: number;
183
+ };
184
+ }
185
+
186
+ export interface AnkhTheme extends ThemeTokens {
187
+ config: ContractsThemeConfig;
188
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Simple deep equality check for JSON-serializable objects.
3
+ * Efficient enough for UI manifests.
4
+ */
5
+ export const isDeepEqual = (a: unknown, b: unknown): boolean => {
6
+ if (a === b) return true;
7
+
8
+ if (a && b && typeof a === 'object' && typeof b === 'object') {
9
+ if (Array.isArray(a)) {
10
+ if (!Array.isArray(b) || a.length !== b.length) return false;
11
+ for (let i = 0; i < a.length; i++) {
12
+ if (!isDeepEqual(a[i], b[i])) return false;
13
+ }
14
+ return true;
15
+ }
16
+
17
+ if (Array.isArray(b)) return false;
18
+
19
+ const keysA = Object.keys(a);
20
+ const keysB = Object.keys(b);
21
+ if (keysA.length !== keysB.length) return false;
22
+
23
+ const objA = a as Record<string, unknown>;
24
+ const objB = b as Record<string, unknown>;
25
+
26
+ for (const key of keysA) {
27
+ if (!Object.prototype.hasOwnProperty.call(objB, key)) return false;
28
+ if (!isDeepEqual(objA[key], objB[key])) return false;
29
+ }
30
+ return true;
31
+ }
32
+
33
+ return Number.isNaN(a) && Number.isNaN(b);
34
+ };
@@ -0,0 +1,117 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+
3
+ import { deepMerge } from './deepMerge';
4
+
5
+ type UnknownObject = Record<string, unknown>;
6
+
7
+ describe('deepMerge', () => {
8
+ it('should merge top-level properties', () => {
9
+ const target: UnknownObject = { a: 1, b: 2 };
10
+ const source: UnknownObject = { b: 3, c: 4 };
11
+ const result = deepMerge(target, source);
12
+ expect(result).toEqual({ a: 1, b: 3, c: 4 });
13
+ });
14
+
15
+ it('should deeply merge nested objects', () => {
16
+ const target: UnknownObject = {
17
+ theme: {
18
+ colors: { primary: 'blue', background: 'white' },
19
+ spacing: { m: 10 },
20
+ },
21
+ };
22
+ const source: UnknownObject = {
23
+ theme: {
24
+ colors: { primary: 'red' },
25
+ spacing: { l: 20 },
26
+ },
27
+ };
28
+ const result = deepMerge(target, source);
29
+ expect(result).toEqual({
30
+ theme: {
31
+ colors: { primary: 'red', background: 'white' },
32
+ spacing: { m: 10, l: 20 },
33
+ },
34
+ });
35
+ });
36
+
37
+ it('should handle undefined in source by keeping target value', () => {
38
+ const target = { a: 1 };
39
+ const source = { a: undefined };
40
+ const result = deepMerge(target, source);
41
+ expect(result).toEqual({ a: 1 });
42
+ });
43
+
44
+ it('should handle null in source by overwriting target value', () => {
45
+ const target: UnknownObject = { a: { b: 1 } };
46
+ const source: UnknownObject = { a: null };
47
+ const result = deepMerge(target, source);
48
+ expect(result).toEqual({ a: null });
49
+ });
50
+
51
+ it('should not merge arrays, but overwrite them', () => {
52
+ const target = { list: [1, 2] };
53
+ const source = { list: [3] };
54
+ const result = deepMerge(target, source);
55
+ expect(result).toEqual({ list: [3] });
56
+ });
57
+
58
+ it('should handle merging an object into a non-object target value', () => {
59
+ const target: UnknownObject = { a: 1 };
60
+ const source: UnknownObject = { a: { b: 2 } };
61
+ const result = deepMerge(target, source);
62
+ expect(result).toEqual({ a: { b: 2 } });
63
+ });
64
+
65
+ it('should handle merging a non-object into an object target value', () => {
66
+ const target: UnknownObject = { a: { b: 1 } };
67
+ const source: UnknownObject = { a: 2 };
68
+ const result = deepMerge(target, source);
69
+ expect(result).toEqual({ a: 2 });
70
+ });
71
+
72
+ it('should return a new object and not mutate target', () => {
73
+ const target = { a: { b: 1 } };
74
+ const source = { a: { b: 2 } };
75
+ const result = deepMerge(target, source);
76
+ expect(result).not.toBe(target);
77
+ expect(result.a).not.toBe(target.a);
78
+ expect(target.a.b).toBe(1);
79
+ });
80
+
81
+ it('should handle complex ThemeConfig structures', () => {
82
+ const target: UnknownObject = {
83
+ id: 'theme-1',
84
+ name: 'Default',
85
+ light: {
86
+ primaryColor: 'blue',
87
+ harmony: 'monochromatic',
88
+ systemTone: 'neutral',
89
+ },
90
+ dark: {
91
+ primaryColor: 'blue',
92
+ harmony: 'monochromatic',
93
+ systemTone: 'neutral',
94
+ },
95
+ };
96
+ const source: UnknownObject = {
97
+ light: {
98
+ primaryColor: 'red',
99
+ },
100
+ };
101
+ const result = deepMerge(target, source);
102
+ expect(result).toEqual({
103
+ id: 'theme-1',
104
+ name: 'Default',
105
+ light: {
106
+ primaryColor: 'red',
107
+ harmony: 'monochromatic',
108
+ systemTone: 'neutral',
109
+ },
110
+ dark: {
111
+ primaryColor: 'blue',
112
+ harmony: 'monochromatic',
113
+ systemTone: 'neutral',
114
+ },
115
+ });
116
+ });
117
+ });
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Performs a deep merge of `source` into `target`.
3
+ * Specifically handles the nested structure of ThemeConfig (light/dark objects).
4
+ */
5
+ export function deepMerge<T extends object>(target: T, source: Partial<T>): T {
6
+ const result = { ...target };
7
+
8
+ (Object.keys(source) as (keyof T)[]).forEach((key) => {
9
+ const sourceValue = source[key];
10
+ const targetValue = target[key];
11
+
12
+ if (
13
+ sourceValue &&
14
+ typeof sourceValue === 'object' &&
15
+ !Array.isArray(sourceValue) &&
16
+ targetValue &&
17
+ typeof targetValue === 'object' &&
18
+ !Array.isArray(targetValue)
19
+ ) {
20
+ // Recursively merge nested objects
21
+ result[key] = deepMerge(targetValue as object, sourceValue as object) as T[keyof T];
22
+ } else if (sourceValue !== undefined) {
23
+ // Direct assignment for non-object values or if target doesn't have an object to merge with
24
+ result[key] = sourceValue as T[keyof T];
25
+ }
26
+ });
27
+
28
+ return result;
29
+ }