@musecat/uikit 0.1.1

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 (180) hide show
  1. package/LICENSE +9 -0
  2. package/README.md +51 -0
  3. package/index.ts +78 -0
  4. package/package.json +103 -0
  5. package/packages/Components/Box/Box.tsx +173 -0
  6. package/packages/Components/Box/Box.types.ts +48 -0
  7. package/packages/Components/Button/Button.tsx +275 -0
  8. package/packages/Components/Button/Button.types.ts +44 -0
  9. package/packages/Components/Card/Card.default.tsx +180 -0
  10. package/packages/Components/Card/Card.foldable.tsx +383 -0
  11. package/packages/Components/Card/Card.module.scss +23 -0
  12. package/packages/Components/Card/Card.tsx +8 -0
  13. package/packages/Components/Card/Card.types.ts +91 -0
  14. package/packages/Components/Card/Card.utils.ts +66 -0
  15. package/packages/Components/Checkbox/Checkbox.module.scss +11 -0
  16. package/packages/Components/Checkbox/Checkbox.tsx +104 -0
  17. package/packages/Components/Checkbox/Checkbox.types.ts +24 -0
  18. package/packages/Components/CodeBox/CodeBox.copy.tsx +43 -0
  19. package/packages/Components/CodeBox/CodeBox.module.scss +72 -0
  20. package/packages/Components/CodeBox/CodeBox.tsx +91 -0
  21. package/packages/Components/CodeBox/CodeBox.types.ts +17 -0
  22. package/packages/Components/ContextMenu/ContextMenu.module.scss +8 -0
  23. package/packages/Components/ContextMenu/ContextMenu.options.tsx +126 -0
  24. package/packages/Components/ContextMenu/ContextMenu.tsx +381 -0
  25. package/packages/Components/ContextMenu/ContextMenu.types.ts +46 -0
  26. package/packages/Components/ContributionGraph/ContributionGraph.module.scss +42 -0
  27. package/packages/Components/ContributionGraph/ContributionGraph.tsx +454 -0
  28. package/packages/Components/ContributionGraph/ContributionGraph.types.ts +27 -0
  29. package/packages/Components/DatePicker/DatePicker.calendar.tsx +161 -0
  30. package/packages/Components/DatePicker/DatePicker.core.tsx +582 -0
  31. package/packages/Components/DatePicker/DatePicker.module.scss +26 -0
  32. package/packages/Components/DatePicker/DatePicker.tsx +786 -0
  33. package/packages/Components/DatePicker/DatePicker.types.ts +36 -0
  34. package/packages/Components/DatePicker/DatePicker.utils.ts +80 -0
  35. package/packages/Components/DatePicker/hooks/useCalendar.ts +61 -0
  36. package/packages/Components/DatePicker/hooks/useSelection.ts +208 -0
  37. package/packages/Components/Divider/Divider.tsx +46 -0
  38. package/packages/Components/Divider/Divider.types.ts +12 -0
  39. package/packages/Components/Header/Header.module.scss +74 -0
  40. package/packages/Components/Header/Header.tsx +47 -0
  41. package/packages/Components/Header/Header.types.ts +8 -0
  42. package/packages/Components/Icon/Icon.group.tsx +122 -0
  43. package/packages/Components/Icon/Icon.group.types.ts +20 -0
  44. package/packages/Components/Icon/Icon.tsx +341 -0
  45. package/packages/Components/Icon/Icon.types.ts +59 -0
  46. package/packages/Components/Icon/SVG/data.tsx +232 -0
  47. package/packages/Components/Input/Input.module.scss +5 -0
  48. package/packages/Components/Input/Input.tsx +87 -0
  49. package/packages/Components/Input/Input.types.ts +14 -0
  50. package/packages/Components/Label/Label.module.scss +19 -0
  51. package/packages/Components/Label/Label.tsx +115 -0
  52. package/packages/Components/Label/Label.types.ts +28 -0
  53. package/packages/Components/Layout/Layout.docs.module.scss +42 -0
  54. package/packages/Components/Layout/Layout.docs.tsx +150 -0
  55. package/packages/Components/Layout/Layout.docs.types.ts +18 -0
  56. package/packages/Components/Layout/Layout.module.scss +91 -0
  57. package/packages/Components/Layout/Layout.tsx +306 -0
  58. package/packages/Components/Layout/Layout.types.ts +71 -0
  59. package/packages/Components/Maps/OSM/MapOSM.module.scss +11 -0
  60. package/packages/Components/Maps/OSM/MapOSM.shared.ts +121 -0
  61. package/packages/Components/Maps/OSM/MapOSM.tsx +279 -0
  62. package/packages/Components/Maps/OSM/MapOSM.types.ts +18 -0
  63. package/packages/Components/Nav/Nav.module.scss +137 -0
  64. package/packages/Components/Nav/Nav.tsx +304 -0
  65. package/packages/Components/Nav/Nav.types.ts +32 -0
  66. package/packages/Components/Nav/hooks/useNavDrag.ts +374 -0
  67. package/packages/Components/Nav/hooks/useNavIndicator.ts +137 -0
  68. package/packages/Components/Pagination/Pagination.tsx +254 -0
  69. package/packages/Components/Pagination/Pagination.types.ts +18 -0
  70. package/packages/Components/Pill/Pill.tsx +127 -0
  71. package/packages/Components/Pill/Pill.types.ts +33 -0
  72. package/packages/Components/Profile/Profile.shared.ts +74 -0
  73. package/packages/Components/Profile/Profile.tsx +382 -0
  74. package/packages/Components/Profile/Profile.types.ts +55 -0
  75. package/packages/Components/Progress/Progress.module.scss +15 -0
  76. package/packages/Components/Progress/Progress.tsx +88 -0
  77. package/packages/Components/Progress/Progress.types.ts +22 -0
  78. package/packages/Components/Radio/Radio.module.scss +11 -0
  79. package/packages/Components/Radio/Radio.tsx +121 -0
  80. package/packages/Components/Radio/Radio.types.ts +20 -0
  81. package/packages/Components/Select/Select.keyboard.tsx +76 -0
  82. package/packages/Components/Select/Select.trigger.tsx +88 -0
  83. package/packages/Components/Select/Select.tsx +11 -0
  84. package/packages/Components/Select/Select.types.ts +38 -0
  85. package/packages/Components/Select/Select.utils.ts +84 -0
  86. package/packages/Components/Select/hooks/useSelectNavigation.ts +461 -0
  87. package/packages/Components/Select/hooks/useSelectSelection.ts +142 -0
  88. package/packages/Components/Select/hooks/useSelectState.ts +314 -0
  89. package/packages/Components/Select/select.control.tsx +228 -0
  90. package/packages/Components/Select/select.inner.tsx +312 -0
  91. package/packages/Components/Skeleton/Skeleton.tsx +30 -0
  92. package/packages/Components/Skeleton/Skeleton.types.ts +12 -0
  93. package/packages/Components/Spinner/Spinner.module.scss +78 -0
  94. package/packages/Components/Spinner/Spinner.tsx +125 -0
  95. package/packages/Components/Spinner/Spinner.types.ts +19 -0
  96. package/packages/Components/Text/Text.module.scss +7 -0
  97. package/packages/Components/Text/Text.tsx +49 -0
  98. package/packages/Components/Text/Text.types.ts +33 -0
  99. package/packages/Components/TimePicker/TimePicker.core.tsx +448 -0
  100. package/packages/Components/TimePicker/TimePicker.module.scss +30 -0
  101. package/packages/Components/TimePicker/TimePicker.tsx +511 -0
  102. package/packages/Components/TimePicker/TimePicker.types.ts +22 -0
  103. package/packages/Components/Timeline/Timeline.tsx +111 -0
  104. package/packages/Components/Timeline/Timeline.types.ts +23 -0
  105. package/packages/Components/Title/Title.tsx +305 -0
  106. package/packages/Components/Title/Title.types.ts +64 -0
  107. package/packages/Components/Toggle/Toggle.module.scss +32 -0
  108. package/packages/Components/Toggle/Toggle.tsx +272 -0
  109. package/packages/Components/Toggle/Toggle.types.ts +17 -0
  110. package/packages/Components/Tooltip/Tooltip.module.scss +44 -0
  111. package/packages/Components/Tooltip/Tooltip.tsx +340 -0
  112. package/packages/Components/Tooltip/Tooltip.types.ts +38 -0
  113. package/packages/Frameworks/Dialog/Dialog.background.tsx +88 -0
  114. package/packages/Frameworks/Dialog/Dialog.boot.tsx +32 -0
  115. package/packages/Frameworks/Dialog/Dialog.store.ts +199 -0
  116. package/packages/Frameworks/Dialog/Dialog.tsx +185 -0
  117. package/packages/Frameworks/Dialog/Dialog.types.ts +264 -0
  118. package/packages/Frameworks/Dialog/Dialog.utils.ts +22 -0
  119. package/packages/Frameworks/Dialog/contents/Dialog.footer.tsx +79 -0
  120. package/packages/Frameworks/Dialog/contents/Dialog.funnel.tsx +260 -0
  121. package/packages/Frameworks/Dialog/contents/Dialog.header.tsx +137 -0
  122. package/packages/Frameworks/Dialog/hooks/useDialogPosition.ts +298 -0
  123. package/packages/Frameworks/Dialog/hooks/useSheetDrag.ts +325 -0
  124. package/packages/Frameworks/Dialog/hooks/useSheetGeometry.ts +201 -0
  125. package/packages/Frameworks/Dialog/hooks/useSheetProgressive.ts +98 -0
  126. package/packages/Frameworks/Dialog/renderers/renderModal.tsx +228 -0
  127. package/packages/Frameworks/Dialog/renderers/renderPopover.tsx +220 -0
  128. package/packages/Frameworks/Dialog/renderers/renderSheet.tsx +571 -0
  129. package/packages/Frameworks/EdgeEffect/EdgeEffect.tsx +52 -0
  130. package/packages/Frameworks/EdgeEffect/EdgeEffect.types.ts +5 -0
  131. package/packages/Frameworks/Motion/Motion.presets.ts +49 -0
  132. package/packages/Frameworks/Pressable/Pressable.module.scss +19 -0
  133. package/packages/Frameworks/Pressable/Pressable.tsx +651 -0
  134. package/packages/Frameworks/Pressable/Pressable.types.ts +73 -0
  135. package/packages/Frameworks/Squircle/Squircle.tsx +263 -0
  136. package/packages/Frameworks/Squircle/Squircle.types.ts +15 -0
  137. package/packages/Frameworks/Theme/Radius.types.ts +55 -0
  138. package/packages/Frameworks/Theme/Theme.types.ts +325 -0
  139. package/packages/Frameworks/Toaster/Toaster.boot.tsx +47 -0
  140. package/packages/Frameworks/Toaster/Toaster.module.scss +168 -0
  141. package/packages/Frameworks/Toaster/Toaster.types.ts +13 -0
  142. package/packages/Frameworks/View/DNDView/DNDView.module.scss +8 -0
  143. package/packages/Frameworks/View/DNDView/DNDView.tsx +470 -0
  144. package/packages/Frameworks/View/DNDView/DNDView.types.ts +61 -0
  145. package/packages/Frameworks/View/HScrollView/HScrollView.module.scss +59 -0
  146. package/packages/Frameworks/View/HScrollView/HScrollView.tsx +386 -0
  147. package/packages/Frameworks/View/HScrollView/HScrollView.types.ts +46 -0
  148. package/packages/Frameworks/View/ImageView/Dialog/ImageDialog.counter.tsx +27 -0
  149. package/packages/Frameworks/View/ImageView/Dialog/ImageDialog.footer.tsx +76 -0
  150. package/packages/Frameworks/View/ImageView/Dialog/ImageDialog.header.tsx +49 -0
  151. package/packages/Frameworks/View/ImageView/Dialog/ImageDialog.list.tsx +74 -0
  152. package/packages/Frameworks/View/ImageView/Dialog/ImageDialog.slide.tsx +71 -0
  153. package/packages/Frameworks/View/ImageView/Dialog/ImageDialog.tsx +175 -0
  154. package/packages/Frameworks/View/ImageView/Image.controls.tsx +39 -0
  155. package/packages/Frameworks/View/ImageView/Image.hooks.ts +82 -0
  156. package/packages/Frameworks/View/ImageView/Image.module.scss +7 -0
  157. package/packages/Frameworks/View/ImageView/Image.tsx +375 -0
  158. package/packages/Frameworks/View/ImageView/Image.types.ts +71 -0
  159. package/packages/Frameworks/View/ImageView/Image.utils.ts +79 -0
  160. package/packages/Frameworks/View/View.module.scss +34 -0
  161. package/packages/Frameworks/View/View.tsx +200 -0
  162. package/packages/Frameworks/View/View.types.ts +36 -0
  163. package/packages/Frameworks/_shared/Padding.types.ts +52 -0
  164. package/packages/Frameworks/_shared/StopParentInteraction.tsx +45 -0
  165. package/packages/Frameworks/_shared/Wind.types.ts +17 -0
  166. package/packages/Frameworks/_shared/bodyScrollLock.ts +127 -0
  167. package/packages/Frameworks/_shared/layer.constants.ts +8 -0
  168. package/packages/Frameworks/_shared/normalize.ts +39 -0
  169. package/packages/Frameworks/_shared/sizing.ts +55 -0
  170. package/packages/Frameworks/_shared/useControllableState.ts +29 -0
  171. package/packages/Styles/_animation.scss +42 -0
  172. package/packages/Styles/_color.scss +131 -0
  173. package/packages/Styles/_font.scss +30 -0
  174. package/packages/Styles/_icon.scss +782 -0
  175. package/packages/Styles/_importer.scss +9 -0
  176. package/packages/Styles/_system.scss +69 -0
  177. package/packages/Styles/_textstyle.scss +111 -0
  178. package/packages/Styles/_theme.scss +452 -0
  179. package/packages/Styles/_viewport-global.scss +99 -0
  180. package/packages/Styles/_viewport.scss +108 -0
