@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.
- package/LICENSE +21 -0
- package/README.md +149 -62
- package/dist/api-pagination-CJ0vR_w6.d.mts +34 -0
- package/dist/api-pagination-DBTE0yk4.mjs +190 -0
- package/dist/chunk-DQk6qfdC.mjs +18 -0
- package/dist/client/calendar.d.mts +105 -0
- package/dist/client/calendar.mjs +202 -0
- package/dist/client/core.d.mts +1614 -0
- package/dist/client/core.mjs +2779 -0
- package/dist/client/error.d.mts +125 -0
- package/dist/client/error.mjs +166 -0
- package/dist/client/hooks.d.mts +162 -0
- package/dist/client/hooks.mjs +447 -0
- package/dist/client/table.d.mts +84 -0
- package/dist/client/table.mjs +373 -0
- package/dist/client/theme.d.mts +6 -0
- package/dist/client/theme.mjs +65 -0
- package/dist/command.d.mts +134 -0
- package/dist/command.mjs +132 -0
- package/dist/compact.d.mts +359 -0
- package/dist/compact.mjs +892 -0
- package/dist/dashboard.d.mts +778 -0
- package/dist/dashboard.mjs +1617 -0
- package/dist/filter-utils-DqMmy_v-.mjs +72 -0
- package/dist/filter-utils-IZ0GtuPo.d.mts +40 -0
- package/dist/forms.d.mts +1549 -0
- package/dist/forms.mjs +3740 -0
- package/dist/index.d.mts +296 -0
- package/dist/index.mjs +432 -0
- package/dist/layouts.d.mts +215 -0
- package/dist/layouts.mjs +460 -0
- package/dist/search-context-DR7DBs7S.mjs +19 -0
- package/dist/search.d.mts +254 -0
- package/dist/search.mjs +523 -0
- package/dist/sheet-wrapper-CWNCvYMD.mjs +211 -0
- package/dist/use-base-search-BGgWnWaF.d.mts +35 -0
- package/dist/use-debounce-xmZucz5e.mjs +53 -0
- package/dist/use-keyboard-shortcut-Bl6YM5Q7.mjs +82 -0
- package/dist/use-keyboard-shortcut-_mRCh3QO.d.mts +24 -0
- package/dist/use-media-query-BnVNIKT4.mjs +17 -0
- package/dist/use-mobile-BX3SQVo2.mjs +20 -0
- package/dist/use-scroll-detection-CsgsQYvy.mjs +43 -0
- package/dist/utils-CDue7cEt.d.mts +6 -0
- package/dist/utils-DQ5SCVoW.mjs +10 -0
- package/package.json +85 -45
- package/styles.css +2 -2
- package/dist/chunk-GUHK2DTW.js +0 -15
- package/dist/chunk-GUHK2DTW.js.map +0 -1
- package/dist/chunk-H3NFL3GJ.js +0 -57
- package/dist/chunk-H3NFL3GJ.js.map +0 -1
- package/dist/chunk-J2YRTQE4.js +0 -293
- package/dist/chunk-J2YRTQE4.js.map +0 -1
- package/dist/compact.d.ts +0 -217
- package/dist/compact.js +0 -986
- package/dist/compact.js.map +0 -1
- package/dist/dashboard.d.ts +0 -386
- package/dist/dashboard.js +0 -1032
- package/dist/dashboard.js.map +0 -1
- package/dist/index.d.ts +0 -2141
- package/dist/index.js +0 -6460
- package/dist/index.js.map +0 -1
- package/dist/layout.d.ts +0 -25
- package/dist/layout.js +0 -4
- package/dist/layout.js.map +0 -1
- package/dist/search.d.ts +0 -172
- package/dist/search.js +0 -341
- package/dist/search.js.map +0 -1
- package/dist/use-base-search-AS5Z3SAy.d.ts +0 -64
- 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 };
|