@refraktor/core 0.0.3 → 0.0.4

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 (284) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/build/components/button/button.styles.js +5 -5
  3. package/build/components/chip/chip-group/chip-group.d.ts +4 -0
  4. package/build/components/chip/chip-group/chip-group.d.ts.map +1 -0
  5. package/build/components/chip/chip-group/chip-group.js +43 -0
  6. package/build/components/chip/chip-group/index.d.ts +2 -0
  7. package/build/components/chip/chip-group/index.d.ts.map +1 -0
  8. package/build/components/chip/chip-group/index.js +1 -0
  9. package/build/components/chip/chip.context.d.ts +15 -0
  10. package/build/components/chip/chip.context.d.ts.map +1 -0
  11. package/build/components/chip/chip.context.js +2 -0
  12. package/build/components/chip/chip.d.ts +4 -0
  13. package/build/components/chip/chip.d.ts.map +1 -0
  14. package/build/components/chip/chip.js +85 -0
  15. package/build/components/chip/chip.styles.d.ts +5 -0
  16. package/build/components/chip/chip.styles.d.ts.map +1 -0
  17. package/build/components/chip/chip.styles.js +19 -0
  18. package/build/components/chip/chip.test.d.ts +2 -0
  19. package/build/components/chip/chip.test.d.ts.map +1 -0
  20. package/build/components/chip/chip.test.js +107 -0
  21. package/build/components/chip/chip.types.d.ts +95 -0
  22. package/build/components/chip/chip.types.d.ts.map +1 -0
  23. package/build/components/chip/chip.types.js +1 -0
  24. package/build/components/chip/index.d.ts +4 -0
  25. package/build/components/chip/index.d.ts.map +1 -0
  26. package/build/components/chip/index.js +2 -0
  27. package/build/components/combobox/combobox-dropdown/combobox-dropdown.d.ts +4 -0
  28. package/build/components/combobox/combobox-dropdown/combobox-dropdown.d.ts.map +1 -0
  29. package/build/components/combobox/combobox-dropdown/combobox-dropdown.js +21 -0
  30. package/build/components/combobox/combobox-dropdown/index.d.ts +2 -0
  31. package/build/components/combobox/combobox-dropdown/index.d.ts.map +1 -0
  32. package/build/components/combobox/combobox-dropdown/index.js +1 -0
  33. package/build/components/combobox/combobox-group/combobox-group.d.ts +4 -0
  34. package/build/components/combobox/combobox-group/combobox-group.d.ts.map +1 -0
  35. package/build/components/combobox/combobox-group/combobox-group.js +15 -0
  36. package/build/components/combobox/combobox-group/index.d.ts +2 -0
  37. package/build/components/combobox/combobox-group/index.d.ts.map +1 -0
  38. package/build/components/combobox/combobox-group/index.js +1 -0
  39. package/build/components/combobox/combobox-input/combobox-input.d.ts +4 -0
  40. package/build/components/combobox/combobox-input/combobox-input.d.ts.map +1 -0
  41. package/build/components/combobox/combobox-input/combobox-input.js +101 -0
  42. package/build/components/combobox/combobox-input/index.d.ts +2 -0
  43. package/build/components/combobox/combobox-input/index.d.ts.map +1 -0
  44. package/build/components/combobox/combobox-input/index.js +1 -0
  45. package/build/components/combobox/combobox-option/combobox-option.d.ts +4 -0
  46. package/build/components/combobox/combobox-option/combobox-option.d.ts.map +1 -0
  47. package/build/components/combobox/combobox-option/combobox-option.js +86 -0
  48. package/build/components/combobox/combobox-option/index.d.ts +2 -0
  49. package/build/components/combobox/combobox-option/index.d.ts.map +1 -0
  50. package/build/components/combobox/combobox-option/index.js +1 -0
  51. package/build/components/combobox/combobox-root/combobox-root.d.ts +4 -0
  52. package/build/components/combobox/combobox-root/combobox-root.d.ts.map +1 -0
  53. package/build/components/combobox/combobox-root/combobox-root.js +282 -0
  54. package/build/components/combobox/combobox-root/index.d.ts +2 -0
  55. package/build/components/combobox/combobox-root/index.d.ts.map +1 -0
  56. package/build/components/combobox/combobox-root/index.js +1 -0
  57. package/build/components/combobox/combobox.context.d.ts +73 -0
  58. package/build/components/combobox/combobox.context.d.ts.map +1 -0
  59. package/build/components/combobox/combobox.context.js +50 -0
  60. package/build/components/combobox/combobox.d.ts +4 -0
  61. package/build/components/combobox/combobox.d.ts.map +1 -0
  62. package/build/components/combobox/combobox.js +31 -0
  63. package/build/components/combobox/combobox.test.d.ts +2 -0
  64. package/build/components/combobox/combobox.test.d.ts.map +1 -0
  65. package/build/components/combobox/combobox.test.js +104 -0
  66. package/build/components/combobox/combobox.types.d.ts +205 -0
  67. package/build/components/combobox/combobox.types.d.ts.map +1 -0
  68. package/build/components/combobox/combobox.types.js +1 -0
  69. package/build/components/combobox/index.d.ts +8 -0
  70. package/build/components/combobox/index.d.ts.map +1 -0
  71. package/build/components/combobox/index.js +6 -0
  72. package/build/components/combobox/use-combobox.d.ts +32 -0
  73. package/build/components/combobox/use-combobox.d.ts.map +1 -0
  74. package/build/components/combobox/use-combobox.js +80 -0
  75. package/build/components/drawer/drawer-body/drawer-body.d.ts +4 -0
  76. package/build/components/drawer/drawer-body/drawer-body.d.ts.map +1 -0
  77. package/build/components/drawer/drawer-body/drawer-body.js +11 -0
  78. package/build/components/drawer/drawer-body/index.d.ts +2 -0
  79. package/build/components/drawer/drawer-body/index.d.ts.map +1 -0
  80. package/build/components/drawer/drawer-body/index.js +1 -0
  81. package/build/components/drawer/drawer-content/drawer-content.d.ts.map +1 -1
  82. package/build/components/drawer/drawer-content/drawer-content.js +9 -6
  83. package/build/components/drawer/drawer-overlay/drawer-overlay.d.ts.map +1 -1
  84. package/build/components/drawer/drawer-overlay/drawer-overlay.js +4 -4
  85. package/build/components/drawer/drawer-root/drawer-root.d.ts.map +1 -1
  86. package/build/components/drawer/drawer-root/drawer-root.js +8 -9
  87. package/build/components/drawer/drawer.context.d.ts +2 -1
  88. package/build/components/drawer/drawer.context.d.ts.map +1 -1
  89. package/build/components/drawer/drawer.d.ts.map +1 -1
  90. package/build/components/drawer/drawer.js +5 -3
  91. package/build/components/drawer/drawer.test.js +55 -16
  92. package/build/components/drawer/drawer.types.d.ts +29 -3
  93. package/build/components/drawer/drawer.types.d.ts.map +1 -1
  94. package/build/components/drawer/index.d.ts +2 -1
  95. package/build/components/drawer/index.d.ts.map +1 -1
  96. package/build/components/drawer/index.js +1 -0
  97. package/build/components/drawer/use-drawer.d.ts +8 -1
  98. package/build/components/drawer/use-drawer.d.ts.map +1 -1
  99. package/build/components/drawer/use-drawer.js +25 -38
  100. package/build/components/file-input/file-input.d.ts +4 -0
  101. package/build/components/file-input/file-input.d.ts.map +1 -0
  102. package/build/components/file-input/file-input.js +88 -0
  103. package/build/components/file-input/file-input.test.d.ts +2 -0
  104. package/build/components/file-input/file-input.test.d.ts.map +1 -0
  105. package/build/components/file-input/file-input.test.js +74 -0
  106. package/build/components/file-input/file-input.types.d.ts +46 -0
  107. package/build/components/file-input/file-input.types.d.ts.map +1 -0
  108. package/build/components/file-input/file-input.types.js +1 -0
  109. package/build/components/file-input/file-input.utils.d.ts +11 -0
  110. package/build/components/file-input/file-input.utils.d.ts.map +1 -0
  111. package/build/components/file-input/file-input.utils.js +67 -0
  112. package/build/components/file-input/file-input.utils.test.d.ts +2 -0
  113. package/build/components/file-input/file-input.utils.test.d.ts.map +1 -0
  114. package/build/components/file-input/file-input.utils.test.js +27 -0
  115. package/build/components/file-input/index.d.ts +3 -0
  116. package/build/components/file-input/index.d.ts.map +1 -0
  117. package/build/components/file-input/index.js +2 -0
  118. package/build/components/for/for.d.ts +8 -0
  119. package/build/components/for/for.d.ts.map +1 -0
  120. package/build/components/for/for.js +32 -0
  121. package/build/components/for/for.test.d.ts +2 -0
  122. package/build/components/for/for.test.d.ts.map +1 -0
  123. package/build/components/for/for.test.js +31 -0
  124. package/build/components/for/for.types.d.ts +33 -0
  125. package/build/components/for/for.types.d.ts.map +1 -0
  126. package/build/components/for/for.types.js +1 -0
  127. package/build/components/for/index.d.ts +3 -0
  128. package/build/components/for/index.d.ts.map +1 -0
  129. package/build/components/for/index.js +1 -0
  130. package/build/components/index.d.ts +6 -0
  131. package/build/components/index.d.ts.map +1 -1
  132. package/build/components/index.js +6 -0
  133. package/build/components/modal/index.d.ts +2 -1
  134. package/build/components/modal/index.d.ts.map +1 -1
  135. package/build/components/modal/index.js +1 -0
  136. package/build/components/modal/modal-body/index.d.ts +2 -0
  137. package/build/components/modal/modal-body/index.d.ts.map +1 -0
  138. package/build/components/modal/modal-body/index.js +1 -0
  139. package/build/components/modal/modal-body/modal-body.d.ts +4 -0
  140. package/build/components/modal/modal-body/modal-body.d.ts.map +1 -0
  141. package/build/components/modal/modal-body/modal-body.js +11 -0
  142. package/build/components/modal/modal-content/modal-content.d.ts.map +1 -1
  143. package/build/components/modal/modal-content/modal-content.js +13 -5
  144. package/build/components/modal/modal-header/modal-header.js +2 -2
  145. package/build/components/modal/modal-overlay/modal-overlay.d.ts.map +1 -1
  146. package/build/components/modal/modal-overlay/modal-overlay.js +4 -4
  147. package/build/components/modal/modal-root/modal-root.d.ts.map +1 -1
  148. package/build/components/modal/modal-root/modal-root.js +12 -9
  149. package/build/components/modal/modal.context.d.ts +5 -2
  150. package/build/components/modal/modal.context.d.ts.map +1 -1
  151. package/build/components/modal/modal.d.ts.map +1 -1
  152. package/build/components/modal/modal.js +5 -3
  153. package/build/components/modal/modal.test.js +78 -13
  154. package/build/components/modal/modal.types.d.ts +34 -5
  155. package/build/components/modal/modal.types.d.ts.map +1 -1
  156. package/build/components/modal/use-modal.d.ts +8 -1
  157. package/build/components/modal/use-modal.d.ts.map +1 -1
  158. package/build/components/modal/use-modal.js +25 -38
  159. package/build/components/password-input/index.d.ts +3 -0
  160. package/build/components/password-input/index.d.ts.map +1 -0
  161. package/build/components/password-input/index.js +2 -0
  162. package/build/components/password-input/password-input.d.ts +4 -0
  163. package/build/components/password-input/password-input.d.ts.map +1 -0
  164. package/build/components/password-input/password-input.js +32 -0
  165. package/build/components/password-input/password-input.test.d.ts +2 -0
  166. package/build/components/password-input/password-input.test.d.ts.map +1 -0
  167. package/build/components/password-input/password-input.test.js +47 -0
  168. package/build/components/password-input/password-input.types.d.ts +24 -0
  169. package/build/components/password-input/password-input.types.d.ts.map +1 -0
  170. package/build/components/password-input/password-input.types.js +1 -0
  171. package/build/components/pin-input/index.d.ts +3 -0
  172. package/build/components/pin-input/index.d.ts.map +1 -0
  173. package/build/components/pin-input/index.js +2 -0
  174. package/build/components/pin-input/pin-input.d.ts +4 -0
  175. package/build/components/pin-input/pin-input.d.ts.map +1 -0
  176. package/build/components/pin-input/pin-input.js +245 -0
  177. package/build/components/pin-input/pin-input.test.d.ts +2 -0
  178. package/build/components/pin-input/pin-input.test.d.ts.map +1 -0
  179. package/build/components/pin-input/pin-input.test.js +87 -0
  180. package/build/components/pin-input/pin-input.types.d.ts +44 -0
  181. package/build/components/pin-input/pin-input.types.d.ts.map +1 -0
  182. package/build/components/pin-input/pin-input.types.js +1 -0
  183. package/build/components/scroll-area/index.d.ts +3 -0
  184. package/build/components/scroll-area/index.d.ts.map +1 -0
  185. package/build/components/scroll-area/index.js +1 -0
  186. package/build/components/scroll-area/scroll-area.d.ts +4 -0
  187. package/build/components/scroll-area/scroll-area.d.ts.map +1 -0
  188. package/build/components/scroll-area/scroll-area.js +30 -0
  189. package/build/components/scroll-area/scroll-area.test.d.ts +2 -0
  190. package/build/components/scroll-area/scroll-area.test.d.ts.map +1 -0
  191. package/build/components/scroll-area/scroll-area.test.js +39 -0
  192. package/build/components/scroll-area/scroll-area.types.d.ts +25 -0
  193. package/build/components/scroll-area/scroll-area.types.d.ts.map +1 -0
  194. package/build/components/scroll-area/scroll-area.types.js +1 -0
  195. package/build/components/segmented-control/segmented-control.d.ts.map +1 -1
  196. package/build/components/segmented-control/segmented-control.js +3 -3
  197. package/build/components/segmented-control/segmented-control.styles.js +13 -13
  198. package/build/components/segmented-control/segmented-control.test.js +11 -0
  199. package/build/components/segmented-control/segmented-control.types.d.ts +2 -0
  200. package/build/components/segmented-control/segmented-control.types.d.ts.map +1 -1
  201. package/build/icons/eye-off.d.ts +4 -0
  202. package/build/icons/eye-off.d.ts.map +1 -0
  203. package/build/icons/eye-off.js +5 -0
  204. package/build/icons/eye.d.ts +4 -0
  205. package/build/icons/eye.d.ts.map +1 -0
  206. package/build/icons/eye.js +5 -0
  207. package/build/icons/index.d.ts +2 -0
  208. package/build/icons/index.d.ts.map +1 -1
  209. package/build/icons/index.js +2 -0
  210. package/build/style.css +1 -1
  211. package/package.json +2 -2
  212. package/src/components/button/button.styles.ts +5 -5
  213. package/src/components/chip/chip-group/chip-group.tsx +107 -0
  214. package/src/components/chip/chip-group/index.ts +1 -0
  215. package/src/components/chip/chip.context.ts +15 -0
  216. package/src/components/chip/chip.styles.ts +36 -0
  217. package/src/components/chip/chip.test.tsx +197 -0
  218. package/src/components/chip/chip.tsx +208 -0
  219. package/src/components/chip/chip.types.ts +134 -0
  220. package/src/components/chip/index.ts +10 -0
  221. package/src/components/drawer/drawer-body/drawer-body.tsx +29 -0
  222. package/src/components/drawer/drawer-body/index.ts +1 -0
  223. package/src/components/drawer/drawer-content/drawer-content.tsx +63 -26
  224. package/src/components/drawer/drawer-overlay/drawer-overlay.tsx +6 -5
  225. package/src/components/drawer/drawer-root/drawer-root.tsx +17 -18
  226. package/src/components/drawer/drawer.context.ts +2 -1
  227. package/src/components/drawer/drawer.test.tsx +144 -36
  228. package/src/components/drawer/drawer.tsx +31 -3
  229. package/src/components/drawer/drawer.types.ts +37 -3
  230. package/src/components/drawer/index.ts +2 -0
  231. package/src/components/drawer/use-drawer.ts +44 -51
  232. package/src/components/file-input/file-input.test.tsx +134 -0
  233. package/src/components/file-input/file-input.tsx +224 -0
  234. package/src/components/file-input/file-input.types.ts +78 -0
  235. package/src/components/file-input/file-input.utils.test.ts +36 -0
  236. package/src/components/file-input/file-input.utils.ts +130 -0
  237. package/src/components/file-input/index.ts +2 -0
  238. package/src/components/for/for.test.tsx +66 -0
  239. package/src/components/for/for.tsx +53 -0
  240. package/src/components/for/for.types.ts +40 -0
  241. package/src/components/for/index.ts +2 -0
  242. package/src/components/index.ts +6 -0
  243. package/src/components/menu/menu-dropdown/menu-dropdown.tsx +220 -220
  244. package/src/components/menu/menu-sub-dropdown/menu-sub-dropdown.tsx +221 -221
  245. package/src/components/modal/index.ts +4 -1
  246. package/src/components/modal/modal-body/index.ts +1 -0
  247. package/src/components/modal/modal-body/modal-body.tsx +29 -0
  248. package/src/components/modal/modal-content/modal-content.tsx +71 -24
  249. package/src/components/modal/modal-header/modal-header.tsx +2 -2
  250. package/src/components/modal/modal-overlay/modal-overlay.tsx +46 -45
  251. package/src/components/modal/modal-root/modal-root.tsx +22 -17
  252. package/src/components/modal/modal.context.ts +5 -2
  253. package/src/components/modal/modal.test.tsx +234 -64
  254. package/src/components/modal/modal.tsx +36 -4
  255. package/src/components/modal/modal.types.ts +49 -8
  256. package/src/components/modal/use-modal.ts +44 -51
  257. package/src/components/password-input/index.ts +2 -0
  258. package/src/components/password-input/password-input.test.tsx +72 -0
  259. package/src/components/password-input/password-input.tsx +85 -0
  260. package/src/components/password-input/password-input.types.ts +30 -0
  261. package/src/components/pin-input/index.ts +2 -0
  262. package/src/components/pin-input/pin-input.test.tsx +149 -0
  263. package/src/components/pin-input/pin-input.tsx +473 -0
  264. package/src/components/pin-input/pin-input.types.ts +78 -0
  265. package/src/components/scroll-area/index.ts +6 -0
  266. package/src/components/scroll-area/scroll-area.test.tsx +72 -0
  267. package/src/components/scroll-area/scroll-area.tsx +70 -0
  268. package/src/components/scroll-area/scroll-area.types.ts +37 -0
  269. package/src/components/segmented-control/segmented-control.styles.ts +13 -13
  270. package/src/components/segmented-control/segmented-control.test.tsx +18 -0
  271. package/src/components/segmented-control/segmented-control.tsx +11 -1
  272. package/src/components/segmented-control/segmented-control.types.ts +3 -0
  273. package/src/components/select/select-dropdown/select-dropdown.tsx +299 -299
  274. package/src/components/select/select-root/select-root.tsx +333 -333
  275. package/src/components/select/select-trigger/select-trigger.tsx +123 -123
  276. package/src/components/select/select.context.ts +140 -140
  277. package/src/components/select/select.test.tsx +190 -190
  278. package/src/components/select/select.types.ts +272 -272
  279. package/src/components/select/use-select.ts +170 -170
  280. package/src/icons/eye-off.tsx +30 -0
  281. package/src/icons/eye.tsx +24 -0
  282. package/src/icons/index.ts +2 -0
  283. package/src/style.css +14 -8
  284. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,473 @@
