@stacksjs/components 0.2.111 → 0.2.113
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/dist/{Sidebar-4hn7vw4n.stx → Sidebar-gbd5ctkp.stx} +84 -3
- package/dist/{SidebarItem-gtrcw4ky.stx → SidebarItem-9rvbwhxr.stx} +11 -7
- package/dist/index.js +14 -11
- package/dist/ui/sidebar/themes.d.ts +25 -3
- package/package.json +5 -4
- package/src/components/CodeBlock.stx +54 -0
- package/src/components/Footer.stx +66 -0
- package/src/components/Hero.stx +25 -0
- package/src/components/Installation.stx +37 -0
- package/src/components/index.ts +4 -0
- package/src/composables/index.ts +4 -0
- package/src/composables/useCopyCode.ts +73 -0
- package/src/composables/useDarkMode.ts +100 -0
- package/src/composables/useSEO.ts +230 -0
- package/src/composables/utilities/index.ts +7 -0
- package/src/composables/utilities/useClickOutside.ts +72 -0
- package/src/composables/utilities/useFocusTrap.ts +159 -0
- package/src/composables/utilities/useKeyboard.ts +124 -0
- package/src/composables/utilities/useLocalStorage.ts +132 -0
- package/src/composables/utilities/useMediaQuery.ts +118 -0
- package/src/index.ts +99 -0
- package/src/stx.d.ts +4 -0
- package/src/ui/accordion/Accordion.stx +146 -0
- package/src/ui/accordion/AccordionItem.stx +37 -0
- package/src/ui/accordion/index.ts +47 -0
- package/src/ui/audio/Audio.stx +50 -0
- package/src/ui/audio/README.md +41 -0
- package/src/ui/audio/index.ts +13 -0
- package/src/ui/auth/Login.stx +161 -0
- package/src/ui/auth/README.md +142 -0
- package/src/ui/auth/Signup.stx +159 -0
- package/src/ui/auth/TwoFactorChallenge.stx +111 -0
- package/src/ui/auth/index.ts +32 -0
- package/src/ui/avatar/Avatar.stx +77 -0
- package/src/ui/avatar/index.ts +12 -0
- package/src/ui/badge/Badge.stx +62 -0
- package/src/ui/badge/index.ts +10 -0
- package/src/ui/breadcrumb/Breadcrumb.stx +79 -0
- package/src/ui/breadcrumb/index.ts +15 -0
- package/src/ui/button/Button.stx +74 -0
- package/src/ui/button/README.md +80 -0
- package/src/ui/button/index.ts +85 -0
- package/src/ui/calendar/Calendar.stx +189 -0
- package/src/ui/calendar/README.md +254 -0
- package/src/ui/calendar/index.ts +14 -0
- package/src/ui/card/Card.stx +68 -0
- package/src/ui/card/index.ts +13 -0
- package/src/ui/checkbox/Checkbox.stx +86 -0
- package/src/ui/checkbox/index.ts +16 -0
- package/src/ui/combobox/Combobox.stx +96 -0
- package/src/ui/combobox/ComboboxButton.stx +25 -0
- package/src/ui/combobox/ComboboxInput.stx +32 -0
- package/src/ui/combobox/ComboboxOption.stx +49 -0
- package/src/ui/combobox/ComboboxOptions.stx +16 -0
- package/src/ui/combobox/README.md +150 -0
- package/src/ui/combobox/index.ts +40 -0
- package/src/ui/command-palette/CommandPalette.stx +81 -0
- package/src/ui/command-palette/CommandPaletteItem.stx +27 -0
- package/src/ui/command-palette/README.md +42 -0
- package/src/ui/command-palette/index.ts +15 -0
- package/src/ui/dialog/Dialog.stx +41 -0
- package/src/ui/dialog/DialogBackdrop.stx +9 -0
- package/src/ui/dialog/DialogDescription.stx +11 -0
- package/src/ui/dialog/DialogPanel.stx +16 -0
- package/src/ui/dialog/DialogTitle.stx +11 -0
- package/src/ui/dialog/README.md +121 -0
- package/src/ui/dialog/index.ts +32 -0
- package/src/ui/drawer/Drawer.stx +120 -0
- package/src/ui/drawer/README.md +90 -0
- package/src/ui/drawer/index.ts +9 -0
- package/src/ui/dropdown/Dropdown.stx +82 -0
- package/src/ui/dropdown/DropdownButton.stx +29 -0
- package/src/ui/dropdown/DropdownItem.stx +35 -0
- package/src/ui/dropdown/DropdownItems.stx +18 -0
- package/src/ui/dropdown/README.md +114 -0
- package/src/ui/dropdown/index.ts +29 -0
- package/src/ui/form/Form.stx +95 -0
- package/src/ui/form/index.ts +67 -0
- package/src/ui/heatmap/Heatmap.stx +294 -0
- package/src/ui/image/Image.stx +165 -0
- package/src/ui/image/README.md +164 -0
- package/src/ui/image/index.ts +22 -0
- package/src/ui/input/EmailInput.stx +30 -0
- package/src/ui/input/NumberInput.stx +126 -0
- package/src/ui/input/PasswordInput.stx +143 -0
- package/src/ui/input/SearchInput.stx +44 -0
- package/src/ui/input/TextInput.stx +132 -0
- package/src/ui/input/index.ts +180 -0
- package/src/ui/listbox/Listbox.stx +101 -0
- package/src/ui/listbox/ListboxButton.stx +28 -0
- package/src/ui/listbox/ListboxLabel.stx +11 -0
- package/src/ui/listbox/ListboxOption.stx +49 -0
- package/src/ui/listbox/ListboxOptions.stx +16 -0
- package/src/ui/listbox/README.md +135 -0
- package/src/ui/listbox/index.ts +39 -0
- package/src/ui/navigator/Navigator.stx +113 -0
- package/src/ui/navigator/README.md +352 -0
- package/src/ui/navigator/index.ts +20 -0
- package/src/ui/notification/Notification.stx +117 -0
- package/src/ui/notification/README.md +114 -0
- package/src/ui/notification/index.ts +12 -0
- package/src/ui/pagination/Pagination.stx +150 -0
- package/src/ui/pagination/index.ts +11 -0
- package/src/ui/payment/Checkout.stx +31 -0
- package/src/ui/payment/DefaultPaymentMethod.stx +62 -0
- package/src/ui/payment/PaymentMethods.stx +104 -0
- package/src/ui/payment/README.md +345 -0
- package/src/ui/payment/SubscriptionCheckout.stx +323 -0
- package/src/ui/payment/index.ts +59 -0
- package/src/ui/popover/Popover.stx +60 -0
- package/src/ui/popover/PopoverButton.stx +29 -0
- package/src/ui/popover/PopoverPanel.stx +23 -0
- package/src/ui/popover/README.md +91 -0
- package/src/ui/popover/index.ts +22 -0
- package/src/ui/portal/Portal.stx +57 -0
- package/src/ui/portal/index.ts +6 -0
- package/src/ui/progress/Progress.stx +131 -0
- package/src/ui/progress/index.ts +12 -0
- package/src/ui/radio/Radio.stx +77 -0
- package/src/ui/radio/index.ts +15 -0
- package/src/ui/radio-group/README.md +139 -0
- package/src/ui/radio-group/RadioGroup.stx +60 -0
- package/src/ui/radio-group/RadioGroupDescription.stx +11 -0
- package/src/ui/radio-group/RadioGroupLabel.stx +11 -0
- package/src/ui/radio-group/RadioGroupOption.stx +58 -0
- package/src/ui/radio-group/index.ts +30 -0
- package/src/ui/select/README.md +180 -0
- package/src/ui/select/Select.stx +82 -0
- package/src/ui/select/index.ts +21 -0
- package/src/ui/sidebar/Sidebar.stx +557 -0
- package/src/ui/sidebar/SidebarFooter.stx +71 -0
- package/src/ui/sidebar/SidebarHeader.stx +103 -0
- package/src/ui/sidebar/SidebarItem.stx +90 -0
- package/src/ui/sidebar/SidebarSection.stx +83 -0
- package/src/ui/sidebar/index.ts +146 -0
- package/src/ui/sidebar/themes.ts +277 -0
- package/src/ui/skeleton/Skeleton.stx +48 -0
- package/src/ui/skeleton/index.ts +11 -0
- package/src/ui/spinner/Spinner.stx +109 -0
- package/src/ui/spinner/index.ts +9 -0
- package/src/ui/stepper/README.md +36 -0
- package/src/ui/stepper/Stepper.stx +17 -0
- package/src/ui/stepper/StepperStep.stx +57 -0
- package/src/ui/stepper/index.ts +19 -0
- package/src/ui/storage/README.md +193 -0
- package/src/ui/storage/index.ts +347 -0
- package/src/ui/switch/README.md +114 -0
- package/src/ui/switch/Switch.stx +72 -0
- package/src/ui/switch/index.ts +10 -0
- package/src/ui/table/README.md +39 -0
- package/src/ui/table/Table.stx +14 -0
- package/src/ui/table/TableBody.stx +10 -0
- package/src/ui/table/TableCell.stx +10 -0
- package/src/ui/table/TableHead.stx +10 -0
- package/src/ui/table/TableHeader.stx +20 -0
- package/src/ui/table/TableRow.stx +11 -0
- package/src/ui/table/index.ts +35 -0
- package/src/ui/tabs/TabPanel.stx +25 -0
- package/src/ui/tabs/Tabs.stx +202 -0
- package/src/ui/tabs/index.ts +50 -0
- package/src/ui/teleport/Teleport.stx +65 -0
- package/src/ui/teleport/index.ts +7 -0
- package/src/ui/textarea/Textarea.stx +108 -0
- package/src/ui/textarea/index.ts +20 -0
- package/src/ui/tooltip/Tooltip.stx +71 -0
- package/src/ui/tooltip/index.ts +10 -0
- package/src/ui/transition/README.md +30 -0
- package/src/ui/transition/Transition.stx +102 -0
- package/src/ui/transition/index.ts +25 -0
- package/src/ui/video/README.md +47 -0
- package/src/ui/video/Video.stx +45 -0
- package/src/ui/video/index.ts +16 -0
- package/src/ui/virtual-list/VirtualList.stx +67 -0
- package/src/ui/virtual-list/index.ts +10 -0
- package/src/ui/virtual-table/VirtualTable.stx +103 -0
- package/src/ui/virtual-table/index.ts +21 -0
- package/src/utils/PROP_VALIDATION.md +458 -0
- package/src/utils/THEME_SYSTEM.md +702 -0
- package/src/utils/accessibility.ts +841 -0
- package/src/utils/animation/easing.ts +368 -0
- package/src/utils/animation/helpers.ts +223 -0
- package/src/utils/animation/index.ts +5 -0
- package/src/utils/animation/keyframes.ts +230 -0
- package/src/utils/error-handling.ts +475 -0
- package/src/utils/highlighter.ts +93 -0
- package/src/utils/i18n.ts +634 -0
- package/src/utils/keep-alive.ts +819 -0
- package/src/utils/lazy-loading.ts +530 -0
- package/src/utils/prop-validation.ts +478 -0
- package/src/utils/theme.ts +626 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# Storage Utilities
|
|
2
|
+
|
|
3
|
+
Type-safe utilities for localStorage and sessionStorage with expiration support.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun add @stacksjs/components
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
### Basic Usage
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { localStorage, sessionStorage } from '@stacksjs/components'
|
|
17
|
+
|
|
18
|
+
// Set a value
|
|
19
|
+
localStorage.set('user', { name: 'John', age: 30 })
|
|
20
|
+
|
|
21
|
+
// Get a value
|
|
22
|
+
const user = localStorage.get('user')
|
|
23
|
+
console.log(user) // { name: 'John', age: 30 }
|
|
24
|
+
|
|
25
|
+
// Remove a value
|
|
26
|
+
localStorage.remove('user')
|
|
27
|
+
|
|
28
|
+
// Clear all
|
|
29
|
+
localStorage.clear()
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### With Expiration
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
// Expire after 1 hour (3600000 ms)
|
|
36
|
+
localStorage.set('token', 'abc123', { expire: 3600000 })
|
|
37
|
+
|
|
38
|
+
// After 1 hour, get() returns null
|
|
39
|
+
const token = localStorage.get('token') // null if expired
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### With Namespace
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
import { createStorage } from '@stacksjs/components'
|
|
46
|
+
|
|
47
|
+
const userStorage = createStorage('local', 'user')
|
|
48
|
+
const appStorage = createStorage('local', 'app')
|
|
49
|
+
|
|
50
|
+
userStorage.set('preferences', { theme: 'dark' })
|
|
51
|
+
appStorage.set('preferences', { language: 'en' })
|
|
52
|
+
|
|
53
|
+
// These are stored as 'user:preferences' and 'app:preferences'
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Session Storage
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
import { sessionStorage } from '@stacksjs/components'
|
|
60
|
+
|
|
61
|
+
// Same API as localStorage
|
|
62
|
+
sessionStorage.set('tempData', { value: 123 })
|
|
63
|
+
const data = sessionStorage.get('tempData')
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## API
|
|
67
|
+
|
|
68
|
+
### Storage Methods
|
|
69
|
+
|
|
70
|
+
#### `set(key: string, value: any, options?: StorageOptions): void`
|
|
71
|
+
Store a value with optional expiration.
|
|
72
|
+
|
|
73
|
+
#### `get<T>(key: string): T | null`
|
|
74
|
+
Retrieve a value. Returns null if expired or not found.
|
|
75
|
+
|
|
76
|
+
#### `remove(key: string): void`
|
|
77
|
+
Remove a specific key.
|
|
78
|
+
|
|
79
|
+
#### `clear(): void`
|
|
80
|
+
Clear all items (or all namespaced items).
|
|
81
|
+
|
|
82
|
+
#### `has(key: string): boolean`
|
|
83
|
+
Check if a key exists and hasn't expired.
|
|
84
|
+
|
|
85
|
+
#### `keys(): string[]`
|
|
86
|
+
Get all keys (without namespace prefix).
|
|
87
|
+
|
|
88
|
+
#### `size(): number`
|
|
89
|
+
Get the number of stored items.
|
|
90
|
+
|
|
91
|
+
## StorageOptions
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
interface StorageOptions {
|
|
95
|
+
expire?: number // Expiration in milliseconds
|
|
96
|
+
namespace?: string // Prefix for keys (in createStorage)
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Features
|
|
101
|
+
|
|
102
|
+
- **Type-safe** - Full TypeScript support
|
|
103
|
+
- **Expiration** - Automatic expiration handling
|
|
104
|
+
- **Namespacing** - Prevent key collisions
|
|
105
|
+
- **Error handling** - Graceful error handling
|
|
106
|
+
- **JSON serialization** - Automatic serialization/deserialization
|
|
107
|
+
- **Both storages** - localStorage and sessionStorage support
|
|
108
|
+
|
|
109
|
+
## Examples
|
|
110
|
+
|
|
111
|
+
### User Authentication Token
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
import { localStorage } from '@stacksjs/components'
|
|
115
|
+
|
|
116
|
+
// Store token with 24-hour expiration
|
|
117
|
+
function login(token: string) {
|
|
118
|
+
localStorage.set('auth_token', token, {
|
|
119
|
+
expire: 24 * 60 * 60 * 1000, // 24 hours
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Get token (returns null if expired)
|
|
124
|
+
function getToken(): string | null {
|
|
125
|
+
return localStorage.get('auth_token')
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Check if logged in
|
|
129
|
+
function isLoggedIn(): boolean {
|
|
130
|
+
return localStorage.has('auth_token')
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Logout
|
|
134
|
+
function logout() {
|
|
135
|
+
localStorage.remove('auth_token')
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### User Preferences
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
import { createStorage } from '@stacksjs/components'
|
|
143
|
+
|
|
144
|
+
const prefs = createStorage('local', 'preferences')
|
|
145
|
+
|
|
146
|
+
interface UserPreferences {
|
|
147
|
+
theme: 'light' | 'dark'
|
|
148
|
+
language: string
|
|
149
|
+
notifications: boolean
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Save preferences
|
|
153
|
+
function savePreferences(preferences: UserPreferences) {
|
|
154
|
+
prefs.set('user', preferences)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Load preferences with defaults
|
|
158
|
+
function loadPreferences(): UserPreferences {
|
|
159
|
+
const saved = prefs.get<UserPreferences>('user')
|
|
160
|
+
return saved || {
|
|
161
|
+
theme: 'light',
|
|
162
|
+
language: 'en',
|
|
163
|
+
notifications: true,
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Shopping Cart
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
import { localStorage } from '@stacksjs/components'
|
|
172
|
+
|
|
173
|
+
interface CartItem {
|
|
174
|
+
id: string
|
|
175
|
+
name: string
|
|
176
|
+
quantity: number
|
|
177
|
+
price: number
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function addToCart(item: CartItem) {
|
|
181
|
+
const cart = localStorage.get<CartItem[]>('cart') || []
|
|
182
|
+
cart.push(item)
|
|
183
|
+
localStorage.set('cart', cart)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function getCart(): CartItem[] {
|
|
187
|
+
return localStorage.get<CartItem[]>('cart') || []
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function clearCart() {
|
|
191
|
+
localStorage.remove('cart')
|
|
192
|
+
}
|
|
193
|
+
```
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
// Storage utilities for localStorage and sessionStorage
|
|
2
|
+
|
|
3
|
+
export interface StorageOptions {
|
|
4
|
+
expire?: number // Expiration in milliseconds
|
|
5
|
+
namespace?: string // Prefix for keys
|
|
6
|
+
encrypt?: boolean // Encrypt data (basic XOR encryption)
|
|
7
|
+
compress?: boolean // Compress large objects
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
type StorageWatchCallback = (_newValue: any, _oldValue: any, _key: string) => void
|
|
11
|
+
|
|
12
|
+
export class StorageManager {
|
|
13
|
+
private storage: globalThis.Storage
|
|
14
|
+
private namespace: string
|
|
15
|
+
private watchers: Map<string, Set<StorageWatchCallback>> = new Map()
|
|
16
|
+
private encryptionKey = 'stx-storage-key' // Simple encryption key
|
|
17
|
+
|
|
18
|
+
constructor(type: 'local' | 'session' = 'local', namespace = '') {
|
|
19
|
+
this.storage = type === 'local' ? globalThis.localStorage : globalThis.sessionStorage
|
|
20
|
+
this.namespace = namespace
|
|
21
|
+
|
|
22
|
+
// Listen for storage events from other tabs/windows
|
|
23
|
+
if (typeof window !== 'undefined') {
|
|
24
|
+
window.addEventListener('storage', this.handleStorageEvent.bind(this))
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
private handleStorageEvent(event: StorageEvent): void {
|
|
29
|
+
if (!event.key)
|
|
30
|
+
return
|
|
31
|
+
|
|
32
|
+
const key = this.namespace && event.key.startsWith(`${this.namespace}:`)
|
|
33
|
+
? event.key.slice(this.namespace.length + 1)
|
|
34
|
+
: event.key
|
|
35
|
+
|
|
36
|
+
const callbacks = this.watchers.get(key)
|
|
37
|
+
if (callbacks) {
|
|
38
|
+
const oldValue = event.oldValue ? JSON.parse(event.oldValue).value : null
|
|
39
|
+
const newValue = event.newValue ? JSON.parse(event.newValue).value : null
|
|
40
|
+
callbacks.forEach(callback => callback(newValue, oldValue, key))
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private encrypt(data: string): string {
|
|
45
|
+
// Simple XOR encryption (not cryptographically secure, just obfuscation)
|
|
46
|
+
let result = ''
|
|
47
|
+
for (let i = 0; i < data.length; i++) {
|
|
48
|
+
result += String.fromCharCode(data.charCodeAt(i) ^ this.encryptionKey.charCodeAt(i % this.encryptionKey.length))
|
|
49
|
+
}
|
|
50
|
+
return btoa(result) // Base64 encode
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private decrypt(data: string): string {
|
|
54
|
+
try {
|
|
55
|
+
const decoded = atob(data) // Base64 decode
|
|
56
|
+
let result = ''
|
|
57
|
+
for (let i = 0; i < decoded.length; i++) {
|
|
58
|
+
result += String.fromCharCode(decoded.charCodeAt(i) ^ this.encryptionKey.charCodeAt(i % this.encryptionKey.length))
|
|
59
|
+
}
|
|
60
|
+
return result
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return data // Return as-is if decryption fails
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private compress(data: string): string {
|
|
68
|
+
// Simple LZ-based compression for large strings
|
|
69
|
+
if (data.length < 100)
|
|
70
|
+
return data // Don't compress small strings
|
|
71
|
+
|
|
72
|
+
const dict: Record<string, number> = {}
|
|
73
|
+
let dictSize = 256
|
|
74
|
+
const result: number[] = []
|
|
75
|
+
let w = ''
|
|
76
|
+
|
|
77
|
+
for (let i = 0; i < data.length; i++) {
|
|
78
|
+
const c = data.charAt(i)
|
|
79
|
+
const wc = w + c
|
|
80
|
+
|
|
81
|
+
if (dict[wc] !== undefined) {
|
|
82
|
+
w = wc
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
result.push(w.length > 1 ? dict[w] : w.charCodeAt(0))
|
|
86
|
+
dict[wc] = dictSize++
|
|
87
|
+
w = c
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (w !== '') {
|
|
92
|
+
result.push(w.length > 1 ? dict[w] : w.charCodeAt(0))
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return JSON.stringify(result)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private decompress(data: string): string {
|
|
99
|
+
try {
|
|
100
|
+
const compressed = JSON.parse(data)
|
|
101
|
+
if (!Array.isArray(compressed))
|
|
102
|
+
return data
|
|
103
|
+
|
|
104
|
+
const dict: Record<number, string> = {}
|
|
105
|
+
let dictSize = 256
|
|
106
|
+
let w = String.fromCharCode(compressed[0])
|
|
107
|
+
let result = w
|
|
108
|
+
|
|
109
|
+
for (let i = 1; i < compressed.length; i++) {
|
|
110
|
+
const k = compressed[i]
|
|
111
|
+
let entry: string
|
|
112
|
+
|
|
113
|
+
if (dict[k] !== undefined) {
|
|
114
|
+
entry = dict[k]
|
|
115
|
+
}
|
|
116
|
+
else if (k === dictSize) {
|
|
117
|
+
entry = w + w.charAt(0)
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
return data // Decompression failed
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
result += entry
|
|
124
|
+
dict[dictSize++] = w + entry.charAt(0)
|
|
125
|
+
w = entry
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return result
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return data // Return as-is if decompression fails
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private getKey(key: string): string {
|
|
136
|
+
return this.namespace ? `${this.namespace}:${key}` : key
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
set(key: string, value: any, options?: StorageOptions): void {
|
|
140
|
+
const fullKey = this.getKey(key)
|
|
141
|
+
const oldValue = this.get(key)
|
|
142
|
+
|
|
143
|
+
const data = {
|
|
144
|
+
value,
|
|
145
|
+
timestamp: Date.now(),
|
|
146
|
+
expire: options?.expire,
|
|
147
|
+
encrypted: options?.encrypt || false,
|
|
148
|
+
compressed: options?.compress || false,
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
let serialized = JSON.stringify(data)
|
|
153
|
+
|
|
154
|
+
// Compress if enabled
|
|
155
|
+
if (options?.compress) {
|
|
156
|
+
serialized = this.compress(serialized)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Encrypt if enabled
|
|
160
|
+
if (options?.encrypt) {
|
|
161
|
+
serialized = this.encrypt(serialized)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
this.storage.setItem(fullKey, serialized)
|
|
165
|
+
|
|
166
|
+
// Notify watchers
|
|
167
|
+
this.notifyWatchers(key, value, oldValue)
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
console.error('Storage set error:', error)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
get<T = any>(key: string): T | null {
|
|
175
|
+
const fullKey = this.getKey(key)
|
|
176
|
+
|
|
177
|
+
try {
|
|
178
|
+
let item = this.storage.getItem(fullKey)
|
|
179
|
+
if (!item)
|
|
180
|
+
return null
|
|
181
|
+
|
|
182
|
+
// Try to decrypt if it looks encrypted (base64)
|
|
183
|
+
if (item.match(/^[A-Z0-9+/]+=*$/i)) {
|
|
184
|
+
try {
|
|
185
|
+
item = this.decrypt(item)
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
// Not encrypted, continue
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Try to decompress if it looks compressed (starts with [)
|
|
193
|
+
if (item.startsWith('[')) {
|
|
194
|
+
try {
|
|
195
|
+
item = this.decompress(item)
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
// Not compressed, continue
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const data = JSON.parse(item)
|
|
203
|
+
|
|
204
|
+
// Check expiration
|
|
205
|
+
if (data.expire && Date.now() - data.timestamp > data.expire) {
|
|
206
|
+
this.remove(key)
|
|
207
|
+
return null
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return data.value as T
|
|
211
|
+
}
|
|
212
|
+
catch (error) {
|
|
213
|
+
console.error('Storage get error:', error)
|
|
214
|
+
return null
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
watch(key: string, callback: StorageWatchCallback): () => void {
|
|
219
|
+
if (!this.watchers.has(key)) {
|
|
220
|
+
this.watchers.set(key, new Set())
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
this.watchers.get(key)!.add(callback)
|
|
224
|
+
|
|
225
|
+
// Return unwatch function
|
|
226
|
+
return () => {
|
|
227
|
+
const callbacks = this.watchers.get(key)
|
|
228
|
+
if (callbacks) {
|
|
229
|
+
callbacks.delete(callback)
|
|
230
|
+
if (callbacks.size === 0) {
|
|
231
|
+
this.watchers.delete(key)
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
private notifyWatchers(key: string, newValue: any, oldValue: any): void {
|
|
238
|
+
const callbacks = this.watchers.get(key)
|
|
239
|
+
if (callbacks) {
|
|
240
|
+
callbacks.forEach(callback => callback(newValue, oldValue, key))
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
remove(key: string): void {
|
|
245
|
+
const fullKey = this.getKey(key)
|
|
246
|
+
const oldValue = this.get(key)
|
|
247
|
+
this.storage.removeItem(fullKey)
|
|
248
|
+
|
|
249
|
+
// Notify watchers
|
|
250
|
+
this.notifyWatchers(key, null, oldValue)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
clear(): void {
|
|
254
|
+
if (this.namespace) {
|
|
255
|
+
// Clear only namespaced items
|
|
256
|
+
const keys = this.keys()
|
|
257
|
+
keys.forEach(key => this.remove(key))
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
this.storage.clear()
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
has(key: string): boolean {
|
|
265
|
+
return this.get(key) !== null
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
keys(): string[] {
|
|
269
|
+
const allKeys = Object.keys(this.storage)
|
|
270
|
+
|
|
271
|
+
if (this.namespace) {
|
|
272
|
+
const prefix = `${this.namespace}:`
|
|
273
|
+
return allKeys
|
|
274
|
+
.filter(key => key.startsWith(prefix))
|
|
275
|
+
.map(key => key.slice(prefix.length))
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return allKeys
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
size(): number {
|
|
282
|
+
return this.keys().length
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Get storage quota information (if available)
|
|
286
|
+
getQuota(): { usage: number, quota: number, percentage: number } | null {
|
|
287
|
+
if ('storage' in navigator && 'estimate' in navigator.storage) {
|
|
288
|
+
// Use async/await pattern but return promise
|
|
289
|
+
return null // This would need to be async, but keeping sync API
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Fallback: estimate based on current data
|
|
293
|
+
try {
|
|
294
|
+
let totalSize = 0
|
|
295
|
+
for (let i = 0; i < this.storage.length; i++) {
|
|
296
|
+
const key = this.storage.key(i)
|
|
297
|
+
if (key) {
|
|
298
|
+
const value = this.storage.getItem(key) || ''
|
|
299
|
+
totalSize += key.length + value.length
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Typical localStorage quota is 5-10MB
|
|
304
|
+
const estimatedQuota = 5 * 1024 * 1024 // 5MB
|
|
305
|
+
return {
|
|
306
|
+
usage: totalSize * 2, // Multiply by 2 for UTF-16 encoding
|
|
307
|
+
quota: estimatedQuota,
|
|
308
|
+
percentage: (totalSize * 2 / estimatedQuota) * 100,
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
catch {
|
|
312
|
+
return null
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Batch operations
|
|
317
|
+
setMany(items: Record<string, any>, options?: StorageOptions): void {
|
|
318
|
+
Object.entries(items).forEach(([key, value]) => {
|
|
319
|
+
this.set(key, value, options)
|
|
320
|
+
})
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
getMany<T = any>(keys: string[]): Record<string, T | null> {
|
|
324
|
+
const result: Record<string, T | null> = {}
|
|
325
|
+
keys.forEach((key) => {
|
|
326
|
+
result[key] = this.get<T>(key)
|
|
327
|
+
})
|
|
328
|
+
return result
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
removeMany(keys: string[]): void {
|
|
332
|
+
keys.forEach(key => this.remove(key))
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Convenience exports
|
|
337
|
+
export const storage: {
|
|
338
|
+
local: StorageManager
|
|
339
|
+
session: StorageManager
|
|
340
|
+
} = {
|
|
341
|
+
local: new StorageManager('local'),
|
|
342
|
+
session: new StorageManager('session'),
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export function createStorage(type: 'local' | 'session' = 'local', namespace?: string): StorageManager {
|
|
346
|
+
return new StorageManager(type, namespace)
|
|
347
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# Switch Component
|
|
2
|
+
|
|
3
|
+
A modern toggle switch component built with STX and headwind utility classes, inspired by Headless UI.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun add @stacksjs/components
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```stx
|
|
14
|
+
<Switch checked label="Enable notifications" />
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Props
|
|
18
|
+
|
|
19
|
+
| Prop | Type | Default | Description |
|
|
20
|
+
|------|------|---------|-------------|
|
|
21
|
+
| `checked` | `boolean` | `false` | Whether the switch is checked |
|
|
22
|
+
| `disabled` | `boolean` | `false` | Disable the switch |
|
|
23
|
+
| `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Switch size |
|
|
24
|
+
| `className` | `string` | `''` | Additional CSS classes |
|
|
25
|
+
| `onChange` | `(checked: boolean) => void` | - | Change handler |
|
|
26
|
+
| `label` | `string` | `''` | Optional label text |
|
|
27
|
+
|
|
28
|
+
## Examples
|
|
29
|
+
|
|
30
|
+
### Basic Switch
|
|
31
|
+
|
|
32
|
+
```stx
|
|
33
|
+
<script server>
|
|
34
|
+
let enabled = false
|
|
35
|
+
|
|
36
|
+
function handleChange(checked) {
|
|
37
|
+
enabled = checked
|
|
38
|
+
console.log('Switch is now:', checked)
|
|
39
|
+
}
|
|
40
|
+
</script>
|
|
41
|
+
|
|
42
|
+
<Switch :checked="enabled" :onChange="handleChange" />
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### With Label
|
|
46
|
+
|
|
47
|
+
```stx
|
|
48
|
+
<Switch checked label="Enable notifications" :onChange="handleChange" />
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Sizes
|
|
52
|
+
|
|
53
|
+
```stx
|
|
54
|
+
<Switch size="sm" checked />
|
|
55
|
+
|
|
56
|
+
<Switch size="md" checked />
|
|
57
|
+
|
|
58
|
+
<Switch size="lg" checked />
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Disabled State
|
|
62
|
+
|
|
63
|
+
```stx
|
|
64
|
+
<Switch checked disabled label="Cannot toggle" />
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Custom Styling
|
|
68
|
+
|
|
69
|
+
```stx
|
|
70
|
+
<Switch checked className="my-4" />
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Accessibility
|
|
74
|
+
|
|
75
|
+
The Switch component follows WAI-ARIA best practices:
|
|
76
|
+
|
|
77
|
+
- Proper `role="switch"` attribute
|
|
78
|
+
- `aria-checked` reflects the current state
|
|
79
|
+
- `aria-disabled` when disabled
|
|
80
|
+
- Screen reader text via `sr-only` class
|
|
81
|
+
- Keyboard accessible (can be toggled with Space/Enter)
|
|
82
|
+
- Focus visible ring for keyboard navigation
|
|
83
|
+
|
|
84
|
+
## Features
|
|
85
|
+
|
|
86
|
+
- Smooth animations and transitions
|
|
87
|
+
- Three size options (sm, md, lg)
|
|
88
|
+
- Dark mode support
|
|
89
|
+
- Disabled state
|
|
90
|
+
- Optional label
|
|
91
|
+
- Accessible by default
|
|
92
|
+
- Headwind utility classes for easy customization
|
|
93
|
+
- Inspired by Headless UI design
|
|
94
|
+
|
|
95
|
+
## Styling
|
|
96
|
+
|
|
97
|
+
The component uses headwind utility classes for styling. You can customize it by:
|
|
98
|
+
|
|
99
|
+
1. Passing additional classes via `className` prop
|
|
100
|
+
2. Modifying the component source
|
|
101
|
+
3. Using headwind configuration to adjust the design system
|
|
102
|
+
|
|
103
|
+
### Color Customization
|
|
104
|
+
|
|
105
|
+
To customize colors, you can:
|
|
106
|
+
|
|
107
|
+
- Override the checked state background color in your headwind config
|
|
108
|
+
- Pass custom classes via `className` prop
|
|
109
|
+
- Modify the component's source code
|
|
110
|
+
|
|
111
|
+
Example:
|
|
112
|
+
```stx
|
|
113
|
+
<Switch checked className="!bg-green-500" />
|
|
114
|
+
```
|