@starui/shared 0.0.1-alpha.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/es/constants/index.d.ts +45 -0
- package/es/constants/index.js +48 -0
- package/es/hooks/index.d.ts +46 -0
- package/es/hooks/index.js +165 -0
- package/es/index.d.ts +4 -0
- package/es/index.js +3 -0
- package/es/types/index.d.ts +18 -0
- package/es/utils/index.d.ts +44 -0
- package/es/utils/index.js +120 -0
- package/package.json +49 -0
- package/src/constants/index.ts +54 -0
- package/src/hooks/index.ts +189 -0
- package/src/index.ts +14 -0
- package/src/theme/index.scss +4 -0
- package/src/theme/mixins.scss +174 -0
- package/src/types/index.ts +30 -0
- package/src/utils/index.ts +133 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export declare const BREAKPOINTS: {
|
|
2
|
+
readonly xs: 0;
|
|
3
|
+
readonly sm: 576;
|
|
4
|
+
readonly md: 768;
|
|
5
|
+
readonly lg: 992;
|
|
6
|
+
readonly xl: 1200;
|
|
7
|
+
readonly xxl: 1600;
|
|
8
|
+
};
|
|
9
|
+
export declare const BREAKPOINT_NAMES: readonly ["xs", "sm", "md", "lg", "xl", "xxl"];
|
|
10
|
+
export type BreakpointName = typeof BREAKPOINT_NAMES[number];
|
|
11
|
+
export declare const KEY_CODES: {
|
|
12
|
+
readonly ENTER: 13;
|
|
13
|
+
readonly ESC: 27;
|
|
14
|
+
readonly SPACE: 32;
|
|
15
|
+
readonly TAB: 9;
|
|
16
|
+
readonly UP: 38;
|
|
17
|
+
readonly DOWN: 40;
|
|
18
|
+
readonly LEFT: 37;
|
|
19
|
+
readonly RIGHT: 39;
|
|
20
|
+
readonly HOME: 36;
|
|
21
|
+
readonly END: 35;
|
|
22
|
+
readonly PAGE_UP: 33;
|
|
23
|
+
readonly PAGE_DOWN: 34;
|
|
24
|
+
};
|
|
25
|
+
export declare const MOUSE_BUTTONS: {
|
|
26
|
+
readonly LEFT: 0;
|
|
27
|
+
readonly MIDDLE: 1;
|
|
28
|
+
readonly RIGHT: 2;
|
|
29
|
+
};
|
|
30
|
+
export declare const ANIMATION_DURATION: {
|
|
31
|
+
readonly FAST: 150;
|
|
32
|
+
readonly BASE: 300;
|
|
33
|
+
readonly SLOW: 500;
|
|
34
|
+
};
|
|
35
|
+
export declare const Z_INDEX: {
|
|
36
|
+
readonly DROPDOWN: 1000;
|
|
37
|
+
readonly STICKY: 1020;
|
|
38
|
+
readonly FIXED: 1030;
|
|
39
|
+
readonly MODAL_BACKDROP: 1040;
|
|
40
|
+
readonly MODAL: 1050;
|
|
41
|
+
readonly POPOVER: 1060;
|
|
42
|
+
readonly TOOLTIP: 1070;
|
|
43
|
+
readonly MESSAGE: 1080;
|
|
44
|
+
readonly NOTIFICATION: 1090;
|
|
45
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Moon UI Shared - Constants
|
|
2
|
+
// ===========================
|
|
3
|
+
const BREAKPOINTS = {
|
|
4
|
+
xs: 0,
|
|
5
|
+
sm: 576,
|
|
6
|
+
md: 768,
|
|
7
|
+
lg: 992,
|
|
8
|
+
xl: 1200,
|
|
9
|
+
xxl: 1600,
|
|
10
|
+
};
|
|
11
|
+
const BREAKPOINT_NAMES = ['xs', 'sm', 'md', 'lg', 'xl', 'xxl'];
|
|
12
|
+
const KEY_CODES = {
|
|
13
|
+
ENTER: 13,
|
|
14
|
+
ESC: 27,
|
|
15
|
+
SPACE: 32,
|
|
16
|
+
TAB: 9,
|
|
17
|
+
UP: 38,
|
|
18
|
+
DOWN: 40,
|
|
19
|
+
LEFT: 37,
|
|
20
|
+
RIGHT: 39,
|
|
21
|
+
HOME: 36,
|
|
22
|
+
END: 35,
|
|
23
|
+
PAGE_UP: 33,
|
|
24
|
+
PAGE_DOWN: 34,
|
|
25
|
+
};
|
|
26
|
+
const MOUSE_BUTTONS = {
|
|
27
|
+
LEFT: 0,
|
|
28
|
+
MIDDLE: 1,
|
|
29
|
+
RIGHT: 2,
|
|
30
|
+
};
|
|
31
|
+
const ANIMATION_DURATION = {
|
|
32
|
+
FAST: 150,
|
|
33
|
+
BASE: 300,
|
|
34
|
+
SLOW: 500,
|
|
35
|
+
};
|
|
36
|
+
const Z_INDEX = {
|
|
37
|
+
DROPDOWN: 1000,
|
|
38
|
+
STICKY: 1020,
|
|
39
|
+
FIXED: 1030,
|
|
40
|
+
MODAL_BACKDROP: 1040,
|
|
41
|
+
MODAL: 1050,
|
|
42
|
+
POPOVER: 1060,
|
|
43
|
+
TOOLTIP: 1070,
|
|
44
|
+
MESSAGE: 1080,
|
|
45
|
+
NOTIFICATION: 1090,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export { ANIMATION_DURATION, BREAKPOINTS, BREAKPOINT_NAMES, KEY_CODES, MOUSE_BUTTONS, Z_INDEX };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook for responsive breakpoint detection
|
|
3
|
+
*/
|
|
4
|
+
export declare function useBreakpoint(): {
|
|
5
|
+
width: any;
|
|
6
|
+
breakpoint: any;
|
|
7
|
+
isMobileView: any;
|
|
8
|
+
isTabletView: any;
|
|
9
|
+
isDesktopView: any;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Hook for theme management
|
|
13
|
+
*/
|
|
14
|
+
export declare function useTheme(): {
|
|
15
|
+
theme: any;
|
|
16
|
+
setTheme: (newTheme: "light" | "dark") => void;
|
|
17
|
+
toggleTheme: () => void;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Hook for click outside detection
|
|
21
|
+
*/
|
|
22
|
+
export declare function useClickOutside(elementRef: any, callback: () => void): void;
|
|
23
|
+
/**
|
|
24
|
+
* Hook for scroll position
|
|
25
|
+
*/
|
|
26
|
+
export declare function useScroll(): {
|
|
27
|
+
scrollX: any;
|
|
28
|
+
scrollY: any;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Hook for async operations with loading state
|
|
32
|
+
*/
|
|
33
|
+
export declare function useAsync<T>(asyncFn: (...args: any[]) => Promise<T>): {
|
|
34
|
+
loading: any;
|
|
35
|
+
error: any;
|
|
36
|
+
data: any;
|
|
37
|
+
execute: (...args: any[]) => Promise<any>;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Hook for localStorage with reactivity
|
|
41
|
+
*/
|
|
42
|
+
export declare function useLocalStorage<T>(key: string, defaultValue: T): {
|
|
43
|
+
value: any;
|
|
44
|
+
setValue: (newValue: T) => void;
|
|
45
|
+
removeValue: () => void;
|
|
46
|
+
};
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { ref, computed, onMounted, onUnmounted } from 'vue';
|
|
2
|
+
import { debounce } from '../utils/index.js';
|
|
3
|
+
|
|
4
|
+
// Moon UI Shared - Vue Composition Hooks
|
|
5
|
+
// =======================================
|
|
6
|
+
/**
|
|
7
|
+
* Hook for responsive breakpoint detection
|
|
8
|
+
*/
|
|
9
|
+
function useBreakpoint() {
|
|
10
|
+
const width = ref(typeof window !== 'undefined' ? window.innerWidth : 0);
|
|
11
|
+
const breakpoint = computed(() => {
|
|
12
|
+
if (width.value >= 1600)
|
|
13
|
+
return 'xxl';
|
|
14
|
+
if (width.value >= 1200)
|
|
15
|
+
return 'xl';
|
|
16
|
+
if (width.value >= 992)
|
|
17
|
+
return 'lg';
|
|
18
|
+
if (width.value >= 768)
|
|
19
|
+
return 'md';
|
|
20
|
+
if (width.value >= 576)
|
|
21
|
+
return 'sm';
|
|
22
|
+
return 'xs';
|
|
23
|
+
});
|
|
24
|
+
const isMobileView = computed(() => width.value < 768);
|
|
25
|
+
const isTabletView = computed(() => width.value >= 768 && width.value < 992);
|
|
26
|
+
const isDesktopView = computed(() => width.value >= 992);
|
|
27
|
+
const updateWidth = debounce(() => {
|
|
28
|
+
width.value = window.innerWidth;
|
|
29
|
+
}, 100);
|
|
30
|
+
onMounted(() => {
|
|
31
|
+
window.addEventListener('resize', updateWidth);
|
|
32
|
+
});
|
|
33
|
+
onUnmounted(() => {
|
|
34
|
+
window.removeEventListener('resize', updateWidth);
|
|
35
|
+
});
|
|
36
|
+
return {
|
|
37
|
+
width,
|
|
38
|
+
breakpoint,
|
|
39
|
+
isMobileView,
|
|
40
|
+
isTabletView,
|
|
41
|
+
isDesktopView,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Hook for theme management
|
|
46
|
+
*/
|
|
47
|
+
function useTheme() {
|
|
48
|
+
const theme = ref('light');
|
|
49
|
+
const setTheme = (newTheme) => {
|
|
50
|
+
theme.value = newTheme;
|
|
51
|
+
if (typeof document !== 'undefined') {
|
|
52
|
+
document.documentElement.setAttribute('data-theme', newTheme);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
const toggleTheme = () => {
|
|
56
|
+
setTheme(theme.value === 'light' ? 'dark' : 'light');
|
|
57
|
+
};
|
|
58
|
+
onMounted(() => {
|
|
59
|
+
if (typeof window !== 'undefined') {
|
|
60
|
+
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
61
|
+
if (prefersDark) {
|
|
62
|
+
setTheme('dark');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
return {
|
|
67
|
+
theme,
|
|
68
|
+
setTheme,
|
|
69
|
+
toggleTheme,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Hook for click outside detection
|
|
74
|
+
*/
|
|
75
|
+
function useClickOutside(elementRef, callback) {
|
|
76
|
+
const handler = (event) => {
|
|
77
|
+
const el = elementRef.value;
|
|
78
|
+
if (el && !el.contains(event.target)) {
|
|
79
|
+
callback();
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
onMounted(() => {
|
|
83
|
+
document.addEventListener('click', handler);
|
|
84
|
+
});
|
|
85
|
+
onUnmounted(() => {
|
|
86
|
+
document.removeEventListener('click', handler);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Hook for scroll position
|
|
91
|
+
*/
|
|
92
|
+
function useScroll() {
|
|
93
|
+
const scrollX = ref(0);
|
|
94
|
+
const scrollY = ref(0);
|
|
95
|
+
const updateScroll = () => {
|
|
96
|
+
scrollX.value = window.scrollX;
|
|
97
|
+
scrollY.value = window.scrollY;
|
|
98
|
+
};
|
|
99
|
+
onMounted(() => {
|
|
100
|
+
window.addEventListener('scroll', updateScroll);
|
|
101
|
+
updateScroll();
|
|
102
|
+
});
|
|
103
|
+
onUnmounted(() => {
|
|
104
|
+
window.removeEventListener('scroll', updateScroll);
|
|
105
|
+
});
|
|
106
|
+
return {
|
|
107
|
+
scrollX,
|
|
108
|
+
scrollY,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Hook for async operations with loading state
|
|
113
|
+
*/
|
|
114
|
+
function useAsync(asyncFn) {
|
|
115
|
+
const loading = ref(false);
|
|
116
|
+
const error = ref(null);
|
|
117
|
+
const data = ref(null);
|
|
118
|
+
const execute = async (...args) => {
|
|
119
|
+
loading.value = true;
|
|
120
|
+
error.value = null;
|
|
121
|
+
try {
|
|
122
|
+
data.value = await asyncFn(...args);
|
|
123
|
+
return data.value;
|
|
124
|
+
}
|
|
125
|
+
catch (err) {
|
|
126
|
+
error.value = err;
|
|
127
|
+
throw err;
|
|
128
|
+
}
|
|
129
|
+
finally {
|
|
130
|
+
loading.value = false;
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
return {
|
|
134
|
+
loading,
|
|
135
|
+
error,
|
|
136
|
+
data,
|
|
137
|
+
execute,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Hook for localStorage with reactivity
|
|
142
|
+
*/
|
|
143
|
+
function useLocalStorage(key, defaultValue) {
|
|
144
|
+
const stored = typeof window !== 'undefined' ? localStorage.getItem(key) : null;
|
|
145
|
+
const value = ref(stored ? JSON.parse(stored) : defaultValue);
|
|
146
|
+
const setValue = (newValue) => {
|
|
147
|
+
value.value = newValue;
|
|
148
|
+
if (typeof window !== 'undefined') {
|
|
149
|
+
localStorage.setItem(key, JSON.stringify(newValue));
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
const removeValue = () => {
|
|
153
|
+
value.value = defaultValue;
|
|
154
|
+
if (typeof window !== 'undefined') {
|
|
155
|
+
localStorage.removeItem(key);
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
return {
|
|
159
|
+
value,
|
|
160
|
+
setValue,
|
|
161
|
+
removeValue,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export { useAsync, useBreakpoint, useClickOutside, useLocalStorage, useScroll, useTheme };
|
package/es/index.d.ts
ADDED
package/es/index.js
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { clamp, copyToClipboard, debounce, deepMerge, formatDate, formatFileSize, generateId, isMobile, isPlainObject, isTouchDevice, throttle } from './utils/index.js';
|
|
2
|
+
export { useAsync, useBreakpoint, useClickOutside, useLocalStorage, useScroll, useTheme } from './hooks/index.js';
|
|
3
|
+
export { ANIMATION_DURATION, BREAKPOINTS, BREAKPOINT_NAMES, KEY_CODES, MOUSE_BUTTONS, Z_INDEX } from './constants/index.js';
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type Nullable<T> = T | null;
|
|
2
|
+
export type Optional<T> = T | undefined;
|
|
3
|
+
export type MaybeRef<T> = T | {
|
|
4
|
+
value: T;
|
|
5
|
+
};
|
|
6
|
+
export type MaybeArray<T> = T | T[];
|
|
7
|
+
export type MaybePromise<T> = T | Promise<T>;
|
|
8
|
+
export type DeepPartial<T> = {
|
|
9
|
+
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
|
|
10
|
+
};
|
|
11
|
+
export type Merge<T, U> = Omit<T, keyof U> & U;
|
|
12
|
+
export type ExtractPropTypes<T> = {
|
|
13
|
+
[K in keyof T]: T[K] extends {
|
|
14
|
+
type: infer R;
|
|
15
|
+
} ? R extends (...args: any[]) => any ? R : R extends new (...args: any[]) => any ? InstanceType<R> : R : T[K] extends {
|
|
16
|
+
type: infer R;
|
|
17
|
+
} ? R : any;
|
|
18
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check if the current environment is mobile
|
|
3
|
+
*/
|
|
4
|
+
export declare function isMobile(): boolean;
|
|
5
|
+
/**
|
|
6
|
+
* Check if the device supports touch
|
|
7
|
+
*/
|
|
8
|
+
export declare function isTouchDevice(): boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Debounce function
|
|
11
|
+
*/
|
|
12
|
+
export declare function debounce<T extends (...args: any[]) => any>(fn: T, delay: number): (...args: Parameters<T>) => void;
|
|
13
|
+
/**
|
|
14
|
+
* Throttle function
|
|
15
|
+
*/
|
|
16
|
+
export declare function throttle<T extends (...args: any[]) => any>(fn: T, limit: number): (...args: Parameters<T>) => void;
|
|
17
|
+
/**
|
|
18
|
+
* Generate unique ID
|
|
19
|
+
*/
|
|
20
|
+
export declare function generateId(prefix?: string): string;
|
|
21
|
+
/**
|
|
22
|
+
* Deep merge objects
|
|
23
|
+
*/
|
|
24
|
+
export declare function deepMerge<T extends Record<string, any>>(target: T, source: Partial<T>): T;
|
|
25
|
+
/**
|
|
26
|
+
* Format file size
|
|
27
|
+
*/
|
|
28
|
+
export declare function formatFileSize(bytes: number): string;
|
|
29
|
+
/**
|
|
30
|
+
* Copy text to clipboard
|
|
31
|
+
*/
|
|
32
|
+
export declare function copyToClipboard(text: string): Promise<boolean>;
|
|
33
|
+
/**
|
|
34
|
+
* Check if value is a plain object
|
|
35
|
+
*/
|
|
36
|
+
export declare function isPlainObject(value: any): value is Record<string, any>;
|
|
37
|
+
/**
|
|
38
|
+
* Clamp a number between min and max
|
|
39
|
+
*/
|
|
40
|
+
export declare function clamp(value: number, min: number, max: number): number;
|
|
41
|
+
/**
|
|
42
|
+
* Format date
|
|
43
|
+
*/
|
|
44
|
+
export declare function formatDate(date: Date | string | number, format?: string): string;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// Moon UI Shared - Utility Functions
|
|
2
|
+
// ====================================
|
|
3
|
+
/**
|
|
4
|
+
* Check if the current environment is mobile
|
|
5
|
+
*/
|
|
6
|
+
function isMobile() {
|
|
7
|
+
if (typeof window === 'undefined')
|
|
8
|
+
return false;
|
|
9
|
+
return window.innerWidth < 768;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Check if the device supports touch
|
|
13
|
+
*/
|
|
14
|
+
function isTouchDevice() {
|
|
15
|
+
if (typeof window === 'undefined')
|
|
16
|
+
return false;
|
|
17
|
+
return 'ontouchstart' in window || navigator.maxTouchPoints > 0;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Debounce function
|
|
21
|
+
*/
|
|
22
|
+
function debounce(fn, delay) {
|
|
23
|
+
let timer = null;
|
|
24
|
+
return function (...args) {
|
|
25
|
+
if (timer)
|
|
26
|
+
clearTimeout(timer);
|
|
27
|
+
timer = setTimeout(() => fn(...args), delay);
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Throttle function
|
|
32
|
+
*/
|
|
33
|
+
function throttle(fn, limit) {
|
|
34
|
+
let inThrottle = false;
|
|
35
|
+
return function (...args) {
|
|
36
|
+
if (!inThrottle) {
|
|
37
|
+
fn(...args);
|
|
38
|
+
inThrottle = true;
|
|
39
|
+
setTimeout(() => (inThrottle = false), limit);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Generate unique ID
|
|
45
|
+
*/
|
|
46
|
+
function generateId(prefix = 'moon') {
|
|
47
|
+
return `${prefix}-${Math.random().toString(36).substr(2, 9)}`;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Deep merge objects
|
|
51
|
+
*/
|
|
52
|
+
function deepMerge(target, source) {
|
|
53
|
+
const result = { ...target };
|
|
54
|
+
for (const key in source) {
|
|
55
|
+
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
|
|
56
|
+
result[key] = deepMerge(result[key] || {}, source[key]);
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
result[key] = source[key];
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Format file size
|
|
66
|
+
*/
|
|
67
|
+
function formatFileSize(bytes) {
|
|
68
|
+
if (bytes === 0)
|
|
69
|
+
return '0 B';
|
|
70
|
+
const k = 1024;
|
|
71
|
+
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
72
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
73
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Copy text to clipboard
|
|
77
|
+
*/
|
|
78
|
+
async function copyToClipboard(text) {
|
|
79
|
+
try {
|
|
80
|
+
await navigator.clipboard.writeText(text);
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
console.error('Failed to copy:', err);
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Check if value is a plain object
|
|
90
|
+
*/
|
|
91
|
+
function isPlainObject(value) {
|
|
92
|
+
return value !== null && typeof value === 'object' && Object.prototype.toString.call(value) === '[object Object]';
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Clamp a number between min and max
|
|
96
|
+
*/
|
|
97
|
+
function clamp(value, min, max) {
|
|
98
|
+
return Math.min(Math.max(value, min), max);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Format date
|
|
102
|
+
*/
|
|
103
|
+
function formatDate(date, format = 'YYYY-MM-DD HH:mm:ss') {
|
|
104
|
+
const d = new Date(date);
|
|
105
|
+
const year = d.getFullYear();
|
|
106
|
+
const month = String(d.getMonth() + 1).padStart(2, '0');
|
|
107
|
+
const day = String(d.getDate()).padStart(2, '0');
|
|
108
|
+
const hours = String(d.getHours()).padStart(2, '0');
|
|
109
|
+
const minutes = String(d.getMinutes()).padStart(2, '0');
|
|
110
|
+
const seconds = String(d.getSeconds()).padStart(2, '0');
|
|
111
|
+
return format
|
|
112
|
+
.replace('YYYY', String(year))
|
|
113
|
+
.replace('MM', month)
|
|
114
|
+
.replace('DD', day)
|
|
115
|
+
.replace('HH', hours)
|
|
116
|
+
.replace('mm', minutes)
|
|
117
|
+
.replace('ss', seconds);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export { clamp, copyToClipboard, debounce, deepMerge, formatDate, formatFileSize, generateId, isMobile, isPlainObject, isTouchDevice, throttle };
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@starui/shared",
|
|
3
|
+
"version": "0.0.1-alpha.0",
|
|
4
|
+
"description": "Star UI Shared - Shared utilities and tools for Star UI",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"module": "es/index.js",
|
|
8
|
+
"types": "es/index.d.ts",
|
|
9
|
+
"files": [
|
|
10
|
+
"lib",
|
|
11
|
+
"es",
|
|
12
|
+
"src"
|
|
13
|
+
],
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"import": "./es/index.js",
|
|
17
|
+
"types": "./es/index.d.ts"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "rollup -c rollup.config.js",
|
|
22
|
+
"test": "vitest",
|
|
23
|
+
"test:coverage": "vitest --coverage",
|
|
24
|
+
"lint": "eslint . --ext .ts,.tsx --fix",
|
|
25
|
+
"format": "prettier --write ."
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"vue3",
|
|
29
|
+
"utils",
|
|
30
|
+
"tools",
|
|
31
|
+
"shared",
|
|
32
|
+
"typescript"
|
|
33
|
+
],
|
|
34
|
+
"author": "Star UI Team",
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "https://github.com/your-org/star-ui.git",
|
|
39
|
+
"directory": "packages/shared"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@rollup/plugin-node-resolve": "^15.2.3",
|
|
43
|
+
"@rollup/plugin-typescript": "^11.1.5",
|
|
44
|
+
"@types/node": "^20.10.0",
|
|
45
|
+
"rollup": "^4.6.0",
|
|
46
|
+
"typescript": "^5.3.0",
|
|
47
|
+
"vitest": "^1.0.0"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Moon UI Shared - Constants
|
|
2
|
+
// ===========================
|
|
3
|
+
|
|
4
|
+
export const BREAKPOINTS = {
|
|
5
|
+
xs: 0,
|
|
6
|
+
sm: 576,
|
|
7
|
+
md: 768,
|
|
8
|
+
lg: 992,
|
|
9
|
+
xl: 1200,
|
|
10
|
+
xxl: 1600,
|
|
11
|
+
} as const
|
|
12
|
+
|
|
13
|
+
export const BREAKPOINT_NAMES = ['xs', 'sm', 'md', 'lg', 'xl', 'xxl'] as const
|
|
14
|
+
|
|
15
|
+
export type BreakpointName = typeof BREAKPOINT_NAMES[number]
|
|
16
|
+
|
|
17
|
+
export const KEY_CODES = {
|
|
18
|
+
ENTER: 13,
|
|
19
|
+
ESC: 27,
|
|
20
|
+
SPACE: 32,
|
|
21
|
+
TAB: 9,
|
|
22
|
+
UP: 38,
|
|
23
|
+
DOWN: 40,
|
|
24
|
+
LEFT: 37,
|
|
25
|
+
RIGHT: 39,
|
|
26
|
+
HOME: 36,
|
|
27
|
+
END: 35,
|
|
28
|
+
PAGE_UP: 33,
|
|
29
|
+
PAGE_DOWN: 34,
|
|
30
|
+
} as const
|
|
31
|
+
|
|
32
|
+
export const MOUSE_BUTTONS = {
|
|
33
|
+
LEFT: 0,
|
|
34
|
+
MIDDLE: 1,
|
|
35
|
+
RIGHT: 2,
|
|
36
|
+
} as const
|
|
37
|
+
|
|
38
|
+
export const ANIMATION_DURATION = {
|
|
39
|
+
FAST: 150,
|
|
40
|
+
BASE: 300,
|
|
41
|
+
SLOW: 500,
|
|
42
|
+
} as const
|
|
43
|
+
|
|
44
|
+
export const Z_INDEX = {
|
|
45
|
+
DROPDOWN: 1000,
|
|
46
|
+
STICKY: 1020,
|
|
47
|
+
FIXED: 1030,
|
|
48
|
+
MODAL_BACKDROP: 1040,
|
|
49
|
+
MODAL: 1050,
|
|
50
|
+
POPOVER: 1060,
|
|
51
|
+
TOOLTIP: 1070,
|
|
52
|
+
MESSAGE: 1080,
|
|
53
|
+
NOTIFICATION: 1090,
|
|
54
|
+
} as const
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
// Moon UI Shared - Vue Composition Hooks
|
|
2
|
+
// =======================================
|
|
3
|
+
|
|
4
|
+
import { ref, onMounted, onUnmounted, computed, watch } from 'vue'
|
|
5
|
+
import { isMobile, debounce } from '../utils'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Hook for responsive breakpoint detection
|
|
9
|
+
*/
|
|
10
|
+
export function useBreakpoint() {
|
|
11
|
+
const width = ref(typeof window !== 'undefined' ? window.innerWidth : 0)
|
|
12
|
+
|
|
13
|
+
const breakpoint = computed(() => {
|
|
14
|
+
if (width.value >= 1600) return 'xxl'
|
|
15
|
+
if (width.value >= 1200) return 'xl'
|
|
16
|
+
if (width.value >= 992) return 'lg'
|
|
17
|
+
if (width.value >= 768) return 'md'
|
|
18
|
+
if (width.value >= 576) return 'sm'
|
|
19
|
+
return 'xs'
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
const isMobileView = computed(() => width.value < 768)
|
|
23
|
+
const isTabletView = computed(() => width.value >= 768 && width.value < 992)
|
|
24
|
+
const isDesktopView = computed(() => width.value >= 992)
|
|
25
|
+
|
|
26
|
+
const updateWidth = debounce(() => {
|
|
27
|
+
width.value = window.innerWidth
|
|
28
|
+
}, 100)
|
|
29
|
+
|
|
30
|
+
onMounted(() => {
|
|
31
|
+
window.addEventListener('resize', updateWidth)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
onUnmounted(() => {
|
|
35
|
+
window.removeEventListener('resize', updateWidth)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
width,
|
|
40
|
+
breakpoint,
|
|
41
|
+
isMobileView,
|
|
42
|
+
isTabletView,
|
|
43
|
+
isDesktopView,
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Hook for theme management
|
|
49
|
+
*/
|
|
50
|
+
export function useTheme() {
|
|
51
|
+
const theme = ref<'light' | 'dark'>('light')
|
|
52
|
+
|
|
53
|
+
const setTheme = (newTheme: 'light' | 'dark') => {
|
|
54
|
+
theme.value = newTheme
|
|
55
|
+
if (typeof document !== 'undefined') {
|
|
56
|
+
document.documentElement.setAttribute('data-theme', newTheme)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const toggleTheme = () => {
|
|
61
|
+
setTheme(theme.value === 'light' ? 'dark' : 'light')
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
onMounted(() => {
|
|
65
|
+
if (typeof window !== 'undefined') {
|
|
66
|
+
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
|
|
67
|
+
if (prefersDark) {
|
|
68
|
+
setTheme('dark')
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
theme,
|
|
75
|
+
setTheme,
|
|
76
|
+
toggleTheme,
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Hook for click outside detection
|
|
82
|
+
*/
|
|
83
|
+
export function useClickOutside(
|
|
84
|
+
elementRef: any,
|
|
85
|
+
callback: () => void
|
|
86
|
+
) {
|
|
87
|
+
const handler = (event: MouseEvent) => {
|
|
88
|
+
const el = elementRef.value
|
|
89
|
+
if (el && !el.contains(event.target as Node)) {
|
|
90
|
+
callback()
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
onMounted(() => {
|
|
95
|
+
document.addEventListener('click', handler)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
onUnmounted(() => {
|
|
99
|
+
document.removeEventListener('click', handler)
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Hook for scroll position
|
|
105
|
+
*/
|
|
106
|
+
export function useScroll() {
|
|
107
|
+
const scrollX = ref(0)
|
|
108
|
+
const scrollY = ref(0)
|
|
109
|
+
|
|
110
|
+
const updateScroll = () => {
|
|
111
|
+
scrollX.value = window.scrollX
|
|
112
|
+
scrollY.value = window.scrollY
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
onMounted(() => {
|
|
116
|
+
window.addEventListener('scroll', updateScroll)
|
|
117
|
+
updateScroll()
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
onUnmounted(() => {
|
|
121
|
+
window.removeEventListener('scroll', updateScroll)
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
scrollX,
|
|
126
|
+
scrollY,
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Hook for async operations with loading state
|
|
132
|
+
*/
|
|
133
|
+
export function useAsync<T>(
|
|
134
|
+
asyncFn: (...args: any[]) => Promise<T>
|
|
135
|
+
) {
|
|
136
|
+
const loading = ref(false)
|
|
137
|
+
const error = ref<Error | null>(null)
|
|
138
|
+
const data = ref<T | null>(null)
|
|
139
|
+
|
|
140
|
+
const execute = async (...args: any[]) => {
|
|
141
|
+
loading.value = true
|
|
142
|
+
error.value = null
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
data.value = await asyncFn(...args)
|
|
146
|
+
return data.value
|
|
147
|
+
} catch (err) {
|
|
148
|
+
error.value = err as Error
|
|
149
|
+
throw err
|
|
150
|
+
} finally {
|
|
151
|
+
loading.value = false
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
loading,
|
|
157
|
+
error,
|
|
158
|
+
data,
|
|
159
|
+
execute,
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Hook for localStorage with reactivity
|
|
165
|
+
*/
|
|
166
|
+
export function useLocalStorage<T>(key: string, defaultValue: T) {
|
|
167
|
+
const stored = typeof window !== 'undefined' ? localStorage.getItem(key) : null
|
|
168
|
+
const value = ref<T>(stored ? JSON.parse(stored) : defaultValue)
|
|
169
|
+
|
|
170
|
+
const setValue = (newValue: T) => {
|
|
171
|
+
value.value = newValue
|
|
172
|
+
if (typeof window !== 'undefined') {
|
|
173
|
+
localStorage.setItem(key, JSON.stringify(newValue))
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const removeValue = () => {
|
|
178
|
+
value.value = defaultValue
|
|
179
|
+
if (typeof window !== 'undefined') {
|
|
180
|
+
localStorage.removeItem(key)
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
value,
|
|
186
|
+
setValue,
|
|
187
|
+
removeValue,
|
|
188
|
+
}
|
|
189
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Moon UI Shared - Utilities and Tools
|
|
2
|
+
// =====================================
|
|
3
|
+
|
|
4
|
+
// Export utilities
|
|
5
|
+
export * from './utils'
|
|
6
|
+
|
|
7
|
+
// Export hooks
|
|
8
|
+
export * from './hooks'
|
|
9
|
+
|
|
10
|
+
// Export constants
|
|
11
|
+
export * from './constants'
|
|
12
|
+
|
|
13
|
+
// Export types
|
|
14
|
+
export * from './types'
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// Moon UI Shared - SCSS Mixins
|
|
2
|
+
// =============================
|
|
3
|
+
|
|
4
|
+
// Hairline border for high DPI displays
|
|
5
|
+
@mixin hairline($color: #ebedf0, $radius: 0) {
|
|
6
|
+
position: relative;
|
|
7
|
+
|
|
8
|
+
&::after {
|
|
9
|
+
content: '';
|
|
10
|
+
position: absolute;
|
|
11
|
+
left: 0;
|
|
12
|
+
top: 0;
|
|
13
|
+
width: 200%;
|
|
14
|
+
height: 200%;
|
|
15
|
+
border: 1px solid $color;
|
|
16
|
+
transform: scale(0.5);
|
|
17
|
+
transform-origin: left top;
|
|
18
|
+
pointer-events: none;
|
|
19
|
+
|
|
20
|
+
@if $radius != 0 {
|
|
21
|
+
border-radius: $radius * 2;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Text overflow ellipsis
|
|
27
|
+
@mixin ellipsis($lines: 1) {
|
|
28
|
+
@if $lines == 1 {
|
|
29
|
+
overflow: hidden;
|
|
30
|
+
white-space: nowrap;
|
|
31
|
+
text-overflow: ellipsis;
|
|
32
|
+
} @else {
|
|
33
|
+
display: -webkit-box;
|
|
34
|
+
overflow: hidden;
|
|
35
|
+
text-overflow: ellipsis;
|
|
36
|
+
-webkit-line-clamp: $lines;
|
|
37
|
+
-webkit-box-orient: vertical;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Safe area padding for mobile devices
|
|
42
|
+
@mixin safe-area-padding-bottom() {
|
|
43
|
+
padding-bottom: constant(safe-area-inset-bottom);
|
|
44
|
+
padding-bottom: env(safe-area-inset-bottom);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@mixin safe-area-padding-top() {
|
|
48
|
+
padding-top: constant(safe-area-inset-top);
|
|
49
|
+
padding-top: env(safe-area-inset-top);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@mixin safe-area-padding-left() {
|
|
53
|
+
padding-left: constant(safe-area-inset-left);
|
|
54
|
+
padding-left: env(safe-area-inset-left);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@mixin safe-area-padding-right() {
|
|
58
|
+
padding-right: constant(safe-area-inset-right);
|
|
59
|
+
padding-right: env(safe-area-inset-right);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Responsive breakpoints
|
|
63
|
+
@mixin respond-to($breakpoint) {
|
|
64
|
+
$breakpoints: (
|
|
65
|
+
'xs': 0,
|
|
66
|
+
'sm': 576px,
|
|
67
|
+
'md': 768px,
|
|
68
|
+
'lg': 992px,
|
|
69
|
+
'xl': 1200px,
|
|
70
|
+
'xxl': 1600px
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
$value: map-get($breakpoints, $breakpoint);
|
|
74
|
+
|
|
75
|
+
@if $value != 0 {
|
|
76
|
+
@media (min-width: $value) {
|
|
77
|
+
@content;
|
|
78
|
+
}
|
|
79
|
+
} @else {
|
|
80
|
+
@content;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
@mixin mobile-only {
|
|
85
|
+
@media (max-width: 767px) {
|
|
86
|
+
@content;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
@mixin tablet-up {
|
|
91
|
+
@media (min-width: 768px) {
|
|
92
|
+
@content;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
@mixin desktop-up {
|
|
97
|
+
@media (min-width: 992px) {
|
|
98
|
+
@content;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Flexbox utilities
|
|
103
|
+
@mixin flex-center {
|
|
104
|
+
display: flex;
|
|
105
|
+
align-items: center;
|
|
106
|
+
justify-content: center;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
@mixin flex-between {
|
|
110
|
+
display: flex;
|
|
111
|
+
align-items: center;
|
|
112
|
+
justify-content: space-between;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
@mixin flex-column {
|
|
116
|
+
display: flex;
|
|
117
|
+
flex-direction: column;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Absolute positioning
|
|
121
|
+
@mixin absolute-center {
|
|
122
|
+
position: absolute;
|
|
123
|
+
top: 50%;
|
|
124
|
+
left: 50%;
|
|
125
|
+
transform: translate(-50%, -50%);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
@mixin absolute-full {
|
|
129
|
+
position: absolute;
|
|
130
|
+
top: 0;
|
|
131
|
+
left: 0;
|
|
132
|
+
right: 0;
|
|
133
|
+
bottom: 0;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Clearfix
|
|
137
|
+
@mixin clearfix {
|
|
138
|
+
&::after {
|
|
139
|
+
content: '';
|
|
140
|
+
display: table;
|
|
141
|
+
clear: both;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Hide scrollbar but keep functionality
|
|
146
|
+
@mixin scrollbar-hide {
|
|
147
|
+
-ms-overflow-style: none;
|
|
148
|
+
scrollbar-width: none;
|
|
149
|
+
|
|
150
|
+
&::-webkit-scrollbar {
|
|
151
|
+
display: none;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Custom scrollbar
|
|
156
|
+
@mixin custom-scrollbar($width: 8px, $track-color: #f1f1f1, $thumb-color: #c1c1c1) {
|
|
157
|
+
&::-webkit-scrollbar {
|
|
158
|
+
width: $width;
|
|
159
|
+
height: $width;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
&::-webkit-scrollbar-track {
|
|
163
|
+
background: $track-color;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
&::-webkit-scrollbar-thumb {
|
|
167
|
+
background: $thumb-color;
|
|
168
|
+
border-radius: 999px;
|
|
169
|
+
|
|
170
|
+
&:hover {
|
|
171
|
+
background: darken($thumb-color, 10%);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Moon UI Shared - Common Types
|
|
2
|
+
// ===============================
|
|
3
|
+
|
|
4
|
+
export type Nullable<T> = T | null
|
|
5
|
+
|
|
6
|
+
export type Optional<T> = T | undefined
|
|
7
|
+
|
|
8
|
+
export type MaybeRef<T> = T | { value: T }
|
|
9
|
+
|
|
10
|
+
export type MaybeArray<T> = T | T[]
|
|
11
|
+
|
|
12
|
+
export type MaybePromise<T> = T | Promise<T>
|
|
13
|
+
|
|
14
|
+
export type DeepPartial<T> = {
|
|
15
|
+
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type Merge<T, U> = Omit<T, keyof U> & U
|
|
19
|
+
|
|
20
|
+
export type ExtractPropTypes<T> = {
|
|
21
|
+
[K in keyof T]: T[K] extends { type: infer R }
|
|
22
|
+
? R extends (...args: any[]) => any
|
|
23
|
+
? R
|
|
24
|
+
: R extends new (...args: any[]) => any
|
|
25
|
+
? InstanceType<R>
|
|
26
|
+
: R
|
|
27
|
+
: T[K] extends { type: infer R }
|
|
28
|
+
? R
|
|
29
|
+
: any
|
|
30
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// Moon UI Shared - Utility Functions
|
|
2
|
+
// ====================================
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Check if the current environment is mobile
|
|
6
|
+
*/
|
|
7
|
+
export function isMobile(): boolean {
|
|
8
|
+
if (typeof window === 'undefined') return false
|
|
9
|
+
return window.innerWidth < 768
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Check if the device supports touch
|
|
14
|
+
*/
|
|
15
|
+
export function isTouchDevice(): boolean {
|
|
16
|
+
if (typeof window === 'undefined') return false
|
|
17
|
+
return 'ontouchstart' in window || navigator.maxTouchPoints > 0
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Debounce function
|
|
22
|
+
*/
|
|
23
|
+
export function debounce<T extends (...args: any[]) => any>(
|
|
24
|
+
fn: T,
|
|
25
|
+
delay: number
|
|
26
|
+
): (...args: Parameters<T>) => void {
|
|
27
|
+
let timer: ReturnType<typeof setTimeout> | null = null
|
|
28
|
+
return function (...args: Parameters<T>) {
|
|
29
|
+
if (timer) clearTimeout(timer)
|
|
30
|
+
timer = setTimeout(() => fn(...args), delay)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Throttle function
|
|
36
|
+
*/
|
|
37
|
+
export function throttle<T extends (...args: any[]) => any>(
|
|
38
|
+
fn: T,
|
|
39
|
+
limit: number
|
|
40
|
+
): (...args: Parameters<T>) => void {
|
|
41
|
+
let inThrottle = false
|
|
42
|
+
return function (...args: Parameters<T>) {
|
|
43
|
+
if (!inThrottle) {
|
|
44
|
+
fn(...args)
|
|
45
|
+
inThrottle = true
|
|
46
|
+
setTimeout(() => (inThrottle = false), limit)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Generate unique ID
|
|
53
|
+
*/
|
|
54
|
+
export function generateId(prefix = 'moon'): string {
|
|
55
|
+
return `${prefix}-${Math.random().toString(36).substr(2, 9)}`
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Deep merge objects
|
|
60
|
+
*/
|
|
61
|
+
export function deepMerge<T extends Record<string, any>>(
|
|
62
|
+
target: T,
|
|
63
|
+
source: Partial<T>
|
|
64
|
+
): T {
|
|
65
|
+
const result = { ...target }
|
|
66
|
+
for (const key in source) {
|
|
67
|
+
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
|
|
68
|
+
result[key] = deepMerge(result[key] || {}, source[key] as any)
|
|
69
|
+
} else {
|
|
70
|
+
result[key] = source[key] as any
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return result
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Format file size
|
|
78
|
+
*/
|
|
79
|
+
export function formatFileSize(bytes: number): string {
|
|
80
|
+
if (bytes === 0) return '0 B'
|
|
81
|
+
const k = 1024
|
|
82
|
+
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
|
|
83
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
|
84
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Copy text to clipboard
|
|
89
|
+
*/
|
|
90
|
+
export async function copyToClipboard(text: string): Promise<boolean> {
|
|
91
|
+
try {
|
|
92
|
+
await navigator.clipboard.writeText(text)
|
|
93
|
+
return true
|
|
94
|
+
} catch (err) {
|
|
95
|
+
console.error('Failed to copy:', err)
|
|
96
|
+
return false
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Check if value is a plain object
|
|
102
|
+
*/
|
|
103
|
+
export function isPlainObject(value: any): value is Record<string, any> {
|
|
104
|
+
return value !== null && typeof value === 'object' && Object.prototype.toString.call(value) === '[object Object]'
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Clamp a number between min and max
|
|
109
|
+
*/
|
|
110
|
+
export function clamp(value: number, min: number, max: number): number {
|
|
111
|
+
return Math.min(Math.max(value, min), max)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Format date
|
|
116
|
+
*/
|
|
117
|
+
export function formatDate(date: Date | string | number, format = 'YYYY-MM-DD HH:mm:ss'): string {
|
|
118
|
+
const d = new Date(date)
|
|
119
|
+
const year = d.getFullYear()
|
|
120
|
+
const month = String(d.getMonth() + 1).padStart(2, '0')
|
|
121
|
+
const day = String(d.getDate()).padStart(2, '0')
|
|
122
|
+
const hours = String(d.getHours()).padStart(2, '0')
|
|
123
|
+
const minutes = String(d.getMinutes()).padStart(2, '0')
|
|
124
|
+
const seconds = String(d.getSeconds()).padStart(2, '0')
|
|
125
|
+
|
|
126
|
+
return format
|
|
127
|
+
.replace('YYYY', String(year))
|
|
128
|
+
.replace('MM', month)
|
|
129
|
+
.replace('DD', day)
|
|
130
|
+
.replace('HH', hours)
|
|
131
|
+
.replace('mm', minutes)
|
|
132
|
+
.replace('ss', seconds)
|
|
133
|
+
}
|