@classytic/fluid 0.2.1 → 0.3.2

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 (69) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +149 -62
  3. package/dist/api-pagination-CJ0vR_w6.d.mts +34 -0
  4. package/dist/api-pagination-DBTE0yk4.mjs +190 -0
  5. package/dist/chunk-DQk6qfdC.mjs +18 -0
  6. package/dist/client/calendar.d.mts +105 -0
  7. package/dist/client/calendar.mjs +202 -0
  8. package/dist/client/core.d.mts +1614 -0
  9. package/dist/client/core.mjs +2779 -0
  10. package/dist/client/error.d.mts +125 -0
  11. package/dist/client/error.mjs +166 -0
  12. package/dist/client/hooks.d.mts +162 -0
  13. package/dist/client/hooks.mjs +447 -0
  14. package/dist/client/table.d.mts +84 -0
  15. package/dist/client/table.mjs +373 -0
  16. package/dist/client/theme.d.mts +6 -0
  17. package/dist/client/theme.mjs +65 -0
  18. package/dist/command.d.mts +134 -0
  19. package/dist/command.mjs +132 -0
  20. package/dist/compact.d.mts +359 -0
  21. package/dist/compact.mjs +892 -0
  22. package/dist/dashboard.d.mts +778 -0
  23. package/dist/dashboard.mjs +1617 -0
  24. package/dist/filter-utils-DqMmy_v-.mjs +72 -0
  25. package/dist/filter-utils-IZ0GtuPo.d.mts +40 -0
  26. package/dist/forms.d.mts +1549 -0
  27. package/dist/forms.mjs +3740 -0
  28. package/dist/index.d.mts +296 -0
  29. package/dist/index.mjs +432 -0
  30. package/dist/layouts.d.mts +215 -0
  31. package/dist/layouts.mjs +460 -0
  32. package/dist/search-context-DR7DBs7S.mjs +19 -0
  33. package/dist/search.d.mts +254 -0
  34. package/dist/search.mjs +523 -0
  35. package/dist/sheet-wrapper-CWNCvYMD.mjs +211 -0
  36. package/dist/use-base-search-BGgWnWaF.d.mts +35 -0
  37. package/dist/use-debounce-xmZucz5e.mjs +53 -0
  38. package/dist/use-keyboard-shortcut-Bl6YM5Q7.mjs +82 -0
  39. package/dist/use-keyboard-shortcut-_mRCh3QO.d.mts +24 -0
  40. package/dist/use-media-query-BnVNIKT4.mjs +17 -0
  41. package/dist/use-mobile-BX3SQVo2.mjs +20 -0
  42. package/dist/use-scroll-detection-CsgsQYvy.mjs +43 -0
  43. package/dist/utils-CDue7cEt.d.mts +6 -0
  44. package/dist/utils-DQ5SCVoW.mjs +10 -0
  45. package/package.json +85 -45
  46. package/styles.css +2 -2
  47. package/dist/chunk-GUHK2DTW.js +0 -15
  48. package/dist/chunk-GUHK2DTW.js.map +0 -1
  49. package/dist/chunk-H3NFL3GJ.js +0 -57
  50. package/dist/chunk-H3NFL3GJ.js.map +0 -1
  51. package/dist/chunk-J2YRTQE4.js +0 -293
  52. package/dist/chunk-J2YRTQE4.js.map +0 -1
  53. package/dist/compact.d.ts +0 -217
  54. package/dist/compact.js +0 -986
  55. package/dist/compact.js.map +0 -1
  56. package/dist/dashboard.d.ts +0 -386
  57. package/dist/dashboard.js +0 -1032
  58. package/dist/dashboard.js.map +0 -1
  59. package/dist/index.d.ts +0 -2141
  60. package/dist/index.js +0 -6460
  61. package/dist/index.js.map +0 -1
  62. package/dist/layout.d.ts +0 -25
  63. package/dist/layout.js +0 -4
  64. package/dist/layout.js.map +0 -1
  65. package/dist/search.d.ts +0 -172
  66. package/dist/search.js +0 -341
  67. package/dist/search.js.map +0 -1
  68. package/dist/use-base-search-AS5Z3SAy.d.ts +0 -64
  69. package/dist/utils-Cbsgs0XP.d.ts +0 -5
