Cleanup: Remove old modes, add Svelte 5 reference library, update orchestrator
- Delete Mode1Test, Mode2Test, Mode3Test, Mode5Test (keep only Mode6Test) - Delete old documentation, Docker files, and k8s configs - Add comprehensive Svelte 5 reference library (svelte-reference/) - Update ModeSwitch orchestrator: - Fix port to 3005 (never use 3000/3001) - Add API endpoints for mode switching - Add bun-types and proper tsconfig - Clean up mode folder mappings
This commit is contained in:
@@ -0,0 +1,735 @@
|
||||
# Svelte Common Utilities & Helpers
|
||||
|
||||
## Overview
|
||||
|
||||
A collection of reusable utilities, helpers, and patterns commonly needed in Svelte 5 applications.
|
||||
|
||||
---
|
||||
|
||||
## 🎣 Custom Hooks (Reactive Utilities)
|
||||
|
||||
### useLocalStorage
|
||||
|
||||
```typescript
|
||||
// src/lib/hooks/useLocalStorage.svelte.ts
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
export function useLocalStorage<T>(key: string, initialValue: T) {
|
||||
let value = $state<T>(initialValue);
|
||||
|
||||
// Load from localStorage on init
|
||||
if (browser) {
|
||||
const stored = localStorage.getItem(key);
|
||||
if (stored) {
|
||||
try {
|
||||
value = JSON.parse(stored);
|
||||
} catch {
|
||||
value = initialValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sync to localStorage on change
|
||||
$effect(() => {
|
||||
if (browser) {
|
||||
localStorage.setItem(key, JSON.stringify(value));
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
get value() { return value; },
|
||||
set value(v: T) { value = v; },
|
||||
reset() { value = initialValue; }
|
||||
};
|
||||
}
|
||||
|
||||
// Usage
|
||||
const theme = useLocalStorage('theme', 'light');
|
||||
// theme.value = 'dark';
|
||||
```
|
||||
|
||||
### useDebounce
|
||||
|
||||
```typescript
|
||||
// src/lib/hooks/useDebounce.svelte.ts
|
||||
export function useDebounce<T>(getValue: () => T, delay: number = 300) {
|
||||
let debouncedValue = $state<T>(getValue());
|
||||
let timeout: ReturnType<typeof setTimeout>;
|
||||
|
||||
$effect(() => {
|
||||
const currentValue = getValue();
|
||||
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
debouncedValue = currentValue;
|
||||
}, delay);
|
||||
|
||||
return () => clearTimeout(timeout);
|
||||
});
|
||||
|
||||
return {
|
||||
get value() { return debouncedValue; }
|
||||
};
|
||||
}
|
||||
|
||||
// Usage
|
||||
let searchQuery = $state('');
|
||||
const debouncedQuery = useDebounce(() => searchQuery, 300);
|
||||
// Use debouncedQuery.value for API calls
|
||||
```
|
||||
|
||||
### useThrottle
|
||||
|
||||
```typescript
|
||||
// src/lib/hooks/useThrottle.svelte.ts
|
||||
export function useThrottle<T>(getValue: () => T, limit: number = 300) {
|
||||
let throttledValue = $state<T>(getValue());
|
||||
let lastRun = 0;
|
||||
|
||||
$effect(() => {
|
||||
const currentValue = getValue();
|
||||
const now = Date.now();
|
||||
|
||||
if (now - lastRun >= limit) {
|
||||
throttledValue = currentValue;
|
||||
lastRun = now;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
get value() { return throttledValue; }
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### useMediaQuery
|
||||
|
||||
```typescript
|
||||
// src/lib/hooks/useMediaQuery.svelte.ts
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
export function useMediaQuery(query: string) {
|
||||
let matches = $state(false);
|
||||
|
||||
$effect(() => {
|
||||
if (!browser) return;
|
||||
|
||||
const mediaQuery = window.matchMedia(query);
|
||||
matches = mediaQuery.matches;
|
||||
|
||||
const handler = (e: MediaQueryListEvent) => {
|
||||
matches = e.matches;
|
||||
};
|
||||
|
||||
mediaQuery.addEventListener('change', handler);
|
||||
return () => mediaQuery.removeEventListener('change', handler);
|
||||
});
|
||||
|
||||
return {
|
||||
get matches() { return matches; }
|
||||
};
|
||||
}
|
||||
|
||||
// Usage
|
||||
const isMobile = useMediaQuery('(max-width: 768px)');
|
||||
// {#if isMobile.matches}...{/if}
|
||||
```
|
||||
|
||||
### useOnline
|
||||
|
||||
```typescript
|
||||
// src/lib/hooks/useOnline.svelte.ts
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
export function useOnline() {
|
||||
let isOnline = $state(browser ? navigator.onLine : true);
|
||||
|
||||
$effect(() => {
|
||||
if (!browser) return;
|
||||
|
||||
const handleOnline = () => { isOnline = true; };
|
||||
const handleOffline = () => { isOnline = false; };
|
||||
|
||||
window.addEventListener('online', handleOnline);
|
||||
window.addEventListener('offline', handleOffline);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('online', handleOnline);
|
||||
window.removeEventListener('offline', handleOffline);
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
get isOnline() { return isOnline; }
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### useClickOutside
|
||||
|
||||
```typescript
|
||||
// src/lib/hooks/useClickOutside.svelte.ts
|
||||
export function useClickOutside(
|
||||
getElement: () => HTMLElement | null,
|
||||
callback: () => void
|
||||
) {
|
||||
$effect(() => {
|
||||
const element = getElement();
|
||||
if (!element) return;
|
||||
|
||||
const handler = (event: MouseEvent) => {
|
||||
if (!element.contains(event.target as Node)) {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('click', handler);
|
||||
return () => document.removeEventListener('click', handler);
|
||||
});
|
||||
}
|
||||
|
||||
// Usage
|
||||
let menuRef = $state<HTMLElement>();
|
||||
let isOpen = $state(false);
|
||||
|
||||
useClickOutside(
|
||||
() => menuRef,
|
||||
() => { isOpen = false; }
|
||||
);
|
||||
```
|
||||
|
||||
### useIntersectionObserver
|
||||
|
||||
```typescript
|
||||
// src/lib/hooks/useIntersectionObserver.svelte.ts
|
||||
interface UseIntersectionOptions {
|
||||
threshold?: number | number[];
|
||||
root?: Element | null;
|
||||
rootMargin?: string;
|
||||
}
|
||||
|
||||
export function useIntersectionObserver(
|
||||
getElement: () => HTMLElement | null,
|
||||
options: UseIntersectionOptions = {}
|
||||
) {
|
||||
let isIntersecting = $state(false);
|
||||
let entry = $state<IntersectionObserverEntry | null>(null);
|
||||
|
||||
$effect(() => {
|
||||
const element = getElement();
|
||||
if (!element) return;
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
([e]) => {
|
||||
isIntersecting = e.isIntersecting;
|
||||
entry = e;
|
||||
},
|
||||
{
|
||||
threshold: options.threshold ?? 0,
|
||||
root: options.root ?? null,
|
||||
rootMargin: options.rootMargin ?? '0px'
|
||||
}
|
||||
);
|
||||
|
||||
observer.observe(element);
|
||||
return () => observer.disconnect();
|
||||
});
|
||||
|
||||
return {
|
||||
get isIntersecting() { return isIntersecting; },
|
||||
get entry() { return entry; }
|
||||
};
|
||||
}
|
||||
|
||||
// Usage - Lazy loading
|
||||
let imageRef = $state<HTMLElement>();
|
||||
const { isIntersecting } = useIntersectionObserver(() => imageRef);
|
||||
// Load image when isIntersecting becomes true
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Utility Functions
|
||||
|
||||
### Class Name Utilities
|
||||
|
||||
```typescript
|
||||
// src/lib/utils/cn.ts
|
||||
import { clsx, type ClassValue } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
// Usage
|
||||
cn('px-4 py-2', isActive && 'bg-blue-500', className)
|
||||
```
|
||||
|
||||
### Format Utilities
|
||||
|
||||
```typescript
|
||||
// src/lib/utils/format.ts
|
||||
|
||||
// Currency formatting
|
||||
export function formatCurrency(
|
||||
amount: number,
|
||||
currency = 'USD',
|
||||
locale = 'en-US'
|
||||
): string {
|
||||
return new Intl.NumberFormat(locale, {
|
||||
style: 'currency',
|
||||
currency
|
||||
}).format(amount);
|
||||
}
|
||||
|
||||
// Date formatting
|
||||
export function formatDate(
|
||||
date: Date | string | number,
|
||||
options: Intl.DateTimeFormatOptions = {},
|
||||
locale = 'en-US'
|
||||
): string {
|
||||
const d = date instanceof Date ? date : new Date(date);
|
||||
return new Intl.DateTimeFormat(locale, {
|
||||
dateStyle: 'medium',
|
||||
...options
|
||||
}).format(d);
|
||||
}
|
||||
|
||||
// Relative time
|
||||
export function formatRelativeTime(date: Date | string | number): string {
|
||||
const d = date instanceof Date ? date : new Date(date);
|
||||
const now = new Date();
|
||||
const diffMs = d.getTime() - now.getTime();
|
||||
const diffSec = Math.round(diffMs / 1000);
|
||||
const diffMin = Math.round(diffSec / 60);
|
||||
const diffHour = Math.round(diffMin / 60);
|
||||
const diffDay = Math.round(diffHour / 24);
|
||||
|
||||
const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' });
|
||||
|
||||
if (Math.abs(diffSec) < 60) return rtf.format(diffSec, 'second');
|
||||
if (Math.abs(diffMin) < 60) return rtf.format(diffMin, 'minute');
|
||||
if (Math.abs(diffHour) < 24) return rtf.format(diffHour, 'hour');
|
||||
return rtf.format(diffDay, 'day');
|
||||
}
|
||||
|
||||
// Number formatting
|
||||
export function formatNumber(
|
||||
num: number,
|
||||
options: Intl.NumberFormatOptions = {}
|
||||
): string {
|
||||
return new Intl.NumberFormat('en-US', options).format(num);
|
||||
}
|
||||
|
||||
// Compact number (1K, 1M, etc.)
|
||||
export function formatCompact(num: number): string {
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
notation: 'compact',
|
||||
compactDisplay: 'short'
|
||||
}).format(num);
|
||||
}
|
||||
|
||||
// File size
|
||||
export function formatFileSize(bytes: number): string {
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
let size = bytes;
|
||||
let unitIndex = 0;
|
||||
|
||||
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||
size /= 1024;
|
||||
unitIndex++;
|
||||
}
|
||||
|
||||
return `${size.toFixed(1)} ${units[unitIndex]}`;
|
||||
}
|
||||
```
|
||||
|
||||
### String Utilities
|
||||
|
||||
```typescript
|
||||
// src/lib/utils/string.ts
|
||||
|
||||
// Truncate text
|
||||
export function truncate(str: string, length: number, ending = '...'): string {
|
||||
if (str.length <= length) return str;
|
||||
return str.slice(0, length - ending.length) + ending;
|
||||
}
|
||||
|
||||
// Slugify
|
||||
export function slugify(str: string): string {
|
||||
return str
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.replace(/[^\w\s-]/g, '')
|
||||
.replace(/[\s_-]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '');
|
||||
}
|
||||
|
||||
// Capitalize
|
||||
export function capitalize(str: string): string {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
}
|
||||
|
||||
// Title case
|
||||
export function titleCase(str: string): string {
|
||||
return str
|
||||
.toLowerCase()
|
||||
.split(' ')
|
||||
.map(word => capitalize(word))
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
// Pluralize
|
||||
export function pluralize(
|
||||
count: number,
|
||||
singular: string,
|
||||
plural?: string
|
||||
): string {
|
||||
return count === 1 ? singular : (plural ?? `${singular}s`);
|
||||
}
|
||||
```
|
||||
|
||||
### Array Utilities
|
||||
|
||||
```typescript
|
||||
// src/lib/utils/array.ts
|
||||
|
||||
// Group by key
|
||||
export function groupBy<T>(
|
||||
array: T[],
|
||||
key: keyof T | ((item: T) => string)
|
||||
): Record<string, T[]> {
|
||||
return array.reduce((groups, item) => {
|
||||
const groupKey = typeof key === 'function' ? key(item) : String(item[key]);
|
||||
groups[groupKey] = [...(groups[groupKey] || []), item];
|
||||
return groups;
|
||||
}, {} as Record<string, T[]>);
|
||||
}
|
||||
|
||||
// Unique values
|
||||
export function unique<T>(array: T[]): T[] {
|
||||
return [...new Set(array)];
|
||||
}
|
||||
|
||||
// Unique by key
|
||||
export function uniqueBy<T>(array: T[], key: keyof T): T[] {
|
||||
const seen = new Set();
|
||||
return array.filter(item => {
|
||||
const k = item[key];
|
||||
if (seen.has(k)) return false;
|
||||
seen.add(k);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
// Chunk array
|
||||
export function chunk<T>(array: T[], size: number): T[][] {
|
||||
const chunks: T[][] = [];
|
||||
for (let i = 0; i < array.length; i += size) {
|
||||
chunks.push(array.slice(i, i + size));
|
||||
}
|
||||
return chunks;
|
||||
}
|
||||
|
||||
// Shuffle array
|
||||
export function shuffle<T>(array: T[]): T[] {
|
||||
const result = [...array];
|
||||
for (let i = result.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[result[i], result[j]] = [result[j], result[i]];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Sort by key
|
||||
export function sortBy<T>(
|
||||
array: T[],
|
||||
key: keyof T,
|
||||
order: 'asc' | 'desc' = 'asc'
|
||||
): T[] {
|
||||
return [...array].sort((a, b) => {
|
||||
const aVal = a[key];
|
||||
const bVal = b[key];
|
||||
const compare = aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
|
||||
return order === 'asc' ? compare : -compare;
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Object Utilities
|
||||
|
||||
```typescript
|
||||
// src/lib/utils/object.ts
|
||||
|
||||
// Deep clone
|
||||
export function deepClone<T>(obj: T): T {
|
||||
return structuredClone(obj);
|
||||
}
|
||||
|
||||
// Pick keys
|
||||
export function pick<T extends object, K extends keyof T>(
|
||||
obj: T,
|
||||
keys: K[]
|
||||
): Pick<T, K> {
|
||||
return keys.reduce((result, key) => {
|
||||
if (key in obj) result[key] = obj[key];
|
||||
return result;
|
||||
}, {} as Pick<T, K>);
|
||||
}
|
||||
|
||||
// Omit keys
|
||||
export function omit<T extends object, K extends keyof T>(
|
||||
obj: T,
|
||||
keys: K[]
|
||||
): Omit<T, K> {
|
||||
const result = { ...obj };
|
||||
for (const key of keys) {
|
||||
delete result[key];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Deep equal
|
||||
export function deepEqual(a: unknown, b: unknown): boolean {
|
||||
if (a === b) return true;
|
||||
if (typeof a !== typeof b) return false;
|
||||
if (a == null || b == null) return false;
|
||||
|
||||
if (Array.isArray(a) && Array.isArray(b)) {
|
||||
if (a.length !== b.length) return false;
|
||||
return a.every((val, i) => deepEqual(val, b[i]));
|
||||
}
|
||||
|
||||
if (typeof a === 'object' && typeof b === 'object') {
|
||||
const keysA = Object.keys(a);
|
||||
const keysB = Object.keys(b);
|
||||
if (keysA.length !== keysB.length) return false;
|
||||
return keysA.every(key =>
|
||||
deepEqual((a as any)[key], (b as any)[key])
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Svelte Actions
|
||||
|
||||
### Focus Trap
|
||||
|
||||
```typescript
|
||||
// src/lib/actions/focusTrap.ts
|
||||
export function focusTrap(node: HTMLElement) {
|
||||
const focusableElements = node.querySelectorAll(
|
||||
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
||||
);
|
||||
|
||||
const first = focusableElements[0] as HTMLElement;
|
||||
const last = focusableElements[focusableElements.length - 1] as HTMLElement;
|
||||
|
||||
function handleKeydown(e: KeyboardEvent) {
|
||||
if (e.key !== 'Tab') return;
|
||||
|
||||
if (e.shiftKey) {
|
||||
if (document.activeElement === first) {
|
||||
last.focus();
|
||||
e.preventDefault();
|
||||
}
|
||||
} else {
|
||||
if (document.activeElement === last) {
|
||||
first.focus();
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
node.addEventListener('keydown', handleKeydown);
|
||||
first?.focus();
|
||||
|
||||
return {
|
||||
destroy() {
|
||||
node.removeEventListener('keydown', handleKeydown);
|
||||
}
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Tooltip
|
||||
|
||||
```typescript
|
||||
// src/lib/actions/tooltip.ts
|
||||
interface TooltipOptions {
|
||||
text: string;
|
||||
position?: 'top' | 'bottom' | 'left' | 'right';
|
||||
}
|
||||
|
||||
export function tooltip(node: HTMLElement, options: TooltipOptions) {
|
||||
let tooltipEl: HTMLDivElement | null = null;
|
||||
|
||||
function show() {
|
||||
tooltipEl = document.createElement('div');
|
||||
tooltipEl.className = `tooltip tooltip-${options.position ?? 'top'}`;
|
||||
tooltipEl.textContent = options.text;
|
||||
|
||||
document.body.appendChild(tooltipEl);
|
||||
|
||||
const rect = node.getBoundingClientRect();
|
||||
const tipRect = tooltipEl.getBoundingClientRect();
|
||||
|
||||
// Position based on option
|
||||
switch (options.position ?? 'top') {
|
||||
case 'top':
|
||||
tooltipEl.style.left = `${rect.left + rect.width / 2 - tipRect.width / 2}px`;
|
||||
tooltipEl.style.top = `${rect.top - tipRect.height - 8}px`;
|
||||
break;
|
||||
case 'bottom':
|
||||
tooltipEl.style.left = `${rect.left + rect.width / 2 - tipRect.width / 2}px`;
|
||||
tooltipEl.style.top = `${rect.bottom + 8}px`;
|
||||
break;
|
||||
// ... other positions
|
||||
}
|
||||
}
|
||||
|
||||
function hide() {
|
||||
tooltipEl?.remove();
|
||||
tooltipEl = null;
|
||||
}
|
||||
|
||||
node.addEventListener('mouseenter', show);
|
||||
node.addEventListener('mouseleave', hide);
|
||||
|
||||
return {
|
||||
update(newOptions: TooltipOptions) {
|
||||
options = newOptions;
|
||||
},
|
||||
destroy() {
|
||||
hide();
|
||||
node.removeEventListener('mouseenter', show);
|
||||
node.removeEventListener('mouseleave', hide);
|
||||
}
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Auto-resize Textarea
|
||||
|
||||
```typescript
|
||||
// src/lib/actions/autoResize.ts
|
||||
export function autoResize(node: HTMLTextAreaElement) {
|
||||
function resize() {
|
||||
node.style.height = 'auto';
|
||||
node.style.height = `${node.scrollHeight}px`;
|
||||
}
|
||||
|
||||
resize();
|
||||
node.addEventListener('input', resize);
|
||||
|
||||
return {
|
||||
destroy() {
|
||||
node.removeEventListener('input', resize);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Usage: <textarea use:autoResize />
|
||||
```
|
||||
|
||||
### Copy to Clipboard
|
||||
|
||||
```typescript
|
||||
// src/lib/actions/clipboard.ts
|
||||
interface ClipboardOptions {
|
||||
text: string;
|
||||
onCopy?: () => void;
|
||||
onError?: (error: Error) => void;
|
||||
}
|
||||
|
||||
export function clipboard(node: HTMLElement, options: ClipboardOptions) {
|
||||
async function handleClick() {
|
||||
try {
|
||||
await navigator.clipboard.writeText(options.text);
|
||||
options.onCopy?.();
|
||||
} catch (error) {
|
||||
options.onError?.(error as Error);
|
||||
}
|
||||
}
|
||||
|
||||
node.addEventListener('click', handleClick);
|
||||
|
||||
return {
|
||||
update(newOptions: ClipboardOptions) {
|
||||
options = newOptions;
|
||||
},
|
||||
destroy() {
|
||||
node.removeEventListener('click', handleClick);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Usage
|
||||
// <button use:clipboard={{ text: 'Copy me!', onCopy: () => showToast('Copied!') }}>
|
||||
// Copy
|
||||
// </button>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 Type Utilities
|
||||
|
||||
```typescript
|
||||
// src/lib/types/utils.ts
|
||||
|
||||
// Make all properties optional recursively
|
||||
export type DeepPartial<T> = {
|
||||
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
|
||||
};
|
||||
|
||||
// Make specified keys required
|
||||
export type RequireKeys<T, K extends keyof T> = T & Required<Pick<T, K>>;
|
||||
|
||||
// Extract array element type
|
||||
export type ArrayElement<T> = T extends readonly (infer U)[] ? U : never;
|
||||
|
||||
// Async function return type
|
||||
export type AsyncReturnType<T extends (...args: any) => Promise<any>> =
|
||||
T extends (...args: any) => Promise<infer R> ? R : never;
|
||||
|
||||
// Make all properties non-nullable
|
||||
export type NonNullableProps<T> = {
|
||||
[P in keyof T]: NonNullable<T[P]>;
|
||||
};
|
||||
|
||||
// Record with known keys
|
||||
export type StrictRecord<K extends string, V> = { [P in K]: V };
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔗 Export Index
|
||||
|
||||
```typescript
|
||||
// src/lib/utils/index.ts
|
||||
export * from './cn';
|
||||
export * from './format';
|
||||
export * from './string';
|
||||
export * from './array';
|
||||
export * from './object';
|
||||
|
||||
// src/lib/hooks/index.ts
|
||||
export * from './useLocalStorage.svelte';
|
||||
export * from './useDebounce.svelte';
|
||||
export * from './useThrottle.svelte';
|
||||
export * from './useMediaQuery.svelte';
|
||||
export * from './useOnline.svelte';
|
||||
export * from './useClickOutside.svelte';
|
||||
export * from './useIntersectionObserver.svelte';
|
||||
|
||||
// src/lib/actions/index.ts
|
||||
export * from './focusTrap';
|
||||
export * from './tooltip';
|
||||
export * from './autoResize';
|
||||
export * from './clipboard';
|
||||
```
|
||||
Reference in New Issue
Block a user