@diwauhris/ui 1.0.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 (272) hide show
  1. package/README.md +75 -0
  2. package/lib/assets/ui.css +154 -0
  3. package/lib/index.cjs +183 -0
  4. package/lib/index.mjs +65 -0
  5. package/lib/index10.cjs +33 -0
  6. package/lib/index10.cjs.map +1 -0
  7. package/lib/index10.mjs +33 -0
  8. package/lib/index10.mjs.map +1 -0
  9. package/lib/index11.cjs +16 -0
  10. package/lib/index11.cjs.map +1 -0
  11. package/lib/index11.mjs +16 -0
  12. package/lib/index11.mjs.map +1 -0
  13. package/lib/index12.cjs +16 -0
  14. package/lib/index12.cjs.map +1 -0
  15. package/lib/index12.mjs +15 -0
  16. package/lib/index12.mjs.map +1 -0
  17. package/lib/index13.cjs +57 -0
  18. package/lib/index13.cjs.map +1 -0
  19. package/lib/index13.mjs +57 -0
  20. package/lib/index13.mjs.map +1 -0
  21. package/lib/index14.cjs +10 -0
  22. package/lib/index14.cjs.map +1 -0
  23. package/lib/index14.mjs +10 -0
  24. package/lib/index14.mjs.map +1 -0
  25. package/lib/index15.cjs +30 -0
  26. package/lib/index15.cjs.map +1 -0
  27. package/lib/index15.mjs +30 -0
  28. package/lib/index15.mjs.map +1 -0
  29. package/lib/index16.cjs +28 -0
  30. package/lib/index16.cjs.map +1 -0
  31. package/lib/index16.mjs +26 -0
  32. package/lib/index16.mjs.map +1 -0
  33. package/lib/index17.cjs +43 -0
  34. package/lib/index17.cjs.map +1 -0
  35. package/lib/index17.mjs +43 -0
  36. package/lib/index17.mjs.map +1 -0
  37. package/lib/index18.cjs +59 -0
  38. package/lib/index18.cjs.map +1 -0
  39. package/lib/index18.mjs +59 -0
  40. package/lib/index18.mjs.map +1 -0
  41. package/lib/index19.cjs +67 -0
  42. package/lib/index19.cjs.map +1 -0
  43. package/lib/index19.mjs +67 -0
  44. package/lib/index19.mjs.map +1 -0
  45. package/lib/index2.cjs +33 -0
  46. package/lib/index2.mjs +13 -0
  47. package/lib/index20.cjs +79 -0
  48. package/lib/index20.cjs.map +1 -0
  49. package/lib/index20.mjs +79 -0
  50. package/lib/index20.mjs.map +1 -0
  51. package/lib/index21.cjs +105 -0
  52. package/lib/index21.cjs.map +1 -0
  53. package/lib/index21.mjs +104 -0
  54. package/lib/index21.mjs.map +1 -0
  55. package/lib/index22.cjs +50 -0
  56. package/lib/index22.cjs.map +1 -0
  57. package/lib/index22.mjs +50 -0
  58. package/lib/index22.mjs.map +1 -0
  59. package/lib/index23.cjs +143 -0
  60. package/lib/index23.cjs.map +1 -0
  61. package/lib/index23.mjs +143 -0
  62. package/lib/index23.mjs.map +1 -0
  63. package/lib/index25.cjs +177 -0
  64. package/lib/index25.cjs.map +1 -0
  65. package/lib/index25.mjs +175 -0
  66. package/lib/index25.mjs.map +1 -0
  67. package/lib/index26.cjs +108 -0
  68. package/lib/index26.cjs.map +1 -0
  69. package/lib/index26.mjs +108 -0
  70. package/lib/index26.mjs.map +1 -0
  71. package/lib/index27.cjs +120 -0
  72. package/lib/index27.cjs.map +1 -0
  73. package/lib/index27.mjs +120 -0
  74. package/lib/index27.mjs.map +1 -0
  75. package/lib/index28.cjs +54 -0
  76. package/lib/index28.cjs.map +1 -0
  77. package/lib/index28.mjs +54 -0
  78. package/lib/index28.mjs.map +1 -0
  79. package/lib/index29.cjs +100 -0
  80. package/lib/index29.cjs.map +1 -0
  81. package/lib/index29.mjs +100 -0
  82. package/lib/index29.mjs.map +1 -0
  83. package/lib/index3.cjs +38 -0
  84. package/lib/index3.cjs.map +1 -0
  85. package/lib/index3.mjs +38 -0
  86. package/lib/index3.mjs.map +1 -0
  87. package/lib/index30.cjs +27 -0
  88. package/lib/index30.cjs.map +1 -0
  89. package/lib/index30.mjs +27 -0
  90. package/lib/index30.mjs.map +1 -0
  91. package/lib/index31.cjs +72 -0
  92. package/lib/index31.cjs.map +1 -0
  93. package/lib/index31.mjs +72 -0
  94. package/lib/index31.mjs.map +1 -0
  95. package/lib/index32.cjs +19 -0
  96. package/lib/index32.cjs.map +1 -0
  97. package/lib/index32.mjs +19 -0
  98. package/lib/index32.mjs.map +1 -0
  99. package/lib/index33.cjs +25 -0
  100. package/lib/index33.cjs.map +1 -0
  101. package/lib/index33.mjs +25 -0
  102. package/lib/index33.mjs.map +1 -0
  103. package/lib/index34.cjs +55 -0
  104. package/lib/index34.cjs.map +1 -0
  105. package/lib/index34.mjs +51 -0
  106. package/lib/index34.mjs.map +1 -0
  107. package/lib/index35.cjs +25 -0
  108. package/lib/index35.cjs.map +1 -0
  109. package/lib/index35.mjs +25 -0
  110. package/lib/index35.mjs.map +1 -0
  111. package/lib/index36.cjs +54 -0
  112. package/lib/index36.cjs.map +1 -0
  113. package/lib/index36.mjs +49 -0
  114. package/lib/index36.mjs.map +1 -0
  115. package/lib/index37.cjs +130 -0
  116. package/lib/index37.cjs.map +1 -0
  117. package/lib/index37.mjs +128 -0
  118. package/lib/index37.mjs.map +1 -0
  119. package/lib/index38.cjs +106 -0
  120. package/lib/index38.cjs.map +1 -0
  121. package/lib/index38.mjs +102 -0
  122. package/lib/index38.mjs.map +1 -0
  123. package/lib/index39.cjs +137 -0
  124. package/lib/index39.cjs.map +1 -0
  125. package/lib/index39.mjs +137 -0
  126. package/lib/index39.mjs.map +1 -0
  127. package/lib/index4.cjs +24 -0
  128. package/lib/index4.cjs.map +1 -0
  129. package/lib/index4.mjs +24 -0
  130. package/lib/index4.mjs.map +1 -0
  131. package/lib/index40.cjs +130 -0
  132. package/lib/index40.cjs.map +1 -0
  133. package/lib/index40.mjs +126 -0
  134. package/lib/index40.mjs.map +1 -0
  135. package/lib/index41.cjs +60 -0
  136. package/lib/index41.cjs.map +1 -0
  137. package/lib/index41.mjs +58 -0
  138. package/lib/index41.mjs.map +1 -0
  139. package/lib/index42.cjs +105 -0
  140. package/lib/index42.cjs.map +1 -0
  141. package/lib/index42.mjs +105 -0
  142. package/lib/index42.mjs.map +1 -0
  143. package/lib/index43.cjs +47 -0
  144. package/lib/index43.cjs.map +1 -0
  145. package/lib/index43.mjs +42 -0
  146. package/lib/index43.mjs.map +1 -0
  147. package/lib/index44.cjs +40 -0
  148. package/lib/index44.cjs.map +1 -0
  149. package/lib/index44.mjs +35 -0
  150. package/lib/index44.mjs.map +1 -0
  151. package/lib/index45.cjs +91 -0
  152. package/lib/index45.cjs.map +1 -0
  153. package/lib/index45.mjs +91 -0
  154. package/lib/index45.mjs.map +1 -0
  155. package/lib/index46.cjs +180 -0
  156. package/lib/index46.cjs.map +1 -0
  157. package/lib/index46.mjs +180 -0
  158. package/lib/index46.mjs.map +1 -0
  159. package/lib/index47.cjs +61 -0
  160. package/lib/index47.cjs.map +1 -0
  161. package/lib/index47.mjs +61 -0
  162. package/lib/index47.mjs.map +1 -0
  163. package/lib/index48.cjs +165 -0
  164. package/lib/index48.cjs.map +1 -0
  165. package/lib/index48.mjs +161 -0
  166. package/lib/index48.mjs.map +1 -0
  167. package/lib/index49.cjs +83 -0
  168. package/lib/index49.cjs.map +1 -0
  169. package/lib/index49.mjs +83 -0
  170. package/lib/index49.mjs.map +1 -0
  171. package/lib/index5.cjs +39 -0
  172. package/lib/index5.cjs.map +1 -0
  173. package/lib/index5.mjs +39 -0
  174. package/lib/index5.mjs.map +1 -0
  175. package/lib/index50.cjs +100 -0
  176. package/lib/index50.cjs.map +1 -0
  177. package/lib/index50.mjs +100 -0
  178. package/lib/index50.mjs.map +1 -0
  179. package/lib/index51.cjs +116 -0
  180. package/lib/index51.cjs.map +1 -0
  181. package/lib/index51.mjs +113 -0
  182. package/lib/index51.mjs.map +1 -0
  183. package/lib/index52.cjs +93 -0
  184. package/lib/index52.cjs.map +1 -0
  185. package/lib/index52.mjs +93 -0
  186. package/lib/index52.mjs.map +1 -0
  187. package/lib/index53.cjs +120 -0
  188. package/lib/index53.cjs.map +1 -0
  189. package/lib/index53.mjs +120 -0
  190. package/lib/index53.mjs.map +1 -0
  191. package/lib/index54.cjs +53 -0
  192. package/lib/index54.cjs.map +1 -0
  193. package/lib/index54.mjs +50 -0
  194. package/lib/index54.mjs.map +1 -0
  195. package/lib/index55.cjs +130 -0
  196. package/lib/index55.cjs.map +1 -0
  197. package/lib/index55.mjs +127 -0
  198. package/lib/index55.mjs.map +1 -0
  199. package/lib/index56.cjs +36 -0
  200. package/lib/index56.cjs.map +1 -0
  201. package/lib/index56.mjs +35 -0
  202. package/lib/index56.mjs.map +1 -0
  203. package/lib/index57.cjs +47 -0
  204. package/lib/index57.cjs.map +1 -0
  205. package/lib/index57.mjs +47 -0
  206. package/lib/index57.mjs.map +1 -0
  207. package/lib/index58.cjs +97 -0
  208. package/lib/index58.cjs.map +1 -0
  209. package/lib/index58.mjs +97 -0
  210. package/lib/index58.mjs.map +1 -0
  211. package/lib/index59.cjs +120 -0
  212. package/lib/index59.cjs.map +1 -0
  213. package/lib/index59.mjs +116 -0
  214. package/lib/index59.mjs.map +1 -0
  215. package/lib/index6.cjs +29 -0
  216. package/lib/index6.cjs.map +1 -0
  217. package/lib/index6.mjs +29 -0
  218. package/lib/index6.mjs.map +1 -0
  219. package/lib/index60.cjs +34 -0
  220. package/lib/index60.cjs.map +1 -0
  221. package/lib/index60.mjs +34 -0
  222. package/lib/index60.mjs.map +1 -0
  223. package/lib/index61.cjs +62 -0
  224. package/lib/index61.cjs.map +1 -0
  225. package/lib/index61.mjs +62 -0
  226. package/lib/index61.mjs.map +1 -0
  227. package/lib/index62.cjs +213 -0
  228. package/lib/index62.cjs.map +1 -0
  229. package/lib/index62.mjs +213 -0
  230. package/lib/index62.mjs.map +1 -0
  231. package/lib/index63.cjs +42 -0
  232. package/lib/index63.cjs.map +1 -0
  233. package/lib/index63.mjs +42 -0
  234. package/lib/index63.mjs.map +1 -0
  235. package/lib/index64.cjs +86 -0
  236. package/lib/index64.cjs.map +1 -0
  237. package/lib/index64.mjs +86 -0
  238. package/lib/index64.mjs.map +1 -0
  239. package/lib/index65.cjs +156 -0
  240. package/lib/index65.cjs.map +1 -0
  241. package/lib/index65.mjs +156 -0
  242. package/lib/index65.mjs.map +1 -0
  243. package/lib/index66.cjs +107 -0
  244. package/lib/index66.cjs.map +1 -0
  245. package/lib/index66.mjs +106 -0
  246. package/lib/index66.mjs.map +1 -0
  247. package/lib/index67.cjs +52 -0
  248. package/lib/index67.cjs.map +1 -0
  249. package/lib/index67.mjs +52 -0
  250. package/lib/index67.mjs.map +1 -0
  251. package/lib/index68.cjs +125 -0
  252. package/lib/index68.cjs.map +1 -0
  253. package/lib/index68.mjs +115 -0
  254. package/lib/index68.mjs.map +1 -0
  255. package/lib/index69.cjs +34 -0
  256. package/lib/index69.cjs.map +1 -0
  257. package/lib/index69.mjs +29 -0
  258. package/lib/index69.mjs.map +1 -0
  259. package/lib/index7.cjs +17 -0
  260. package/lib/index7.cjs.map +1 -0
  261. package/lib/index7.mjs +17 -0
  262. package/lib/index7.mjs.map +1 -0
  263. package/lib/index8.cjs +86 -0
  264. package/lib/index8.cjs.map +1 -0
  265. package/lib/index8.mjs +86 -0
  266. package/lib/index8.mjs.map +1 -0
  267. package/lib/index9.cjs +29 -0
  268. package/lib/index9.cjs.map +1 -0
  269. package/lib/index9.mjs +29 -0
  270. package/lib/index9.mjs.map +1 -0
  271. package/lib/types/index.d.ts +1 -0
  272. package/package.json +68 -0
