@cleen/ui-core 0.1.0
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/package.json +42 -0
- package/src/hooks/useAnimateNumber.ts +56 -0
- package/src/hooks/useControlled.ts +40 -0
- package/src/hooks/useDebounce.ts +17 -0
- package/src/hooks/useDisclosure.ts +33 -0
- package/src/hooks/useForm.ts +38 -0
- package/src/hooks/useOutsideClick.ts +42 -0
- package/src/hooks/usePaginationState.ts +39 -0
- package/src/hooks/usePositionClose.ts +69 -0
- package/src/hooks/useValidation.ts +33 -0
- package/src/hooks/useWatchResize.ts +52 -0
- package/src/index.ts +21 -0
- package/src/store/colors.ts +98 -0
- package/src/types/position.ts +9 -0
- package/src/types/styles.ts +24 -0
- package/src/types/utils.ts +6 -0
- package/src/utils/audio.ts +69 -0
- package/src/utils/cn.ts +13 -0
- package/src/utils/colors.ts +159 -0
- package/src/utils/images.ts +42 -0
- package/src/utils/object.ts +86 -0
- package/src/utils/position.ts +140 -0
- package/src/utils/string.ts +27 -0
- package/styles/react-day-styles.css +457 -0
- package/tailwind-entry.css +81 -0
- package/tailwind.config.js +10 -0
- package/tailwind.preset.js +30 -0
- package/tsconfig.json +27 -0
- package/tsup.config.ts +10 -0
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cleen/ui-core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts"
|
|
13
|
+
},
|
|
14
|
+
"./tailwind-preset": "./tailwind.preset.js",
|
|
15
|
+
"./tailwind-entry.css": "./tailwind-entry.css",
|
|
16
|
+
"./styles.css": "./dist/styles.css"
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"dev": "concurrently \"tsup --watch --onSuccess 'tsc'\" \"tailwindcss -w -c ./tailwind.config.js -i ./tailwind-entry.css -o ./dist/styles.css\"",
|
|
20
|
+
"build": "tsup && tailwindcss -m -c ./tailwind.config.js -i ./tailwind-entry.css -o ./dist/styles.css",
|
|
21
|
+
"typecheck": "tsc --noEmit",
|
|
22
|
+
"lint": "eslint \"src/**/*.{ts,tsx}\"",
|
|
23
|
+
"format": "prettier --write \"src/**/*.{ts,tsx}\""
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"clsx": "^2.1.1",
|
|
27
|
+
"color2k": "^2.0.3",
|
|
28
|
+
"tailwind-merge": "^2.6.0",
|
|
29
|
+
"zustand": "^5.0.8"
|
|
30
|
+
},
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"react": "^18.3.1",
|
|
33
|
+
"react-dom": "^18.3.1"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/react": "^19.1.10",
|
|
37
|
+
"concurrently": "^9.2.1",
|
|
38
|
+
"tailwindcss": "^3.4.17",
|
|
39
|
+
"tsup": "^8.5.1",
|
|
40
|
+
"typescript": "~5.8.3"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
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
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export * from './hooks/useAnimateNumber';
|
|
2
|
+
export * from './hooks/useControlled';
|
|
3
|
+
export * from './hooks/useDebounce';
|
|
4
|
+
export * from './hooks/useDisclosure';
|
|
5
|
+
export * from './hooks/useForm';
|
|
6
|
+
export * from './hooks/useOutsideClick';
|
|
7
|
+
export * from './hooks/usePaginationState';
|
|
8
|
+
export * from './hooks/usePositionClose';
|
|
9
|
+
export * from './hooks/useValidation';
|
|
10
|
+
export * from './hooks/useWatchResize';
|
|
11
|
+
export * from './store/colors';
|
|
12
|
+
export * from './types/position';
|
|
13
|
+
export * from './types/styles';
|
|
14
|
+
export * from './types/utils';
|
|
15
|
+
export * from './utils/audio';
|
|
16
|
+
export * from './utils/cn';
|
|
17
|
+
export * from './utils/colors';
|
|
18
|
+
export * from './utils/images';
|
|
19
|
+
export * from './utils/object';
|
|
20
|
+
export * from './utils/position';
|
|
21
|
+
export * from './utils/string';
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { create } from 'zustand';
|
|
2
|
+
import { persist } from 'zustand/middleware';
|
|
3
|
+
|
|
4
|
+
const PREFIX = '--cleen-';
|
|
5
|
+
|
|
6
|
+
const colorVars = [
|
|
7
|
+
'white',
|
|
8
|
+
'black',
|
|
9
|
+
'gray',
|
|
10
|
+
'pink',
|
|
11
|
+
'purple',
|
|
12
|
+
'indigo',
|
|
13
|
+
'blue',
|
|
14
|
+
'primary',
|
|
15
|
+
'success',
|
|
16
|
+
'warning',
|
|
17
|
+
'error',
|
|
18
|
+
'brand',
|
|
19
|
+
'sidebar',
|
|
20
|
+
'background',
|
|
21
|
+
] as const;
|
|
22
|
+
|
|
23
|
+
export type ColorVar = (typeof colorVars)[number];
|
|
24
|
+
|
|
25
|
+
interface UseCleenColorsStore {
|
|
26
|
+
colors: Partial<Record<ColorVar, string>>;
|
|
27
|
+
getColors: () => Partial<Record<ColorVar, string>>;
|
|
28
|
+
getColor: (color: ColorVar) => string;
|
|
29
|
+
setColor: (colorVar: ColorVar, value: string) => void;
|
|
30
|
+
setColors: (colors: Partial<Record<ColorVar, string>>) => void;
|
|
31
|
+
resetColors: () => void;
|
|
32
|
+
resetColor: (color: ColorVar) => void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const useCleenColors = create<UseCleenColorsStore>()(
|
|
36
|
+
persist(
|
|
37
|
+
(set, get) => ({
|
|
38
|
+
colors: {},
|
|
39
|
+
getColors: () => {
|
|
40
|
+
const colors = get().colors;
|
|
41
|
+
|
|
42
|
+
return colorVars.reduce<Partial<Record<ColorVar, string>>>(
|
|
43
|
+
(prev, colorVar) => {
|
|
44
|
+
if (colors[colorVar]) {
|
|
45
|
+
prev[colorVar] = colors[colorVar];
|
|
46
|
+
} else {
|
|
47
|
+
prev[colorVar] = getComputedStyle(
|
|
48
|
+
document.documentElement
|
|
49
|
+
).getPropertyValue(`${PREFIX}${colorVar}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return prev;
|
|
53
|
+
},
|
|
54
|
+
{}
|
|
55
|
+
);
|
|
56
|
+
},
|
|
57
|
+
getColor: (color: ColorVar) => {
|
|
58
|
+
const colors = get().colors;
|
|
59
|
+
|
|
60
|
+
if (colors[color]) {
|
|
61
|
+
return colors[color];
|
|
62
|
+
} else {
|
|
63
|
+
return getComputedStyle(document.documentElement).getPropertyValue(
|
|
64
|
+
`${PREFIX}${color}`
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
setColors: (colors: Partial<Record<ColorVar, string>>) =>
|
|
69
|
+
Object.entries(colors).map(([colorVar, value]) =>
|
|
70
|
+
set(state => ({
|
|
71
|
+
colors: {
|
|
72
|
+
...state.colors,
|
|
73
|
+
[colorVar]: value,
|
|
74
|
+
},
|
|
75
|
+
}))
|
|
76
|
+
),
|
|
77
|
+
setColor: (colorVar: ColorVar, value: string) => {
|
|
78
|
+
set(state => ({
|
|
79
|
+
colors: {
|
|
80
|
+
...state.colors,
|
|
81
|
+
[colorVar]: value,
|
|
82
|
+
},
|
|
83
|
+
}));
|
|
84
|
+
},
|
|
85
|
+
resetColors: () => {
|
|
86
|
+
set({ colors: {} });
|
|
87
|
+
},
|
|
88
|
+
resetColor: (colorVar: ColorVar) => {
|
|
89
|
+
const colors = get().colors;
|
|
90
|
+
delete colors[colorVar];
|
|
91
|
+
set({ colors });
|
|
92
|
+
},
|
|
93
|
+
}),
|
|
94
|
+
{
|
|
95
|
+
name: 'cleen-colors',
|
|
96
|
+
}
|
|
97
|
+
)
|
|
98
|
+
);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { CSSProperties } from 'react';
|
|
2
|
+
|
|
3
|
+
// Helper interfaces to extract types of the classNames or styles props and pass in additional reserved class/style for className/style prop
|
|
4
|
+
export interface ComponentClassnames<
|
|
5
|
+
T extends {
|
|
6
|
+
classNames?: {
|
|
7
|
+
[key: string]: string | object;
|
|
8
|
+
};
|
|
9
|
+
},
|
|
10
|
+
> {
|
|
11
|
+
className?: string;
|
|
12
|
+
classNames?: T['classNames'];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ComponentStyles<
|
|
16
|
+
T extends {
|
|
17
|
+
styles?: {
|
|
18
|
+
[key: string]: CSSProperties | object;
|
|
19
|
+
};
|
|
20
|
+
},
|
|
21
|
+
> {
|
|
22
|
+
style?: CSSProperties;
|
|
23
|
+
styles?: T['styles'];
|
|
24
|
+
}
|