1
+ import { useId, useUncontrolled } from "@refraktor/utils";
2
+ import { useCallback, useMemo, useRef } from "react";
3
+ import { useTheme } from "../../theme";
4
+ import {
5
+ createClassNamesConfig,
6
+ createComponentConfig,
7
+ factory,
8
+ useClassNames,
9
+ useProps
10
+ } from "../../utils";
11
+ import { getVariant } from "../input/input-field/input-field.styles";
12
+ import { InputWrapper } from "../input/input-wrapper";
13
+ import {
14
+ PinInputCharacterSet,
15
+ PinInputClassNames,
16
+ PinInputFactoryPayload,
17
+ PinInputProps,
18
+ PinInputTransform
19
+ } from "./pin-input.types";
20
+
21
+ const CHARACTER_SET_PATTERNS: Record<PinInputCharacterSet, RegExp> = {
22
+ numeric: /^[0-9]$/,
23
+ alphabetic: /^[A-Za-z]$/,
24
+ alphanumeric: /^[A-Za-z0-9]$/,
25
+ all: /^.$/
26
+ };
27
+
28
+ const CELL_SIZES = {
29
+ xs: "h-7 w-7 text-[10px]",
30
+ sm: "h-8 w-8 text-xs",
31
+ md: "h-10 w-10 text-sm",
32
+ lg: "h-12 w-12 text-base",
33
+ xl: "h-14 w-14 text-lg"
34
+ } as const;
35
+
36
+ const defaultProps = {
37
+ length: 6,
38
+ variant: "default",
39
+ size: "md",
40
+ radius: "default",
41
+ mask: false,
42
+ characterSet: "numeric",
43
+ transform: "none",
44
+ ariaLabelPrefix: "Character",
45
+ autoComplete: "one-time-code"
46
+ } satisfies Partial<PinInputProps>;
47
+
48
+ function normalizeLength(value: number | undefined): number {
49
+ const length = Number.isFinite(value) ? Math.trunc(value as number) : 6;
50
+ return Math.max(1, length);
51
+ }
52
+
53
+ function transformCharacter(
54
+ char: string,
55
+ transform: PinInputTransform
56
+ ): string {
57
+ if (transform === "uppercase") {
58
+ return char.toUpperCase();
59
+ }
60
+
61
+ if (transform === "lowercase") {
62
+ return char.toLowerCase();
63
+ }
64
+
65
+ return char;
66
+ }
67
+
68
+ function isAllowedCharacter(
69
+ char: string,
70
+ characterSet: PinInputCharacterSet,
71
+ characterPattern?: RegExp
72
+ ): boolean {
73
+ const pattern = characterPattern ?? CHARACTER_SET_PATTERNS[characterSet];
74
+ pattern.lastIndex = 0;
75
+ return pattern.test(char);
76
+ }
77
+
78
+ function toSanitizedChars(
79
+ value: string,
80
+ options: {
81
+ characterSet: PinInputCharacterSet;
82
+ characterPattern?: RegExp;
83
+ transform: PinInputTransform;
84
+ }
85
+ ): string[] {
86
+ const result: string[] = [];
87
+
88
+ for (const rawChar of value) {
89
+ const transformedChar = transformCharacter(rawChar, options.transform);
90
+
91
+ if (
92
+ transformedChar &&
93
+ isAllowedCharacter(
94
+ transformedChar,
95
+ options.characterSet,
96
+ options.characterPattern
97
+ )
98
+ ) {
99
+ result.push(transformedChar);
100
+ }
101
+ }
102
+
103
+ return result;
104
+ }
105
+
106
+ function toCellArray(
107
+ value: string,
108
+ length: number,
109
+ options: {
110
+ characterSet: PinInputCharacterSet;
111
+ characterPattern?: RegExp;
112
+ transform: PinInputTransform;
113
+ }
114
+ ): string[] {
115
+ const chars = toSanitizedChars(value, options).slice(0, length);
116
+
117
+ return Array.from({ length }, (_, index) => chars[index] ?? "");
118
+ }
119
+
120
+ function normalizeCells(cells: string[] | undefined, length: number): string[] {
121
+ return Array.from({ length }, (_, index) => cells?.[index] ?? "");
122
+ }
123
+
124
+ const PinInput = factory<PinInputFactoryPayload>((_props, ref) => {
125
+ const { cx, getRadius } = useTheme();
126
+ const {
127
+ id,
128
+ label,
129
+ description,
130
+ error,
131
+ required,
132
+ withAsterisk,
133
+ length,
134
+ value,
135
+ defaultValue,
136
+ onChange,
137
+ onComplete,
138
+ mask,
139
+ characterSet,
140
+ characterPattern,
141
+ transform,
142
+ ariaLabelPrefix,
143
+ name,
144
+ variant,
145
+ size,
146
+ radius,
147
+ disabled,
148
+ className,
149
+ classNames,
150
+ inputMode,
151
+ autoComplete,
152
+ placeholder,
153
+ ...props
154
+ } = useProps("PinInput", defaultProps, _props);
155
+
156
+ const classes = useClassNames<PinInputClassNames>("PinInput", classNames);
157
+ const _id = useId(id);
158
+
159
+ const resolvedLength = normalizeLength(length);
160
+ const sanitizingOptions = useMemo(
161
+ () => ({
162
+ characterSet,
163
+ characterPattern,
164
+ transform
165
+ }),
166
+ [characterSet, characterPattern, transform]
167
+ );
168
+
169
+ const emptyCells = useMemo(
170
+ () => Array.from({ length: resolvedLength }, () => ""),
171
+ [resolvedLength]
172
+ );
173
+
174
+ const [_valueCells, setValueCells] = useUncontrolled<string[]>({
175
+ value:
176
+ value === undefined
177
+ ? undefined
178
+ : toCellArray(String(value), resolvedLength, sanitizingOptions),
179
+ defaultValue:
180
+ defaultValue === undefined
181
+ ? undefined
182
+ : toCellArray(
183
+ String(defaultValue),
184
+ resolvedLength,
185
+ sanitizingOptions
186
+ ),
187
+ finalValue: emptyCells,
188
+ onChange: (nextCells) => onChange?.(nextCells.join(""))
189
+ });
190
+
191
+ const chars = useMemo(
192
+ () => normalizeCells(_valueCells, resolvedLength),
193
+ [_valueCells, resolvedLength]
194
+ );
195
+
196
+ const charsRef = useRef(chars);
197
+ charsRef.current = chars;
198
+
199
+ const completedValueRef = useRef<string | null>(null);
200
+ const inputRefs = useRef<Array<HTMLInputElement | null>>([]);
201
+
202
+ const hasWrapper = label || description || error;
203
+ const describedBy = error
204
+ ? `${_id}-error`
205
+ : description
206
+ ? `${_id}-description`
207
+ : undefined;
208
+
209
+ const resolvedInputMode =
210
+ inputMode ?? (characterSet === "numeric" ? "numeric" : "text");
211
+
212
+ const emitValue = useCallback(
213
+ (nextChars: string[]) => {
214
+ setValueCells(nextChars);
215
+
216
+ const nextValue = nextChars.join("");
217
+
218
+ if (!nextChars.includes("")) {
219
+ if (completedValueRef.current !== nextValue) {
220
+ onComplete?.(nextValue);
221
+ completedValueRef.current = nextValue;
222
+ }
223
+ } else {
224
+ completedValueRef.current = null;
225
+ }
226
+ },
227
+ [onComplete, setValueCells]
228
+ );
229
+
230
+ const focusCell = useCallback(
231
+ (index: number) => {
232
+ if (index < 0 || index >= resolvedLength) {
233
+ return;
234
+ }
235
+
236
+ setTimeout(() => {
237
+ const input = inputRefs.current[index];
238
+ if (!input) {
239
+ return;
240
+ }
241
+
242
+ input.focus();
243
+ input.select();
244
+ }, 0);
245
+ },
246
+ [resolvedLength]
247
+ );
248
+
249
+ const fillFromIndex = useCallback(
250
+ (index: number, incoming: string[]) => {
251
+ const next = [...charsRef.current];
252
+ let writeIndex = index;
253
+
254
+ for (const char of incoming) {
255
+ if (writeIndex >= resolvedLength) {
256
+ break;
257
+ }
258
+
259
+ next[writeIndex] = char;
260
+ writeIndex += 1;
261
+ }
262
+
263
+ emitValue(next);
264
+
265
+ if (writeIndex < resolvedLength) {
266
+ focusCell(writeIndex);
267
+ return;
268
+ }
269
+
270
+ focusCell(resolvedLength - 1);
271
+ },
272
+ [emitValue, focusCell, resolvedLength]
273
+ );
274
+
275
+ const handleCellChange = useCallback(
276
+ (index: number, event: React.ChangeEvent<HTMLInputElement>) => {
277
+ if (disabled) {
278
+ return;
279
+ }
280
+
281
+ const incoming = toSanitizedChars(
282
+ event.currentTarget.value,
283
+ sanitizingOptions
284
+ );
285
+
286
+ if (incoming.length === 0) {
287
+ if (event.currentTarget.value !== "") {
288
+ return;
289
+ }
290
+
291
+ const next = [...charsRef.current];
292
+ next[index] = "";
293
+ emitValue(next);
294
+ return;
295
+ }
296
+
297
+ if (incoming.length === 1) {
298
+ const next = [...charsRef.current];
299
+ next[index] = incoming[0];
300
+ emitValue(next);
301
+
302
+ if (index < resolvedLength - 1) {
303
+ focusCell(index + 1);
304
+ }
305
+
306
+ return;
307
+ }
308
+
309
+ fillFromIndex(index, incoming);
310
+ },
311
+ [
312
+ disabled,
313
+ sanitizingOptions,
314
+ emitValue,
315
+ resolvedLength,
316
+ focusCell,
317
+ fillFromIndex
318
+ ]
319
+ );
320
+
321
+ const handlePaste = useCallback(
322
+ (index: number, event: React.ClipboardEvent<HTMLInputElement>) => {
323
+ event.preventDefault();
324
+
325
+ if (disabled) {
326
+ return;
327
+ }
328
+
329
+ const incoming = toSanitizedChars(
330
+ event.clipboardData.getData("text"),
331
+ sanitizingOptions
332
+ );
333
+
334
+ if (incoming.length === 0) {
335
+ return;
336
+ }
337
+
338
+ fillFromIndex(index, incoming);
339
+ },
340
+ [disabled, sanitizingOptions, fillFromIndex]
341
+ );
342
+
343
+ const handleKeyDown = useCallback(
344
+ (index: number, event: React.KeyboardEvent<HTMLInputElement>) => {
345
+ if (disabled) {
346
+ return;
347
+ }
348
+
349
+ if (event.key === "ArrowLeft") {
350
+ event.preventDefault();
351
+ focusCell(index - 1);
352
+ return;
353
+ }
354
+
355
+ if (event.key === "ArrowRight") {
356
+ event.preventDefault();
357
+ focusCell(index + 1);
358
+ return;
359
+ }
360
+
361
+ if (event.key === "Home") {
362
+ event.preventDefault();
363
+ focusCell(0);
364
+ return;
365
+ }
366
+
367
+ if (event.key === "End") {
368
+ event.preventDefault();
369
+ focusCell(resolvedLength - 1);
370
+ return;
371
+ }
372
+
373
+ if (event.key === "Backspace") {
374
+ event.preventDefault();
375
+ const next = [...charsRef.current];
376
+
377
+ if (next[index]) {
378
+ next[index] = "";
379
+ emitValue(next);
380
+ return;
381
+ }
382
+
383
+ if (index > 0) {
384
+ next[index - 1] = "";
385
+ emitValue(next);
386
+ focusCell(index - 1);
387
+ }
388
+
389
+ return;
390
+ }
391
+
392
+ if (event.key === "Delete") {
393
+ event.preventDefault();
394
+ const next = [...charsRef.current];
395
+ next[index] = "";
396
+ emitValue(next);
397
+ }
398
+ },
399
+ [disabled, emitValue, focusCell, resolvedLength]
400
+ );
401
+
402
+ const field = (
403
+ <div
404
+ ref={ref}
405
+ className={cx(
406
+ "flex w-full items-center gap-2",
407
+ className,
408
+ classes.root
409
+ )}
410
+ >
411
+ {chars.map((char, index) => (
412
+ <input
413
+ key={`${_id}-cell-${index}`}
414
+ {...props}
415
+ id={`${_id}-${index}`}
416
+ ref={(node) => {
417
+ inputRefs.current[index] = node;
418
+ }}
419
+ type={mask ? "password" : "text"}
420
+ inputMode={resolvedInputMode}
421
+ autoComplete={index === 0 ? autoComplete : "off"}
422
+ maxLength={resolvedLength}
423
+ value={char}
424
+ disabled={disabled}
425
+ required={required && index === 0}
426
+ placeholder={placeholder}
427
+ aria-invalid={error ? true : undefined}
428
+ aria-describedby={describedBy}
429
+ aria-label={`${ariaLabelPrefix} ${index + 1} of ${resolvedLength}`}
430
+ className={cx(
431
+ "flex-none text-center font-medium tabular-nums outline-none transition-colors",
432
+ "focus:border-[var(--refraktor-primary)]",
433
+ getVariant(variant),
434
+ getRadius(radius),
435
+ CELL_SIZES[size],
436
+ error && "border-[var(--refraktor-colors-red-6)]",
437
+ disabled && "cursor-not-allowed opacity-50",
438
+ classes.cell
439
+ )}
440
+ onChange={(event) => handleCellChange(index, event)}
441
+ onKeyDown={(event) => handleKeyDown(index, event)}
442
+ onPaste={(event) => handlePaste(index, event)}
443
+ onFocus={(event) => event.currentTarget.select()}
444
+ />
445
+ ))}
446
+
447
+ {name && <input type="hidden" name={name} value={chars.join("")} />}
448
+ </div>
449
+ );
450
+
451
+ if (!hasWrapper) {
452
+ return field;
453
+ }
454
+
455
+ return (
456
+ <InputWrapper
457
+ label={label}
458
+ description={description}
459
+ error={error}
460
+ required={required}
461
+ withAsterisk={withAsterisk}
462
+ inputId={`${_id}-0`}
463
+ >
464
+ {field}
465
+ </InputWrapper>
466
+ );
467
+ });
468
+
469
+ PinInput.displayName = "@refraktor/core/PinInput";
470
+ PinInput.configure = createComponentConfig<PinInputProps>();
471
+ PinInput.classNames = createClassNamesConfig<PinInputClassNames>();
472
+
473
+ export default PinInput;
@@ -0,0 +1,78 @@
1
+ import {
2
+ createClassNamesConfig,
3
+ createComponentConfig,
4
+ FactoryPayload
5
+ } from "../../utils";
6
+ import { InputProps } from "../input";
7
+
8
+ export type PinInputCharacterSet =
9
+ | "numeric"
10
+ | "alphabetic"
11
+ | "alphanumeric"
12
+ | "all";
13
+
14
+ export type PinInputTransform = "none" | "uppercase" | "lowercase";
15
+
16
+ export type PinInputClassNames = {
17
+ root?: string;
18
+ cell?: string;
19
+ };
20
+
21
+ export interface _PinInputProps {
22
+ /** OTP length @default `6` */
23
+ length?: number;
24
+
25
+ /** Value (controlled) */
26
+ value?: string;
27
+
28
+ /** Default value (uncontrolled) */
29
+ defaultValue?: string;
30
+
31
+ /** Callback called whenever the code value changes */
32
+ onChange?: (value: string) => void;
33
+
34
+ /** Callback called when all cells are filled */
35
+ onComplete?: (value: string) => void;
36
+
37
+ /** Masks entered characters @default `false` */
38
+ mask?: boolean;
39
+
40
+ /** Character set filter @default `numeric` */
41
+ characterSet?: PinInputCharacterSet;
42
+
43
+ /** Optional custom character pattern (overrides characterSet) */
44
+ characterPattern?: RegExp;
45
+
46
+ /** Character transform @default `none` */
47
+ transform?: PinInputTransform;
48
+
49
+ /** Accessible label prefix for each cell @default `Character` */
50
+ ariaLabelPrefix?: string;
51
+
52
+ /** Hidden input name for form submission */
53
+ name?: string;
54
+
55
+ /** Used for styling different parts of the component */
56
+ classNames?: PinInputClassNames;
57
+ }
58
+
59
+ export type PinInputProps = _PinInputProps &
60
+ Omit<
61
+ InputProps,
62
+ | "type"
63
+ | "value"
64
+ | "defaultValue"
65
+ | "onChange"
66
+ | "leftSection"
67
+ | "rightSection"
68
+ | "maxLength"
69
+ >;
70
+
71
+ export interface PinInputFactoryPayload extends FactoryPayload {
72
+ props: PinInputProps;
73
+ ref: HTMLDivElement;
74
+ compound: {
75
+ configure: ReturnType<typeof createComponentConfig<PinInputProps>>;
76
+ classNames: ReturnType<typeof createClassNamesConfig<PinInputClassNames>>;
77
+ };
78
+ }
@@ -0,0 +1,6 @@
1
+ export { default as ScrollArea } from "./scroll-area";
2
+ export type {
3
+ ScrollAreaProps,
4
+ ScrollAreaClassNames,
5
+ ScrollAreaOrientation
6
+ } from "./scroll-area.types";
@@ -0,0 +1,72 @@
1
+ import { createRef } from "react";
2
+ import { describe, expect, it } from "vitest";
3
+ import { render, screen } from "../../vitest";
4
+ import ScrollArea from "./scroll-area";
5
+
6
+ describe("@refraktor/core/ScrollArea", () => {
7
+ it("renders children with default vertical scroll behavior", async () => {
8
+ await render(
9
+ <ScrollArea data-testid="scroll-area">
10
+ <div>Content</div>
11
+ </ScrollArea>
12
+ );
13
+
14
+ const scrollArea = screen.getByTestId("scroll-area");
15
+
16
+ expect(scrollArea).toHaveClass("refraktor-scrollbar");
17
+ expect(scrollArea).toHaveClass("overflow-y-auto");
18
+ expect(scrollArea).toHaveClass("overflow-x-hidden");
19
+ expect(scrollArea).toHaveTextContent("Content");
20
+ });
21
+
22
+ it("supports horizontal and bidirectional orientation", async () => {
23
+ const { rerender } = await render(
24
+ <ScrollArea data-testid="scroll-area" orientation="horizontal" />
25
+ );
26
+
27
+ expect(screen.getByTestId("scroll-area")).toHaveClass(
28
+ "overflow-x-auto"
29
+ );
30
+ expect(screen.getByTestId("scroll-area")).toHaveClass(
31
+ "overflow-y-hidden"
32
+ );
33
+
34
+ rerender(<ScrollArea data-testid="scroll-area" orientation="both" />);
35
+
36
+ expect(screen.getByTestId("scroll-area")).toHaveClass("overflow-auto");
37
+ });
38
+
39
+ it("applies scrollbar size css variable", async () => {
40
+ await render(<ScrollArea data-testid="scroll-area" scrollbarSize={10} />);
41
+
42
+ const scrollArea = screen.getByTestId("scroll-area");
43
+
44
+ expect(scrollArea.style.getPropertyValue("--refraktor-scrollbar-size")).toBe(
45
+ "10px"
46
+ );
47
+ });
48
+
49
+ it("supports root and slot class names", async () => {
50
+ await render(
51
+ <ScrollArea
52
+ data-testid="scroll-area"
53
+ className="custom-root"
54
+ classNames={{ root: "slot-root" }}
55
+ />
56
+ );
57
+
58
+ const scrollArea = screen.getByTestId("scroll-area");
59
+
60
+ expect(scrollArea).toHaveClass("custom-root");
61
+ expect(scrollArea).toHaveClass("slot-root");
62
+ });
63
+
64
+ it("forwards ref to root element", async () => {
65
+ const ref = createRef<HTMLDivElement>();
66
+
67
+ await render(<ScrollArea ref={ref} />);
68
+
69
+ expect(ref.current).toBeInstanceOf(HTMLDivElement);
70
+ expect(ref.current?.tagName).toBe("DIV");
71
+ });
72
+ });
@@ -0,0 +1,70 @@
1
+ import type { CSSProperties } from "react";
2
+ import {
3
+ createClassNamesConfig,
4
+ createComponentConfig,
5
+ factory,
6
+ useClassNames,
7
+ useProps
8
+ } from "../../utils";
9
+ import type {
10
+ ScrollAreaClassNames,
11
+ ScrollAreaFactoryPayload,
12
+ ScrollAreaOrientation,
13
+ ScrollAreaProps
14
+ } from "./scroll-area.types";
15
+ import { useTheme } from "../../theme";
16
+
17
+ const defaultProps = {
18
+ orientation: "vertical",
19
+ scrollbarSize: 6
20
+ } satisfies Partial<ScrollAreaProps>;
21
+
22
+ function getOrientationStyles(orientation: ScrollAreaOrientation): string {
23
+ if (orientation === "horizontal") {
24
+ return "overflow-x-auto overflow-y-hidden";
25
+ }
26
+
27
+ if (orientation === "both") {
28
+ return "overflow-auto";
29
+ }
30
+
31
+ return "overflow-y-auto overflow-x-hidden";
32
+ }
33
+
34
+ const ScrollArea = factory<ScrollAreaFactoryPayload>((_props, ref) => {
35
+ const { cx } = useTheme();
36
+ const {
37
+ orientation,
38
+ scrollbarSize,
39
+ className,
40
+ classNames,
41
+ style,
42
+ ...props
43
+ } = useProps("ScrollArea", defaultProps, _props);
44
+ const classes = useClassNames("ScrollArea", classNames);
45
+
46
+ const resolvedStyle = {
47
+ ...(style ?? {}),
48
+ "--refraktor-scrollbar-size": `${scrollbarSize}px`
49
+ } as CSSProperties;
50
+
51
+ return (
52
+ <div
53
+ ref={ref}
54
+ className={cx(
55
+ "refraktor-scrollbar",
56
+ getOrientationStyles(orientation),
57
+ classes.root,
58
+ className
59
+ )}
60
+ style={resolvedStyle}
61
+ {...props}
62
+ />
63
+ );
64
+ });
65
+
66
+ ScrollArea.displayName = "@refraktor/core/ScrollArea";
67
+ ScrollArea.configure = createComponentConfig<ScrollAreaProps>();
68
+ ScrollArea.classNames = createClassNamesConfig<ScrollAreaClassNames>();
69
+
70
+ export default ScrollArea;