@cdx-ui/primitives 0.1.7 → 0.2.0

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/lib/commonjs/color/Color.js +69 -0
  2. package/lib/commonjs/color/Color.js.map +1 -0
  3. package/lib/commonjs/color/index.js +13 -0
  4. package/lib/commonjs/color/index.js.map +1 -0
  5. package/lib/commonjs/color/types.js +2 -0
  6. package/lib/commonjs/color/types.js.map +1 -0
  7. package/lib/commonjs/color-area/ColorAreaAccessibility.js +100 -0
  8. package/lib/commonjs/color-area/ColorAreaAccessibility.js.map +1 -0
  9. package/lib/commonjs/color-area/ColorAreaAccessibility.web.js +58 -0
  10. package/lib/commonjs/color-area/ColorAreaAccessibility.web.js.map +1 -0
  11. package/lib/commonjs/color-area/ColorAreaRoot.js +240 -0
  12. package/lib/commonjs/color-area/ColorAreaRoot.js.map +1 -0
  13. package/lib/commonjs/color-area/ColorAreaThumb.js +71 -0
  14. package/lib/commonjs/color-area/ColorAreaThumb.js.map +1 -0
  15. package/lib/commonjs/color-area/context.js +11 -0
  16. package/lib/commonjs/color-area/context.js.map +1 -0
  17. package/lib/commonjs/color-area/index.js +27 -0
  18. package/lib/commonjs/color-area/index.js.map +1 -0
  19. package/lib/commonjs/color-area/types.js +6 -0
  20. package/lib/commonjs/color-area/types.js.map +1 -0
  21. package/lib/commonjs/color-area/utils.js +51 -0
  22. package/lib/commonjs/color-area/utils.js.map +1 -0
  23. package/lib/commonjs/color-field/ColorFieldInput.js +67 -0
  24. package/lib/commonjs/color-field/ColorFieldInput.js.map +1 -0
  25. package/lib/commonjs/color-field/ColorFieldRoot.js +104 -0
  26. package/lib/commonjs/color-field/ColorFieldRoot.js.map +1 -0
  27. package/lib/commonjs/color-field/context.js +11 -0
  28. package/lib/commonjs/color-field/context.js.map +1 -0
  29. package/lib/commonjs/color-field/index.js +27 -0
  30. package/lib/commonjs/color-field/index.js.map +1 -0
  31. package/lib/commonjs/color-field/types.js +6 -0
  32. package/lib/commonjs/color-field/types.js.map +1 -0
  33. package/lib/commonjs/color-field/utils.js +60 -0
  34. package/lib/commonjs/color-field/utils.js.map +1 -0
  35. package/lib/commonjs/color-picker/ColorPickerRoot.js +69 -0
  36. package/lib/commonjs/color-picker/ColorPickerRoot.js.map +1 -0
  37. package/lib/commonjs/color-picker/ColorPickerTrigger.js +98 -0
  38. package/lib/commonjs/color-picker/ColorPickerTrigger.js.map +1 -0
  39. package/lib/commonjs/color-picker/context.js +32 -0
  40. package/lib/commonjs/color-picker/context.js.map +1 -0
  41. package/lib/commonjs/color-picker/index.js +51 -0
  42. package/lib/commonjs/color-picker/index.js.map +1 -0
  43. package/lib/commonjs/color-picker/types.js +6 -0
  44. package/lib/commonjs/color-picker/types.js.map +1 -0
  45. package/lib/commonjs/color-slider/ColorSliderRail.js +119 -0
  46. package/lib/commonjs/color-slider/ColorSliderRail.js.map +1 -0
  47. package/lib/commonjs/color-slider/ColorSliderRoot.js +153 -0
  48. package/lib/commonjs/color-slider/ColorSliderRoot.js.map +1 -0
  49. package/lib/commonjs/color-slider/ColorSliderThumb.js +265 -0
  50. package/lib/commonjs/color-slider/ColorSliderThumb.js.map +1 -0
  51. package/lib/commonjs/color-slider/context.js +11 -0
  52. package/lib/commonjs/color-slider/context.js.map +1 -0
  53. package/lib/commonjs/color-slider/index.js +34 -0
  54. package/lib/commonjs/color-slider/index.js.map +1 -0
  55. package/lib/commonjs/color-slider/types.js +6 -0
  56. package/lib/commonjs/color-slider/types.js.map +1 -0
  57. package/lib/commonjs/color-slider/utils.js +75 -0
  58. package/lib/commonjs/color-slider/utils.js.map +1 -0
  59. package/lib/commonjs/color-swatch-picker/ColorSwatchPickerItem.js +46 -0
  60. package/lib/commonjs/color-swatch-picker/ColorSwatchPickerItem.js.map +1 -0
  61. package/lib/commonjs/color-swatch-picker/ColorSwatchPickerRoot.js +71 -0
  62. package/lib/commonjs/color-swatch-picker/ColorSwatchPickerRoot.js.map +1 -0
  63. package/lib/commonjs/color-swatch-picker/context.js +22 -0
  64. package/lib/commonjs/color-swatch-picker/context.js.map +1 -0
  65. package/lib/commonjs/color-swatch-picker/index.js +33 -0
  66. package/lib/commonjs/color-swatch-picker/index.js.map +1 -0
  67. package/lib/commonjs/color-swatch-picker/types.js +6 -0
  68. package/lib/commonjs/color-swatch-picker/types.js.map +1 -0
  69. package/lib/commonjs/index.js +80 -0
  70. package/lib/commonjs/index.js.map +1 -1
  71. package/lib/commonjs/react-native-web.d.js +2 -0
  72. package/lib/commonjs/react-native-web.d.js.map +1 -0
  73. package/lib/commonjs/slider/useSliderPointer.js +2 -0
  74. package/lib/commonjs/slider/useSliderPointer.js.map +1 -1
  75. package/lib/commonjs/utils/createPortal.web.js +10 -1
  76. package/lib/commonjs/utils/createPortal.web.js.map +1 -1
  77. package/lib/commonjs/utils/textAncestor.js +25 -0
  78. package/lib/commonjs/utils/textAncestor.js.map +1 -0
  79. package/lib/commonjs/utils/textAncestor.web.js +27 -0
  80. package/lib/commonjs/utils/textAncestor.web.js.map +1 -0
  81. package/lib/module/color/Color.js +65 -0
  82. package/lib/module/color/Color.js.map +1 -0
  83. package/lib/module/color/index.js +4 -0
  84. package/lib/module/color/index.js.map +1 -0
  85. package/lib/module/color/types.js +2 -0
  86. package/lib/module/color/types.js.map +1 -0
  87. package/lib/module/color-area/ColorAreaAccessibility.js +96 -0
  88. package/lib/module/color-area/ColorAreaAccessibility.js.map +1 -0
  89. package/lib/module/color-area/ColorAreaAccessibility.web.js +54 -0
  90. package/lib/module/color-area/ColorAreaAccessibility.web.js.map +1 -0
  91. package/lib/module/color-area/ColorAreaRoot.js +236 -0
  92. package/lib/module/color-area/ColorAreaRoot.js.map +1 -0
  93. package/lib/module/color-area/ColorAreaThumb.js +67 -0
  94. package/lib/module/color-area/ColorAreaThumb.js.map +1 -0
  95. package/lib/module/color-area/context.js +5 -0
  96. package/lib/module/color-area/context.js.map +1 -0
  97. package/lib/module/color-area/index.js +6 -0
  98. package/lib/module/color-area/index.js.map +1 -0
  99. package/lib/module/color-area/types.js +4 -0
  100. package/lib/module/color-area/types.js.map +1 -0
  101. package/lib/module/color-area/utils.js +45 -0
  102. package/lib/module/color-area/utils.js.map +1 -0
  103. package/lib/module/color-field/ColorFieldInput.js +63 -0
  104. package/lib/module/color-field/ColorFieldInput.js.map +1 -0
  105. package/lib/module/color-field/ColorFieldRoot.js +100 -0
  106. package/lib/module/color-field/ColorFieldRoot.js.map +1 -0
  107. package/lib/module/color-field/context.js +5 -0
  108. package/lib/module/color-field/context.js.map +1 -0
  109. package/lib/module/color-field/index.js +6 -0
  110. package/lib/module/color-field/index.js.map +1 -0
  111. package/lib/module/color-field/types.js +4 -0
  112. package/lib/module/color-field/types.js.map +1 -0
  113. package/lib/module/color-field/utils.js +55 -0
  114. package/lib/module/color-field/utils.js.map +1 -0
  115. package/lib/module/color-picker/ColorPickerRoot.js +65 -0
  116. package/lib/module/color-picker/ColorPickerRoot.js.map +1 -0
  117. package/lib/module/color-picker/ColorPickerTrigger.js +94 -0
  118. package/lib/module/color-picker/ColorPickerTrigger.js.map +1 -0
  119. package/lib/module/color-picker/context.js +25 -0
  120. package/lib/module/color-picker/context.js.map +1 -0
  121. package/lib/module/color-picker/index.js +6 -0
  122. package/lib/module/color-picker/index.js.map +1 -0
  123. package/lib/module/color-picker/types.js +4 -0
  124. package/lib/module/color-picker/types.js.map +1 -0
  125. package/lib/module/color-slider/ColorSliderRail.js +115 -0
  126. package/lib/module/color-slider/ColorSliderRail.js.map +1 -0
  127. package/lib/module/color-slider/ColorSliderRoot.js +149 -0
  128. package/lib/module/color-slider/ColorSliderRoot.js.map +1 -0
  129. package/lib/module/color-slider/ColorSliderThumb.js +261 -0
  130. package/lib/module/color-slider/ColorSliderThumb.js.map +1 -0
  131. package/lib/module/color-slider/context.js +5 -0
  132. package/lib/module/color-slider/context.js.map +1 -0
  133. package/lib/module/color-slider/index.js +7 -0
  134. package/lib/module/color-slider/index.js.map +1 -0
  135. package/lib/module/color-slider/types.js +4 -0
  136. package/lib/module/color-slider/types.js.map +1 -0
  137. package/lib/module/color-slider/utils.js +66 -0
  138. package/lib/module/color-slider/utils.js.map +1 -0
  139. package/lib/module/color-swatch-picker/ColorSwatchPickerItem.js +42 -0
  140. package/lib/module/color-swatch-picker/ColorSwatchPickerItem.js.map +1 -0
  141. package/lib/module/color-swatch-picker/ColorSwatchPickerRoot.js +67 -0
  142. package/lib/module/color-swatch-picker/ColorSwatchPickerRoot.js.map +1 -0
  143. package/lib/module/color-swatch-picker/context.js +15 -0
  144. package/lib/module/color-swatch-picker/context.js.map +1 -0
  145. package/lib/module/color-swatch-picker/index.js +6 -0
  146. package/lib/module/color-swatch-picker/index.js.map +1 -0
  147. package/lib/module/color-swatch-picker/types.js +4 -0
  148. package/lib/module/color-swatch-picker/types.js.map +1 -0
  149. package/lib/module/index.js +7 -0
  150. package/lib/module/index.js.map +1 -1
  151. package/lib/module/react-native-web.d.js +2 -0
  152. package/lib/module/react-native-web.d.js.map +1 -0
  153. package/lib/module/slider/useSliderPointer.js +2 -0
  154. package/lib/module/slider/useSliderPointer.js.map +1 -1
  155. package/lib/module/utils/createPortal.web.js +9 -1
  156. package/lib/module/utils/createPortal.web.js.map +1 -1
  157. package/lib/module/utils/textAncestor.js +20 -0
  158. package/lib/module/utils/textAncestor.js.map +1 -0
  159. package/lib/module/utils/textAncestor.web.js +22 -0
  160. package/lib/module/utils/textAncestor.web.js.map +1 -0
  161. package/lib/typescript/color/Color.d.ts +3 -0
  162. package/lib/typescript/color/Color.d.ts.map +1 -0
  163. package/lib/typescript/color/index.d.ts +3 -0
  164. package/lib/typescript/color/index.d.ts.map +1 -0
  165. package/lib/typescript/color/types.d.ts +13 -0
  166. package/lib/typescript/color/types.d.ts.map +1 -0
  167. package/lib/typescript/color-area/ColorAreaAccessibility.d.ts +8 -0
  168. package/lib/typescript/color-area/ColorAreaAccessibility.d.ts.map +1 -0
  169. package/lib/typescript/color-area/ColorAreaAccessibility.web.d.ts +8 -0
  170. package/lib/typescript/color-area/ColorAreaAccessibility.web.d.ts.map +1 -0
  171. package/lib/typescript/color-area/ColorAreaRoot.d.ts +7 -0
  172. package/lib/typescript/color-area/ColorAreaRoot.d.ts.map +1 -0
  173. package/lib/typescript/color-area/ColorAreaThumb.d.ts +6 -0
  174. package/lib/typescript/color-area/ColorAreaThumb.d.ts.map +1 -0
  175. package/lib/typescript/color-area/context.d.ts +6 -0
  176. package/lib/typescript/color-area/context.d.ts.map +1 -0
  177. package/lib/typescript/color-area/index.d.ts +5 -0
  178. package/lib/typescript/color-area/index.d.ts.map +1 -0
  179. package/lib/typescript/color-area/types.d.ts +65 -0
  180. package/lib/typescript/color-area/types.d.ts.map +1 -0
  181. package/lib/typescript/color-area/utils.d.ts +5 -0
  182. package/lib/typescript/color-area/utils.d.ts.map +1 -0
  183. package/lib/typescript/color-field/ColorFieldInput.d.ts +5 -0
  184. package/lib/typescript/color-field/ColorFieldInput.d.ts.map +1 -0
  185. package/lib/typescript/color-field/ColorFieldRoot.d.ts +7 -0
  186. package/lib/typescript/color-field/ColorFieldRoot.d.ts.map +1 -0
  187. package/lib/typescript/color-field/context.d.ts +6 -0
  188. package/lib/typescript/color-field/context.d.ts.map +1 -0
  189. package/lib/typescript/color-field/index.d.ts +7 -0
  190. package/lib/typescript/color-field/index.d.ts.map +1 -0
  191. package/lib/typescript/color-field/types.d.ts +39 -0
  192. package/lib/typescript/color-field/types.d.ts.map +1 -0
  193. package/lib/typescript/color-field/utils.d.ts +5 -0
  194. package/lib/typescript/color-field/utils.d.ts.map +1 -0
  195. package/lib/typescript/color-picker/ColorPickerRoot.d.ts +7 -0
  196. package/lib/typescript/color-picker/ColorPickerRoot.d.ts.map +1 -0
  197. package/lib/typescript/color-picker/ColorPickerTrigger.d.ts +4 -0
  198. package/lib/typescript/color-picker/ColorPickerTrigger.d.ts.map +1 -0
  199. package/lib/typescript/color-picker/context.d.ts +19 -0
  200. package/lib/typescript/color-picker/context.d.ts.map +1 -0
  201. package/lib/typescript/color-picker/index.d.ts +5 -0
  202. package/lib/typescript/color-picker/index.d.ts.map +1 -0
  203. package/lib/typescript/color-picker/types.d.ts +24 -0
  204. package/lib/typescript/color-picker/types.d.ts.map +1 -0
  205. package/lib/typescript/color-slider/ColorSliderRail.d.ts +6 -0
  206. package/lib/typescript/color-slider/ColorSliderRail.d.ts.map +1 -0
  207. package/lib/typescript/color-slider/ColorSliderRoot.d.ts +7 -0
  208. package/lib/typescript/color-slider/ColorSliderRoot.d.ts.map +1 -0
  209. package/lib/typescript/color-slider/ColorSliderThumb.d.ts +6 -0
  210. package/lib/typescript/color-slider/ColorSliderThumb.d.ts.map +1 -0
  211. package/lib/typescript/color-slider/context.d.ts +6 -0
  212. package/lib/typescript/color-slider/context.d.ts.map +1 -0
  213. package/lib/typescript/color-slider/index.d.ts +6 -0
  214. package/lib/typescript/color-slider/index.d.ts.map +1 -0
  215. package/lib/typescript/color-slider/types.d.ts +82 -0
  216. package/lib/typescript/color-slider/types.d.ts.map +1 -0
  217. package/lib/typescript/color-slider/utils.d.ts +12 -0
  218. package/lib/typescript/color-slider/utils.d.ts.map +1 -0
  219. package/lib/typescript/color-swatch-picker/ColorSwatchPickerItem.d.ts +6 -0
  220. package/lib/typescript/color-swatch-picker/ColorSwatchPickerItem.d.ts.map +1 -0
  221. package/lib/typescript/color-swatch-picker/ColorSwatchPickerRoot.d.ts +7 -0
  222. package/lib/typescript/color-swatch-picker/ColorSwatchPickerRoot.d.ts.map +1 -0
  223. package/lib/typescript/color-swatch-picker/context.d.ts +8 -0
  224. package/lib/typescript/color-swatch-picker/context.d.ts.map +1 -0
  225. package/lib/typescript/color-swatch-picker/index.d.ts +5 -0
  226. package/lib/typescript/color-swatch-picker/index.d.ts.map +1 -0
  227. package/lib/typescript/color-swatch-picker/types.d.ts +26 -0
  228. package/lib/typescript/color-swatch-picker/types.d.ts.map +1 -0
  229. package/lib/typescript/index.d.ts +7 -0
  230. package/lib/typescript/index.d.ts.map +1 -1
  231. package/lib/typescript/slider/useSliderPointer.d.ts.map +1 -1
  232. package/lib/typescript/utils/createPortal.web.d.ts +6 -2
  233. package/lib/typescript/utils/createPortal.web.d.ts.map +1 -1
  234. package/lib/typescript/utils/textAncestor.d.ts +13 -0
  235. package/lib/typescript/utils/textAncestor.d.ts.map +1 -0
  236. package/lib/typescript/utils/textAncestor.web.d.ts +15 -0
  237. package/lib/typescript/utils/textAncestor.web.d.ts.map +1 -0
  238. package/package.json +4 -3
  239. package/src/color/Color.ts +95 -0
  240. package/src/color/index.ts +2 -0
  241. package/src/color/types.ts +19 -0
  242. package/src/color-area/ColorAreaAccessibility.tsx +94 -0
  243. package/src/color-area/ColorAreaAccessibility.web.tsx +52 -0
  244. package/src/color-area/ColorAreaRoot.tsx +326 -0
  245. package/src/color-area/ColorAreaThumb.tsx +56 -0
  246. package/src/color-area/context.tsx +5 -0
  247. package/src/color-area/index.ts +10 -0
  248. package/src/color-area/types.ts +69 -0
  249. package/src/color-area/utils.ts +51 -0
  250. package/src/color-field/ColorFieldInput.tsx +66 -0
  251. package/src/color-field/ColorFieldRoot.tsx +132 -0
  252. package/src/color-field/context.tsx +5 -0
  253. package/src/color-field/index.ts +11 -0
  254. package/src/color-field/types.ts +46 -0
  255. package/src/color-field/utils.ts +75 -0
  256. package/src/color-picker/ColorPickerRoot.tsx +91 -0
  257. package/src/color-picker/ColorPickerTrigger.tsx +96 -0
  258. package/src/color-picker/context.tsx +47 -0
  259. package/src/color-picker/index.ts +12 -0
  260. package/src/color-picker/types.ts +25 -0
  261. package/src/color-slider/ColorSliderRail.tsx +130 -0
  262. package/src/color-slider/ColorSliderRoot.tsx +222 -0
  263. package/src/color-slider/ColorSliderThumb.tsx +294 -0
  264. package/src/color-slider/context.tsx +5 -0
  265. package/src/color-slider/index.ts +16 -0
  266. package/src/color-slider/types.ts +94 -0
  267. package/src/color-slider/utils.ts +79 -0
  268. package/src/color-swatch-picker/ColorSwatchPickerItem.tsx +54 -0
  269. package/src/color-swatch-picker/ColorSwatchPickerRoot.tsx +94 -0
  270. package/src/color-swatch-picker/context.tsx +22 -0
  271. package/src/color-swatch-picker/index.ts +4 -0
  272. package/src/color-swatch-picker/types.ts +29 -0
  273. package/src/index.ts +7 -0
  274. package/src/react-native-web.d.ts +6 -0
  275. package/src/slider/useSliderPointer.ts +3 -0
  276. package/src/utils/createPortal.web.ts +9 -3
  277. package/src/utils/textAncestor.ts +19 -0
  278. package/src/utils/textAncestor.web.ts +19 -0