@@ -0,0 +1,211 @@
1
+ import { t as cn } from "./utils-DQ5SCVoW.mjs";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { memo, useCallback, useMemo } from "react";
4
+ import { LoaderIcon } from "lucide-react";
5
+ import { Button } from "@/components/ui/button";
6
+ import { Sheet, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle } from "@/components/ui/sheet";
7
+
8
+ //#region src/components/client-submit-button.tsx
9
+ function ClientSubmitButton({ children, disabled, loading = false, loadingText, className, variant, size, form, ...props }) {
10
+ const isDisabled = loading || disabled;
11
+ const content = loading && loadingText ? loadingText : children;
12
+ return /* @__PURE__ */ jsxs(Button, {
13
+ type: "submit",
14
+ form,
15
+ "aria-disabled": isDisabled,
16
+ "aria-busy": loading,
17
+ className: cn("relative", className),
18
+ disabled: isDisabled,
19
+ variant,
20
+ size,
21
+ ...props,
22
+ children: [
23
+ content,
24
+ loading && /* @__PURE__ */ jsx("span", {
25
+ className: "animate-spin absolute right-4",
26
+ children: /* @__PURE__ */ jsx(LoaderIcon, {})
27
+ }),
28
+ /* @__PURE__ */ jsx("span", {
29
+ "aria-live": "polite",
30
+ className: "sr-only",
31
+ role: "status",
32
+ children: loading ? "Loading" : "Submit form"
33
+ })
34
+ ]
35
+ });
36
+ }
37
+
38
+ //#endregion
39
+ //#region src/components/sheet-wrapper.tsx
40
+ const SIZE_CLASSES = {
41
+ right: {
42
+ sm: "data-[side=right]:sm:max-w-md",
43
+ default: "data-[side=right]:w-full data-[side=right]:sm:max-w-md data-[side=right]:md:max-w-lg",
44
+ lg: "data-[side=right]:w-full data-[side=right]:sm:max-w-lg data-[side=right]:md:max-w-2xl data-[side=right]:lg:max-w-4xl",
45
+ xl: "data-[side=right]:w-full data-[side=right]:sm:max-w-2xl data-[side=right]:md:max-w-4xl data-[side=right]:lg:max-w-5xl",
46
+ full: "data-[side=right]:w-full data-[side=right]:max-w-full data-[side=right]:sm:max-w-full",
47
+ mobile: "data-[side=right]:w-[85%] data-[side=right]:sm:max-w-sm",
48
+ "mobile-nav": "data-[side=right]:w-[300px] data-[side=right]:sm:w-[350px] data-[side=right]:sm:max-w-none"
49
+ },
50
+ left: {
51
+ sm: "data-[side=left]:sm:max-w-md",
52
+ default: "data-[side=left]:w-full data-[side=left]:sm:max-w-md data-[side=left]:md:max-w-lg",
53
+ lg: "data-[side=left]:w-full data-[side=left]:sm:max-w-lg data-[side=left]:md:max-w-2xl data-[side=left]:lg:max-w-4xl",
54
+ xl: "data-[side=left]:w-full data-[side=left]:sm:max-w-2xl data-[side=left]:md:max-w-4xl data-[side=left]:lg:max-w-5xl",
55
+ full: "data-[side=left]:w-full data-[side=left]:max-w-full data-[side=left]:sm:max-w-full",
56
+ mobile: "data-[side=left]:w-[85%] data-[side=left]:sm:max-w-sm",
57
+ "mobile-nav": "data-[side=left]:w-[300px] data-[side=left]:sm:w-[350px] data-[side=left]:sm:max-w-none"
58
+ },
59
+ vertical: {
60
+ sm: "sm:max-w-md",
61
+ default: "w-full sm:max-w-md md:max-w-lg",
62
+ lg: "w-full sm:max-w-lg md:max-w-2xl lg:max-w-4xl",
63
+ xl: "w-full sm:max-w-2xl md:max-w-4xl lg:max-w-5xl",
64
+ full: "w-full max-w-full",
65
+ mobile: "w-[85%] max-w-sm",
66
+ "mobile-nav": "w-[300px] sm:w-[350px]"
67
+ }
68
+ };
69
+ function getSizeClasses(size, side) {
70
+ return SIZE_CLASSES[side === "left" || side === "right" ? side : "vertical"][size];
71
+ }
72
+ const getPadding = (size, type = "default") => {
73
+ const isFullSize = size === "full";
74
+ if (type === "header" || type === "footer") return isFullSize ? "px-6 lg:px-8" : "px-4";
75
+ return isFullSize ? "p-6 lg:p-8" : "p-4";
76
+ };
77
+ const SheetWrapper = memo(function SheetWrapper({ open, onOpenChange, title, description, children, footer, header, side = "right", size = "default", modal = true, className, headerClassName, contentClassName, footerClassName, innerClassName, hideHeader = false, hideTitle = false, hideDescription = false, hideCloseButton = false, disableContentPadding = false }) {
78
+ const computedClasses = useMemo(() => ({
79
+ header: cn("border-b pb-4 pt-6", getPadding(size, "header"), headerClassName),
80
+ inner: cn("flex-1 overflow-y-auto", !disableContentPadding && getPadding(size), innerClassName),
81
+ footer: cn("border-t bg-muted/30 pt-4 pb-6 mt-auto", getPadding(size, "footer"), footerClassName)
82
+ }), [
83
+ size,
84
+ headerClassName,
85
+ innerClassName,
86
+ footerClassName,
87
+ disableContentPadding
88
+ ]);
89
+ const shouldHideTitle = !!header || hideTitle;
90
+ const shouldHideDescription = !!header || hideDescription;
91
+ return /* @__PURE__ */ jsx(Sheet, {
92
+ open,
93
+ onOpenChange,
94
+ modal,
95
+ children: /* @__PURE__ */ jsxs(SheetContent, {
96
+ side,
97
+ showCloseButton: !hideCloseButton,
98
+ className: cn(getSizeClasses(size, side), "flex flex-col p-0", contentClassName, className),
99
+ children: [
100
+ !hideHeader && /* @__PURE__ */ jsxs(SheetHeader, {
101
+ className: computedClasses.header,
102
+ children: [
103
+ /* @__PURE__ */ jsx(SheetTitle, {
104
+ className: shouldHideTitle ? "sr-only" : "",
105
+ children: title || "Sheet"
106
+ }),
107
+ description && /* @__PURE__ */ jsx(SheetDescription, {
108
+ className: shouldHideDescription ? "sr-only" : "",
109
+ children: description
110
+ }),
111
+ header
112
+ ]
113
+ }),
114
+ /* @__PURE__ */ jsx("div", {
115
+ className: computedClasses.inner,
116
+ children
117
+ }),
118
+ footer && /* @__PURE__ */ jsx(SheetFooter, {
119
+ className: computedClasses.footer,
120
+ children: footer
121
+ })
122
+ ]
123
+ })
124
+ });
125
+ });
126
+ const FormSheet = memo(function FormSheet({ open, onOpenChange, title, description, children, onSubmit, onCancel, submitLabel = "Submit", cancelLabel = "Cancel", submitDisabled = false, submitLoading = false, formId, size = "lg", ...props }) {
127
+ const handleCancel = useCallback(() => {
128
+ onCancel?.();
129
+ onOpenChange?.(false);
130
+ }, [onCancel, onOpenChange]);
131
+ return /* @__PURE__ */ jsx(SheetWrapper, {
132
+ open,
133
+ onOpenChange,
134
+ title,
135
+ description,
136
+ size,
137
+ footer: useMemo(() => /* @__PURE__ */ jsxs("div", {
138
+ className: "flex flex-col sm:flex-row gap-2 w-full",
139
+ children: [/* @__PURE__ */ jsx(Button, {
140
+ type: "button",
141
+ variant: "outline",
142
+ className: "flex-1",
143
+ onClick: handleCancel,
144
+ disabled: submitDisabled || submitLoading,
145
+ children: cancelLabel
146
+ }), /* @__PURE__ */ jsx(ClientSubmitButton, {
147
+ form: formId,
148
+ className: "flex-1",
149
+ disabled: submitDisabled,
150
+ loading: submitLoading,
151
+ loadingText: "Saving...",
152
+ children: submitLabel
153
+ })]
154
+ }), [
155
+ cancelLabel,
156
+ submitLabel,
157
+ submitDisabled,
158
+ submitLoading,
159
+ formId,
160
+ handleCancel
161
+ ]),
162
+ ...props,
163
+ children
164
+ });
165
+ });
166
+ const ConfirmSheet = memo(function ConfirmSheet({ open, onOpenChange, title = "Confirm Action", description, children, onConfirm, onCancel, confirmLabel = "Confirm", cancelLabel = "Cancel", confirmVariant = "default", confirmDisabled = false, confirmLoading = false, size = "sm", ...props }) {
167
+ const handleConfirm = useCallback(() => {
168
+ onConfirm?.();
169
+ }, [onConfirm]);
170
+ const handleCancel = useCallback(() => {
171
+ onCancel?.();
172
+ onOpenChange?.(false);
173
+ }, [onCancel, onOpenChange]);
174
+ return /* @__PURE__ */ jsx(SheetWrapper, {
175
+ open,
176
+ onOpenChange,
177
+ title,
178
+ description,
179
+ size,
180
+ footer: useMemo(() => /* @__PURE__ */ jsxs("div", {
181
+ className: "flex gap-2 w-full",
182
+ children: [/* @__PURE__ */ jsx(Button, {
183
+ type: "button",
184
+ variant: "outline",
185
+ className: "flex-1",
186
+ onClick: handleCancel,
187
+ children: cancelLabel
188
+ }), /* @__PURE__ */ jsx(Button, {
189
+ type: "button",
190
+ variant: confirmVariant,
191
+ className: "flex-1",
192
+ onClick: handleConfirm,
193
+ disabled: confirmDisabled || confirmLoading,
194
+ children: confirmLoading ? "Loading..." : confirmLabel
195
+ })]
196
+ }), [
197
+ cancelLabel,
198
+ confirmLabel,
199
+ confirmVariant,
200
+ confirmDisabled,
201
+ confirmLoading,
202
+ handleConfirm,
203
+ handleCancel
204
+ ]),
205
+ ...props,
206
+ children
207
+ });
208
+ });
209
+
210
+ //#endregion
211
+ export { ClientSubmitButton as i, FormSheet as n, SheetWrapper as r, ConfirmSheet as t };
@@ -0,0 +1,35 @@
1
+ import { t as FilterConfig } from "./filter-utils-IZ0GtuPo.mjs";
2
+
3
+ //#region src/hooks/use-base-search.d.ts
4
+ interface UseBaseSearchConfig {
5
+ basePath: string;
6
+ searchFields?: Record<string, string>;
7
+ filterFields?: Record<string, FilterConfig>;
8
+ defaultSearchType?: string;
9
+ }
10
+ interface UseBaseSearchReturn {
11
+ searchType: string;
12
+ setSearchType: (type: string) => void;
13
+ searchValue: string;
14
+ setSearchValue: (value: string) => void;
15
+ filters: Record<string, unknown>;
16
+ setFilters: React.Dispatch<React.SetStateAction<Record<string, unknown>>>;
17
+ updateFilter: (key: string, value: unknown) => void;
18
+ handleSearch: () => void;
19
+ clearSearch: () => void;
20
+ clearSearchValue: () => void;
21
+ clearFilters: () => void;
22
+ removeFilter: (key: string) => void;
23
+ getSearchParams: () => Record<string, string>;
24
+ filterFields: Record<string, FilterConfig>;
25
+ hasActiveSearch: boolean;
26
+ hasActiveFilters: boolean;
27
+ }
28
+ /**
29
+ * Base search hook that provides common search functionality
30
+ * Can be extended by specific search hooks for different entities
31
+ * Supports bracket syntax: field[operator]=value
32
+ */
33
+ declare function useBaseSearch(config: UseBaseSearchConfig): UseBaseSearchReturn;
34
+ //#endregion
35
+ export { UseBaseSearchReturn as n, useBaseSearch as r, UseBaseSearchConfig as t };
@@ -0,0 +1,53 @@
1
+ import { useEffect, useRef, useState } from "react";
2
+
3
+ //#region src/hooks/use-debounce.ts
4
+ /**
5
+ * useDebounce — Returns a debounced version of the input value.
6
+ *
7
+ * @example
8
+ * ```tsx
9
+ * const [search, setSearch] = useState("");
10
+ * const debouncedSearch = useDebounce(search, 300);
11
+ *
12
+ * useEffect(() => {
13
+ * fetchResults(debouncedSearch);
14
+ * }, [debouncedSearch]);
15
+ * ```
16
+ */
17
+ function useDebounce(value, delay = 300) {
18
+ const [debounced, setDebounced] = useState(value);
19
+ useEffect(() => {
20
+ const timer = setTimeout(() => setDebounced(value), delay);
21
+ return () => clearTimeout(timer);
22
+ }, [value, delay]);
23
+ return debounced;
24
+ }
25
+ /**
26
+ * useDebouncedCallback — Returns a debounced version of a callback function.
27
+ *
28
+ * @example
29
+ * ```tsx
30
+ * const debouncedSave = useDebouncedCallback((value: string) => {
31
+ * saveToApi(value);
32
+ * }, 500);
33
+ *
34
+ * <Input onChange={(e) => debouncedSave(e.target.value)} />
35
+ * ```
36
+ */
37
+ function useDebouncedCallback(callback, delay = 300) {
38
+ const timerRef = useRef(void 0);
39
+ const callbackRef = useRef(callback);
40
+ callbackRef.current = callback;
41
+ useEffect(() => {
42
+ return () => {
43
+ if (timerRef.current) clearTimeout(timerRef.current);
44
+ };
45
+ }, []);
46
+ return (...args) => {
47
+ if (timerRef.current) clearTimeout(timerRef.current);
48
+ timerRef.current = setTimeout(() => callbackRef.current(...args), delay);
49
+ };
50
+ }
51
+
52
+ //#endregion
53
+ export { useDebouncedCallback as n, useDebounce as t };
@@ -0,0 +1,82 @@
1
+ import { useEffect, useMemo, useRef } from "react";
2
+
3
+ //#region src/hooks/use-keyboard-shortcut.ts
4
+ function parseShortcut(shortcut) {
5
+ const parts = shortcut.toLowerCase().split("+");
6
+ const result = {
7
+ key: "",
8
+ ctrl: false,
9
+ shift: false,
10
+ alt: false,
11
+ meta: false,
12
+ mod: false
13
+ };
14
+ for (const part of parts) switch (part) {
15
+ case "mod":
16
+ result.mod = true;
17
+ break;
18
+ case "ctrl":
19
+ result.ctrl = true;
20
+ break;
21
+ case "shift":
22
+ result.shift = true;
23
+ break;
24
+ case "alt":
25
+ result.alt = true;
26
+ break;
27
+ case "meta":
28
+ result.meta = true;
29
+ break;
30
+ default: result.key = part;
31
+ }
32
+ return result;
33
+ }
34
+ /**
35
+ * useKeyboardShortcut — Register global keyboard shortcuts.
36
+ *
37
+ * `"mod"` maps to Cmd on macOS, Ctrl on Windows/Linux.
38
+ *
39
+ * @example
40
+ * ```tsx
41
+ * useKeyboardShortcut("mod+k", () => setOpen(true));
42
+ * useKeyboardShortcut("mod+shift+p", () => openPalette());
43
+ * useKeyboardShortcut("Escape", () => close(), { disableInInputs: false });
44
+ * ```
45
+ */
46
+ function useKeyboardShortcut(shortcut, callback, options = {}) {
47
+ const { enabled = true, preventDefault = true, disableInInputs = true } = options;
48
+ const callbackRef = useRef(callback);
49
+ callbackRef.current = callback;
50
+ const parsed = useMemo(() => parseShortcut(shortcut), [shortcut]);
51
+ useEffect(() => {
52
+ if (!enabled) return;
53
+ const isMac = typeof navigator !== "undefined" && /Mac|iPod|iPhone|iPad/.test(navigator.userAgent);
54
+ function handler(event) {
55
+ if (disableInInputs) {
56
+ const target = event.target;
57
+ if (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.isContentEditable) return;
58
+ }
59
+ if (event.key.toLowerCase() !== parsed.key && event.code.toLowerCase() !== parsed.key) return;
60
+ if (parsed.mod) {
61
+ if (isMac ? !event.metaKey : !event.ctrlKey) return;
62
+ } else {
63
+ if (parsed.ctrl !== event.ctrlKey) return;
64
+ if (parsed.meta !== event.metaKey) return;
65
+ }
66
+ if (parsed.shift !== event.shiftKey) return;
67
+ if (parsed.alt !== event.altKey) return;
68
+ if (preventDefault) event.preventDefault();
69
+ callbackRef.current(event);
70
+ }
71
+ document.addEventListener("keydown", handler);
72
+ return () => document.removeEventListener("keydown", handler);
73
+ }, [
74
+ enabled,
75
+ parsed,
76
+ preventDefault,
77
+ disableInInputs
78
+ ]);
79
+ }
80
+
81
+ //#endregion
82
+ export { useKeyboardShortcut as t };
@@ -0,0 +1,24 @@
1
+ //#region src/hooks/use-keyboard-shortcut.d.ts
2
+ interface UseKeyboardShortcutOptions {
3
+ /** Whether the shortcut is enabled (default: true) */
4
+ enabled?: boolean;
5
+ /** Prevent default browser behavior (default: true) */
6
+ preventDefault?: boolean;
7
+ /** Disable when input/textarea/contentEditable is focused (default: true) */
8
+ disableInInputs?: boolean;
9
+ }
10
+ /**
11
+ * useKeyboardShortcut — Register global keyboard shortcuts.
12
+ *
13
+ * `"mod"` maps to Cmd on macOS, Ctrl on Windows/Linux.
14
+ *
15
+ * @example
16
+ * ```tsx
17
+ * useKeyboardShortcut("mod+k", () => setOpen(true));
18
+ * useKeyboardShortcut("mod+shift+p", () => openPalette());
19
+ * useKeyboardShortcut("Escape", () => close(), { disableInInputs: false });
20
+ * ```
21
+ */
22
+ declare function useKeyboardShortcut(shortcut: string, callback: (event: KeyboardEvent) => void, options?: UseKeyboardShortcutOptions): void;
23
+ //#endregion
24
+ export { useKeyboardShortcut as n, UseKeyboardShortcutOptions as t };
@@ -0,0 +1,17 @@
1
+ import { useSyncExternalStore } from "react";
2
+
3
+ //#region src/hooks/use-media-query.ts
4
+ function useMediaQuery(query, defaultValue = false) {
5
+ const getSnapshot = () => window.matchMedia(query).matches;
6
+ const getServerSnapshot = () => defaultValue;
7
+ const subscribe = (onStoreChange) => {
8
+ const media = window.matchMedia(query);
9
+ const handler = () => onStoreChange();
10
+ media.addEventListener("change", handler);
11
+ return () => media.removeEventListener("change", handler);
12
+ };
13
+ return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
14
+ }
15
+
16
+ //#endregion
17
+ export { useMediaQuery as t };
@@ -0,0 +1,20 @@
1
+ import * as React$1 from "react";
2
+
3
+ //#region src/hooks/use-mobile.ts
4
+ const MOBILE_BREAKPOINT = 768;
5
+ function useIsMobile() {
6
+ const [isMobile, setIsMobile] = React$1.useState(void 0);
7
+ React$1.useEffect(() => {
8
+ const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
9
+ const onChange = () => {
10
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
11
+ };
12
+ mql.addEventListener("change", onChange);
13
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
14
+ return () => mql.removeEventListener("change", onChange);
15
+ }, []);
16
+ return !!isMobile;
17
+ }
18
+
19
+ //#endregion
20
+ export { useIsMobile as t };
@@ -0,0 +1,43 @@
1
+ import { useCallback, useEffect, useRef, useState } from "react";
2
+
3
+ //#region src/hooks/use-scroll-detection.ts
4
+ const useScrollDetection = (ref, delay = 100) => {
5
+ const [scrollState, setScrollState] = useState({
6
+ canScrollLeft: false,
7
+ canScrollRight: false,
8
+ isScrollable: false
9
+ });
10
+ const timeoutRef = useRef(void 0);
11
+ useEffect(() => {
12
+ return () => {
13
+ if (timeoutRef.current) clearTimeout(timeoutRef.current);
14
+ };
15
+ }, []);
16
+ const checkScroll = useCallback(() => {
17
+ const scrollContainer = ref.current?.querySelector("[data-slot=\"scroll-area-viewport\"]");
18
+ if (!scrollContainer) return;
19
+ const { scrollLeft, scrollWidth, clientWidth } = scrollContainer;
20
+ const isScrollable = scrollWidth > clientWidth;
21
+ const canScrollLeft = scrollLeft > 5;
22
+ const canScrollRight = scrollLeft < scrollWidth - clientWidth - 5;
23
+ setScrollState((prev) => {
24
+ if (prev.canScrollLeft !== canScrollLeft || prev.canScrollRight !== canScrollRight || prev.isScrollable !== isScrollable) return {
25
+ canScrollLeft,
26
+ canScrollRight,
27
+ isScrollable
28
+ };
29
+ return prev;
30
+ });
31
+ }, [ref]);
32
+ const debouncedCheckScroll = useCallback(() => {
33
+ if (timeoutRef.current) clearTimeout(timeoutRef.current);
34
+ timeoutRef.current = setTimeout(checkScroll, delay);
35
+ }, [checkScroll, delay]);
36
+ return {
37
+ ...scrollState,
38
+ checkScroll: debouncedCheckScroll
39
+ };
40
+ };
41
+
42
+ //#endregion
43
+ export { useScrollDetection as t };
@@ -0,0 +1,6 @@
1
+ import { ClassValue } from "clsx";
2
+
3
+ //#region src/utils.d.ts
4
+ declare function cn(...inputs: ClassValue[]): string;
5
+ //#endregion
6
+ export { cn as t };
@@ -0,0 +1,10 @@
1
+ import { clsx } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ //#region src/utils.ts
5
+ function cn(...inputs) {
6
+ return twMerge(clsx(inputs));
7
+ }
8
+
9
+ //#endregion
10
+ export { cn as t };