@@ -0,0 +1,511 @@
1
+ "use client";
2
+
3
+ import clsx from "clsx";
4
+ import {
5
+ type ChangeEvent,
6
+ type KeyboardEvent,
7
+ useCallback,
8
+ useEffect,
9
+ useMemo,
10
+ useRef,
11
+ useState,
12
+ } from "react";
13
+ import Icon from "@/packages/Components/Icon/Icon";
14
+ import Label from "@/packages/Components/Label/Label";
15
+ import Text from "@/packages/Components/Text/Text";
16
+ import styles from "@/packages/Components/TimePicker/TimePicker.module.scss";
17
+ import type { TimePickerProps } from "@/packages/Components/TimePicker/TimePicker.types";
18
+ import Pressable from "@/packages/Frameworks/Pressable/Pressable";
19
+ import View from "@/packages/Frameworks/View/View";
20
+
21
+ type Segment = "hour" | "minute" | "second" | "ampm";
22
+
23
+ function pad(n: number): string {
24
+ return String(n).padStart(2, "0");
25
+ }
26
+
27
+ function parseValue(
28
+ raw: string | undefined,
29
+ use12h: boolean,
30
+ ): { hour: number; minute: number; second: number; ampm: "AM" | "PM" } {
31
+ const parts = (raw ?? "").split(":");
32
+ const hourPart = Number(parts[0]);
33
+ const minutePart = Number(parts[1]);
34
+ const secondPart = Number(parts[2]);
35
+
36
+ let hour = Number.isNaN(hourPart) ? 0 : Math.max(0, Math.min(23, hourPart));
37
+ const minute = Number.isNaN(minutePart)
38
+ ? 0
39
+ : Math.max(0, Math.min(59, minutePart));
40
+ const second = Number.isNaN(secondPart)
41
+ ? 0
42
+ : Math.max(0, Math.min(59, secondPart));
43
+ const ampm: "AM" | "PM" = hour < 12 ? "AM" : "PM";
44
+
45
+ if (use12h) {
46
+ if (hour === 0) hour = 12;
47
+ else if (hour > 12) hour -= 12;
48
+ }
49
+
50
+ return { hour, minute, second, ampm };
51
+ }
52
+
53
+ function toTimeString(
54
+ hour: number,
55
+ minute: number,
56
+ second: number,
57
+ ampm: "AM" | "PM",
58
+ use12h: boolean,
59
+ showSeconds: boolean,
60
+ ): string {
61
+ let normalizedHour = hour;
62
+ if (use12h) {
63
+ normalizedHour =
64
+ ampm === "AM" ? (hour === 12 ? 0 : hour) : hour === 12 ? 12 : hour + 12;
65
+ }
66
+
67
+ return showSeconds
68
+ ? `${pad(normalizedHour)}:${pad(minute)}:${pad(second)}`
69
+ : `${pad(normalizedHour)}:${pad(minute)}`;
70
+ }
71
+
72
+ export default function TimePicker({
73
+ title,
74
+ required,
75
+ readOnly,
76
+ disabled,
77
+ hint,
78
+ className,
79
+ style,
80
+ value: valueProp,
81
+ defaultValue,
82
+ showSeconds = false,
83
+ use12h = false,
84
+ onChange,
85
+ onHourOverflow,
86
+ "data-color-mode": dataTheme,
87
+ }: TimePickerProps) {
88
+ const isControlled = valueProp !== undefined;
89
+ const hasInitialValue = valueProp !== undefined || defaultValue !== undefined;
90
+ const initial = hasInitialValue
91
+ ? parseValue(valueProp ?? defaultValue, use12h)
92
+ : null;
93
+
94
+ const [hour, setHour] = useState<number | null>(
95
+ initial ? initial.hour : null,
96
+ );
97
+ const [minute, setMinute] = useState<number | null>(
98
+ initial ? initial.minute : null,
99
+ );
100
+ const [second, setSecond] = useState<number | null>(
101
+ initial ? initial.second : null,
102
+ );
103
+ const [ampm, setAmpm] = useState<"AM" | "PM">(initial ? initial.ampm : "AM");
104
+
105
+ useEffect(() => {
106
+ if (!isControlled || valueProp === undefined) return;
107
+
108
+ if (!valueProp) {
109
+ setHour(null);
110
+ setMinute(null);
111
+ setSecond(null);
112
+ return;
113
+ }
114
+
115
+ const parsed = parseValue(valueProp, use12h);
116
+ setHour(parsed.hour);
117
+ setMinute(parsed.minute);
118
+ setSecond(parsed.second);
119
+ setAmpm(parsed.ampm);
120
+ }, [valueProp, use12h, isControlled]);
121
+
122
+ const emit = useCallback(
123
+ (
124
+ nextHour: number | null,
125
+ nextMinute: number | null,
126
+ nextSecond: number | null,
127
+ nextAmpm: "AM" | "PM",
128
+ ) => {
129
+ if (nextHour === null || nextMinute === null) return;
130
+ if (showSeconds && nextSecond === null) return;
131
+ const secondValue = showSeconds ? (nextSecond as number) : 0;
132
+ onChange?.(
133
+ toTimeString(
134
+ nextHour,
135
+ nextMinute,
136
+ secondValue,
137
+ nextAmpm,
138
+ use12h,
139
+ showSeconds,
140
+ ),
141
+ );
142
+ },
143
+ [onChange, showSeconds, use12h],
144
+ );
145
+
146
+ const hourRef = useRef<HTMLInputElement>(null);
147
+ const minuteRef = useRef<HTMLInputElement>(null);
148
+ const secondRef = useRef<HTMLInputElement>(null);
149
+ const ampmRef = useRef<HTMLButtonElement>(null);
150
+
151
+ const order = useMemo<Segment[]>(() => {
152
+ const list: Segment[] = ["hour", "minute"];
153
+ if (showSeconds) list.push("second");
154
+ if (use12h) list.push("ampm");
155
+ return list;
156
+ }, [showSeconds, use12h]);
157
+
158
+ const getSegmentRef = useCallback((segment: Segment) => {
159
+ switch (segment) {
160
+ case "hour":
161
+ return hourRef;
162
+ case "minute":
163
+ return minuteRef;
164
+ case "second":
165
+ return secondRef;
166
+ case "ampm":
167
+ return ampmRef;
168
+ }
169
+ }, []);
170
+
171
+ const moveNext = useCallback(
172
+ (current: Segment) => {
173
+ const next = order[order.indexOf(current) + 1];
174
+ if (next) getSegmentRef(next).current?.focus();
175
+ },
176
+ [getSegmentRef, order],
177
+ );
178
+
179
+ const movePrev = useCallback(
180
+ (current: Segment) => {
181
+ const prev = order[order.indexOf(current) - 1];
182
+ if (prev) getSegmentRef(prev).current?.focus();
183
+ },
184
+ [getSegmentRef, order],
185
+ );
186
+
187
+ const focusAndSelect = useCallback(
188
+ (segment: Segment) => {
189
+ const target = getSegmentRef(segment).current;
190
+ if (!target) return;
191
+
192
+ requestAnimationFrame(() => {
193
+ target.focus();
194
+
195
+ if ("select" in target && typeof target.select === "function") {
196
+ target.select();
197
+ }
198
+ });
199
+ },
200
+ [getSegmentRef],
201
+ );
202
+
203
+ const displayHour =
204
+ hour !== null ? (use12h ? pad(hour === 0 ? 12 : hour) : pad(hour)) : "";
205
+ const displayMinute = minute !== null ? pad(minute) : "";
206
+ const displaySecond = second !== null ? pad(second) : "";
207
+
208
+ const bufRef = useRef("");
209
+
210
+ const getUpdated = useCallback(
211
+ (
212
+ changedSegment: "hour" | "minute" | "second",
213
+ newValue: number,
214
+ ): [number | null, number | null, number | null, "AM" | "PM"] => [
215
+ changedSegment === "hour" ? newValue : hour,
216
+ changedSegment === "minute" ? newValue : minute,
217
+ changedSegment === "second" ? newValue : second,
218
+ ampm,
219
+ ],
220
+ [ampm, hour, minute, second],
221
+ );
222
+
223
+ const normalizeHourInput = useCallback(
224
+ (rawValue: number) => (use12h || rawValue < 24 ? rawValue : rawValue % 24),
225
+ [use12h],
226
+ );
227
+
228
+ const commitNumericSegment = useCallback(
229
+ (segment: "hour" | "minute" | "second", rawValue: string) => {
230
+ const digits = rawValue.replace(/\D/g, "").slice(-2);
231
+ const max = segment === "hour" ? (use12h ? 12 : 23) : 59;
232
+ const min = segment === "hour" && use12h ? 1 : 0;
233
+ const setter =
234
+ segment === "hour"
235
+ ? setHour
236
+ : segment === "minute"
237
+ ? setMinute
238
+ : setSecond;
239
+
240
+ if (!digits) {
241
+ setter(min);
242
+ emit(...getUpdated(segment, min));
243
+ return;
244
+ }
245
+
246
+ const parsed = Number(digits);
247
+ const shouldNormalizeHour = segment === "hour" && !use12h && parsed >= 24;
248
+ const next = shouldNormalizeHour
249
+ ? normalizeHourInput(parsed)
250
+ : Math.max(min, Math.min(max, parsed));
251
+
252
+ if (shouldNormalizeHour) {
253
+ onHourOverflow?.({
254
+ input: parsed,
255
+ normalized: next,
256
+ });
257
+ }
258
+
259
+ setter(next);
260
+ emit(...getUpdated(segment, next));
261
+
262
+ if (digits.length >= 2 || parsed * 10 > max) {
263
+ moveNext(segment);
264
+ }
265
+ },
266
+ [emit, getUpdated, moveNext, normalizeHourInput, onHourOverflow, use12h],
267
+ );
268
+
269
+ const updateNumericSegment = useCallback(
270
+ (segment: "hour" | "minute" | "second", rawValue: string) => {
271
+ commitNumericSegment(segment, rawValue);
272
+ },
273
+ [commitNumericSegment],
274
+ );
275
+
276
+ const handleNumericChange = useCallback(
277
+ (
278
+ e: ChangeEvent<HTMLInputElement>,
279
+ segment: "hour" | "minute" | "second",
280
+ ) => {
281
+ updateNumericSegment(segment, e.target.value);
282
+ },
283
+ [updateNumericSegment],
284
+ );
285
+
286
+ const updateAmpm = useCallback(
287
+ (next: "AM" | "PM") => {
288
+ setAmpm(next);
289
+ emit(hour, minute, second, next);
290
+ },
291
+ [emit, hour, minute, second],
292
+ );
293
+
294
+ const handleNumericKeyDown = (
295
+ e: KeyboardEvent<HTMLInputElement>,
296
+ segment: "hour" | "minute" | "second",
297
+ ) => {
298
+ const max = segment === "hour" ? (use12h ? 12 : 23) : 59;
299
+ const min = segment === "hour" && use12h ? 1 : 0;
300
+ const current =
301
+ segment === "hour" ? hour : segment === "minute" ? minute : second;
302
+ const base = current ?? min;
303
+ const setter =
304
+ segment === "hour"
305
+ ? setHour
306
+ : segment === "minute"
307
+ ? setMinute
308
+ : setSecond;
309
+
310
+ if (e.key === "Tab") return;
311
+
312
+ e.preventDefault();
313
+
314
+ if (e.key === "ArrowUp") {
315
+ const next = base + 1 > max ? min : base + 1;
316
+ setter(next);
317
+ emit(...getUpdated(segment, next));
318
+ return;
319
+ }
320
+
321
+ if (e.key === "ArrowDown") {
322
+ const next = base - 1 < min ? max : base - 1;
323
+ setter(next);
324
+ emit(...getUpdated(segment, next));
325
+ return;
326
+ }
327
+
328
+ if (e.key === "ArrowRight") {
329
+ moveNext(segment);
330
+ return;
331
+ }
332
+
333
+ if (e.key === "ArrowLeft") {
334
+ movePrev(segment);
335
+ return;
336
+ }
337
+
338
+ if (!/^\d$/.test(e.key)) return;
339
+
340
+ bufRef.current += e.key;
341
+ const attempt = Number(bufRef.current);
342
+
343
+ if (bufRef.current.length >= 2 || attempt * 10 > max) {
344
+ bufRef.current = "";
345
+ commitNumericSegment(segment, String(attempt));
346
+ return;
347
+ }
348
+
349
+ setter(attempt);
350
+ emit(...getUpdated(segment, attempt));
351
+ };
352
+
353
+ const handleAmpmKeyDown = (e: React.KeyboardEvent) => {
354
+ if (e.key === "Tab") return;
355
+
356
+ if (e.key === "ArrowLeft") {
357
+ e.preventDefault();
358
+ movePrev("ampm");
359
+ return;
360
+ }
361
+
362
+ if (
363
+ e.key === "Enter" ||
364
+ e.key === " " ||
365
+ e.key === "ArrowUp" ||
366
+ e.key === "ArrowDown"
367
+ ) {
368
+ e.preventDefault();
369
+ updateAmpm(ampm === "AM" ? "PM" : "AM");
370
+ return;
371
+ }
372
+
373
+ if (e.key.toLowerCase() === "a" || e.key.toLowerCase() === "p") {
374
+ e.preventDefault();
375
+ updateAmpm(e.key.toLowerCase() === "a" ? "AM" : "PM");
376
+ }
377
+ };
378
+
379
+ const isDisabledOrReadOnly = !!(disabled || readOnly);
380
+
381
+ return (
382
+ <Label
383
+ data-color-mode={dataTheme}
384
+ title={title}
385
+ required={required}
386
+ readOnly={isDisabledOrReadOnly}
387
+ hint={hint}
388
+ className={className}
389
+ style={style}
390
+ >
391
+ <View alignItems="center" gap={2}>
392
+ <Icon icon="iSchedule" size={16} iconFill />
393
+
394
+ <View
395
+ alignItems="center"
396
+ gap={2}
397
+ style={{ flex: 1, marginLeft: ".4rem" }}
398
+ >
399
+ {}
400
+ <input
401
+ ref={hourRef}
402
+ type="text"
403
+ inputMode="numeric"
404
+ pattern="[0-9]*"
405
+ autoComplete="off"
406
+ maxLength={2}
407
+ placeholder="--"
408
+ className={clsx(styles.Field, "Subheadline")}
409
+ value={displayHour}
410
+ readOnly={isDisabledOrReadOnly}
411
+ disabled={isDisabledOrReadOnly}
412
+ onFocus={() => {
413
+ bufRef.current = "";
414
+ focusAndSelect("hour");
415
+ }}
416
+ onClick={() => focusAndSelect("hour")}
417
+ onChange={(e) => handleNumericChange(e, "hour")}
418
+ onKeyDown={(e) => handleNumericKeyDown(e, "hour")}
419
+ />
420
+
421
+ <Text type="Subheadline" verticalTrim className={styles.Sep}>
422
+ :
423
+ </Text>
424
+
425
+ {}
426
+ <input
427
+ ref={minuteRef}
428
+ type="text"
429
+ inputMode="numeric"
430
+ pattern="[0-9]*"
431
+ autoComplete="off"
432
+ maxLength={2}
433
+ placeholder="--"
434
+ className={clsx(styles.Field, "Subheadline")}
435
+ value={displayMinute}
436
+ readOnly={isDisabledOrReadOnly}
437
+ disabled={isDisabledOrReadOnly}
438
+ onFocus={() => {
439
+ bufRef.current = "";
440
+ focusAndSelect("minute");
441
+ }}
442
+ onClick={() => focusAndSelect("minute")}
443
+ onChange={(e) => handleNumericChange(e, "minute")}
444
+ onKeyDown={(e) => handleNumericKeyDown(e, "minute")}
445
+ />
446
+
447
+ {showSeconds && (
448
+ <>
449
+ {}
450
+ <Text type="Subheadline" verticalTrim className={styles.Sep}>
451
+ :
452
+ </Text>
453
+ <input
454
+ ref={secondRef}
455
+ type="text"
456
+ inputMode="numeric"
457
+ pattern="[0-9]*"
458
+ autoComplete="off"
459
+ maxLength={2}
460
+ placeholder="--"
461
+ className={clsx(styles.Field, "Subheadline")}
462
+ value={displaySecond}
463
+ readOnly={isDisabledOrReadOnly}
464
+ disabled={isDisabledOrReadOnly}
465
+ onFocus={() => {
466
+ bufRef.current = "";
467
+ focusAndSelect("second");
468
+ }}
469
+ onClick={() => focusAndSelect("second")}
470
+ onChange={(e) => handleNumericChange(e, "second")}
471
+ onKeyDown={(e) => handleNumericKeyDown(e, "second")}
472
+ />
473
+ </>
474
+ )}
475
+
476
+ {use12h && (
477
+ <>
478
+ <Text type="Subheadline" verticalTrim className={styles.Sep}>
479
+ &nbsp;
480
+ </Text>
481
+
482
+ <View style={{ position: "relative" }}>
483
+ <Pressable
484
+ ref={ampmRef}
485
+ className={clsx(
486
+ styles.Field,
487
+ styles.FieldAmpm,
488
+ styles.FieldButton,
489
+ )}
490
+ disabled={isDisabledOrReadOnly}
491
+ onFocus={() => {
492
+ bufRef.current = "";
493
+ }}
494
+ onClick={() => {
495
+ if (isDisabledOrReadOnly) return;
496
+ updateAmpm(ampm === "AM" ? "PM" : "AM");
497
+ }}
498
+ onKeyDown={handleAmpmKeyDown}
499
+ >
500
+ <Text type="Subheadline" verticalTrim>
501
+ {ampm}
502
+ </Text>
503
+ </Pressable>
504
+ </View>
505
+ </>
506
+ )}
507
+ </View>
508
+ </View>
509
+ </Label>
510
+ );
511
+ }
@@ -0,0 +1,22 @@
1
+ import type { LabelProps } from "@/packages/Components/Label/Label.types";
2
+
3
+ export interface TimePickerProps {
4
+ "data-color-mode"?: string;
5
+ className?: string;
6
+ style?: React.CSSProperties;
7
+ title?: string;
8
+ required?: boolean;
9
+ readOnly?: boolean;
10
+ disabled?: boolean;
11
+ hint?: LabelProps["hint"];
12
+
13
+ value?: string;
14
+ defaultValue?: string;
15
+
16
+ showSeconds?: boolean;
17
+ use12h?: boolean;
18
+
19
+ placeholder?: string;
20
+ onChange?: (value: string) => void;
21
+ onHourOverflow?: (params: { input: number; normalized: number }) => void;
22
+ }
@@ -0,0 +1,111 @@
1
+ import Divider from "@/packages/Components/Divider/Divider";
2
+ import Icon from "@/packages/Components/Icon/Icon";
3
+ import type {
4
+ TimelineItemProps,
5
+ TimelineProps,
6
+ } from "@/packages/Components/Timeline/Timeline.types";
7
+ import Pressable from "@/packages/Frameworks/Pressable/Pressable";
8
+ import View from "@/packages/Frameworks/View/View";
9
+
10
+ function TimelineItem({
11
+ item,
12
+ isLast,
13
+ nodePreset,
14
+ nodeBackground,
15
+ nodeColor,
16
+ }: {
17
+ item: TimelineItemProps;
18
+ isLast: boolean;
19
+ nodePreset?: TimelineProps["nodePreset"];
20
+ nodeBackground?: TimelineProps["nodeBackground"];
21
+ nodeColor?: TimelineProps["nodeColor"];
22
+ }) {
23
+ const resolvedNodePreset = item.nodePreset ?? nodePreset;
24
+ const resolvedNodeBackground = item.nodeBackground ?? nodeBackground;
25
+ const resolvedNodeColor = item.nodeColor ?? nodeColor;
26
+
27
+ return (
28
+ <View
29
+ style={{
30
+ position: "relative",
31
+ minWidth: 0,
32
+ paddingBottom: isLast ? 0 : ".4rem",
33
+ }}
34
+ gap={12}
35
+ >
36
+ <View
37
+ justifyContent="center"
38
+ style={{ flexShrink: 0, width: "max-content" }}
39
+ column
40
+ alignItems="center"
41
+ gap={6}
42
+ >
43
+ <View
44
+ style={{ position: "relative", zIndex: 1, flexShrink: 0 }}
45
+ padding={6}
46
+ alignItems="center"
47
+ justifyContent="center"
48
+ radius="Circle"
49
+ themePreset={resolvedNodePreset ?? "UIPrimary"}
50
+ background={resolvedNodeBackground}
51
+ color={resolvedNodeColor}
52
+ >
53
+ {item.node ??
54
+ (item.icon ? (
55
+ <Icon size={20} {...item.icon} />
56
+ ) : (
57
+ <View
58
+ style={{ width: "3.2rem", height: "3.2rem" }}
59
+ radius="Circle"
60
+ />
61
+ ))}
62
+ </View>
63
+ {!isLast && (
64
+ <Divider
65
+ vertical
66
+ style={{
67
+ flex: 1,
68
+ alignSelf: "center",
69
+ }}
70
+ />
71
+ )}
72
+ </View>
73
+
74
+ <Pressable
75
+ style={{
76
+ flex: 1,
77
+ minWidth: 0,
78
+ paddingTop: ".6rem",
79
+ paddingBottom: "1.6rem",
80
+ }}
81
+ column
82
+ {...(item.pressable ?? {})}
83
+ >
84
+ {item.children}
85
+ </Pressable>
86
+ </View>
87
+ );
88
+ }
89
+ export default function Timeline({
90
+ items,
91
+ nodePreset,
92
+ nodeBackground,
93
+ nodeColor,
94
+ className,
95
+ style,
96
+ }: TimelineProps) {
97
+ return (
98
+ <View className={className} fullWidth style={style} column gap={6}>
99
+ {items.map((item, index) => (
100
+ <TimelineItem
101
+ key={item.id ?? index}
102
+ item={item}
103
+ isLast={index === items.length - 1}
104
+ nodePreset={nodePreset}
105
+ nodeBackground={nodeBackground}
106
+ nodeColor={nodeColor}
107
+ />
108
+ ))}
109
+ </View>
110
+ );
111
+ }
@@ -0,0 +1,23 @@
1
+ import type { IconProps } from "@/packages/Components/Icon/Icon.types";
2
+ import type { PressableProps } from "@/packages/Frameworks/Pressable/Pressable.types";
3
+ import type { ThemeSystemProps } from "@/packages/Frameworks/Theme/Theme.types";
4
+
5
+ export interface TimelineItemProps {
6
+ children?: React.ReactNode;
7
+ id?: string | number;
8
+ icon?: IconProps;
9
+ node?: React.ReactNode;
10
+ nodePreset?: ThemeSystemProps["themePreset"];
11
+ nodeBackground?: ThemeSystemProps["background"];
12
+ nodeColor?: ThemeSystemProps["color"];
13
+ pressable?: PressableProps;
14
+ }
15
+
16
+ export interface TimelineProps {
17
+ className?: string;
18
+ style?: React.CSSProperties;
19
+ items: TimelineItemProps[];
20
+ nodePreset?: ThemeSystemProps["themePreset"];
21
+ nodeBackground?: ThemeSystemProps["background"];
22
+ nodeColor?: ThemeSystemProps["color"];
23
+ }