@@ -0,0 +1,95 @@
1
+ import { parseColor as parseStatelyColor } from '@react-stately/color';
2
+ import type { Color, ColorChannelBySpace, ColorFormat, ColorSpace } from './types';
3
+
4
+ const NUMBER_PATTERN = '[+-]?(?:\\d+(?:\\.\\d+)?|\\.\\d+)';
5
+ const HEX_PATTERN = /^#[\da-f]{3}(?:[\da-f]{3})?$/iu;
6
+ const RGB_PATTERN = new RegExp(
7
+ `^rgb\\(\\s*${NUMBER_PATTERN}\\s*,\\s*${NUMBER_PATTERN}\\s*,\\s*${NUMBER_PATTERN}\\s*\\)$`,
8
+ 'iu',
9
+ );
10
+ const HSL_PATTERN = new RegExp(
11
+ `^hsl\\(\\s*${NUMBER_PATTERN}\\s*,\\s*${NUMBER_PATTERN}%\\s*,\\s*${NUMBER_PATTERN}%\\s*\\)$`,
12
+ 'iu',
13
+ );
14
+
15
+ type StatelyColor = ReturnType<typeof parseStatelyColor>;
16
+
17
+ function isSupportedOpaqueColor(value: string): boolean {
18
+ return HEX_PATTERN.test(value) || RGB_PATTERN.test(value) || HSL_PATTERN.test(value);
19
+ }
20
+
21
+ function normalizeHue(value: number): number {
22
+ if (!Number.isFinite(value)) {
23
+ return 0;
24
+ }
25
+
26
+ return ((value % 360) + 360) % 360;
27
+ }
28
+
29
+ function normalizeChannelValue(
30
+ color: StatelyColor,
31
+ channel: ColorChannelBySpace[ColorSpace],
32
+ value: number,
33
+ ): number {
34
+ if (!Number.isFinite(value)) {
35
+ return color.getChannelValue(channel);
36
+ }
37
+
38
+ if (channel === 'hue') {
39
+ return normalizeHue(value);
40
+ }
41
+
42
+ const { minValue, maxValue } = color.getChannelRange(channel);
43
+ const clampedValue = Math.min(maxValue, Math.max(minValue, value));
44
+
45
+ return channel === 'red' || channel === 'green' || channel === 'blue'
46
+ ? Math.round(clampedValue)
47
+ : clampedValue;
48
+ }
49
+
50
+ function createColor(color: StatelyColor): Color {
51
+ return {
52
+ toString(format: ColorFormat): string {
53
+ const value = color.toString(format);
54
+ return format === 'hex' ? value.toUpperCase() : value;
55
+ },
56
+ getChannel<S extends ColorSpace>(space: S, channel: ColorChannelBySpace[S]): number {
57
+ const value = color.toFormat(space).getChannelValue(channel);
58
+ return channel === 'hue' ? normalizeHue(value) : value;
59
+ },
60
+ withChannel<S extends ColorSpace>(
61
+ space: S,
62
+ channel: ColorChannelBySpace[S],
63
+ value: number,
64
+ ): Color {
65
+ const convertedColor = color.toFormat(space);
66
+ const channelValue = normalizeChannelValue(convertedColor, channel, value);
67
+ return createColor(convertedColor.withChannelValue(channel, channelValue));
68
+ },
69
+ };
70
+ }
71
+
72
+ export function parseColor(value: string): Color | undefined {
73
+ const normalizedValue = value.trim().toLowerCase();
74
+
75
+ if (!isSupportedOpaqueColor(normalizedValue)) {
76
+ return undefined;
77
+ }
78
+
79
+ try {
80
+ let color = parseStatelyColor(normalizedValue);
81
+
82
+ if (RGB_PATTERN.test(normalizedValue)) {
83
+ color = color
84
+ .withChannelValue('red', Math.round(color.getChannelValue('red')))
85
+ .withChannelValue('green', Math.round(color.getChannelValue('green')))
86
+ .withChannelValue('blue', Math.round(color.getChannelValue('blue')));
87
+ } else if (HSL_PATTERN.test(normalizedValue)) {
88
+ color = color.withChannelValue('hue', normalizeHue(color.getChannelValue('hue')));
89
+ }
90
+
91
+ return createColor(color);
92
+ } catch {
93
+ return undefined;
94
+ }
95
+ }
@@ -0,0 +1,2 @@
1
+ export { parseColor } from './Color';
2
+ export type { Color, ColorChannelBySpace, ColorFormat, ColorSpace } from './types';
@@ -0,0 +1,19 @@
1
+ export type ColorFormat = 'hex' | 'rgb' | 'hsl';
2
+
3
+ export type ColorSpace = 'rgb' | 'hsl' | 'hsb';
4
+
5
+ export interface ColorChannelBySpace {
6
+ rgb: 'red' | 'green' | 'blue';
7
+ hsl: 'hue' | 'saturation' | 'lightness';
8
+ hsb: 'hue' | 'saturation' | 'brightness';
9
+ }
10
+
11
+ export interface Color {
12
+ toString(format: ColorFormat): string;
13
+ getChannel<S extends ColorSpace>(space: S, channel: ColorChannelBySpace[S]): number;
14
+ withChannel<S extends ColorSpace>(
15
+ space: S,
16
+ channel: ColorChannelBySpace[S],
17
+ value: number,
18
+ ): Color;
19
+ }
@@ -0,0 +1,94 @@
1
+ import { useCallback } from 'react';
2
+ import { StyleSheet, View, type AccessibilityActionEvent } from 'react-native';
3
+ import { useColorAreaContext } from './context';
4
+ import { clampColorChannel, colorAreaValueText } from './utils';
5
+
6
+ export interface ColorAreaAccessibilityProps {
7
+ readonly accessibilityLabel?: string;
8
+ }
9
+
10
+ export function ColorAreaAccessibility({ accessibilityLabel }: ColorAreaAccessibilityProps) {
11
+ const { value, xValue, yValue, step, isDisabled, setXValue, setYValue } = useColorAreaContext();
12
+
13
+ const handleSaturationAction = useCallback(
14
+ (event: AccessibilityActionEvent) => {
15
+ if (event.nativeEvent.actionName === 'increment') {
16
+ setXValue(xValue + step, true);
17
+ } else if (event.nativeEvent.actionName === 'decrement') {
18
+ setXValue(xValue - step, true);
19
+ }
20
+ },
21
+ [setXValue, step, xValue],
22
+ );
23
+
24
+ const handleBrightnessAction = useCallback(
25
+ (event: AccessibilityActionEvent) => {
26
+ if (event.nativeEvent.actionName === 'increment') {
27
+ setYValue(yValue + step, true);
28
+ } else if (event.nativeEvent.actionName === 'decrement') {
29
+ setYValue(yValue - step, true);
30
+ }
31
+ },
32
+ [setYValue, step, yValue],
33
+ );
34
+
35
+ const actionProps = {
36
+ accessibilityActions: [{ name: 'increment' }, { name: 'decrement' }],
37
+ } as const;
38
+
39
+ return (
40
+ <>
41
+ <View
42
+ {...actionProps}
43
+ accessible
44
+ style={styles.saturationProxy}
45
+ accessibilityRole="adjustable"
46
+ accessibilityLabel={accessibilityLabel ? `${accessibilityLabel}, Saturation` : 'Saturation'}
47
+ accessibilityValue={{
48
+ min: 0,
49
+ max: 100,
50
+ now: Math.round(clampColorChannel(xValue)),
51
+ text: colorAreaValueText(value),
52
+ }}
53
+ accessibilityState={{ disabled: isDisabled }}
54
+ onAccessibilityAction={handleSaturationAction}
55
+ />
56
+ <View
57
+ {...actionProps}
58
+ accessible
59
+ style={styles.brightnessProxy}
60
+ accessibilityRole="adjustable"
61
+ accessibilityLabel={accessibilityLabel ? `${accessibilityLabel}, Brightness` : 'Brightness'}
62
+ accessibilityValue={{
63
+ min: 0,
64
+ max: 100,
65
+ now: Math.round(clampColorChannel(yValue)),
66
+ text: colorAreaValueText(value),
67
+ }}
68
+ accessibilityState={{ disabled: isDisabled }}
69
+ onAccessibilityAction={handleBrightnessAction}
70
+ />
71
+ </>
72
+ );
73
+ }
74
+
75
+ ColorAreaAccessibility.displayName = 'ColorAreaAccessibility';
76
+
77
+ const styles = StyleSheet.create({
78
+ saturationProxy: {
79
+ position: 'absolute',
80
+ top: 0,
81
+ bottom: 0,
82
+ left: 0,
83
+ width: '50%',
84
+ pointerEvents: 'none',
85
+ },
86
+ brightnessProxy: {
87
+ position: 'absolute',
88
+ top: 0,
89
+ right: 0,
90
+ bottom: 0,
91
+ width: '50%',
92
+ pointerEvents: 'none',
93
+ },
94
+ });
@@ -0,0 +1,52 @@
1
+ import { VisuallyHidden } from '@react-aria/visually-hidden';
2
+ import { useColorAreaContext } from './context';
3
+ import type { ColorAreaKeyboardEvent } from './types';
4
+ import { colorAreaValueText, snapColorChannel } from './utils';
5
+
6
+ export interface ColorAreaAccessibilityProps {
7
+ readonly accessibilityLabel?: string;
8
+ }
9
+
10
+ export function ColorAreaAccessibility({ accessibilityLabel }: ColorAreaAccessibilityProps) {
11
+ const { value, xValue, yValue, step, isDisabled, setXValue, setYValue, handleKeyDown } =
12
+ useColorAreaContext();
13
+
14
+ const handleInputKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
15
+ handleKeyDown(event as unknown as ColorAreaKeyboardEvent);
16
+ };
17
+
18
+ return (
19
+ <>
20
+ <VisuallyHidden>
21
+ <input
22
+ type="range"
23
+ min={0}
24
+ max={100}
25
+ step={step}
26
+ value={snapColorChannel(xValue, step)}
27
+ disabled={isDisabled}
28
+ aria-label={accessibilityLabel ? `${accessibilityLabel}, Saturation` : 'Saturation'}
29
+ aria-valuetext={colorAreaValueText(value)}
30
+ onKeyDown={handleInputKeyDown}
31
+ onChange={(event) => setXValue(Number(event.currentTarget.value), true)}
32
+ />
33
+ </VisuallyHidden>
34
+ <VisuallyHidden>
35
+ <input
36
+ type="range"
37
+ min={0}
38
+ max={100}
39
+ step={step}
40
+ value={snapColorChannel(yValue, step)}
41
+ disabled={isDisabled}
42
+ aria-label={accessibilityLabel ? `${accessibilityLabel}, Brightness` : 'Brightness'}
43
+ aria-valuetext={colorAreaValueText(value)}
44
+ onKeyDown={handleInputKeyDown}
45
+ onChange={(event) => setYValue(Number(event.currentTarget.value), true)}
46
+ />
47
+ </VisuallyHidden>
48
+ </>
49
+ );
50
+ }
51
+
52
+ ColorAreaAccessibility.displayName = 'ColorAreaAccessibility';
@@ -0,0 +1,326 @@
1
+ import { forwardRef, useCallback, useMemo, useRef, useState } from 'react';
2
+ import {
3
+ I18nManager,
4
+ PanResponder,
5
+ Platform,
6
+ View,
7
+ type GestureResponderEvent,
8
+ type LayoutChangeEvent,
9
+ } from 'react-native';
10
+ import { useControllableState } from '@cdx-ui/utils';
11
+ import { parseColor, type Color } from '../color';
12
+ import { useOptionalColorPickerContext } from '../color-picker';
13
+ import { Slot } from '../slot';
14
+ import { dataAttributes, mergeDataAttributes } from '../utils/dataAttributes';
15
+ import { ColorAreaAccessibility } from './ColorAreaAccessibility';
16
+ import { ColorAreaProvider } from './context';
17
+ import type {
18
+ ColorAreaContextValue,
19
+ ColorAreaKeyboardEvent,
20
+ ColorAreaLayout,
21
+ IColorAreaProps,
22
+ } from './types';
23
+ import { clampColorChannel, snapColorChannel } from './utils';
24
+
25
+ function createDefaultColor(): Color {
26
+ const color = parseColor('#FF0000');
27
+ if (!color) {
28
+ throw new Error('ColorArea default color could not be parsed');
29
+ }
30
+ return color;
31
+ }
32
+
33
+ const DEFAULT_COLOR = createDefaultColor();
34
+ const EMPTY_LAYOUT: ColorAreaLayout = { width: 0, height: 0 };
35
+
36
+ function getFinitePoint(event: GestureResponderEvent): { x: number; y: number } | null {
37
+ const { locationX: x, locationY: y } = event.nativeEvent;
38
+ return Number.isFinite(x) && Number.isFinite(y) ? { x, y } : null;
39
+ }
40
+
41
+ export interface ColorAreaRootProps extends IColorAreaProps {
42
+ readonly asChild?: boolean;
43
+ }
44
+
45
+ export const ColorAreaRoot = forwardRef<View, ColorAreaRootProps>(
46
+ (
47
+ {
48
+ asChild,
49
+ value: valueProp,
50
+ defaultValue,
51
+ onValueChange,
52
+ onValueChangeComplete,
53
+ colorSpace = 'hsb',
54
+ xChannel = 'saturation',
55
+ yChannel = 'brightness',
56
+ step = 1,
57
+ pageStep = 10,
58
+ isRTL = I18nManager.isRTL,
59
+ isDisabled = false,
60
+ accessibilityLabel,
61
+ children,
62
+ onLayout,
63
+ ...props
64
+ },
65
+ ref,
66
+ ) => {
67
+ const picker = useOptionalColorPickerContext();
68
+ const usesPickerContext =
69
+ picker !== null && valueProp === undefined && defaultValue === undefined;
70
+ const [localValue, setLocalValue] = useControllableState<Color>({
71
+ prop: valueProp,
72
+ defaultProp: defaultValue ?? DEFAULT_COLOR,
73
+ onChange: onValueChange,
74
+ });
75
+ const value = (usesPickerContext ? picker.value : localValue) ?? DEFAULT_COLOR;
76
+ const [layout, setLayoutState] = useState<ColorAreaLayout>(EMPTY_LAYOUT);
77
+ const [thumbSize, setThumbSizeState] = useState<ColorAreaLayout>(EMPTY_LAYOUT);
78
+
79
+ const valueRef = useRef(value);
80
+ valueRef.current = value;
81
+ const layoutRef = useRef(layout);
82
+ layoutRef.current = layout;
83
+ const isDisabledRef = useRef(isDisabled);
84
+ isDisabledRef.current = isDisabled;
85
+ const setChannelsFromPointRef = useRef<
86
+ ((x: number, y: number, complete?: boolean) => void) | undefined
87
+ >(undefined);
88
+ const lastValidPointRef = useRef<{ x: number; y: number } | null>(null);
89
+
90
+ const commitValue = useCallback(
91
+ (next: Color, complete = false) => {
92
+ if (isDisabled) {
93
+ return;
94
+ }
95
+
96
+ valueRef.current = next;
97
+ if (usesPickerContext) {
98
+ onValueChange?.(next);
99
+ picker.onValueChange(next);
100
+ } else {
101
+ setLocalValue(next);
102
+ }
103
+ if (complete) {
104
+ onValueChangeComplete?.(next);
105
+ }
106
+ },
107
+ [isDisabled, onValueChange, onValueChangeComplete, picker, setLocalValue, usesPickerContext],
108
+ );
109
+
110
+ const setXValue = useCallback(
111
+ (nextValue: number, complete = false) => {
112
+ commitValue(
113
+ valueRef.current.withChannel(colorSpace, xChannel, snapColorChannel(nextValue, step)),
114
+ complete,
115
+ );
116
+ },
117
+ [colorSpace, commitValue, step, xChannel],
118
+ );
119
+
120
+ const setYValue = useCallback(
121
+ (nextValue: number, complete = false) => {
122
+ commitValue(
123
+ valueRef.current.withChannel(colorSpace, yChannel, snapColorChannel(nextValue, step)),
124
+ complete,
125
+ );
126
+ },
127
+ [colorSpace, commitValue, step, yChannel],
128
+ );
129
+
130
+ const setChannelsFromPoint = useCallback(
131
+ (x: number, y: number, complete = false) => {
132
+ const currentLayout = layoutRef.current;
133
+ if (
134
+ isDisabledRef.current ||
135
+ !Number.isFinite(x) ||
136
+ !Number.isFinite(y) ||
137
+ !Number.isFinite(currentLayout.width) ||
138
+ !Number.isFinite(currentLayout.height) ||
139
+ currentLayout.width <= 0 ||
140
+ currentLayout.height <= 0
141
+ ) {
142
+ return;
143
+ }
144
+
145
+ const visualXRatio = clampColorChannel(x, 0, currentLayout.width) / currentLayout.width;
146
+ const xRatio = isRTL ? 1 - visualXRatio : visualXRatio;
147
+ const yRatio = 1 - clampColorChannel(y, 0, currentLayout.height) / currentLayout.height;
148
+ const next = valueRef.current
149
+ .withChannel(colorSpace, xChannel, xRatio * 100)
150
+ .withChannel(colorSpace, yChannel, yRatio * 100);
151
+
152
+ commitValue(next, complete);
153
+ },
154
+ [colorSpace, commitValue, isRTL, xChannel, yChannel],
155
+ );
156
+ setChannelsFromPointRef.current = setChannelsFromPoint;
157
+
158
+ const handleKeyDown = useCallback(
159
+ (event: ColorAreaKeyboardEvent) => {
160
+ if (isDisabled) {
161
+ return;
162
+ }
163
+
164
+ const key = event.nativeEvent?.key ?? event.key;
165
+ const shiftKey = event.nativeEvent?.shiftKey ?? event.shiftKey;
166
+ const increment = shiftKey ? pageStep : step;
167
+ const currentX = valueRef.current.getChannel(colorSpace, xChannel);
168
+ const currentY = valueRef.current.getChannel(colorSpace, yChannel);
169
+ let handled = true;
170
+
171
+ if (key === 'ArrowLeft') {
172
+ setXValue(currentX + (isRTL ? increment : -increment), true);
173
+ } else if (key === 'ArrowRight') {
174
+ setXValue(currentX + (isRTL ? -increment : increment), true);
175
+ } else if (key === 'ArrowUp') {
176
+ setYValue(currentY + increment, true);
177
+ } else if (key === 'ArrowDown') {
178
+ setYValue(currentY - increment, true);
179
+ } else if (key === 'PageUp') {
180
+ setYValue(currentY + pageStep, true);
181
+ } else if (key === 'PageDown') {
182
+ setYValue(currentY - pageStep, true);
183
+ } else if (key === 'Home') {
184
+ setXValue(currentX + (isRTL ? pageStep : -pageStep), true);
185
+ } else if (key === 'End') {
186
+ setXValue(currentX + (isRTL ? -pageStep : pageStep), true);
187
+ } else {
188
+ handled = false;
189
+ }
190
+
191
+ if (handled) {
192
+ event.preventDefault?.();
193
+ }
194
+ },
195
+ [colorSpace, isDisabled, isRTL, pageStep, setXValue, setYValue, step, xChannel, yChannel],
196
+ );
197
+
198
+ const panResponder = useMemo(() => {
199
+ const updateFromEvent = (event: GestureResponderEvent, complete = false) => {
200
+ const point = getFinitePoint(event);
201
+ if (point) {
202
+ lastValidPointRef.current = point;
203
+ }
204
+ const resolvedPoint = point ?? lastValidPointRef.current;
205
+ if (resolvedPoint) {
206
+ setChannelsFromPointRef.current?.(resolvedPoint.x, resolvedPoint.y, complete);
207
+ }
208
+ };
209
+
210
+ return PanResponder.create({
211
+ onStartShouldSetPanResponder: () => !isDisabledRef.current,
212
+ onMoveShouldSetPanResponder: () => !isDisabledRef.current,
213
+ onPanResponderTerminationRequest: () => false,
214
+ onShouldBlockNativeResponder: () => true,
215
+ onPanResponderGrant: (event) => {
216
+ lastValidPointRef.current = null;
217
+ updateFromEvent(event);
218
+ },
219
+ onPanResponderMove: (event) => {
220
+ updateFromEvent(event);
221
+ },
222
+ onPanResponderRelease: (event) => {
223
+ updateFromEvent(event, true);
224
+ lastValidPointRef.current = null;
225
+ },
226
+ onPanResponderTerminate: (event) => {
227
+ updateFromEvent(event, true);
228
+ lastValidPointRef.current = null;
229
+ },
230
+ });
231
+ }, []);
232
+
233
+ const setLayout = useCallback((next: ColorAreaLayout) => {
234
+ setLayoutState((previous) =>
235
+ previous.width === next.width && previous.height === next.height ? previous : next,
236
+ );
237
+ }, []);
238
+
239
+ const setThumbSize = useCallback((next: ColorAreaLayout) => {
240
+ setThumbSizeState((previous) =>
241
+ previous.width === next.width && previous.height === next.height ? previous : next,
242
+ );
243
+ }, []);
244
+
245
+ const handleLayout = useCallback(
246
+ (event: LayoutChangeEvent) => {
247
+ const { width, height } = event.nativeEvent.layout;
248
+ setLayout({ width, height });
249
+ onLayout?.(event);
250
+ },
251
+ [onLayout, setLayout],
252
+ );
253
+
254
+ const xValue = value.getChannel(colorSpace, xChannel);
255
+ const yValue = value.getChannel(colorSpace, yChannel);
256
+ const contextValue = useMemo<ColorAreaContextValue>(
257
+ () => ({
258
+ value,
259
+ xValue,
260
+ yValue,
261
+ xPercent: clampColorChannel(xValue),
262
+ yPercent: clampColorChannel(yValue),
263
+ layout,
264
+ thumbSize,
265
+ isRTL,
266
+ isDisabled,
267
+ step,
268
+ pageStep,
269
+ setLayout,
270
+ setThumbSize,
271
+ setChannelsFromPoint,
272
+ setXValue,
273
+ setYValue,
274
+ handleKeyDown,
275
+ }),
276
+ [
277
+ handleKeyDown,
278
+ isDisabled,
279
+ isRTL,
280
+ layout,
281
+ pageStep,
282
+ setChannelsFromPoint,
283
+ setLayout,
284
+ setThumbSize,
285
+ setXValue,
286
+ setYValue,
287
+ step,
288
+ thumbSize,
289
+ value,
290
+ xValue,
291
+ yValue,
292
+ ],
293
+ );
294
+
295
+ const Comp = asChild ? Slot : View;
296
+
297
+ return (
298
+ <ColorAreaProvider value={contextValue}>
299
+ <Comp
300
+ ref={ref}
301
+ {...props}
302
+ {...panResponder.panHandlers}
303
+ onLayout={handleLayout}
304
+ role={Platform.OS === 'web' ? 'group' : undefined}
305
+ aria-label={props['aria-label'] ?? accessibilityLabel}
306
+ accessibilityLabel={accessibilityLabel}
307
+ accessibilityState={{ disabled: isDisabled }}
308
+ {...mergeDataAttributes(
309
+ props,
310
+ dataAttributes({
311
+ disabled: isDisabled,
312
+ colorSpace,
313
+ xChannel,
314
+ yChannel,
315
+ }),
316
+ )}
317
+ >
318
+ {children}
319
+ <ColorAreaAccessibility accessibilityLabel={accessibilityLabel} />
320
+ </Comp>
321
+ </ColorAreaProvider>
322
+ );
323
+ },
324
+ );
325
+
326
+ ColorAreaRoot.displayName = 'ColorAreaRoot';
@@ -0,0 +1,56 @@
1
+ import { forwardRef, useCallback, useMemo } from 'react';
2
+ import { View, type LayoutChangeEvent, type ViewStyle } from 'react-native';
3
+ import { Slot } from '../slot';
4
+ import { dataAttributes, mergeDataAttributes } from '../utils/dataAttributes';
5
+ import { useColorAreaContext } from './context';
6
+ import type { IColorAreaThumbProps } from './types';
7
+
8
+ export interface ColorAreaThumbProps extends IColorAreaThumbProps {}
9
+
10
+ export const ColorAreaThumb = forwardRef<View, ColorAreaThumbProps>(
11
+ ({ asChild, style, onLayout, children, ...props }, ref) => {
12
+ const { xPercent, yPercent, layout, thumbSize, isRTL, setThumbSize } = useColorAreaContext();
13
+
14
+ const positionStyle = useMemo<ViewStyle>(() => {
15
+ const visualXPercent = isRTL ? 100 - xPercent : xPercent;
16
+ const translateX = (visualXPercent / 100) * layout.width - thumbSize.width / 2;
17
+ const translateY = ((100 - yPercent) / 100) * layout.height - thumbSize.height / 2;
18
+
19
+ return {
20
+ position: 'absolute',
21
+ top: 0,
22
+ left: 0,
23
+ pointerEvents: 'none',
24
+ transform: [{ translateX }, { translateY }],
25
+ };
26
+ }, [isRTL, layout.height, layout.width, thumbSize.height, thumbSize.width, xPercent, yPercent]);
27
+
28
+ const handleLayout = useCallback(
29
+ (event: LayoutChangeEvent) => {
30
+ const { width, height } = event.nativeEvent.layout;
31
+ setThumbSize({ width, height });
32
+ onLayout?.(event);
33
+ },
34
+ [onLayout, setThumbSize],
35
+ );
36
+
37
+ const Comp = asChild ? Slot : View;
38
+
39
+ return (
40
+ <Comp
41
+ ref={ref}
42
+ {...props}
43
+ accessible={false}
44
+ accessibilityElementsHidden
45
+ importantForAccessibility="no-hide-descendants"
46
+ onLayout={handleLayout}
47
+ style={[positionStyle, style]}
48
+ {...mergeDataAttributes(props, dataAttributes({ slot: 'color-area-thumb' }))}
49
+ >
50
+ {children}
51
+ </Comp>
52
+ );
53
+ },
54
+ );
55
+
56
+ ColorAreaThumb.displayName = 'ColorAreaThumb';
@@ -0,0 +1,5 @@
1
+ import { createContext } from '@cdx-ui/utils';
2
+ import type { ColorAreaContextValue } from './types';
3
+
4
+ export const [ColorAreaProvider, useColorAreaContext] =
5
+ createContext<ColorAreaContextValue>('ColorAreaContext');
@@ -0,0 +1,10 @@
1
+ export { ColorAreaRoot, type ColorAreaRootProps } from './ColorAreaRoot';
2
+ export { ColorAreaThumb, type ColorAreaThumbProps } from './ColorAreaThumb';
3
+ export { useColorAreaContext } from './context';
4
+ export type {
5
+ ColorAreaContextValue,
6
+ ColorAreaKeyboardEvent,
7
+ ColorAreaLayout,
8
+ IColorAreaProps,
9
+ IColorAreaThumbProps,
10
+ } from './types';