@@ -0,0 +1,175 @@
1
+ import { useFieldContext } from "./index12.mjs";
2
+ /* empty css */
3
+ import { jsx, jsxs } from "react/jsx-runtime";
4
+ import { useCallback, useEffect, useId, useRef, useState } from "react";
5
+ import { CalendarDays, ChevronLeft, ChevronRight } from "lucide-react";
6
+ import Calendar from "react-calendar";
7
+ import { format, isValid, parse } from "date-fns";
8
+ //#region src/ui-library/gallery/date-picker/DatePicker.tsx
9
+ /**
10
+ * DatePicker — Gallery-native date picker.
11
+ * Copied from shared/components/PisDatePicker.tsx.
12
+ * Updated to import FieldContext from gallery-local field directory.
13
+ * Uses react-calendar and date-fns.
14
+ * Note: CSS for .pis-datepicker-popover and .pis-rc-calendar is in index.css.
15
+ */
16
+ var MAX_DATE = new Date(2026, 11, 31);
17
+ var POPOVER_WIDTH = 352;
18
+ var CALENDAR_HEIGHT = 360;
19
+ function DatePicker({ value, onChange, placeholder, className, disabled, error, minDate, id, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, required }) {
20
+ const [isOpen, setIsOpen] = useState(false);
21
+ const [popupStyle, setPopupStyle] = useState({});
22
+ const wrapperRef = useRef(null);
23
+ const inputRef = useRef(null);
24
+ const internalId = useId();
25
+ const ctx = useFieldContext();
26
+ const inputId = id ?? ctx?.inputId ?? internalId;
27
+ const resolvedRequired = required ?? ctx?.required ?? false;
28
+ const resolvedInvalid = error ? true : ctx?.invalid ?? false;
29
+ const describedBy = ctx?.invalid && ctx?.errorId ? ctx.errorId : void 0;
30
+ const selectedDate = value ? (() => {
31
+ const d = parse(value, "yyyy-MM-dd", /* @__PURE__ */ new Date());
32
+ if (isValid(d)) return d;
33
+ const d2 = new Date(value);
34
+ return isNaN(d2.getTime()) ? void 0 : d2;
35
+ })() : void 0;
36
+ const displayValue = selectedDate ? format(selectedDate, "MMM d, yyyy") : "";
37
+ const skipNextOpen = useRef(false);
38
+ const openPopup = useCallback(() => {
39
+ if (disabled || !wrapperRef.current) return;
40
+ if (skipNextOpen.current) {
41
+ skipNextOpen.current = false;
42
+ return;
43
+ }
44
+ const rect = wrapperRef.current.getBoundingClientRect();
45
+ const spaceBelow = window.innerHeight - rect.bottom;
46
+ const spaceAbove = rect.top;
47
+ const isNarrow = window.innerWidth < 368;
48
+ const style = {
49
+ position: "fixed",
50
+ left: isNarrow ? 8 : Math.max(8, Math.min(rect.left, window.innerWidth - POPOVER_WIDTH - 8)),
51
+ width: isNarrow ? `calc(100vw - 16px)` : void 0
52
+ };
53
+ if (spaceBelow >= CALENDAR_HEIGHT || spaceBelow >= spaceAbove) style.top = rect.bottom + 6;
54
+ else style.top = rect.top - CALENDAR_HEIGHT - 6;
55
+ setPopupStyle(style);
56
+ setIsOpen(true);
57
+ }, [disabled]);
58
+ useEffect(() => {
59
+ function handleClickOutside(e) {
60
+ if (wrapperRef.current && !wrapperRef.current.contains(e.target)) setIsOpen(false);
61
+ }
62
+ document.addEventListener("mousedown", handleClickOutside);
63
+ return () => document.removeEventListener("mousedown", handleClickOutside);
64
+ }, []);
65
+ useEffect(() => {
66
+ if (!isOpen) return;
67
+ function handleScroll() {
68
+ setIsOpen(false);
69
+ }
70
+ window.addEventListener("scroll", handleScroll, { capture: true });
71
+ window.addEventListener("resize", handleScroll);
72
+ return () => {
73
+ window.removeEventListener("scroll", handleScroll, { capture: true });
74
+ window.removeEventListener("resize", handleScroll);
75
+ };
76
+ }, [isOpen]);
77
+ useEffect(() => {
78
+ if (!isOpen) return;
79
+ function handleFocusOut(e) {
80
+ if (!e.relatedTarget) return;
81
+ if (wrapperRef.current && !wrapperRef.current.contains(e.relatedTarget)) setIsOpen(false);
82
+ }
83
+ const wrapper = wrapperRef.current;
84
+ wrapper?.addEventListener("focusout", handleFocusOut);
85
+ return () => wrapper?.removeEventListener("focusout", handleFocusOut);
86
+ }, [isOpen]);
87
+ function handleKeyDown(e) {
88
+ if (e.key === "Escape") {
89
+ e.preventDefault();
90
+ setIsOpen(false);
91
+ inputRef.current?.focus();
92
+ return;
93
+ }
94
+ if (e.key === " " || e.key === "Enter") {
95
+ e.preventDefault();
96
+ if (isOpen) setIsOpen(false);
97
+ else openPopup();
98
+ }
99
+ }
100
+ const inputClasses = [
101
+ "w-full rounded-xl border bg-slate-50 px-4 py-2.5 text-sm font-medium text-slate-900 outline-none transition placeholder:text-slate-400 pr-8 cursor-pointer select-none",
102
+ error ? "border-rose-300 focus:border-rose-400 focus:ring-2 focus:ring-rose-100" : "border-slate-200 focus:border-brand-blue/40 focus:ring-2 focus:ring-brand-blue/15",
103
+ disabled ? "cursor-not-allowed opacity-60" : "",
104
+ className ?? ""
105
+ ].filter(Boolean).join(" ");
106
+ return /* @__PURE__ */ jsxs("div", {
107
+ ref: wrapperRef,
108
+ className: "relative",
109
+ children: [/* @__PURE__ */ jsxs("div", {
110
+ className: "relative",
111
+ children: [/* @__PURE__ */ jsx("input", {
112
+ ref: inputRef,
113
+ id: inputId,
114
+ type: "text",
115
+ readOnly: true,
116
+ value: displayValue,
117
+ onFocus: openPopup,
118
+ onClick: openPopup,
119
+ onKeyDown: handleKeyDown,
120
+ placeholder,
121
+ disabled,
122
+ "aria-label": ariaLabel,
123
+ "aria-labelledby": ariaLabelledBy,
124
+ "aria-required": resolvedRequired ? "true" : void 0,
125
+ "aria-invalid": resolvedInvalid ? "true" : void 0,
126
+ "aria-describedby": describedBy,
127
+ "aria-haspopup": "dialog",
128
+ "aria-expanded": isOpen,
129
+ role: "combobox",
130
+ className: inputClasses
131
+ }), /* @__PURE__ */ jsx(CalendarDays, {
132
+ size: 16,
133
+ className: `absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none ${isOpen ? "text-brand-blue" : "text-slate-300"}`,
134
+ "aria-hidden": "true"
135
+ })]
136
+ }), isOpen && !disabled && /* @__PURE__ */ jsx("div", {
137
+ className: "pis-datepicker-popover",
138
+ style: popupStyle,
139
+ role: "dialog",
140
+ "aria-label": ariaLabel ? `${ariaLabel} calendar` : "Date picker",
141
+ "aria-modal": "false",
142
+ children: /* @__PURE__ */ jsx(Calendar, {
143
+ value: selectedDate ?? null,
144
+ onChange: (val) => {
145
+ if (val instanceof Date) {
146
+ onChange(format(val, "yyyy-MM-dd"));
147
+ setIsOpen(false);
148
+ skipNextOpen.current = true;
149
+ requestAnimationFrame(() => inputRef.current?.focus());
150
+ }
151
+ },
152
+ defaultActiveStartDate: selectedDate ?? minDate,
153
+ minDate,
154
+ maxDate: MAX_DATE,
155
+ defaultView: "month",
156
+ showNeighboringDecade: true,
157
+ showNeighboringCentury: true,
158
+ prevLabel: /* @__PURE__ */ jsx(ChevronLeft, { size: 14 }),
159
+ nextLabel: /* @__PURE__ */ jsx(ChevronRight, { size: 14 }),
160
+ prev2Label: null,
161
+ next2Label: null,
162
+ navigationLabel: ({ date, view }) => {
163
+ if (view === "month") return format(date, "MMMM yyyy");
164
+ if (view === "year") return format(date, "yyyy");
165
+ return `${Math.floor(date.getFullYear() / 10) * 10} – ${Math.floor(date.getFullYear() / 10) * 10 + 9}`;
166
+ },
167
+ className: "pis-rc-calendar"
168
+ }, isOpen ? "open" : "closed")
169
+ })]
170
+ });
171
+ }
172
+ //#endregion
173
+ export { DatePicker };
174
+
175
+ //# sourceMappingURL=index25.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index25.mjs","names":[],"sources":["../src/ui-library/gallery/date-picker/DatePicker.tsx"],"sourcesContent":["/**\n * DatePicker — Gallery-native date picker.\n * Copied from shared/components/PisDatePicker.tsx.\n * Updated to import FieldContext from gallery-local field directory.\n * Uses react-calendar and date-fns.\n * Note: CSS for .pis-datepicker-popover and .pis-rc-calendar is in index.css.\n */\nimport { useState, useRef, useEffect, useCallback, useId } from 'react';\nimport Calendar from 'react-calendar';\nimport { format, parse, isValid } from 'date-fns';\nimport { CalendarDays, ChevronLeft, ChevronRight } from 'lucide-react';\nimport 'react-calendar/dist/Calendar.css';\nimport { useFieldContext } from '../field/FieldContext';\n\ninterface DatePickerProps {\n value: string;\n onChange: (value: string) => void;\n placeholder?: string;\n className?: string;\n disabled?: boolean;\n error?: string;\n minDate?: Date;\n id?: string;\n 'aria-label'?: string;\n 'aria-labelledby'?: string;\n required?: boolean;\n}\n\nconst MAX_DATE = new Date(2026, 11, 31);\nconst CALENDAR_WIDTH = 320;\nconst POPOVER_WIDTH = CALENDAR_WIDTH + 32;\nconst CALENDAR_HEIGHT = 360;\n\nexport function DatePicker({\n value,\n onChange,\n placeholder,\n className,\n disabled,\n error,\n minDate,\n id,\n 'aria-label': ariaLabel,\n 'aria-labelledby': ariaLabelledBy,\n required,\n}: DatePickerProps) {\n const [isOpen, setIsOpen] = useState(false);\n const [popupStyle, setPopupStyle] = useState<React.CSSProperties>({});\n const wrapperRef = useRef<HTMLDivElement>(null);\n const inputRef = useRef<HTMLInputElement>(null);\n const internalId = useId();\n\n const ctx = useFieldContext();\n const inputId = id ?? ctx?.inputId ?? internalId;\n const resolvedRequired = required ?? ctx?.required ?? false;\n const resolvedInvalid = error ? true : (ctx?.invalid ?? false);\n const describedBy = ctx?.invalid && ctx?.errorId ? ctx.errorId : undefined;\n\n const selectedDate = value\n ? (() => {\n const d = parse(value, 'yyyy-MM-dd', new Date());\n if (isValid(d)) return d;\n const d2 = new Date(value);\n return isNaN(d2.getTime()) ? undefined : d2;\n })()\n : undefined;\n\n const displayValue = selectedDate ? format(selectedDate, 'MMM d, yyyy') : '';\n\n const skipNextOpen = useRef(false);\n\n const openPopup = useCallback(() => {\n if (disabled || !wrapperRef.current) return;\n if (skipNextOpen.current) {\n skipNextOpen.current = false;\n return;\n }\n const rect = wrapperRef.current.getBoundingClientRect();\n const spaceBelow = window.innerHeight - rect.bottom;\n const spaceAbove = rect.top;\n\n const isNarrow = window.innerWidth < POPOVER_WIDTH + 16;\n const style: React.CSSProperties = {\n position: 'fixed',\n left: isNarrow ? 8 : Math.max(8, Math.min(rect.left, window.innerWidth - POPOVER_WIDTH - 8)),\n width: isNarrow ? `calc(100vw - 16px)` : undefined,\n };\n\n if (spaceBelow >= CALENDAR_HEIGHT || spaceBelow >= spaceAbove) {\n style.top = rect.bottom + 6;\n } else {\n style.top = rect.top - CALENDAR_HEIGHT - 6;\n }\n\n setPopupStyle(style);\n setIsOpen(true);\n }, [disabled]);\n\n useEffect(() => {\n function handleClickOutside(e: MouseEvent) {\n if (wrapperRef.current && !wrapperRef.current.contains(e.target as Node)) {\n setIsOpen(false);\n }\n }\n document.addEventListener('mousedown', handleClickOutside);\n return () => document.removeEventListener('mousedown', handleClickOutside);\n }, []);\n\n useEffect(() => {\n if (!isOpen) return;\n function handleScroll() { setIsOpen(false); }\n window.addEventListener('scroll', handleScroll, { capture: true });\n window.addEventListener('resize', handleScroll);\n return () => {\n window.removeEventListener('scroll', handleScroll, { capture: true });\n window.removeEventListener('resize', handleScroll);\n };\n }, [isOpen]);\n\n useEffect(() => {\n if (!isOpen) return;\n function handleFocusOut(e: FocusEvent) {\n if (!e.relatedTarget) return;\n if (wrapperRef.current && !wrapperRef.current.contains(e.relatedTarget as Node)) {\n setIsOpen(false);\n }\n }\n const wrapper = wrapperRef.current;\n wrapper?.addEventListener('focusout', handleFocusOut);\n return () => wrapper?.removeEventListener('focusout', handleFocusOut);\n }, [isOpen]);\n\n function handleKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {\n if (e.key === 'Escape') {\n e.preventDefault();\n setIsOpen(false);\n inputRef.current?.focus();\n return;\n }\n if (e.key === ' ' || e.key === 'Enter') {\n e.preventDefault();\n if (isOpen) setIsOpen(false);\n else openPopup();\n }\n }\n\n const inputClasses = [\n 'w-full rounded-xl border bg-slate-50 px-4 py-2.5 text-sm font-medium text-slate-900 outline-none transition placeholder:text-slate-400 pr-8 cursor-pointer select-none',\n error ? 'border-rose-300 focus:border-rose-400 focus:ring-2 focus:ring-rose-100' : 'border-slate-200 focus:border-brand-blue/40 focus:ring-2 focus:ring-brand-blue/15',\n disabled ? 'cursor-not-allowed opacity-60' : '',\n className ?? '',\n ].filter(Boolean).join(' ');\n\n return (\n <div ref={wrapperRef} className=\"relative\">\n <div className=\"relative\">\n <input\n ref={inputRef}\n id={inputId}\n type=\"text\"\n readOnly\n value={displayValue}\n onFocus={openPopup}\n onClick={openPopup}\n onKeyDown={handleKeyDown}\n placeholder={placeholder}\n disabled={disabled}\n aria-label={ariaLabel}\n aria-labelledby={ariaLabelledBy}\n aria-required={resolvedRequired ? 'true' : undefined}\n aria-invalid={resolvedInvalid ? 'true' : undefined}\n aria-describedby={describedBy}\n aria-haspopup=\"dialog\"\n aria-expanded={isOpen}\n role=\"combobox\"\n className={inputClasses}\n />\n <CalendarDays\n size={16}\n className={`absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none ${isOpen ? 'text-brand-blue' : 'text-slate-300'}`}\n aria-hidden=\"true\"\n />\n </div>\n\n {isOpen && !disabled && (\n <div\n className=\"pis-datepicker-popover\"\n style={popupStyle}\n role=\"dialog\"\n aria-label={ariaLabel ? `${ariaLabel} calendar` : 'Date picker'}\n aria-modal=\"false\"\n >\n <Calendar\n key={isOpen ? 'open' : 'closed'}\n value={selectedDate ?? null}\n onChange={(val) => {\n if (val instanceof Date) {\n onChange(format(val, 'yyyy-MM-dd'));\n setIsOpen(false);\n skipNextOpen.current = true;\n requestAnimationFrame(() => inputRef.current?.focus());\n }\n }}\n defaultActiveStartDate={selectedDate ?? minDate}\n minDate={minDate}\n maxDate={MAX_DATE}\n defaultView=\"month\"\n showNeighboringDecade={true}\n showNeighboringCentury={true}\n prevLabel={<ChevronLeft size={14} />}\n nextLabel={<ChevronRight size={14} />}\n prev2Label={null}\n next2Label={null}\n navigationLabel={({ date, view }) => {\n if (view === 'month') return format(date, 'MMMM yyyy');\n if (view === 'year') return format(date, 'yyyy');\n return `${Math.floor(date.getFullYear() / 10) * 10} – ${Math.floor(date.getFullYear() / 10) * 10 + 9}`;\n }}\n className=\"pis-rc-calendar\"\n />\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,108 @@
1
+ let react_jsx_runtime = require("react/jsx-runtime");
2
+ let react = require("react");
3
+ //#region src/ui-library/gallery/otp-input/OtpInput.tsx
4
+ /**
5
+ * OtpInput — Design System Component
6
+ *
7
+ * A segmented one-time-password / PIN entry field.
8
+ * Each digit occupies its own visually-separated cell.
9
+ *
10
+ * Features:
11
+ * - Configurable length (default: 6)
12
+ * - Auto-advance on digit entry
13
+ * - Backspace moves to previous cell
14
+ * - Paste fills all cells
15
+ * - Error state with aria-invalid
16
+ * - Disabled state
17
+ *
18
+ * Accessibility:
19
+ * - Single logical input via aria-label on the first visible cell
20
+ * - role="group" + aria-label on the container
21
+ * - aria-invalid on cells when error is present
22
+ * - All cells share a common aria-describedby pointing to the error message
23
+ */
24
+ function OtpInput({ length = 6, value, onChange, disabled = false, error = false, errorMessage, "aria-label": ariaLabel = "One-time password", className = "" }) {
25
+ const [internal, setInternal] = (0, react.useState)(() => Array(length).fill(""));
26
+ const cells = value ?? internal;
27
+ const inputRefs = (0, react.useRef)([]);
28
+ const errorId = "otp-error";
29
+ const update = (next) => {
30
+ if (!value) setInternal(next);
31
+ onChange?.(next);
32
+ };
33
+ const handleKeyDown = (i) => (e) => {
34
+ if (e.key === "Backspace") {
35
+ e.preventDefault();
36
+ if (cells[i]) {
37
+ const next = [...cells];
38
+ next[i] = "";
39
+ update(next);
40
+ } else if (i > 0) {
41
+ const next = [...cells];
42
+ next[i - 1] = "";
43
+ update(next);
44
+ inputRefs.current[i - 1]?.focus();
45
+ }
46
+ } else if (e.key === "ArrowLeft" && i > 0) inputRefs.current[i - 1]?.focus();
47
+ else if (e.key === "ArrowRight" && i < length - 1) inputRefs.current[i + 1]?.focus();
48
+ };
49
+ const handleInput = (i) => (e) => {
50
+ const raw = e.target.value.replace(/\D/g, "").slice(-1);
51
+ const next = [...cells];
52
+ next[i] = raw;
53
+ update(next);
54
+ if (raw && i < length - 1) inputRefs.current[i + 1]?.focus();
55
+ };
56
+ const handlePaste = (e) => {
57
+ e.preventDefault();
58
+ const digits = e.clipboardData.getData("text").replace(/\D/g, "").slice(0, length);
59
+ const next = Array(length).fill("");
60
+ for (let i = 0; i < digits.length; i++) next[i] = digits[i] ?? "";
61
+ update(next);
62
+ const focusIdx = Math.min(digits.length, length - 1);
63
+ inputRefs.current[focusIdx]?.focus();
64
+ };
65
+ const cellBase = [
66
+ "h-12 w-10 rounded-xl border text-center text-lg font-bold",
67
+ "outline-none transition",
68
+ "focus:border-brand-blue/50 focus:ring-2 focus:ring-brand-blue/15",
69
+ "disabled:cursor-not-allowed disabled:bg-slate-50 disabled:text-slate-400"
70
+ ].join(" ");
71
+ const cellClass = error ? `${cellBase} border-rose-400 bg-rose-50/40 text-rose-800 focus:ring-rose-100` : `${cellBase} border-slate-200 bg-white text-slate-900`;
72
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
73
+ className: `space-y-2 ${className}`,
74
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
75
+ role: "group",
76
+ "aria-label": ariaLabel,
77
+ "aria-describedby": errorMessage ? errorId : void 0,
78
+ className: "flex items-center gap-2",
79
+ children: Array.from({ length }).map((_, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
80
+ ref: (el) => {
81
+ inputRefs.current[i] = el;
82
+ },
83
+ type: "text",
84
+ inputMode: "numeric",
85
+ maxLength: 1,
86
+ value: cells[i] ?? "",
87
+ disabled,
88
+ "aria-invalid": error,
89
+ "aria-label": i === 0 ? `${ariaLabel}, digit ${i + 1} of ${length}` : `digit ${i + 1} of ${length}`,
90
+ "aria-describedby": errorMessage ? errorId : void 0,
91
+ className: cellClass,
92
+ onChange: handleInput(i),
93
+ onKeyDown: handleKeyDown(i),
94
+ onPaste: handlePaste,
95
+ onFocus: (e) => e.target.select()
96
+ }, i))
97
+ }), errorMessage && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
98
+ id: errorId,
99
+ className: "text-xs font-semibold text-rose-600",
100
+ role: "alert",
101
+ children: errorMessage
102
+ })]
103
+ });
104
+ }
105
+ //#endregion
106
+ exports.OtpInput = OtpInput;
107
+
108
+ //# sourceMappingURL=index26.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index26.cjs","names":[],"sources":["../src/ui-library/gallery/otp-input/OtpInput.tsx"],"sourcesContent":["/**\n * OtpInput — Design System Component\n *\n * A segmented one-time-password / PIN entry field.\n * Each digit occupies its own visually-separated cell.\n *\n * Features:\n * - Configurable length (default: 6)\n * - Auto-advance on digit entry\n * - Backspace moves to previous cell\n * - Paste fills all cells\n * - Error state with aria-invalid\n * - Disabled state\n *\n * Accessibility:\n * - Single logical input via aria-label on the first visible cell\n * - role=\"group\" + aria-label on the container\n * - aria-invalid on cells when error is present\n * - All cells share a common aria-describedby pointing to the error message\n */\n\nimport { useRef, useState, type KeyboardEvent, type ClipboardEvent } from 'react';\n\nexport interface OtpInputProps {\n /** Number of digits. Default: 6 */\n length?: number;\n /** Current value (array of digit strings). Controlled. */\n value?: string[];\n /** Called when the value changes. */\n onChange?: (value: string[]) => void;\n /** Disables all cells. */\n disabled?: boolean;\n /** Shows error styling. */\n error?: boolean;\n /** Error message text. If provided, shown below and linked via aria-describedby. */\n errorMessage?: string;\n /** Accessible label for the group. Default: \"One-time password\" */\n 'aria-label'?: string;\n /** Additional class on the root element. */\n className?: string;\n}\n\nexport function OtpInput({\n length = 6,\n value,\n onChange,\n disabled = false,\n error = false,\n errorMessage,\n 'aria-label': ariaLabel = 'One-time password',\n className = '',\n}: OtpInputProps) {\n const [internal, setInternal] = useState<string[]>(() => Array(length).fill(''));\n const cells = value ?? internal;\n const inputRefs = useRef<Array<HTMLInputElement | null>>([]);\n const errorId = 'otp-error';\n\n const update = (next: string[]) => {\n if (!value) setInternal(next);\n onChange?.(next);\n };\n\n const handleKeyDown = (i: number) => (e: KeyboardEvent<HTMLInputElement>) => {\n if (e.key === 'Backspace') {\n e.preventDefault();\n if (cells[i]) {\n const next = [...cells];\n next[i] = '';\n update(next);\n } else if (i > 0) {\n const next = [...cells];\n next[i - 1] = '';\n update(next);\n inputRefs.current[i - 1]?.focus();\n }\n } else if (e.key === 'ArrowLeft' && i > 0) {\n inputRefs.current[i - 1]?.focus();\n } else if (e.key === 'ArrowRight' && i < length - 1) {\n inputRefs.current[i + 1]?.focus();\n }\n };\n\n const handleInput = (i: number) => (e: React.ChangeEvent<HTMLInputElement>) => {\n const raw = e.target.value.replace(/\\D/g, '').slice(-1);\n const next = [...cells];\n next[i] = raw;\n update(next);\n if (raw && i < length - 1) {\n inputRefs.current[i + 1]?.focus();\n }\n };\n\n const handlePaste = (e: ClipboardEvent<HTMLInputElement>) => {\n e.preventDefault();\n const digits = e.clipboardData.getData('text').replace(/\\D/g, '').slice(0, length);\n const next = Array(length).fill('');\n for (let i = 0; i < digits.length; i++) next[i] = digits[i] ?? '';\n update(next);\n const focusIdx = Math.min(digits.length, length - 1);\n inputRefs.current[focusIdx]?.focus();\n };\n\n const cellBase = [\n 'h-12 w-10 rounded-xl border text-center text-lg font-bold',\n 'outline-none transition',\n 'focus:border-brand-blue/50 focus:ring-2 focus:ring-brand-blue/15',\n 'disabled:cursor-not-allowed disabled:bg-slate-50 disabled:text-slate-400',\n ].join(' ');\n\n const cellClass = error\n ? `${cellBase} border-rose-400 bg-rose-50/40 text-rose-800 focus:ring-rose-100`\n : `${cellBase} border-slate-200 bg-white text-slate-900`;\n\n return (\n <div className={`space-y-2 ${className}`}>\n <div\n role=\"group\"\n aria-label={ariaLabel}\n aria-describedby={errorMessage ? errorId : undefined}\n className=\"flex items-center gap-2\"\n >\n {Array.from({ length }).map((_, i) => (\n <input\n key={i}\n ref={(el) => { inputRefs.current[i] = el; }}\n type=\"text\"\n inputMode=\"numeric\"\n maxLength={1}\n value={cells[i] ?? ''}\n disabled={disabled}\n aria-invalid={error}\n aria-label={i === 0 ? `${ariaLabel}, digit ${i + 1} of ${length}` : `digit ${i + 1} of ${length}`}\n aria-describedby={errorMessage ? errorId : undefined}\n className={cellClass}\n onChange={handleInput(i)}\n onKeyDown={handleKeyDown(i)}\n onPaste={handlePaste}\n onFocus={(e) => e.target.select()}\n />\n ))}\n </div>\n\n {errorMessage && (\n <p id={errorId} className=\"text-xs font-semibold text-rose-600\" role=\"alert\">\n {errorMessage}\n </p>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,108 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { useRef, useState } from "react";
3
+ //#region src/ui-library/gallery/otp-input/OtpInput.tsx
4
+ /**
5
+ * OtpInput — Design System Component
6
+ *
7
+ * A segmented one-time-password / PIN entry field.
8
+ * Each digit occupies its own visually-separated cell.
9
+ *
10
+ * Features:
11
+ * - Configurable length (default: 6)
12
+ * - Auto-advance on digit entry
13
+ * - Backspace moves to previous cell
14
+ * - Paste fills all cells
15
+ * - Error state with aria-invalid
16
+ * - Disabled state
17
+ *
18
+ * Accessibility:
19
+ * - Single logical input via aria-label on the first visible cell
20
+ * - role="group" + aria-label on the container
21
+ * - aria-invalid on cells when error is present
22
+ * - All cells share a common aria-describedby pointing to the error message
23
+ */
24
+ function OtpInput({ length = 6, value, onChange, disabled = false, error = false, errorMessage, "aria-label": ariaLabel = "One-time password", className = "" }) {
25
+ const [internal, setInternal] = useState(() => Array(length).fill(""));
26
+ const cells = value ?? internal;
27
+ const inputRefs = useRef([]);
28
+ const errorId = "otp-error";
29
+ const update = (next) => {
30
+ if (!value) setInternal(next);
31
+ onChange?.(next);
32
+ };
33
+ const handleKeyDown = (i) => (e) => {
34
+ if (e.key === "Backspace") {
35
+ e.preventDefault();
36
+ if (cells[i]) {
37
+ const next = [...cells];
38
+ next[i] = "";
39
+ update(next);
40
+ } else if (i > 0) {
41
+ const next = [...cells];
42
+ next[i - 1] = "";
43
+ update(next);
44
+ inputRefs.current[i - 1]?.focus();
45
+ }
46
+ } else if (e.key === "ArrowLeft" && i > 0) inputRefs.current[i - 1]?.focus();
47
+ else if (e.key === "ArrowRight" && i < length - 1) inputRefs.current[i + 1]?.focus();
48
+ };
49
+ const handleInput = (i) => (e) => {
50
+ const raw = e.target.value.replace(/\D/g, "").slice(-1);
51
+ const next = [...cells];
52
+ next[i] = raw;
53
+ update(next);
54
+ if (raw && i < length - 1) inputRefs.current[i + 1]?.focus();
55
+ };
56
+ const handlePaste = (e) => {
57
+ e.preventDefault();
58
+ const digits = e.clipboardData.getData("text").replace(/\D/g, "").slice(0, length);
59
+ const next = Array(length).fill("");
60
+ for (let i = 0; i < digits.length; i++) next[i] = digits[i] ?? "";
61
+ update(next);
62
+ const focusIdx = Math.min(digits.length, length - 1);
63
+ inputRefs.current[focusIdx]?.focus();
64
+ };
65
+ const cellBase = [
66
+ "h-12 w-10 rounded-xl border text-center text-lg font-bold",
67
+ "outline-none transition",
68
+ "focus:border-brand-blue/50 focus:ring-2 focus:ring-brand-blue/15",
69
+ "disabled:cursor-not-allowed disabled:bg-slate-50 disabled:text-slate-400"
70
+ ].join(" ");
71
+ const cellClass = error ? `${cellBase} border-rose-400 bg-rose-50/40 text-rose-800 focus:ring-rose-100` : `${cellBase} border-slate-200 bg-white text-slate-900`;
72
+ return /* @__PURE__ */ jsxs("div", {
73
+ className: `space-y-2 ${className}`,
74
+ children: [/* @__PURE__ */ jsx("div", {
75
+ role: "group",
76
+ "aria-label": ariaLabel,
77
+ "aria-describedby": errorMessage ? errorId : void 0,
78
+ className: "flex items-center gap-2",
79
+ children: Array.from({ length }).map((_, i) => /* @__PURE__ */ jsx("input", {
80
+ ref: (el) => {
81
+ inputRefs.current[i] = el;
82
+ },
83
+ type: "text",
84
+ inputMode: "numeric",
85
+ maxLength: 1,
86
+ value: cells[i] ?? "",
87
+ disabled,
88
+ "aria-invalid": error,
89
+ "aria-label": i === 0 ? `${ariaLabel}, digit ${i + 1} of ${length}` : `digit ${i + 1} of ${length}`,
90
+ "aria-describedby": errorMessage ? errorId : void 0,
91
+ className: cellClass,
92
+ onChange: handleInput(i),
93
+ onKeyDown: handleKeyDown(i),
94
+ onPaste: handlePaste,
95
+ onFocus: (e) => e.target.select()
96
+ }, i))
97
+ }), errorMessage && /* @__PURE__ */ jsx("p", {
98
+ id: errorId,
99
+ className: "text-xs font-semibold text-rose-600",
100
+ role: "alert",
101
+ children: errorMessage
102
+ })]
103
+ });
104
+ }
105
+ //#endregion
106
+ export { OtpInput };
107
+
108
+ //# sourceMappingURL=index26.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index26.mjs","names":[],"sources":["../src/ui-library/gallery/otp-input/OtpInput.tsx"],"sourcesContent":["/**\n * OtpInput — Design System Component\n *\n * A segmented one-time-password / PIN entry field.\n * Each digit occupies its own visually-separated cell.\n *\n * Features:\n * - Configurable length (default: 6)\n * - Auto-advance on digit entry\n * - Backspace moves to previous cell\n * - Paste fills all cells\n * - Error state with aria-invalid\n * - Disabled state\n *\n * Accessibility:\n * - Single logical input via aria-label on the first visible cell\n * - role=\"group\" + aria-label on the container\n * - aria-invalid on cells when error is present\n * - All cells share a common aria-describedby pointing to the error message\n */\n\nimport { useRef, useState, type KeyboardEvent, type ClipboardEvent } from 'react';\n\nexport interface OtpInputProps {\n /** Number of digits. Default: 6 */\n length?: number;\n /** Current value (array of digit strings). Controlled. */\n value?: string[];\n /** Called when the value changes. */\n onChange?: (value: string[]) => void;\n /** Disables all cells. */\n disabled?: boolean;\n /** Shows error styling. */\n error?: boolean;\n /** Error message text. If provided, shown below and linked via aria-describedby. */\n errorMessage?: string;\n /** Accessible label for the group. Default: \"One-time password\" */\n 'aria-label'?: string;\n /** Additional class on the root element. */\n className?: string;\n}\n\nexport function OtpInput({\n length = 6,\n value,\n onChange,\n disabled = false,\n error = false,\n errorMessage,\n 'aria-label': ariaLabel = 'One-time password',\n className = '',\n}: OtpInputProps) {\n const [internal, setInternal] = useState<string[]>(() => Array(length).fill(''));\n const cells = value ?? internal;\n const inputRefs = useRef<Array<HTMLInputElement | null>>([]);\n const errorId = 'otp-error';\n\n const update = (next: string[]) => {\n if (!value) setInternal(next);\n onChange?.(next);\n };\n\n const handleKeyDown = (i: number) => (e: KeyboardEvent<HTMLInputElement>) => {\n if (e.key === 'Backspace') {\n e.preventDefault();\n if (cells[i]) {\n const next = [...cells];\n next[i] = '';\n update(next);\n } else if (i > 0) {\n const next = [...cells];\n next[i - 1] = '';\n update(next);\n inputRefs.current[i - 1]?.focus();\n }\n } else if (e.key === 'ArrowLeft' && i > 0) {\n inputRefs.current[i - 1]?.focus();\n } else if (e.key === 'ArrowRight' && i < length - 1) {\n inputRefs.current[i + 1]?.focus();\n }\n };\n\n const handleInput = (i: number) => (e: React.ChangeEvent<HTMLInputElement>) => {\n const raw = e.target.value.replace(/\\D/g, '').slice(-1);\n const next = [...cells];\n next[i] = raw;\n update(next);\n if (raw && i < length - 1) {\n inputRefs.current[i + 1]?.focus();\n }\n };\n\n const handlePaste = (e: ClipboardEvent<HTMLInputElement>) => {\n e.preventDefault();\n const digits = e.clipboardData.getData('text').replace(/\\D/g, '').slice(0, length);\n const next = Array(length).fill('');\n for (let i = 0; i < digits.length; i++) next[i] = digits[i] ?? '';\n update(next);\n const focusIdx = Math.min(digits.length, length - 1);\n inputRefs.current[focusIdx]?.focus();\n };\n\n const cellBase = [\n 'h-12 w-10 rounded-xl border text-center text-lg font-bold',\n 'outline-none transition',\n 'focus:border-brand-blue/50 focus:ring-2 focus:ring-brand-blue/15',\n 'disabled:cursor-not-allowed disabled:bg-slate-50 disabled:text-slate-400',\n ].join(' ');\n\n const cellClass = error\n ? `${cellBase} border-rose-400 bg-rose-50/40 text-rose-800 focus:ring-rose-100`\n : `${cellBase} border-slate-200 bg-white text-slate-900`;\n\n return (\n <div className={`space-y-2 ${className}`}>\n <div\n role=\"group\"\n aria-label={ariaLabel}\n aria-describedby={errorMessage ? errorId : undefined}\n className=\"flex items-center gap-2\"\n >\n {Array.from({ length }).map((_, i) => (\n <input\n key={i}\n ref={(el) => { inputRefs.current[i] = el; }}\n type=\"text\"\n inputMode=\"numeric\"\n maxLength={1}\n value={cells[i] ?? ''}\n disabled={disabled}\n aria-invalid={error}\n aria-label={i === 0 ? `${ariaLabel}, digit ${i + 1} of ${length}` : `digit ${i + 1} of ${length}`}\n aria-describedby={errorMessage ? errorId : undefined}\n className={cellClass}\n onChange={handleInput(i)}\n onKeyDown={handleKeyDown(i)}\n onPaste={handlePaste}\n onFocus={(e) => e.target.select()}\n />\n ))}\n </div>\n\n {errorMessage && (\n <p id={errorId} className=\"text-xs font-semibold text-rose-600\" role=\"alert\">\n {errorMessage}\n </p>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,120 @@
1
+ let react_jsx_runtime = require("react/jsx-runtime");
2
+ let react = require("react");
3
+ let lucide_react = require("lucide-react");
4
+ //#region src/ui-library/gallery/file-upload/FileUpload.tsx
5
+ /**
6
+ * FileUpload — Design System Component
7
+ *
8
+ * A styled file-selection trigger. Wraps a native <input type="file"> with a
9
+ * drag-and-drop zone. Presentation-only — no HTTP upload logic.
10
+ *
11
+ * States: idle, drag-active, selected, disabled
12
+ *
13
+ * Accessibility:
14
+ * - The visible zone is a <label> wrapping a visually-hidden <input>.
15
+ * - <input> has aria-label describing the expected file.
16
+ * - Keyboard: Tab to focus the input; Enter/Space to open the picker.
17
+ * - Disabled: input has disabled attr; zone has aria-disabled="true".
18
+ */
19
+ function FileUpload({ onFiles, accept, multiple = false, disabled = false, "aria-label": ariaLabel = "Upload file", className = "" }) {
20
+ const inputRef = (0, react.useRef)(null);
21
+ const [dragging, setDragging] = (0, react.useState)(false);
22
+ const [selected, setSelected] = (0, react.useState)([]);
23
+ const handleFiles = (files) => {
24
+ if (!files || files.length === 0) return;
25
+ const arr = Array.from(files);
26
+ setSelected(multiple ? arr : [arr[0]]);
27
+ onFiles?.(multiple ? arr : [arr[0]]);
28
+ };
29
+ const handleChange = (e) => {
30
+ handleFiles(e.target.files);
31
+ e.target.value = "";
32
+ };
33
+ const handleDrop = (e) => {
34
+ e.preventDefault();
35
+ setDragging(false);
36
+ if (disabled) return;
37
+ handleFiles(e.dataTransfer.files);
38
+ };
39
+ const handleDragOver = (e) => {
40
+ e.preventDefault();
41
+ if (!disabled) setDragging(true);
42
+ };
43
+ const handleDragLeave = () => setDragging(false);
44
+ const removeFile = (i) => {
45
+ setSelected((prev) => prev.filter((_, idx) => idx !== i));
46
+ };
47
+ const zoneClass = [
48
+ "flex flex-col items-center justify-center gap-3",
49
+ "rounded-2xl border-2 border-dashed p-8 text-center cursor-pointer",
50
+ "transition-colors focus-within:outline-none focus-within:ring-2 focus-within:ring-brand-blue/20",
51
+ dragging ? "border-brand-blue/60 bg-blue-50" : disabled ? "border-slate-100 bg-slate-50/40 cursor-not-allowed" : "border-slate-200 bg-white hover:border-brand-sky/50 hover:bg-[#eef2f8]",
52
+ className
53
+ ].filter(Boolean).join(" ");
54
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
55
+ className: "space-y-3",
56
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
57
+ className: zoneClass,
58
+ onDrop: handleDrop,
59
+ onDragOver: handleDragOver,
60
+ onDragLeave: handleDragLeave,
61
+ "aria-disabled": disabled,
62
+ children: [
63
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
64
+ ref: inputRef,
65
+ type: "file",
66
+ accept,
67
+ multiple,
68
+ disabled,
69
+ onChange: handleChange,
70
+ "aria-label": ariaLabel,
71
+ className: "sr-only"
72
+ }),
73
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.UploadCloud, {
74
+ size: 32,
75
+ className: dragging ? "text-brand-blue" : disabled ? "text-slate-300" : "text-slate-400",
76
+ "aria-hidden": "true"
77
+ }),
78
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
79
+ className: `text-sm font-semibold ${disabled ? "text-slate-400" : "text-slate-700"}`,
80
+ children: dragging ? "Drop to upload" : "Click or drag files here"
81
+ }), accept && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
82
+ className: "mt-0.5 text-xs font-medium text-slate-400",
83
+ children: accept
84
+ })] })
85
+ ]
86
+ }), selected.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
87
+ className: "space-y-1.5",
88
+ "aria-label": "Selected files",
89
+ children: selected.map((file, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
90
+ className: "flex items-center gap-3 rounded-xl border border-slate-200 bg-white px-3 py-2.5",
91
+ children: [
92
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.File, {
93
+ size: 14,
94
+ className: "shrink-0 text-brand-sky",
95
+ "aria-hidden": "true"
96
+ }),
97
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
98
+ className: "min-w-0 flex-1 truncate text-xs font-semibold text-slate-700",
99
+ children: file.name
100
+ }),
101
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
102
+ className: "shrink-0 text-[10px] font-medium text-slate-400",
103
+ children: [(file.size / 1024).toFixed(0), " KB"]
104
+ }),
105
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
106
+ type: "button",
107
+ onClick: () => removeFile(i),
108
+ "aria-label": `Remove ${file.name}`,
109
+ className: "ml-1 shrink-0 rounded p-0.5 text-slate-400 hover:bg-rose-50 hover:text-rose-500 transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-rose-200",
110
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.X, { size: 12 })
111
+ })
112
+ ]
113
+ }, `${file.name}-${i}`))
114
+ })]
115
+ });
116
+ }
117
+ //#endregion
118
+ exports.FileUpload = FileUpload;
119
+
120
+ //# sourceMappingURL=index27.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index27.cjs","names":[],"sources":["../src/ui-library/gallery/file-upload/FileUpload.tsx"],"sourcesContent":["/**\n * FileUpload — Design System Component\n *\n * A styled file-selection trigger. Wraps a native <input type=\"file\"> with a\n * drag-and-drop zone. Presentation-only — no HTTP upload logic.\n *\n * States: idle, drag-active, selected, disabled\n *\n * Accessibility:\n * - The visible zone is a <label> wrapping a visually-hidden <input>.\n * - <input> has aria-label describing the expected file.\n * - Keyboard: Tab to focus the input; Enter/Space to open the picker.\n * - Disabled: input has disabled attr; zone has aria-disabled=\"true\".\n */\n\nimport { useRef, useState, type ChangeEvent, type DragEvent } from 'react';\nimport { UploadCloud, X, File } from 'lucide-react';\n\nexport interface FileUploadProps {\n /** Called when files are selected via picker or drop. */\n onFiles?: (files: File[]) => void;\n /** Accepted MIME types / extensions, forwarded to <input accept>. */\n accept?: string;\n /** Allow selecting multiple files. Default: false */\n multiple?: boolean;\n /** Disable the upload zone. */\n disabled?: boolean;\n /** Accessible label describing the expected file type. */\n 'aria-label'?: string;\n /** Additional class on the root element. */\n className?: string;\n}\n\nexport function FileUpload({\n onFiles,\n accept,\n multiple = false,\n disabled = false,\n 'aria-label': ariaLabel = 'Upload file',\n className = '',\n}: FileUploadProps) {\n const inputRef = useRef<HTMLInputElement>(null);\n const [dragging, setDragging] = useState(false);\n const [selected, setSelected] = useState<File[]>([]);\n\n const handleFiles = (files: FileList | null) => {\n if (!files || files.length === 0) return;\n const arr = Array.from(files);\n setSelected(multiple ? arr : [arr[0]!]);\n onFiles?.(multiple ? arr : [arr[0]!]);\n };\n\n const handleChange = (e: ChangeEvent<HTMLInputElement>) => {\n handleFiles(e.target.files);\n // Reset so same file can be re-selected\n e.target.value = '';\n };\n\n const handleDrop = (e: DragEvent<HTMLLabelElement>) => {\n e.preventDefault();\n setDragging(false);\n if (disabled) return;\n handleFiles(e.dataTransfer.files);\n };\n\n const handleDragOver = (e: DragEvent<HTMLLabelElement>) => {\n e.preventDefault();\n if (!disabled) setDragging(true);\n };\n\n const handleDragLeave = () => setDragging(false);\n\n const removeFile = (i: number) => {\n setSelected((prev) => prev.filter((_, idx) => idx !== i));\n };\n\n const zoneClass = [\n 'flex flex-col items-center justify-center gap-3',\n 'rounded-2xl border-2 border-dashed p-8 text-center cursor-pointer',\n 'transition-colors focus-within:outline-none focus-within:ring-2 focus-within:ring-brand-blue/20',\n dragging\n ? 'border-brand-blue/60 bg-blue-50'\n : disabled\n ? 'border-slate-100 bg-slate-50/40 cursor-not-allowed'\n : 'border-slate-200 bg-white hover:border-brand-sky/50 hover:bg-[#eef2f8]',\n className,\n ]\n .filter(Boolean)\n .join(' ');\n\n return (\n <div className=\"space-y-3\">\n <label\n className={zoneClass}\n onDrop={handleDrop}\n onDragOver={handleDragOver}\n onDragLeave={handleDragLeave}\n aria-disabled={disabled}\n >\n <input\n ref={inputRef}\n type=\"file\"\n accept={accept}\n multiple={multiple}\n disabled={disabled}\n onChange={handleChange}\n aria-label={ariaLabel}\n className=\"sr-only\"\n />\n <UploadCloud\n size={32}\n className={dragging ? 'text-brand-blue' : disabled ? 'text-slate-300' : 'text-slate-400'}\n aria-hidden=\"true\"\n />\n <div>\n <p className={`text-sm font-semibold ${disabled ? 'text-slate-400' : 'text-slate-700'}`}>\n {dragging ? 'Drop to upload' : 'Click or drag files here'}\n </p>\n {accept && (\n <p className=\"mt-0.5 text-xs font-medium text-slate-400\">{accept}</p>\n )}\n </div>\n </label>\n\n {selected.length > 0 && (\n <ul className=\"space-y-1.5\" aria-label=\"Selected files\">\n {selected.map((file, i) => (\n <li\n key={`${file.name}-${i}`}\n className=\"flex items-center gap-3 rounded-xl border border-slate-200 bg-white px-3 py-2.5\"\n >\n <File size={14} className=\"shrink-0 text-brand-sky\" aria-hidden=\"true\" />\n <span className=\"min-w-0 flex-1 truncate text-xs font-semibold text-slate-700\">\n {file.name}\n </span>\n <span className=\"shrink-0 text-[10px] font-medium text-slate-400\">\n {(file.size / 1024).toFixed(0)} KB\n </span>\n <button\n type=\"button\"\n onClick={() => removeFile(i)}\n aria-label={`Remove ${file.name}`}\n className=\"ml-1 shrink-0 rounded p-0.5 text-slate-400 hover:bg-rose-50 hover:text-rose-500 transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-rose-200\"\n >\n <X size={12} />\n </button>\n </li>\n ))}\n </ul>\n )}\n </div>\n );\n}\n"],"mappings":"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"}