@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.
@@ -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
@@ -0,0 +1,4 @@
1
+ export * from './utils';
2
+ export * from './hooks';
3
+ export * from './constants';
4
+ export * from './types';
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,4 @@
1
+ // Moon UI Shared - Theme Entry Point
2
+ // ===================================
3
+
4
+ @import './mixins.scss';
@@ -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
+ }