@cleen/ui-core 0.1.0 → 0.1.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/README.md +40 -0
- package/dist/index.d.ts +258 -0
- package/dist/index.js +1 -0
- package/dist/styles.css +15 -0
- package/package.json +42 -42
- package/tailwind-entry.css +86 -81
- package/tailwind.config.js +10 -10
- package/tailwind.preset.js +30 -30
- package/src/hooks/useAnimateNumber.ts +0 -56
- package/src/hooks/useControlled.ts +0 -40
- package/src/hooks/useDebounce.ts +0 -17
- package/src/hooks/useDisclosure.ts +0 -33
- package/src/hooks/useForm.ts +0 -38
- package/src/hooks/useOutsideClick.ts +0 -42
- package/src/hooks/usePaginationState.ts +0 -39
- package/src/hooks/usePositionClose.ts +0 -69
- package/src/hooks/useValidation.ts +0 -33
- package/src/hooks/useWatchResize.ts +0 -52
- package/src/index.ts +0 -21
- package/src/store/colors.ts +0 -98
- package/src/types/position.ts +0 -9
- package/src/types/styles.ts +0 -24
- package/src/types/utils.ts +0 -6
- package/src/utils/audio.ts +0 -69
- package/src/utils/cn.ts +0 -13
- package/src/utils/colors.ts +0 -159
- package/src/utils/images.ts +0 -42
- package/src/utils/object.ts +0 -86
- package/src/utils/position.ts +0 -140
- package/src/utils/string.ts +0 -27
- package/styles/react-day-styles.css +0 -457
- package/tsconfig.json +0 -27
- package/tsup.config.ts +0 -10
package/tailwind-entry.css
CHANGED
|
@@ -1,81 +1,86 @@
|
|
|
1
|
-
@import 'react-toastify/dist/ReactToastify.css';
|
|
2
|
-
@import './styles/react-day-styles.css';
|
|
3
|
-
|
|
4
|
-
@tailwind base;
|
|
5
|
-
@tailwind components;
|
|
6
|
-
@tailwind utilities;
|
|
7
|
-
|
|
8
|
-
/* Hide scrollbar for Chrome, Safari and Opera */
|
|
9
|
-
.cleen-no-scrollbar::-webkit-scrollbar {
|
|
10
|
-
display: none;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/* Hide scrollbar for IE, Edge and Firefox */
|
|
14
|
-
.cleen-no-scrollbar {
|
|
15
|
-
-ms-overflow-style: none; /* IE and Edge */
|
|
16
|
-
scrollbar-width: none; /* Firefox */
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
:root {
|
|
20
|
-
--cleen-white: 255, 255, 255;
|
|
21
|
-
--cleen-black: 0, 0, 0;
|
|
22
|
-
--cleen-gray: 38, 38, 38;
|
|
23
|
-
--cleen-pink: 193, 21, 116;
|
|
24
|
-
--cleen-purple: 89, 37, 220;
|
|
25
|
-
--cleen-indigo: 53, 56, 205;
|
|
26
|
-
--cleen-blue: 23, 92, 211;
|
|
27
|
-
--cleen-light-gray: 200, 200, 200;
|
|
28
|
-
|
|
29
|
-
--cleen-primary: 0, 133, 211;
|
|
30
|
-
--cleen-success: 6, 118, 71;
|
|
31
|
-
--cleen-warning: 181, 71, 8;
|
|
32
|
-
--cleen-error: 180, 35, 24;
|
|
33
|
-
|
|
34
|
-
--cleen-brand: 68, 129, 193;
|
|
35
|
-
--cleen-sidebar: 249, 250, 251;
|
|
36
|
-
--cleen-background: 255, 255, 255;
|
|
37
|
-
--cleen-accent: 65, 70, 81;
|
|
38
|
-
|
|
39
|
-
--toastify-color-light: rgb(var(--cleen-background));
|
|
40
|
-
--toastify-text-color-light: rgba(var(--cleen-gray), 0.6);
|
|
41
|
-
--toastify-toast-shadow: 0px 4px 12px rgba(var(--cleen-black), 0.1);
|
|
42
|
-
|
|
43
|
-
--toastify-color-info: rgb(var(--cleen-primary));
|
|
44
|
-
--toastify-color-success: rgb(var(--cleen-success));
|
|
45
|
-
--toastify-color-warning: rgb(var(--cleen-warning));
|
|
46
|
-
--toastify-color-error: rgb(var(--cleen-error));
|
|
47
|
-
|
|
48
|
-
.Toastify__close-button {
|
|
49
|
-
color: rgb(var(--cleen-black));
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
.rdp-root {
|
|
53
|
-
--rdp-accent-color: rgb(var(--cleen-primary));
|
|
54
|
-
--rdp-accent-background-color: rgb(var(--cleen-background));
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
--cleen-
|
|
65
|
-
--cleen-
|
|
66
|
-
--cleen-
|
|
67
|
-
--cleen-
|
|
68
|
-
--cleen-
|
|
69
|
-
--cleen-
|
|
70
|
-
--cleen-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
--cleen-
|
|
74
|
-
--cleen-
|
|
75
|
-
--cleen-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
--cleen-
|
|
79
|
-
--cleen-
|
|
80
|
-
--cleen-
|
|
81
|
-
|
|
1
|
+
@import 'react-toastify/dist/ReactToastify.css';
|
|
2
|
+
@import './styles/react-day-styles.css';
|
|
3
|
+
|
|
4
|
+
@tailwind base;
|
|
5
|
+
@tailwind components;
|
|
6
|
+
@tailwind utilities;
|
|
7
|
+
|
|
8
|
+
/* Hide scrollbar for Chrome, Safari and Opera */
|
|
9
|
+
.cleen-no-scrollbar::-webkit-scrollbar {
|
|
10
|
+
display: none;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/* Hide scrollbar for IE, Edge and Firefox */
|
|
14
|
+
.cleen-no-scrollbar {
|
|
15
|
+
-ms-overflow-style: none; /* IE and Edge */
|
|
16
|
+
scrollbar-width: none; /* Firefox */
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
:root {
|
|
20
|
+
--cleen-white: 255, 255, 255;
|
|
21
|
+
--cleen-black: 0, 0, 0;
|
|
22
|
+
--cleen-gray: 38, 38, 38;
|
|
23
|
+
--cleen-pink: 193, 21, 116;
|
|
24
|
+
--cleen-purple: 89, 37, 220;
|
|
25
|
+
--cleen-indigo: 53, 56, 205;
|
|
26
|
+
--cleen-blue: 23, 92, 211;
|
|
27
|
+
--cleen-light-gray: 200, 200, 200;
|
|
28
|
+
|
|
29
|
+
--cleen-primary: 0, 133, 211;
|
|
30
|
+
--cleen-success: 6, 118, 71;
|
|
31
|
+
--cleen-warning: 181, 71, 8;
|
|
32
|
+
--cleen-error: 180, 35, 24;
|
|
33
|
+
|
|
34
|
+
--cleen-brand: 68, 129, 193;
|
|
35
|
+
--cleen-sidebar: 249, 250, 251;
|
|
36
|
+
--cleen-background: 255, 255, 255;
|
|
37
|
+
--cleen-accent: 65, 70, 81;
|
|
38
|
+
|
|
39
|
+
--toastify-color-light: rgb(var(--cleen-background));
|
|
40
|
+
--toastify-text-color-light: rgba(var(--cleen-gray), 0.6);
|
|
41
|
+
--toastify-toast-shadow: 0px 4px 12px rgba(var(--cleen-black), 0.1);
|
|
42
|
+
|
|
43
|
+
--toastify-color-info: rgb(var(--cleen-primary));
|
|
44
|
+
--toastify-color-success: rgb(var(--cleen-success));
|
|
45
|
+
--toastify-color-warning: rgb(var(--cleen-warning));
|
|
46
|
+
--toastify-color-error: rgb(var(--cleen-error));
|
|
47
|
+
|
|
48
|
+
.Toastify__close-button {
|
|
49
|
+
color: rgb(var(--cleen-black));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.rdp-root {
|
|
53
|
+
--rdp-accent-color: rgb(var(--cleen-primary));
|
|
54
|
+
--rdp-accent-background-color: rgb(var(--cleen-background));
|
|
55
|
+
--rdp-range_middle-color: rgb(var(--cleen-white));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.cleen-waveform ::part(region-handle) {
|
|
59
|
+
border-color: rgb(var(--cleen-black)) !important;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.dark {
|
|
64
|
+
--cleen-white: 3, 7, 18;
|
|
65
|
+
--cleen-black: 243, 244, 246;
|
|
66
|
+
--cleen-gray: 209, 213, 219;
|
|
67
|
+
--cleen-pink: 236, 72, 153;
|
|
68
|
+
--cleen-purple: 168, 85, 247;
|
|
69
|
+
--cleen-indigo: 99, 102, 241;
|
|
70
|
+
--cleen-blue: 59, 130, 246;
|
|
71
|
+
--cleen-light-gray: 100, 100, 100;
|
|
72
|
+
|
|
73
|
+
--cleen-primary: 0, 157, 248;
|
|
74
|
+
--cleen-success: 34, 197, 94;
|
|
75
|
+
--cleen-warning: 251, 191, 36;
|
|
76
|
+
--cleen-error: 239, 68, 68;
|
|
77
|
+
|
|
78
|
+
--cleen-brand: 0, 122, 204;
|
|
79
|
+
--cleen-sidebar: 17, 24, 39;
|
|
80
|
+
--cleen-background: 3, 7, 18;
|
|
81
|
+
--cleen-accent: 255, 255, 255;
|
|
82
|
+
|
|
83
|
+
.rdp-root {
|
|
84
|
+
--rdp-range_middle-color: rgb(var(--cleen-black));
|
|
85
|
+
}
|
|
86
|
+
}
|
package/tailwind.config.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import preset from './tailwind.preset.js';
|
|
2
|
-
|
|
3
|
-
/** @type {import('tailwindcss').Config} */
|
|
4
|
-
export default {
|
|
5
|
-
presets: [preset],
|
|
6
|
-
content: [
|
|
7
|
-
'../cleen-ui-free/src/**/*.{ts,tsx,js,jsx}',
|
|
8
|
-
'../cleen-ui-pro/src/**/*.{ts,tsx,js,jsx}',
|
|
9
|
-
],
|
|
10
|
-
};
|
|
1
|
+
import preset from './tailwind.preset.js';
|
|
2
|
+
|
|
3
|
+
/** @type {import('tailwindcss').Config} */
|
|
4
|
+
export default {
|
|
5
|
+
presets: [preset],
|
|
6
|
+
content: [
|
|
7
|
+
'../cleen-ui-free/src/**/*.{ts,tsx,js,jsx}',
|
|
8
|
+
'../cleen-ui-pro/src/**/*.{ts,tsx,js,jsx}',
|
|
9
|
+
],
|
|
10
|
+
};
|
package/tailwind.preset.js
CHANGED
|
@@ -1,30 +1,30 @@
|
|
|
1
|
-
/** @type {import('tailwindcss').Config} */
|
|
2
|
-
export default {
|
|
3
|
-
important: '.cleen',
|
|
4
|
-
prefix: 'cleen-',
|
|
5
|
-
darkMode: ['class', '[class*="dark"]'],
|
|
6
|
-
theme: {
|
|
7
|
-
extend: {
|
|
8
|
-
colors: {
|
|
9
|
-
white: 'rgba(var(--cleen-white))',
|
|
10
|
-
black: 'rgba(var(--cleen-black))',
|
|
11
|
-
gray: 'rgba(var(--cleen-gray))',
|
|
12
|
-
pink: 'rgba(var(--cleen-pink))',
|
|
13
|
-
purple: 'rgba(var(--cleen-purple))',
|
|
14
|
-
indigo: 'rgba(var(--cleen-indigo))',
|
|
15
|
-
blue: 'rgba(var(--cleen-blue))',
|
|
16
|
-
|
|
17
|
-
primary: 'rgba(var(--cleen-primary))',
|
|
18
|
-
success: 'rgba(var(--cleen-success))',
|
|
19
|
-
warning: 'rgba(var(--cleen-warning))',
|
|
20
|
-
error: 'rgba(var(--cleen-error))',
|
|
21
|
-
|
|
22
|
-
brand: 'rgba(var(--cleen-brand))',
|
|
23
|
-
sidebar: 'rgba(var(--cleen-sidebar))',
|
|
24
|
-
background: 'rgba(var(--cleen-background))',
|
|
25
|
-
accent: 'rgba(var(--cleen-accent))',
|
|
26
|
-
},
|
|
27
|
-
},
|
|
28
|
-
},
|
|
29
|
-
plugins: [],
|
|
30
|
-
};
|
|
1
|
+
/** @type {import('tailwindcss').Config} */
|
|
2
|
+
export default {
|
|
3
|
+
important: '.cleen',
|
|
4
|
+
prefix: 'cleen-',
|
|
5
|
+
darkMode: ['class', '[class*="dark"]'],
|
|
6
|
+
theme: {
|
|
7
|
+
extend: {
|
|
8
|
+
colors: {
|
|
9
|
+
white: 'rgba(var(--cleen-white))',
|
|
10
|
+
black: 'rgba(var(--cleen-black))',
|
|
11
|
+
gray: 'rgba(var(--cleen-gray))',
|
|
12
|
+
pink: 'rgba(var(--cleen-pink))',
|
|
13
|
+
purple: 'rgba(var(--cleen-purple))',
|
|
14
|
+
indigo: 'rgba(var(--cleen-indigo))',
|
|
15
|
+
blue: 'rgba(var(--cleen-blue))',
|
|
16
|
+
|
|
17
|
+
primary: 'rgba(var(--cleen-primary))',
|
|
18
|
+
success: 'rgba(var(--cleen-success))',
|
|
19
|
+
warning: 'rgba(var(--cleen-warning))',
|
|
20
|
+
error: 'rgba(var(--cleen-error))',
|
|
21
|
+
|
|
22
|
+
brand: 'rgba(var(--cleen-brand))',
|
|
23
|
+
sidebar: 'rgba(var(--cleen-sidebar))',
|
|
24
|
+
background: 'rgba(var(--cleen-background))',
|
|
25
|
+
accent: 'rgba(var(--cleen-accent))',
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
plugins: [],
|
|
30
|
+
};
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { useEffect, useState } from 'react';
|
|
2
|
-
|
|
3
|
-
interface UseAnimateNumberProps {
|
|
4
|
-
targetNumber: number;
|
|
5
|
-
defaultNumber?: number;
|
|
6
|
-
duration?: number;
|
|
7
|
-
disabled?: boolean;
|
|
8
|
-
easeOut?: boolean;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
// Easing function for smooth deceleration
|
|
12
|
-
const easeOutQuad = (progress: number): number => {
|
|
13
|
-
return 1 - (1 - progress) * (1 - progress);
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
export const useAnimateNumber = ({
|
|
17
|
-
targetNumber,
|
|
18
|
-
defaultNumber = 0,
|
|
19
|
-
duration = 1000,
|
|
20
|
-
disabled = false,
|
|
21
|
-
easeOut = true,
|
|
22
|
-
}: UseAnimateNumberProps): number => {
|
|
23
|
-
const [animatedNumber, setAnimatedNumber] = useState(defaultNumber);
|
|
24
|
-
|
|
25
|
-
useEffect(() => {
|
|
26
|
-
if (disabled) {
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
let start: number | null = null;
|
|
31
|
-
|
|
32
|
-
const animate = (timestamp: number) => {
|
|
33
|
-
if (!start) start = timestamp;
|
|
34
|
-
const progressTime = timestamp - start;
|
|
35
|
-
let progress = Math.min(progressTime / duration, 1);
|
|
36
|
-
|
|
37
|
-
if (easeOut) {
|
|
38
|
-
progress = easeOutQuad(progress);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const progressValue = progress * targetNumber;
|
|
42
|
-
setAnimatedNumber(progressValue);
|
|
43
|
-
if (progressValue < targetNumber) {
|
|
44
|
-
requestAnimationFrame(animate);
|
|
45
|
-
}
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
requestAnimationFrame(animate);
|
|
49
|
-
}, [targetNumber, duration, disabled, easeOut]);
|
|
50
|
-
|
|
51
|
-
if (disabled) {
|
|
52
|
-
return targetNumber;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
return animatedNumber;
|
|
56
|
-
};
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import { useEffect, useState } from 'react';
|
|
2
|
-
|
|
3
|
-
interface UseControlledProps<T> {
|
|
4
|
-
value?: T;
|
|
5
|
-
defaultValue?: T;
|
|
6
|
-
onChange?: (value: T) => void;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export const useControlled = <T>({
|
|
10
|
-
value,
|
|
11
|
-
defaultValue,
|
|
12
|
-
onChange,
|
|
13
|
-
}: UseControlledProps<T>) => {
|
|
14
|
-
const isControlled = value !== undefined;
|
|
15
|
-
const [internalValue, setInternalValue] = useState<T | undefined>(
|
|
16
|
-
defaultValue
|
|
17
|
-
);
|
|
18
|
-
|
|
19
|
-
const currentValue = isControlled ? value : internalValue;
|
|
20
|
-
|
|
21
|
-
const handleChange = (newValue: T) => {
|
|
22
|
-
if (!isControlled) {
|
|
23
|
-
setInternalValue(newValue);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
onChange?.(newValue);
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
useEffect(() => {
|
|
30
|
-
if (!isControlled) {
|
|
31
|
-
setInternalValue(value ?? defaultValue ?? undefined);
|
|
32
|
-
}
|
|
33
|
-
}, [value, defaultValue, isControlled]);
|
|
34
|
-
|
|
35
|
-
return {
|
|
36
|
-
value: currentValue,
|
|
37
|
-
isControlled,
|
|
38
|
-
handleChange,
|
|
39
|
-
};
|
|
40
|
-
};
|
package/src/hooks/useDebounce.ts
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { useEffect, useState } from 'react';
|
|
2
|
-
|
|
3
|
-
export const useDebounce = <T>(value: T, delay: number): T => {
|
|
4
|
-
const [debouncedValue, setDebouncedValue] = useState<T>(value);
|
|
5
|
-
|
|
6
|
-
useEffect(() => {
|
|
7
|
-
const handler = setTimeout(() => {
|
|
8
|
-
setDebouncedValue(value);
|
|
9
|
-
}, delay);
|
|
10
|
-
|
|
11
|
-
return () => {
|
|
12
|
-
clearTimeout(handler);
|
|
13
|
-
};
|
|
14
|
-
}, [value, delay]);
|
|
15
|
-
|
|
16
|
-
return debouncedValue;
|
|
17
|
-
};
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { useState } from 'react';
|
|
2
|
-
|
|
3
|
-
interface UseDisclosureProps {
|
|
4
|
-
value?: boolean;
|
|
5
|
-
setValue?: (value: boolean) => void;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export const useDisclosure = (props?: UseDisclosureProps) => {
|
|
9
|
-
const [isOpenInternal, setIsOpenInternal] = useState(false);
|
|
10
|
-
const isOpen = props?.value !== undefined ? props.value : isOpenInternal;
|
|
11
|
-
|
|
12
|
-
const open = () => {
|
|
13
|
-
setIsOpenInternal(true);
|
|
14
|
-
props?.setValue?.(true);
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
const close = () => {
|
|
18
|
-
setIsOpenInternal(false);
|
|
19
|
-
props?.setValue?.(false);
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
const toggle = () => {
|
|
23
|
-
setIsOpenInternal(prev => !prev);
|
|
24
|
-
props?.setValue?.(!isOpen);
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
return {
|
|
28
|
-
isOpen,
|
|
29
|
-
open,
|
|
30
|
-
close,
|
|
31
|
-
toggle,
|
|
32
|
-
};
|
|
33
|
-
};
|
package/src/hooks/useForm.ts
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import { useEffect, useMemo, useState } from 'react';
|
|
2
|
-
|
|
3
|
-
interface UseFormProps<T> {
|
|
4
|
-
defaultValue: T | null;
|
|
5
|
-
resetOnDefaultValueChange?: boolean;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export const useForm = <T extends Record<string, unknown>>({
|
|
9
|
-
defaultValue,
|
|
10
|
-
resetOnDefaultValueChange = true,
|
|
11
|
-
}: UseFormProps<T>) => {
|
|
12
|
-
const [form, setForm] = useState<T | null>(defaultValue);
|
|
13
|
-
|
|
14
|
-
const reset = () => setForm(defaultValue);
|
|
15
|
-
|
|
16
|
-
const setField = (field: keyof T, value: T[keyof T]) => {
|
|
17
|
-
setForm(prev => ({ ...prev, [field]: value }) as T);
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
const isDirty = useMemo(
|
|
21
|
-
() => JSON.stringify(form) !== JSON.stringify(defaultValue),
|
|
22
|
-
[form, defaultValue]
|
|
23
|
-
);
|
|
24
|
-
|
|
25
|
-
useEffect(() => {
|
|
26
|
-
if (defaultValue && resetOnDefaultValueChange) {
|
|
27
|
-
setForm(defaultValue);
|
|
28
|
-
}
|
|
29
|
-
}, [defaultValue, resetOnDefaultValueChange]);
|
|
30
|
-
|
|
31
|
-
return {
|
|
32
|
-
form,
|
|
33
|
-
isDirty,
|
|
34
|
-
setForm,
|
|
35
|
-
setField,
|
|
36
|
-
reset,
|
|
37
|
-
};
|
|
38
|
-
};
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { useEffect, type RefObject } from 'react';
|
|
2
|
-
|
|
3
|
-
interface UseOutsideClickProps {
|
|
4
|
-
refs: RefObject<HTMLElement | null>[];
|
|
5
|
-
handler: (event: MouseEvent | TouchEvent) => void;
|
|
6
|
-
enabled?: boolean;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export function useOutsideClick({
|
|
10
|
-
refs,
|
|
11
|
-
handler,
|
|
12
|
-
enabled = true,
|
|
13
|
-
}: UseOutsideClickProps) {
|
|
14
|
-
useEffect(() => {
|
|
15
|
-
if (!enabled) return;
|
|
16
|
-
|
|
17
|
-
const listener = (event: MouseEvent | TouchEvent) => {
|
|
18
|
-
// Check if click is inside any of the referenced elements
|
|
19
|
-
const clickedInside = refs.some(ref => {
|
|
20
|
-
const el = ref.current;
|
|
21
|
-
if (!el) return false;
|
|
22
|
-
return el.contains(event.target as Node);
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
// If click is inside at least one element — ignore
|
|
26
|
-
if (clickedInside) return;
|
|
27
|
-
|
|
28
|
-
// Defer handler to next tick to allow all listeners to evaluate
|
|
29
|
-
// before any DOM mutations occur (prevents conflicts when multiple
|
|
30
|
-
// components use this hook simultaneously)
|
|
31
|
-
setTimeout(() => handler(event), 0);
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
document.addEventListener('mousedown', listener);
|
|
35
|
-
document.addEventListener('touchstart', listener);
|
|
36
|
-
|
|
37
|
-
return () => {
|
|
38
|
-
document.removeEventListener('mousedown', listener);
|
|
39
|
-
document.removeEventListener('touchstart', listener);
|
|
40
|
-
};
|
|
41
|
-
}, [refs, handler, enabled]);
|
|
42
|
-
}
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { useState } from 'react';
|
|
2
|
-
|
|
3
|
-
interface PaginationOptions {
|
|
4
|
-
initialPage?: number;
|
|
5
|
-
initialPageSize?: number;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
interface PaginationState {
|
|
9
|
-
page: number;
|
|
10
|
-
setPage: (page: number) => void;
|
|
11
|
-
pageSize: number;
|
|
12
|
-
setPageSize: (size: number) => void;
|
|
13
|
-
handleNextPage: (page: number) => void;
|
|
14
|
-
handlePreviousPage: (page: number) => void;
|
|
15
|
-
handlePageChange: (page: number) => void;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function usePaginationState(
|
|
19
|
-
options: PaginationOptions = {}
|
|
20
|
-
): PaginationState {
|
|
21
|
-
const { initialPage = 1, initialPageSize = 10 } = options;
|
|
22
|
-
|
|
23
|
-
const [page, setPage] = useState<number>(initialPage);
|
|
24
|
-
const [pageSize, setPageSize] = useState<number>(initialPageSize);
|
|
25
|
-
|
|
26
|
-
const handleNextPage = (nextPage: number) => setPage(nextPage);
|
|
27
|
-
const handlePreviousPage = (prevPage: number) => setPage(prevPage);
|
|
28
|
-
const handlePageChange = (newPage: number) => setPage(newPage);
|
|
29
|
-
|
|
30
|
-
return {
|
|
31
|
-
page,
|
|
32
|
-
setPage,
|
|
33
|
-
pageSize,
|
|
34
|
-
setPageSize,
|
|
35
|
-
handleNextPage,
|
|
36
|
-
handlePreviousPage,
|
|
37
|
-
handlePageChange,
|
|
38
|
-
};
|
|
39
|
-
}
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import type { Position } from '@/types/position';
|
|
2
|
-
import {
|
|
3
|
-
calculateOptimalPosition,
|
|
4
|
-
calculatePositionValues,
|
|
5
|
-
} from '@/utils/position';
|
|
6
|
-
import type { RefObject } from 'react';
|
|
7
|
-
import { useEffect, useMemo, useState } from 'react';
|
|
8
|
-
|
|
9
|
-
interface UsePositionCloseProps {
|
|
10
|
-
triggerRef: RefObject<HTMLElement | null>;
|
|
11
|
-
targetRef: RefObject<HTMLElement | null>;
|
|
12
|
-
position?: Position;
|
|
13
|
-
offset?: number;
|
|
14
|
-
isOpen: boolean;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Hook to calculate fixed positioning for overlays relative to trigger elements.
|
|
19
|
-
* Handles viewport-aware position optimization and click-outside detection.
|
|
20
|
-
*/
|
|
21
|
-
export const usePositionClose = ({
|
|
22
|
-
triggerRef,
|
|
23
|
-
targetRef,
|
|
24
|
-
position = 'bottom-left',
|
|
25
|
-
offset = 8,
|
|
26
|
-
isOpen,
|
|
27
|
-
}: UsePositionCloseProps) => {
|
|
28
|
-
const [isMounted, setIsMounted] = useState(false);
|
|
29
|
-
|
|
30
|
-
// Calculate optimal position based on viewport constraints
|
|
31
|
-
const { positionStyles, optimalPosition } = useMemo(() => {
|
|
32
|
-
if (isMounted && targetRef.current && triggerRef.current) {
|
|
33
|
-
const overlayRect = targetRef.current.getBoundingClientRect();
|
|
34
|
-
const triggerRect = triggerRef.current.getBoundingClientRect();
|
|
35
|
-
|
|
36
|
-
// Find the best position to avoid viewport overflow
|
|
37
|
-
const optimalPosition = calculateOptimalPosition(
|
|
38
|
-
overlayRect,
|
|
39
|
-
triggerRect,
|
|
40
|
-
position
|
|
41
|
-
);
|
|
42
|
-
|
|
43
|
-
const coords = calculatePositionValues(
|
|
44
|
-
overlayRect,
|
|
45
|
-
triggerRect,
|
|
46
|
-
optimalPosition,
|
|
47
|
-
offset
|
|
48
|
-
);
|
|
49
|
-
|
|
50
|
-
return { positionStyles: coords, optimalPosition };
|
|
51
|
-
}
|
|
52
|
-
return { positionStyles: { top: 0, left: 0 }, optimal: position };
|
|
53
|
-
}, [triggerRef, targetRef, position, offset, isMounted]);
|
|
54
|
-
|
|
55
|
-
// Track mounting state for smooth positioning
|
|
56
|
-
useEffect(() => {
|
|
57
|
-
if (isOpen) {
|
|
58
|
-
setIsMounted(true);
|
|
59
|
-
} else {
|
|
60
|
-
setIsMounted(false);
|
|
61
|
-
}
|
|
62
|
-
}, [isOpen]);
|
|
63
|
-
|
|
64
|
-
return {
|
|
65
|
-
positionStyles,
|
|
66
|
-
optimalPosition,
|
|
67
|
-
isMounted,
|
|
68
|
-
};
|
|
69
|
-
};
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { useCallback, useState } from 'react';
|
|
2
|
-
|
|
3
|
-
export const useValidation = <T>(defaultValue: Partial<T>) => {
|
|
4
|
-
const [errors, setErrors] = useState<Partial<T>>(defaultValue);
|
|
5
|
-
|
|
6
|
-
const setError = useCallback((field: keyof T, value: Partial<T>[keyof T]) => {
|
|
7
|
-
setErrors(prev => {
|
|
8
|
-
prev[field] = value;
|
|
9
|
-
|
|
10
|
-
return { ...prev };
|
|
11
|
-
});
|
|
12
|
-
}, []);
|
|
13
|
-
|
|
14
|
-
const clearErrors = useCallback(() => {
|
|
15
|
-
setErrors(defaultValue);
|
|
16
|
-
}, [defaultValue]);
|
|
17
|
-
|
|
18
|
-
const clearError = useCallback((field: keyof T) => {
|
|
19
|
-
setErrors(prev => {
|
|
20
|
-
delete prev[field];
|
|
21
|
-
|
|
22
|
-
return { ...prev };
|
|
23
|
-
});
|
|
24
|
-
}, []);
|
|
25
|
-
|
|
26
|
-
return {
|
|
27
|
-
errors,
|
|
28
|
-
setError,
|
|
29
|
-
setErrors,
|
|
30
|
-
clearError,
|
|
31
|
-
clearErrors,
|
|
32
|
-
};
|
|
33
|
-
};
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import { useEffect, type RefObject } from 'react';
|
|
2
|
-
|
|
3
|
-
interface UseWidthDynamicResizeParams<T> {
|
|
4
|
-
ref: RefObject<T>;
|
|
5
|
-
skip?: boolean;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export const useWidthDynamicResize = <T extends HTMLElement | null>(
|
|
9
|
-
{ ref, skip }: UseWidthDynamicResizeParams<T>,
|
|
10
|
-
deps: unknown[]
|
|
11
|
-
) => {
|
|
12
|
-
useEffect(() => {
|
|
13
|
-
const node = ref.current;
|
|
14
|
-
|
|
15
|
-
if (!node || skip) {
|
|
16
|
-
return;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
// Capture current width before content changes
|
|
20
|
-
const currentWidth = node.offsetWidth;
|
|
21
|
-
|
|
22
|
-
// Set explicit width to enable smooth transitions
|
|
23
|
-
node.style.width = `${currentWidth}px`;
|
|
24
|
-
|
|
25
|
-
// Use requestAnimationFrame to allow DOM to update with new content
|
|
26
|
-
requestAnimationFrame(() => {
|
|
27
|
-
// Measure the natural width with new content
|
|
28
|
-
node.style.width = 'auto';
|
|
29
|
-
const newWidth = node.offsetWidth;
|
|
30
|
-
|
|
31
|
-
// Set back to old width momentarily
|
|
32
|
-
node.style.width = `${currentWidth}px`;
|
|
33
|
-
|
|
34
|
-
// Force reflow
|
|
35
|
-
void node.offsetHeight;
|
|
36
|
-
|
|
37
|
-
// Transition to new width
|
|
38
|
-
node.style.width = `${newWidth}px`;
|
|
39
|
-
|
|
40
|
-
// After transition, set to auto for flexibility
|
|
41
|
-
const handleTransitionEnd = () => {
|
|
42
|
-
if (node) {
|
|
43
|
-
node.style.width = 'auto';
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
node?.removeEventListener('transitionend', handleTransitionEnd);
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
node.addEventListener('transitionend', handleTransitionEnd);
|
|
50
|
-
});
|
|
51
|
-
}, deps);
|
|
52
|
-
};
|