@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,304 @@
1
+ "use client";
2
+
3
+ import { useTimeout } from "@musecat/functionkit";
4
+ import clsx from "clsx";
5
+ import { useRouter } from "next/navigation";
6
+ import type React from "react";
7
+ import { useCallback, useEffect, useRef, useState } from "react";
8
+ import Icon from "@/packages/Components/Icon/Icon";
9
+ import { useNavDrag } from "@/packages/Components/Nav/hooks/useNavDrag";
10
+ import { useNavIndicator } from "@/packages/Components/Nav/hooks/useNavIndicator";
11
+ import styles from "@/packages/Components/Nav/Nav.module.scss";
12
+ import type { NavProps } from "@/packages/Components/Nav/Nav.types";
13
+ import Text from "@/packages/Components/Text/Text";
14
+ import Pressable from "@/packages/Frameworks/Pressable/Pressable";
15
+ import View from "@/packages/Frameworks/View/View";
16
+
17
+ export default function Nav({
18
+ name,
19
+ items,
20
+ className,
21
+ radius,
22
+ padding,
23
+ paddingHorizontal,
24
+ paddingVertical,
25
+ sizeFull,
26
+ radio,
27
+ dragSelection = true,
28
+ dragSelectionCommit = "end",
29
+ value,
30
+ defaultValue,
31
+ onChange,
32
+ titleType,
33
+ titleSize,
34
+ style,
35
+ "data-color-mode": dataTheme,
36
+ }: NavProps) {
37
+ const [internalValue, setInternalValue] = useState<
38
+ string | number | undefined
39
+ >(radio ? defaultValue : undefined);
40
+ const isControlled = value !== undefined;
41
+ const currentCheckedValue = isControlled ? value : internalValue;
42
+ const [dragPreviewValue, setDragPreviewValue] = useState<
43
+ string | number | undefined
44
+ >();
45
+ const displayCheckedValue =
46
+ dragPreviewValue !== undefined ? dragPreviewValue : currentCheckedValue;
47
+
48
+ const itemRefs = useRef<(HTMLElement | null)[]>([]);
49
+ const indicatorRef = useRef<HTMLDivElement>(null);
50
+ const navRef = useRef<HTMLDivElement>(null);
51
+ const [isDraggingSelection, setIsDraggingSelection] = useState(false);
52
+ const [isReleasingSelection, setIsReleasingSelection] = useState(false);
53
+ const [pressed, setPressed] = useState(false);
54
+ const committedSelectedIndex = items.findIndex(
55
+ (item) =>
56
+ item.checked ??
57
+ (item.value !== undefined &&
58
+ currentCheckedValue !== undefined &&
59
+ item.value === currentCheckedValue),
60
+ );
61
+
62
+ const { applyIndicatorStyle, committedIndicatorStyleRef } = useNavIndicator(
63
+ committedSelectedIndex,
64
+ isDraggingSelection,
65
+ isReleasingSelection,
66
+ itemRefs,
67
+ indicatorRef,
68
+ navRef,
69
+ );
70
+
71
+ const dragReleaseTimer = useTimeout(
72
+ () => {
73
+ setDragPreviewValue(undefined);
74
+ setIsReleasingSelection(false);
75
+ applyIndicatorStyle(committedIndicatorStyleRef.current);
76
+ },
77
+ 240,
78
+ { enabled: false },
79
+ );
80
+
81
+ const getDefaultItemPadding = useCallback(
82
+ (item: NavProps["items"][number]) =>
83
+ item.icon && item.title ? ".8rem 1.2rem .8rem 1rem" : ".8rem 1.2rem",
84
+ [],
85
+ );
86
+
87
+ const router = useRouter();
88
+
89
+ const selectValue = useCallback(
90
+ (nextValue: string | number | undefined) => {
91
+ if (nextValue === undefined || nextValue === currentCheckedValue) {
92
+ return;
93
+ }
94
+
95
+ if (!isControlled) {
96
+ setInternalValue(nextValue);
97
+ }
98
+
99
+ onChange?.(nextValue);
100
+
101
+ const item = items.find((i) => i.value === nextValue);
102
+ if (!item) return;
103
+
104
+ const fakeEvent = {
105
+ preventDefault: () => {},
106
+ stopPropagation: () => {},
107
+ } as React.MouseEvent;
108
+
109
+ item.onClick?.(fakeEvent);
110
+
111
+ if (item.href) {
112
+ const href = item.href;
113
+ const isExternal =
114
+ typeof href === "string" &&
115
+ (href.startsWith("//") || /^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(href));
116
+ if (isExternal || item.target === "_blank") {
117
+ window.open(
118
+ typeof href === "string" ? href : (href.pathname ?? ""),
119
+ item.target ?? "_blank",
120
+ item.rel,
121
+ );
122
+ } else {
123
+ router.push(href as string);
124
+ }
125
+ }
126
+ },
127
+ [currentCheckedValue, isControlled, onChange, items, router],
128
+ );
129
+
130
+ const {
131
+ handlePointerDown,
132
+ handlePointerMove,
133
+ stopDragging,
134
+ canDragSelection,
135
+ dragStartFrameRef,
136
+ dragMoveFrameRef,
137
+ } = useNavDrag({
138
+ items,
139
+ itemRefs,
140
+ dragSelection: dragSelection ?? false,
141
+ radio: radio ?? false,
142
+ dragSelectionCommit,
143
+ selectValue,
144
+ setDragPreviewValue,
145
+ applyIndicatorStyle,
146
+ committedIndicatorStyleRef,
147
+ setIsDraggingSelection,
148
+ setIsReleasingSelection,
149
+ setPressed,
150
+ dragReleaseTimer,
151
+ });
152
+
153
+ useEffect(
154
+ () => () => {
155
+ if (dragStartFrameRef.current !== null) {
156
+ cancelAnimationFrame(dragStartFrameRef.current);
157
+ }
158
+
159
+ if (dragMoveFrameRef.current !== null) {
160
+ cancelAnimationFrame(dragMoveFrameRef.current);
161
+ }
162
+
163
+ dragReleaseTimer.clear();
164
+ },
165
+ [dragReleaseTimer, dragStartFrameRef, dragMoveFrameRef],
166
+ );
167
+
168
+ return (
169
+ <View
170
+ ref={navRef}
171
+ data-color-mode={dataTheme}
172
+ className={clsx(
173
+ styles.Nav,
174
+ sizeFull && styles.SizeFull,
175
+ canDragSelection && styles.DragSelection,
176
+ isDraggingSelection && styles.Dragging,
177
+ className,
178
+ )}
179
+ gap={0}
180
+ background="Base6TP6"
181
+ onPointerDown={handlePointerDown}
182
+ onPointerMove={handlePointerMove}
183
+ onPointerUp={stopDragging}
184
+ onPointerCancel={stopDragging}
185
+ onPointerLeave={() => {
186
+ setPressed(false);
187
+ }}
188
+ style={style}
189
+ >
190
+ {}
191
+ <View
192
+ radius={radius ?? "Circle"}
193
+ data-color-mode={dataTheme}
194
+ background="Base1"
195
+ ref={indicatorRef}
196
+ className={clsx(
197
+ styles.ActiveIndicator,
198
+ isDraggingSelection && styles.ActiveIndicatorDragging,
199
+ isReleasingSelection && styles.ActiveIndicatorReleasing,
200
+ pressed && !isDraggingSelection && styles.ActiveIndicatorPressed,
201
+ )}
202
+ />
203
+ {items.map((item, index) => {
204
+ const {
205
+ checked: itemChecked,
206
+ icon: itemIcon,
207
+ title: itemTitle,
208
+ children: itemChildren,
209
+ className: itemClassName,
210
+ onChange: itemOnChange,
211
+ ...restItem
212
+ } = item;
213
+ const isChecked =
214
+ itemChecked ??
215
+ (item.value !== undefined &&
216
+ displayCheckedValue !== undefined &&
217
+ item.value === displayCheckedValue);
218
+ const nextValue = item.value;
219
+ const itemKey =
220
+ nextValue ??
221
+ itemTitle ??
222
+ itemIcon?.icon ??
223
+ itemIcon?.svg ??
224
+ itemIcon?.image;
225
+
226
+ if (!itemTitle && itemIcon) {
227
+ return (
228
+ <Icon
229
+ key={String(itemKey)}
230
+ box
231
+ iconFill={itemIcon.iconFill ?? isChecked}
232
+ boxOptions={{
233
+ padding,
234
+ paddingHorizontal,
235
+ paddingVertical,
236
+ ...itemIcon.boxOptions,
237
+ }}
238
+ className={clsx(styles.Item, isChecked && styles.Checked)}
239
+ pressable={{
240
+ ref: (el) => {
241
+ itemRefs.current[index] = el;
242
+ },
243
+ type: radio ? "radio" : undefined,
244
+ name: name,
245
+ checked: isChecked,
246
+ themeInteractive: false,
247
+ "data-color-mode": dataTheme,
248
+ onChange: (e) => {
249
+ selectValue(nextValue);
250
+ itemOnChange?.(e);
251
+ },
252
+ }}
253
+ {...itemIcon}
254
+ />
255
+ );
256
+ }
257
+
258
+ return (
259
+ <Pressable
260
+ key={String(itemKey)}
261
+ ref={(el) => {
262
+ itemRefs.current[index] = el;
263
+ }}
264
+ className={clsx(
265
+ styles.Item,
266
+ isChecked && styles.Checked,
267
+ itemClassName,
268
+ )}
269
+ type={radio ? "radio" : undefined}
270
+ name={name}
271
+ {...restItem}
272
+ themeInteractive={false}
273
+ data-color-mode={dataTheme}
274
+ checked={isChecked}
275
+ onChange={(e) => {
276
+ selectValue(nextValue);
277
+ itemOnChange?.(e);
278
+ }}
279
+ padding={
280
+ padding !== undefined ? padding : getDefaultItemPadding(item)
281
+ }
282
+ paddingHorizontal={paddingHorizontal}
283
+ paddingVertical={paddingVertical}
284
+ >
285
+ {itemIcon && (
286
+ <Icon
287
+ size={itemIcon.size ?? 22}
288
+ iconFill={itemIcon.iconFill ?? isChecked}
289
+ {...itemIcon}
290
+ pressable={undefined}
291
+ />
292
+ )}
293
+ {itemTitle && (
294
+ <Text type={titleType ?? "Headline"} size={titleSize}>
295
+ {itemTitle}
296
+ </Text>
297
+ )}
298
+ {itemChildren}
299
+ </Pressable>
300
+ );
301
+ })}
302
+ </View>
303
+ );
304
+ }
@@ -0,0 +1,32 @@
1
+ import type { ReactNode } from "react";
2
+ import type { IconProps } from "@/packages/Components/Icon/Icon.types";
3
+ import type { TextProps } from "@/packages/Components/Text/Text.types";
4
+ import type { UIKitSizeValue } from "@/packages/Frameworks/_shared/sizing";
5
+ import type { PressableProps } from "@/packages/Frameworks/Pressable/Pressable.types";
6
+ import type { RadiusProps } from "@/packages/Frameworks/Theme/Radius.types";
7
+
8
+ export interface NavOption extends Omit<PressableProps, "title"> {
9
+ checked?: boolean;
10
+ icon?: IconProps;
11
+ title?: ReactNode;
12
+ }
13
+
14
+ export interface NavProps extends RadiusProps {
15
+ className?: string;
16
+ style?: React.CSSProperties;
17
+ "data-color-mode"?: string;
18
+ name?: string;
19
+ padding?: UIKitSizeValue;
20
+ paddingHorizontal?: UIKitSizeValue;
21
+ paddingVertical?: UIKitSizeValue;
22
+ sizeFull?: boolean;
23
+ radio?: boolean;
24
+ dragSelection?: boolean;
25
+ dragSelectionCommit?: "change" | "end";
26
+ value?: string | number;
27
+ defaultValue?: string | number;
28
+ onChange?: (value: string | number) => void;
29
+ titleType?: TextProps["type"];
30
+ titleSize?: TextProps["size"];
31
+ items: NavOption[];
32
+ }
@@ -0,0 +1,374 @@
1
+ "use client";
2
+
3
+ import { useCallback, useRef } from "react";
4
+ import type { NavProps } from "@/packages/Components/Nav/Nav.types";
5
+
6
+ const DRAG_SELECTION_TOUCH_ACTIVATION_DISTANCE = 10;
7
+ const DRAG_SELECTION_TOUCH_HORIZONTAL_RATIO = 1.75;
8
+
9
+ export type DragSelectionMetric = {
10
+ centerX: number;
11
+ index: number;
12
+ offsetLeft: number;
13
+ width: number;
14
+ value: string | number;
15
+ };
16
+
17
+ export interface UseNavDragProps {
18
+ items: NavProps["items"];
19
+ itemRefs: React.MutableRefObject<(HTMLElement | null)[]>;
20
+ dragSelection: boolean;
21
+ radio: boolean;
22
+ dragSelectionCommit: "change" | "end";
23
+ selectValue: (nextValue: string | number | undefined) => void;
24
+ setDragPreviewValue: (value: string | number | undefined) => void;
25
+ applyIndicatorStyle: (style?: React.CSSProperties) => void;
26
+ committedIndicatorStyleRef: React.MutableRefObject<
27
+ React.CSSProperties | undefined
28
+ >;
29
+ setIsDraggingSelection: (value: boolean) => void;
30
+ setIsReleasingSelection: (value: boolean) => void;
31
+ setPressed: (value: boolean) => void;
32
+ dragReleaseTimer: { clear: () => void; start: (ms: number) => void };
33
+ }
34
+
35
+ export function useNavDrag({
36
+ items,
37
+ itemRefs,
38
+ dragSelection,
39
+ radio,
40
+ dragSelectionCommit,
41
+ selectValue,
42
+ setDragPreviewValue,
43
+ applyIndicatorStyle,
44
+ committedIndicatorStyleRef,
45
+ setIsDraggingSelection,
46
+ setIsReleasingSelection,
47
+ setPressed,
48
+ dragReleaseTimer,
49
+ }: UseNavDragProps) {
50
+ const dragPointerIdRef = useRef<number | null>(null);
51
+ const dragSelectedValueRef = useRef<string | number | undefined>(undefined);
52
+ const dragStartPointRef = useRef<{ x: number; y: number } | null>(null);
53
+ const dragGestureStateRef = useRef<"idle" | "pending" | "active">("idle");
54
+ const dragStartFrameRef = useRef<number | null>(null);
55
+ const dragMoveFrameRef = useRef<number | null>(null);
56
+ const dragLatestClientXRef = useRef<number | null>(null);
57
+ const dragRootLeftRef = useRef(0);
58
+ const dragMetricsRef = useRef<DragSelectionMetric[]>([]);
59
+
60
+ const commitDragPreviewValue = useCallback(
61
+ (nextValue: string | number | undefined) => {
62
+ if (nextValue === undefined) return;
63
+
64
+ if (dragSelectionCommit === "change") {
65
+ selectValue(nextValue);
66
+ return;
67
+ }
68
+
69
+ setDragPreviewValue(nextValue);
70
+ },
71
+ [dragSelectionCommit, selectValue, setDragPreviewValue],
72
+ );
73
+
74
+ const getDragSelectionFromClientX = useCallback(
75
+ (clientX: number): { element?: HTMLElement; value?: string | number } => {
76
+ const dragMetrics = dragMetricsRef.current;
77
+ if (dragMetrics.length > 0) {
78
+ let closestMetric: DragSelectionMetric | undefined;
79
+ let closestDistance = Number.POSITIVE_INFINITY;
80
+
81
+ dragMetrics.forEach((metric) => {
82
+ const distance = Math.abs(clientX - metric.centerX);
83
+
84
+ if (distance < closestDistance) {
85
+ closestDistance = distance;
86
+ closestMetric = metric;
87
+ }
88
+ });
89
+
90
+ if (!closestMetric) {
91
+ return {};
92
+ }
93
+
94
+ return {
95
+ element: itemRefs.current[closestMetric.index] ?? undefined,
96
+ value: closestMetric.value,
97
+ };
98
+ }
99
+
100
+ let closestValue: string | number | undefined;
101
+ let closestEl: HTMLElement | undefined;
102
+ let closestDistance = Number.POSITIVE_INFINITY;
103
+
104
+ itemRefs.current.forEach((el, index) => {
105
+ const value = items[index]?.value;
106
+ if (!el || value === undefined) return;
107
+
108
+ const rect = el.getBoundingClientRect();
109
+ const centerX = rect.left + rect.width / 2;
110
+ const distance = Math.abs(clientX - centerX);
111
+
112
+ if (distance < closestDistance) {
113
+ closestDistance = distance;
114
+ closestValue = value;
115
+ closestEl = el;
116
+ }
117
+ });
118
+
119
+ return { element: closestEl, value: closestValue };
120
+ },
121
+ [items, itemRefs],
122
+ );
123
+
124
+ const getIndicatorStyleForValue = useCallback(
125
+ (
126
+ nextValue: string | number | undefined,
127
+ ): React.CSSProperties | undefined => {
128
+ if (nextValue === undefined) return undefined;
129
+
130
+ const index = items.findIndex((item) => item.value === nextValue);
131
+ const el = index >= 0 ? itemRefs.current[index] : null;
132
+ if (!el) return undefined;
133
+
134
+ return {
135
+ left: el.offsetLeft,
136
+ width: el.offsetWidth,
137
+ opacity: 1,
138
+ };
139
+ },
140
+ [items, itemRefs],
141
+ );
142
+
143
+ const updateDragSelection = useCallback(
144
+ (clientX: number) => {
145
+ const { element, value } = getDragSelectionFromClientX(clientX);
146
+ if (!element || value === undefined) return;
147
+
148
+ const previousValue = dragSelectedValueRef.current;
149
+ dragSelectedValueRef.current = value;
150
+ if (value !== previousValue) {
151
+ commitDragPreviewValue(value);
152
+ }
153
+
154
+ const dragMetrics = dragMetricsRef.current;
155
+ const firstMetric = dragMetrics[0];
156
+ const lastMetric = dragMetrics.at(-1);
157
+ const width = element.offsetWidth;
158
+ const minLeft = firstMetric?.offsetLeft ?? 0;
159
+ const maxLeft =
160
+ lastMetric !== undefined
161
+ ? lastMetric.offsetLeft + lastMetric.width - width
162
+ : minLeft;
163
+ const pointerLeft = clientX - dragRootLeftRef.current - width / 2;
164
+ const left = Math.min(Math.max(pointerLeft, minLeft), maxLeft);
165
+
166
+ applyIndicatorStyle({
167
+ left,
168
+ width,
169
+ opacity: 1,
170
+ });
171
+ },
172
+ [applyIndicatorStyle, commitDragPreviewValue, getDragSelectionFromClientX],
173
+ );
174
+
175
+ const canDragSelection = Boolean(dragSelection && radio);
176
+
177
+ const resetDragGesture = useCallback(() => {
178
+ dragPointerIdRef.current = null;
179
+ dragStartPointRef.current = null;
180
+ dragGestureStateRef.current = "idle";
181
+ dragSelectedValueRef.current = undefined;
182
+ dragLatestClientXRef.current = null;
183
+ dragMetricsRef.current = [];
184
+ }, []);
185
+
186
+ const measureDragSelection = useCallback(
187
+ (rootEl: HTMLElement) => {
188
+ dragRootLeftRef.current = rootEl.getBoundingClientRect().left;
189
+ dragMetricsRef.current = itemRefs.current.flatMap((el, index) => {
190
+ const value = items[index]?.value;
191
+ if (!el || value === undefined) return [];
192
+
193
+ const rect = el.getBoundingClientRect();
194
+ return [
195
+ {
196
+ centerX: rect.left + rect.width / 2,
197
+ index,
198
+ offsetLeft: el.offsetLeft,
199
+ width: el.offsetWidth,
200
+ value,
201
+ },
202
+ ];
203
+ });
204
+ },
205
+ [items, itemRefs],
206
+ );
207
+
208
+ const queueDragSelectionUpdate = useCallback(() => {
209
+ if (dragMoveFrameRef.current !== null) {
210
+ return;
211
+ }
212
+
213
+ dragMoveFrameRef.current = requestAnimationFrame(() => {
214
+ dragMoveFrameRef.current = null;
215
+ const clientX = dragLatestClientXRef.current;
216
+ if (clientX === null) return;
217
+
218
+ updateDragSelection(clientX);
219
+ });
220
+ }, [updateDragSelection]);
221
+
222
+ const beginDragSelection = useCallback(
223
+ (rootEl: HTMLDivElement, pointerId: number, clientX: number) => {
224
+ dragGestureStateRef.current = "active";
225
+ dragLatestClientXRef.current = clientX;
226
+ setDragPreviewValue(undefined);
227
+ setIsDraggingSelection(true);
228
+ measureDragSelection(rootEl);
229
+
230
+ if (!rootEl.hasPointerCapture(pointerId)) {
231
+ rootEl.setPointerCapture(pointerId);
232
+ }
233
+
234
+ dragStartFrameRef.current = requestAnimationFrame(() => {
235
+ updateDragSelection(clientX);
236
+ dragStartFrameRef.current = null;
237
+ });
238
+ },
239
+ [
240
+ measureDragSelection,
241
+ updateDragSelection,
242
+ setDragPreviewValue,
243
+ setIsDraggingSelection,
244
+ ],
245
+ );
246
+
247
+ const handlePointerDown = useCallback(
248
+ (event: React.PointerEvent<HTMLDivElement>) => {
249
+ if (event.button !== 0) return;
250
+
251
+ setPressed(true);
252
+
253
+ if (!canDragSelection) return;
254
+
255
+ dragPointerIdRef.current = event.pointerId;
256
+ dragStartPointRef.current = { x: event.clientX, y: event.clientY };
257
+ dragGestureStateRef.current =
258
+ event.pointerType === "mouse" ? "active" : "pending";
259
+ dragSelectedValueRef.current = undefined;
260
+ dragReleaseTimer.clear();
261
+ setIsReleasingSelection(false);
262
+ if (event.pointerType === "mouse") {
263
+ beginDragSelection(event.currentTarget, event.pointerId, event.clientX);
264
+ event.preventDefault();
265
+ }
266
+ },
267
+ [
268
+ beginDragSelection,
269
+ canDragSelection,
270
+ dragReleaseTimer,
271
+ setIsReleasingSelection,
272
+ setPressed,
273
+ ],
274
+ );
275
+
276
+ const handlePointerMove = useCallback(
277
+ (event: React.PointerEvent<HTMLDivElement>) => {
278
+ if (dragPointerIdRef.current !== event.pointerId) return;
279
+
280
+ if (dragGestureStateRef.current === "pending") {
281
+ const startPoint = dragStartPointRef.current;
282
+ if (!startPoint) return;
283
+
284
+ const deltaX = event.clientX - startPoint.x;
285
+ const deltaY = event.clientY - startPoint.y;
286
+ const absX = Math.abs(deltaX);
287
+ const absY = Math.abs(deltaY);
288
+
289
+ if (Math.max(absX, absY) < DRAG_SELECTION_TOUCH_ACTIVATION_DISTANCE) {
290
+ return;
291
+ }
292
+
293
+ if (absX < absY * DRAG_SELECTION_TOUCH_HORIZONTAL_RATIO) {
294
+ resetDragGesture();
295
+ return;
296
+ }
297
+
298
+ beginDragSelection(event.currentTarget, event.pointerId, event.clientX);
299
+ event.preventDefault();
300
+ return;
301
+ }
302
+
303
+ if (dragGestureStateRef.current !== "active") return;
304
+
305
+ if (dragStartFrameRef.current !== null) {
306
+ cancelAnimationFrame(dragStartFrameRef.current);
307
+ dragStartFrameRef.current = null;
308
+ }
309
+ dragLatestClientXRef.current = event.clientX;
310
+ queueDragSelectionUpdate();
311
+ event.preventDefault();
312
+ },
313
+ [beginDragSelection, queueDragSelectionUpdate, resetDragGesture],
314
+ );
315
+
316
+ const stopDragging = useCallback(
317
+ (event: React.PointerEvent<HTMLDivElement>) => {
318
+ setPressed(false);
319
+ if (dragPointerIdRef.current !== event.pointerId) return;
320
+
321
+ if (dragGestureStateRef.current !== "active") {
322
+ resetDragGesture();
323
+ return;
324
+ }
325
+
326
+ if (event.currentTarget.hasPointerCapture(event.pointerId)) {
327
+ event.currentTarget.releasePointerCapture(event.pointerId);
328
+ }
329
+ if (dragStartFrameRef.current !== null) {
330
+ cancelAnimationFrame(dragStartFrameRef.current);
331
+ dragStartFrameRef.current = null;
332
+ }
333
+ if (dragMoveFrameRef.current !== null) {
334
+ cancelAnimationFrame(dragMoveFrameRef.current);
335
+ dragMoveFrameRef.current = null;
336
+ }
337
+ if (dragSelectionCommit === "end") {
338
+ selectValue(dragSelectedValueRef.current);
339
+ }
340
+ const finalIndicatorStyle = getIndicatorStyleForValue(
341
+ dragSelectedValueRef.current,
342
+ );
343
+ resetDragGesture();
344
+ setIsDraggingSelection(false);
345
+ setIsReleasingSelection(true);
346
+ applyIndicatorStyle(
347
+ finalIndicatorStyle ?? committedIndicatorStyleRef.current,
348
+ );
349
+
350
+ dragReleaseTimer.start(240);
351
+ },
352
+ [
353
+ applyIndicatorStyle,
354
+ dragSelectionCommit,
355
+ getIndicatorStyleForValue,
356
+ resetDragGesture,
357
+ selectValue,
358
+ setIsDraggingSelection,
359
+ setIsReleasingSelection,
360
+ setPressed,
361
+ dragReleaseTimer,
362
+ committedIndicatorStyleRef,
363
+ ],
364
+ );
365
+
366
+ return {
367
+ handlePointerDown,
368
+ handlePointerMove,
369
+ stopDragging,
370
+ canDragSelection,
371
+ dragStartFrameRef,
372
+ dragMoveFrameRef,
373
+ };
374
+ }