sigpro 1.0.14 → 1.2.39
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/Readme.md +164 -1008
- package/dist/sigpro.editor.js +1 -0
- package/dist/sigpro.grid.js +78 -0
- package/dist/sigpro.js +1 -0
- package/dist/sigpro.ui.css +2 -0
- package/dist/sigpro.ui.js +1 -0
- package/dist/sigpro.utils.js +1 -0
- package/dist/sigpro.vite.js +4 -0
- package/package.json +64 -14
- package/sigpro.d.ts +395 -0
- package/.github/workflows/publish.yml +0 -25
- package/bun.lock +0 -385
- package/docs/404.html +0 -22
- package/docs/api/components.html +0 -595
- package/docs/api/effects.html +0 -787
- package/docs/api/fetch.html +0 -873
- package/docs/api/pages.html +0 -405
- package/docs/api/quick.html +0 -217
- package/docs/api/routing.html +0 -628
- package/docs/api/signals.html +0 -683
- package/docs/api/storage.html +0 -820
- package/docs/assets/api_components.md.BlFwj17l.js +0 -571
- package/docs/assets/api_components.md.BlFwj17l.lean.js +0 -1
- package/docs/assets/api_effects.md.Br_yStBS.js +0 -763
- package/docs/assets/api_effects.md.Br_yStBS.lean.js +0 -1
- package/docs/assets/api_fetch.md.DQLBJSoq.js +0 -849
- package/docs/assets/api_fetch.md.DQLBJSoq.lean.js +0 -1
- package/docs/assets/api_pages.md.BP19nHXw.js +0 -381
- package/docs/assets/api_pages.md.BP19nHXw.lean.js +0 -1
- package/docs/assets/api_quick.md.BDS3ttnt.js +0 -193
- package/docs/assets/api_quick.md.BDS3ttnt.lean.js +0 -1
- package/docs/assets/api_routing.md.7SNAZXtp.js +0 -604
- package/docs/assets/api_routing.md.7SNAZXtp.lean.js +0 -1
- package/docs/assets/api_signals.md.CrW68-BA.js +0 -659
- package/docs/assets/api_signals.md.CrW68-BA.lean.js +0 -1
- package/docs/assets/api_storage.md.COEWBXHk.js +0 -796
- package/docs/assets/api_storage.md.COEWBXHk.lean.js +0 -1
- package/docs/assets/app.DtmzNmNl.js +0 -1
- package/docs/assets/chunks/framework.C8AWLET_.js +0 -19
- package/docs/assets/chunks/theme.yfWKMLQM.js +0 -1
- package/docs/assets/guide_getting-started.md.BeQpK3vd.js +0 -172
- package/docs/assets/guide_getting-started.md.BeQpK3vd.lean.js +0 -1
- package/docs/assets/guide_why.md.DXchYMN-.js +0 -23
- package/docs/assets/guide_why.md.DXchYMN-.lean.js +0 -1
- package/docs/assets/index.md.uvMJmU4o.js +0 -1
- package/docs/assets/index.md.uvMJmU4o.lean.js +0 -1
- package/docs/assets/inter-italic-cyrillic-ext.r48I6akx.woff2 +0 -0
- package/docs/assets/inter-italic-cyrillic.By2_1cv3.woff2 +0 -0
- package/docs/assets/inter-italic-greek-ext.1u6EdAuj.woff2 +0 -0
- package/docs/assets/inter-italic-greek.DJ8dCoTZ.woff2 +0 -0
- package/docs/assets/inter-italic-latin-ext.CN1xVJS-.woff2 +0 -0
- package/docs/assets/inter-italic-latin.C2AdPX0b.woff2 +0 -0
- package/docs/assets/inter-italic-vietnamese.BSbpV94h.woff2 +0 -0
- package/docs/assets/inter-roman-cyrillic-ext.BBPuwvHQ.woff2 +0 -0
- package/docs/assets/inter-roman-cyrillic.C5lxZ8CY.woff2 +0 -0
- package/docs/assets/inter-roman-greek-ext.CqjqNYQ-.woff2 +0 -0
- package/docs/assets/inter-roman-greek.BBVDIX6e.woff2 +0 -0
- package/docs/assets/inter-roman-latin-ext.4ZJIpNVo.woff2 +0 -0
- package/docs/assets/inter-roman-latin.Di8DUHzh.woff2 +0 -0
- package/docs/assets/inter-roman-vietnamese.BjW4sHH5.woff2 +0 -0
- package/docs/assets/style.DJRheFKp.css +0 -1
- package/docs/assets/ui_intro.md.gZ21GFqo.js +0 -1
- package/docs/assets/ui_intro.md.gZ21GFqo.lean.js +0 -1
- package/docs/assets/vite_plugin.md.gDWEi8f0.js +0 -225
- package/docs/assets/vite_plugin.md.gDWEi8f0.lean.js +0 -1
- package/docs/guide/getting-started.html +0 -196
- package/docs/guide/why.html +0 -47
- package/docs/hashmap.json +0 -1
- package/docs/index.html +0 -25
- package/docs/logo.svg +0 -118
- package/docs/ui/intro.html +0 -25
- package/docs/vite/plugin.html +0 -249
- package/docs/vp-icons.css +0 -1
- package/index.js +0 -3
- package/packages/docs/.vitepress/cache/deps/@theme_index.js +0 -275
- package/packages/docs/.vitepress/cache/deps/@theme_index.js.map +0 -7
- package/packages/docs/.vitepress/cache/deps/_metadata.json +0 -40
- package/packages/docs/.vitepress/cache/deps/chunk-3S55Y3P7.js +0 -12951
- package/packages/docs/.vitepress/cache/deps/chunk-3S55Y3P7.js.map +0 -7
- package/packages/docs/.vitepress/cache/deps/chunk-RLEUDPPB.js +0 -9719
- package/packages/docs/.vitepress/cache/deps/chunk-RLEUDPPB.js.map +0 -7
- package/packages/docs/.vitepress/cache/deps/package.json +0 -3
- package/packages/docs/.vitepress/cache/deps/vitepress___@vue_devtools-api.js +0 -4505
- package/packages/docs/.vitepress/cache/deps/vitepress___@vue_devtools-api.js.map +0 -7
- package/packages/docs/.vitepress/cache/deps/vitepress___@vueuse_core.js +0 -583
- package/packages/docs/.vitepress/cache/deps/vitepress___@vueuse_core.js.map +0 -7
- package/packages/docs/.vitepress/cache/deps/vue.js +0 -347
- package/packages/docs/.vitepress/cache/deps/vue.js.map +0 -7
- package/packages/docs/.vitepress/config.js +0 -68
- package/packages/docs/api/components.md +0 -760
- package/packages/docs/api/effects.md +0 -1039
- package/packages/docs/api/fetch.md +0 -998
- package/packages/docs/api/pages.md +0 -497
- package/packages/docs/api/quick.md +0 -436
- package/packages/docs/api/routing.md +0 -784
- package/packages/docs/api/signals.md +0 -899
- package/packages/docs/api/storage.md +0 -952
- package/packages/docs/guide/getting-started.md +0 -308
- package/packages/docs/guide/why.md +0 -135
- package/packages/docs/index.md +0 -84
- package/packages/docs/logo.svg +0 -118
- package/packages/docs/public/logo.svg +0 -118
- package/packages/docs/ui/intro.md +0 -16
- package/packages/docs/vite/plugin.md +0 -423
- package/packages/sigpro/plugin.js +0 -91
- package/packages/sigpro/plugin.min.js +0 -1
- package/packages/sigpro/sigpro.js +0 -631
- package/packages/sigpro/sigpro.min.js +0 -1
- package/vite.config.js +0 -24
|
@@ -1,952 +0,0 @@
|
|
|
1
|
-
# Storage API 💾
|
|
2
|
-
|
|
3
|
-
SigPro provides persistent signals that automatically synchronize with browser storage APIs. This allows you to create reactive state that survives page reloads and browser sessions with zero additional code.
|
|
4
|
-
|
|
5
|
-
## Core Concepts
|
|
6
|
-
|
|
7
|
-
### What is Persistent Storage?
|
|
8
|
-
|
|
9
|
-
Persistent signals are special signals that:
|
|
10
|
-
- **Initialize from storage** (localStorage/sessionStorage) if a saved value exists
|
|
11
|
-
- **Auto-save** whenever the signal value changes
|
|
12
|
-
- **Handle JSON serialization** automatically
|
|
13
|
-
- **Clean up** when set to `null` or `undefined`
|
|
14
|
-
|
|
15
|
-
### Storage Types
|
|
16
|
-
|
|
17
|
-
| Storage | Persistence | Use Case |
|
|
18
|
-
|---------|-------------|----------|
|
|
19
|
-
| `localStorage` | Forever (until cleared) | User preferences, themes, saved data |
|
|
20
|
-
| `sessionStorage` | Until tab/window closes | Form drafts, temporary state |
|
|
21
|
-
|
|
22
|
-
## `$.storage(key, initialValue, [storage])`
|
|
23
|
-
|
|
24
|
-
Creates a persistent signal that syncs with browser storage.
|
|
25
|
-
|
|
26
|
-
```javascript
|
|
27
|
-
import { $ } from 'sigpro';
|
|
28
|
-
|
|
29
|
-
// localStorage (default)
|
|
30
|
-
const theme = $.storage('theme', 'light');
|
|
31
|
-
const user = $.storage('user', null);
|
|
32
|
-
const settings = $.storage('settings', { notifications: true });
|
|
33
|
-
|
|
34
|
-
// sessionStorage
|
|
35
|
-
const draft = $.storage('draft', '', sessionStorage);
|
|
36
|
-
const formData = $.storage('form', {}, sessionStorage);
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
## 📋 API Reference
|
|
40
|
-
|
|
41
|
-
### Parameters
|
|
42
|
-
|
|
43
|
-
| Parameter | Type | Default | Description |
|
|
44
|
-
|-----------|------|---------|-------------|
|
|
45
|
-
| `key` | `string` | required | Storage key name |
|
|
46
|
-
| `initialValue` | `any` | required | Default value if none stored |
|
|
47
|
-
| `storage` | `Storage` | `localStorage` | Storage type (`localStorage` or `sessionStorage`) |
|
|
48
|
-
|
|
49
|
-
### Returns
|
|
50
|
-
|
|
51
|
-
| Return | Description |
|
|
52
|
-
|--------|-------------|
|
|
53
|
-
| `Function` | Signal function (getter/setter) with persistence |
|
|
54
|
-
|
|
55
|
-
## 🎯 Basic Examples
|
|
56
|
-
|
|
57
|
-
### Theme Preference
|
|
58
|
-
|
|
59
|
-
```javascript
|
|
60
|
-
import { $, html } from 'sigpro';
|
|
61
|
-
|
|
62
|
-
// Persistent theme signal
|
|
63
|
-
const theme = $.storage('theme', 'light');
|
|
64
|
-
|
|
65
|
-
// Apply theme to document
|
|
66
|
-
$.effect(() => {
|
|
67
|
-
document.body.className = `theme-${theme()}`;
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
// Toggle theme
|
|
71
|
-
const toggleTheme = () => {
|
|
72
|
-
theme(t => t === 'light' ? 'dark' : 'light');
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
// Template
|
|
76
|
-
html`
|
|
77
|
-
<div>
|
|
78
|
-
<p>Current theme: ${theme}</p>
|
|
79
|
-
<button @click=${toggleTheme}>
|
|
80
|
-
Toggle Theme
|
|
81
|
-
</button>
|
|
82
|
-
</div>
|
|
83
|
-
`;
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
### User Preferences
|
|
87
|
-
|
|
88
|
-
```javascript
|
|
89
|
-
import { $ } from 'sigpro';
|
|
90
|
-
|
|
91
|
-
// Complex preferences object
|
|
92
|
-
const preferences = $.storage('preferences', {
|
|
93
|
-
language: 'en',
|
|
94
|
-
fontSize: 'medium',
|
|
95
|
-
notifications: true,
|
|
96
|
-
compactView: false,
|
|
97
|
-
sidebarOpen: true
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
// Update single preference
|
|
101
|
-
const setPreference = (key, value) => {
|
|
102
|
-
preferences({
|
|
103
|
-
...preferences(),
|
|
104
|
-
[key]: value
|
|
105
|
-
});
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
// Usage
|
|
109
|
-
setPreference('language', 'es');
|
|
110
|
-
setPreference('fontSize', 'large');
|
|
111
|
-
console.log(preferences().language); // 'es'
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
### Form Draft
|
|
115
|
-
|
|
116
|
-
```javascript
|
|
117
|
-
import { $, html } from 'sigpro';
|
|
118
|
-
|
|
119
|
-
// Session-based draft (clears when tab closes)
|
|
120
|
-
const draft = $.storage('contact-form', {
|
|
121
|
-
name: '',
|
|
122
|
-
email: '',
|
|
123
|
-
message: ''
|
|
124
|
-
}, sessionStorage);
|
|
125
|
-
|
|
126
|
-
// Auto-save on input
|
|
127
|
-
const handleInput = (field, value) => {
|
|
128
|
-
draft({
|
|
129
|
-
...draft(),
|
|
130
|
-
[field]: value
|
|
131
|
-
});
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
// Clear draft after submit
|
|
135
|
-
const handleSubmit = async () => {
|
|
136
|
-
await submitForm(draft());
|
|
137
|
-
draft(null); // Clears from storage
|
|
138
|
-
};
|
|
139
|
-
|
|
140
|
-
// Template
|
|
141
|
-
html`
|
|
142
|
-
<form @submit=${handleSubmit}>
|
|
143
|
-
<input
|
|
144
|
-
type="text"
|
|
145
|
-
:value=${() => draft().name}
|
|
146
|
-
@input=${(e) => handleInput('name', e.target.value)}
|
|
147
|
-
placeholder="Name"
|
|
148
|
-
/>
|
|
149
|
-
<input
|
|
150
|
-
type="email"
|
|
151
|
-
:value=${() => draft().email}
|
|
152
|
-
@input=${(e) => handleInput('email', e.target.value)}
|
|
153
|
-
placeholder="Email"
|
|
154
|
-
/>
|
|
155
|
-
<textarea
|
|
156
|
-
:value=${() => draft().message}
|
|
157
|
-
@input=${(e) => handleInput('message', e.target.value)}
|
|
158
|
-
placeholder="Message"
|
|
159
|
-
></textarea>
|
|
160
|
-
<button type="submit">Send</button>
|
|
161
|
-
</form>
|
|
162
|
-
`;
|
|
163
|
-
```
|
|
164
|
-
|
|
165
|
-
## 🚀 Advanced Examples
|
|
166
|
-
|
|
167
|
-
### Authentication State
|
|
168
|
-
|
|
169
|
-
```javascript
|
|
170
|
-
import { $, html } from 'sigpro';
|
|
171
|
-
|
|
172
|
-
// Persistent auth state
|
|
173
|
-
const auth = $.storage('auth', {
|
|
174
|
-
token: null,
|
|
175
|
-
user: null,
|
|
176
|
-
expiresAt: null
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
// Computed helpers
|
|
180
|
-
const isAuthenticated = $(() => {
|
|
181
|
-
const { token, expiresAt } = auth();
|
|
182
|
-
if (!token || !expiresAt) return false;
|
|
183
|
-
return new Date(expiresAt) > new Date();
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
const user = $(() => auth().user);
|
|
187
|
-
|
|
188
|
-
// Login function
|
|
189
|
-
const login = async (email, password) => {
|
|
190
|
-
const response = await fetch('/api/login', {
|
|
191
|
-
method: 'POST',
|
|
192
|
-
body: JSON.stringify({ email, password })
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
if (response.ok) {
|
|
196
|
-
const { token, user, expiresIn } = await response.json();
|
|
197
|
-
auth({
|
|
198
|
-
token,
|
|
199
|
-
user,
|
|
200
|
-
expiresAt: new Date(Date.now() + expiresIn * 1000).toISOString()
|
|
201
|
-
});
|
|
202
|
-
return true;
|
|
203
|
-
}
|
|
204
|
-
return false;
|
|
205
|
-
};
|
|
206
|
-
|
|
207
|
-
// Logout
|
|
208
|
-
const logout = () => {
|
|
209
|
-
auth(null); // Clear from storage
|
|
210
|
-
};
|
|
211
|
-
|
|
212
|
-
// Auto-refresh token
|
|
213
|
-
$.effect(() => {
|
|
214
|
-
if (!isAuthenticated()) return;
|
|
215
|
-
|
|
216
|
-
const { expiresAt } = auth();
|
|
217
|
-
const expiresIn = new Date(expiresAt) - new Date();
|
|
218
|
-
const refreshTime = expiresIn - 60000; // 1 minute before expiry
|
|
219
|
-
|
|
220
|
-
if (refreshTime > 0) {
|
|
221
|
-
const timer = setTimeout(refreshToken, refreshTime);
|
|
222
|
-
return () => clearTimeout(timer);
|
|
223
|
-
}
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
// Navigation guard
|
|
227
|
-
$.effect(() => {
|
|
228
|
-
if (!isAuthenticated() && window.location.pathname !== '/login') {
|
|
229
|
-
$.router.go('/login');
|
|
230
|
-
}
|
|
231
|
-
});
|
|
232
|
-
```
|
|
233
|
-
|
|
234
|
-
### Multi-tab Synchronization
|
|
235
|
-
|
|
236
|
-
```javascript
|
|
237
|
-
import { $ } from 'sigpro';
|
|
238
|
-
|
|
239
|
-
// Storage key for cross-tab communication
|
|
240
|
-
const STORAGE_KEY = 'app-state';
|
|
241
|
-
|
|
242
|
-
// Create persistent signal
|
|
243
|
-
const appState = $.storage(STORAGE_KEY, {
|
|
244
|
-
count: 0,
|
|
245
|
-
lastUpdated: null
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
// Listen for storage events (changes from other tabs)
|
|
249
|
-
window.addEventListener('storage', (event) => {
|
|
250
|
-
if (event.key === STORAGE_KEY && event.newValue) {
|
|
251
|
-
try {
|
|
252
|
-
// Update signal without triggering save loop
|
|
253
|
-
const newValue = JSON.parse(event.newValue);
|
|
254
|
-
appState(newValue);
|
|
255
|
-
} catch (e) {
|
|
256
|
-
console.error('Failed to parse storage event:', e);
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
// Update state (syncs across all tabs)
|
|
262
|
-
const increment = () => {
|
|
263
|
-
appState({
|
|
264
|
-
count: appState().count + 1,
|
|
265
|
-
lastUpdated: new Date().toISOString()
|
|
266
|
-
});
|
|
267
|
-
};
|
|
268
|
-
|
|
269
|
-
// Tab counter
|
|
270
|
-
const tabCount = $(1);
|
|
271
|
-
|
|
272
|
-
// Track number of tabs open
|
|
273
|
-
window.addEventListener('storage', (event) => {
|
|
274
|
-
if (event.key === 'tab-heartbeat') {
|
|
275
|
-
tabCount(parseInt(event.newValue) || 1);
|
|
276
|
-
}
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
// Send heartbeat
|
|
280
|
-
setInterval(() => {
|
|
281
|
-
localStorage.setItem('tab-heartbeat', tabCount());
|
|
282
|
-
}, 1000);
|
|
283
|
-
```
|
|
284
|
-
|
|
285
|
-
### Settings Manager
|
|
286
|
-
|
|
287
|
-
```javascript
|
|
288
|
-
import { $, html } from 'sigpro';
|
|
289
|
-
|
|
290
|
-
// Settings schema
|
|
291
|
-
const settingsSchema = {
|
|
292
|
-
theme: {
|
|
293
|
-
type: 'select',
|
|
294
|
-
options: ['light', 'dark', 'system'],
|
|
295
|
-
default: 'system'
|
|
296
|
-
},
|
|
297
|
-
fontSize: {
|
|
298
|
-
type: 'range',
|
|
299
|
-
min: 12,
|
|
300
|
-
max: 24,
|
|
301
|
-
default: 16
|
|
302
|
-
},
|
|
303
|
-
notifications: {
|
|
304
|
-
type: 'checkbox',
|
|
305
|
-
default: true
|
|
306
|
-
},
|
|
307
|
-
language: {
|
|
308
|
-
type: 'select',
|
|
309
|
-
options: ['en', 'es', 'fr', 'de'],
|
|
310
|
-
default: 'en'
|
|
311
|
-
}
|
|
312
|
-
};
|
|
313
|
-
|
|
314
|
-
// Persistent settings
|
|
315
|
-
const settings = $.storage('app-settings',
|
|
316
|
-
Object.entries(settingsSchema).reduce((acc, [key, config]) => ({
|
|
317
|
-
...acc,
|
|
318
|
-
[key]: config.default
|
|
319
|
-
}), {})
|
|
320
|
-
);
|
|
321
|
-
|
|
322
|
-
// Settings component
|
|
323
|
-
const SettingsPanel = () => {
|
|
324
|
-
return html`
|
|
325
|
-
<div class="settings-panel">
|
|
326
|
-
<h2>Settings</h2>
|
|
327
|
-
|
|
328
|
-
${Object.entries(settingsSchema).map(([key, config]) => {
|
|
329
|
-
switch(config.type) {
|
|
330
|
-
case 'select':
|
|
331
|
-
return html`
|
|
332
|
-
<div class="setting">
|
|
333
|
-
<label>${key}:</label>
|
|
334
|
-
<select
|
|
335
|
-
:value=${() => settings()[key]}
|
|
336
|
-
@change=${(e) => updateSetting(key, e.target.value)}
|
|
337
|
-
>
|
|
338
|
-
${config.options.map(opt => html`
|
|
339
|
-
<option value="${opt}" ?selected=${() => settings()[key] === opt}>
|
|
340
|
-
${opt}
|
|
341
|
-
</option>
|
|
342
|
-
`)}
|
|
343
|
-
</select>
|
|
344
|
-
</div>
|
|
345
|
-
`;
|
|
346
|
-
|
|
347
|
-
case 'range':
|
|
348
|
-
return html`
|
|
349
|
-
<div class="setting">
|
|
350
|
-
<label>${key}: ${() => settings()[key]}</label>
|
|
351
|
-
<input
|
|
352
|
-
type="range"
|
|
353
|
-
min="${config.min}"
|
|
354
|
-
max="${config.max}"
|
|
355
|
-
:value=${() => settings()[key]}
|
|
356
|
-
@input=${(e) => updateSetting(key, parseInt(e.target.value))}
|
|
357
|
-
/>
|
|
358
|
-
</div>
|
|
359
|
-
`;
|
|
360
|
-
|
|
361
|
-
case 'checkbox':
|
|
362
|
-
return html`
|
|
363
|
-
<div class="setting">
|
|
364
|
-
<label>
|
|
365
|
-
<input
|
|
366
|
-
type="checkbox"
|
|
367
|
-
:checked=${() => settings()[key]}
|
|
368
|
-
@change=${(e) => updateSetting(key, e.target.checked)}
|
|
369
|
-
/>
|
|
370
|
-
${key}
|
|
371
|
-
</label>
|
|
372
|
-
</div>
|
|
373
|
-
`;
|
|
374
|
-
}
|
|
375
|
-
})}
|
|
376
|
-
|
|
377
|
-
<button @click=${resetDefaults}>Reset to Defaults</button>
|
|
378
|
-
</div>
|
|
379
|
-
`;
|
|
380
|
-
};
|
|
381
|
-
|
|
382
|
-
// Helper functions
|
|
383
|
-
const updateSetting = (key, value) => {
|
|
384
|
-
settings({
|
|
385
|
-
...settings(),
|
|
386
|
-
[key]: value
|
|
387
|
-
});
|
|
388
|
-
};
|
|
389
|
-
|
|
390
|
-
const resetDefaults = () => {
|
|
391
|
-
const defaults = Object.entries(settingsSchema).reduce((acc, [key, config]) => ({
|
|
392
|
-
...acc,
|
|
393
|
-
[key]: config.default
|
|
394
|
-
}), {});
|
|
395
|
-
settings(defaults);
|
|
396
|
-
};
|
|
397
|
-
|
|
398
|
-
// Apply settings globally
|
|
399
|
-
$.effect(() => {
|
|
400
|
-
const { theme, fontSize } = settings();
|
|
401
|
-
|
|
402
|
-
// Apply theme
|
|
403
|
-
document.documentElement.setAttribute('data-theme', theme);
|
|
404
|
-
|
|
405
|
-
// Apply font size
|
|
406
|
-
document.documentElement.style.fontSize = `${fontSize}px`;
|
|
407
|
-
});
|
|
408
|
-
```
|
|
409
|
-
|
|
410
|
-
### Shopping Cart Persistence
|
|
411
|
-
|
|
412
|
-
```javascript
|
|
413
|
-
import { $, html } from 'sigpro';
|
|
414
|
-
|
|
415
|
-
// Persistent shopping cart
|
|
416
|
-
const cart = $.storage('shopping-cart', {
|
|
417
|
-
items: [],
|
|
418
|
-
lastUpdated: null
|
|
419
|
-
});
|
|
420
|
-
|
|
421
|
-
// Computed values
|
|
422
|
-
const cartItems = $(() => cart().items);
|
|
423
|
-
const itemCount = $(() => cartItems().reduce((sum, item) => sum + item.quantity, 0));
|
|
424
|
-
const subtotal = $(() => cartItems().reduce((sum, item) => sum + (item.price * item.quantity), 0));
|
|
425
|
-
const tax = $(() => subtotal() * 0.1);
|
|
426
|
-
const total = $(() => subtotal() + tax());
|
|
427
|
-
|
|
428
|
-
// Cart actions
|
|
429
|
-
const addToCart = (product, quantity = 1) => {
|
|
430
|
-
const existing = cartItems().findIndex(item => item.id === product.id);
|
|
431
|
-
|
|
432
|
-
if (existing >= 0) {
|
|
433
|
-
// Update quantity
|
|
434
|
-
const newItems = [...cartItems()];
|
|
435
|
-
newItems[existing] = {
|
|
436
|
-
...newItems[existing],
|
|
437
|
-
quantity: newItems[existing].quantity + quantity
|
|
438
|
-
};
|
|
439
|
-
|
|
440
|
-
cart({
|
|
441
|
-
items: newItems,
|
|
442
|
-
lastUpdated: new Date().toISOString()
|
|
443
|
-
});
|
|
444
|
-
} else {
|
|
445
|
-
// Add new item
|
|
446
|
-
cart({
|
|
447
|
-
items: [...cartItems(), { ...product, quantity }],
|
|
448
|
-
lastUpdated: new Date().toISOString()
|
|
449
|
-
});
|
|
450
|
-
}
|
|
451
|
-
};
|
|
452
|
-
|
|
453
|
-
const removeFromCart = (productId) => {
|
|
454
|
-
cart({
|
|
455
|
-
items: cartItems().filter(item => item.id !== productId),
|
|
456
|
-
lastUpdated: new Date().toISOString()
|
|
457
|
-
});
|
|
458
|
-
};
|
|
459
|
-
|
|
460
|
-
const updateQuantity = (productId, quantity) => {
|
|
461
|
-
if (quantity <= 0) {
|
|
462
|
-
removeFromCart(productId);
|
|
463
|
-
} else {
|
|
464
|
-
const newItems = cartItems().map(item =>
|
|
465
|
-
item.id === productId ? { ...item, quantity } : item
|
|
466
|
-
);
|
|
467
|
-
|
|
468
|
-
cart({
|
|
469
|
-
items: newItems,
|
|
470
|
-
lastUpdated: new Date().toISOString()
|
|
471
|
-
});
|
|
472
|
-
}
|
|
473
|
-
};
|
|
474
|
-
|
|
475
|
-
const clearCart = () => {
|
|
476
|
-
cart({
|
|
477
|
-
items: [],
|
|
478
|
-
lastUpdated: new Date().toISOString()
|
|
479
|
-
});
|
|
480
|
-
};
|
|
481
|
-
|
|
482
|
-
// Cart expiration (7 days)
|
|
483
|
-
const CART_EXPIRY_DAYS = 7;
|
|
484
|
-
|
|
485
|
-
$.effect(() => {
|
|
486
|
-
const lastUpdated = cart().lastUpdated;
|
|
487
|
-
if (lastUpdated) {
|
|
488
|
-
const expiryDate = new Date(lastUpdated);
|
|
489
|
-
expiryDate.setDate(expiryDate.getDate() + CART_EXPIRY_DAYS);
|
|
490
|
-
|
|
491
|
-
if (new Date() > expiryDate) {
|
|
492
|
-
clearCart();
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
});
|
|
496
|
-
|
|
497
|
-
// Cart display component
|
|
498
|
-
const CartDisplay = () => html`
|
|
499
|
-
<div class="cart">
|
|
500
|
-
<h3>Shopping Cart (${itemCount} items)</h3>
|
|
501
|
-
|
|
502
|
-
${cartItems().map(item => html`
|
|
503
|
-
<div class="cart-item">
|
|
504
|
-
<span>${item.name}</span>
|
|
505
|
-
<span>$${item.price} x ${item.quantity}</span>
|
|
506
|
-
<span>$${item.price * item.quantity}</span>
|
|
507
|
-
<button @click=${() => removeFromCart(item.id)}>Remove</button>
|
|
508
|
-
<input
|
|
509
|
-
type="number"
|
|
510
|
-
min="1"
|
|
511
|
-
:value=${item.quantity}
|
|
512
|
-
@change=${(e) => updateQuantity(item.id, parseInt(e.target.value))}
|
|
513
|
-
/>
|
|
514
|
-
</div>
|
|
515
|
-
`)}
|
|
516
|
-
|
|
517
|
-
<div class="cart-totals">
|
|
518
|
-
<p>Subtotal: $${subtotal}</p>
|
|
519
|
-
<p>Tax (10%): $${tax}</p>
|
|
520
|
-
<p><strong>Total: $${total}</strong></p>
|
|
521
|
-
</div>
|
|
522
|
-
|
|
523
|
-
${() => cartItems().length > 0 ? html`
|
|
524
|
-
<button @click=${checkout}>Checkout</button>
|
|
525
|
-
<button @click=${clearCart}>Clear Cart</button>
|
|
526
|
-
` : html`
|
|
527
|
-
<p>Your cart is empty</p>
|
|
528
|
-
`}
|
|
529
|
-
</div>
|
|
530
|
-
`;
|
|
531
|
-
```
|
|
532
|
-
|
|
533
|
-
### Recent Searches History
|
|
534
|
-
|
|
535
|
-
```javascript
|
|
536
|
-
import { $, html } from 'sigpro';
|
|
537
|
-
|
|
538
|
-
// Persistent search history (max 10 items)
|
|
539
|
-
const searchHistory = $.storage('search-history', []);
|
|
540
|
-
|
|
541
|
-
// Add search to history
|
|
542
|
-
const addSearch = (query) => {
|
|
543
|
-
if (!query.trim()) return;
|
|
544
|
-
|
|
545
|
-
const current = searchHistory();
|
|
546
|
-
const newHistory = [
|
|
547
|
-
{ query, timestamp: new Date().toISOString() },
|
|
548
|
-
...current.filter(item => item.query !== query)
|
|
549
|
-
].slice(0, 10); // Keep only last 10
|
|
550
|
-
|
|
551
|
-
searchHistory(newHistory);
|
|
552
|
-
};
|
|
553
|
-
|
|
554
|
-
// Clear history
|
|
555
|
-
const clearHistory = () => {
|
|
556
|
-
searchHistory([]);
|
|
557
|
-
};
|
|
558
|
-
|
|
559
|
-
// Remove specific item
|
|
560
|
-
const removeFromHistory = (query) => {
|
|
561
|
-
searchHistory(searchHistory().filter(item => item.query !== query));
|
|
562
|
-
};
|
|
563
|
-
|
|
564
|
-
// Search component
|
|
565
|
-
const SearchWithHistory = () => {
|
|
566
|
-
const searchInput = $('');
|
|
567
|
-
|
|
568
|
-
const handleSearch = () => {
|
|
569
|
-
const query = searchInput();
|
|
570
|
-
if (query) {
|
|
571
|
-
addSearch(query);
|
|
572
|
-
performSearch(query);
|
|
573
|
-
searchInput('');
|
|
574
|
-
}
|
|
575
|
-
};
|
|
576
|
-
|
|
577
|
-
return html`
|
|
578
|
-
<div class="search-container">
|
|
579
|
-
<div class="search-box">
|
|
580
|
-
<input
|
|
581
|
-
type="search"
|
|
582
|
-
:value=${searchInput}
|
|
583
|
-
@keydown.enter=${handleSearch}
|
|
584
|
-
placeholder="Search..."
|
|
585
|
-
/>
|
|
586
|
-
<button @click=${handleSearch}>Search</button>
|
|
587
|
-
</div>
|
|
588
|
-
|
|
589
|
-
${() => searchHistory().length > 0 ? html`
|
|
590
|
-
<div class="search-history">
|
|
591
|
-
<h4>Recent Searches</h4>
|
|
592
|
-
${searchHistory().map(item => html`
|
|
593
|
-
<div class="history-item">
|
|
594
|
-
<button
|
|
595
|
-
class="history-query"
|
|
596
|
-
@click=${() => {
|
|
597
|
-
searchInput(item.query);
|
|
598
|
-
handleSearch();
|
|
599
|
-
}}
|
|
600
|
-
>
|
|
601
|
-
🔍 ${item.query}
|
|
602
|
-
</button>
|
|
603
|
-
<small>${new Date(item.timestamp).toLocaleString()}</small>
|
|
604
|
-
<button
|
|
605
|
-
class="remove-btn"
|
|
606
|
-
@click=${() => removeFromHistory(item.query)}
|
|
607
|
-
>
|
|
608
|
-
✕
|
|
609
|
-
</button>
|
|
610
|
-
</div>
|
|
611
|
-
`)}
|
|
612
|
-
<button class="clear-btn" @click=${clearHistory}>
|
|
613
|
-
Clear History
|
|
614
|
-
</button>
|
|
615
|
-
</div>
|
|
616
|
-
` : ''}
|
|
617
|
-
</div>
|
|
618
|
-
`;
|
|
619
|
-
};
|
|
620
|
-
```
|
|
621
|
-
|
|
622
|
-
### Multiple Profiles / Accounts
|
|
623
|
-
|
|
624
|
-
```javascript
|
|
625
|
-
import { $, html } from 'sigpro';
|
|
626
|
-
|
|
627
|
-
// Profile manager
|
|
628
|
-
const profiles = $.storage('user-profiles', {
|
|
629
|
-
current: 'default',
|
|
630
|
-
list: {
|
|
631
|
-
default: {
|
|
632
|
-
name: 'Default',
|
|
633
|
-
theme: 'light',
|
|
634
|
-
preferences: {}
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
});
|
|
638
|
-
|
|
639
|
-
// Switch profile
|
|
640
|
-
const switchProfile = (profileId) => {
|
|
641
|
-
profiles({
|
|
642
|
-
...profiles(),
|
|
643
|
-
current: profileId
|
|
644
|
-
});
|
|
645
|
-
};
|
|
646
|
-
|
|
647
|
-
// Create profile
|
|
648
|
-
const createProfile = (name) => {
|
|
649
|
-
const id = `profile-${Date.now()}`;
|
|
650
|
-
profiles({
|
|
651
|
-
current: id,
|
|
652
|
-
list: {
|
|
653
|
-
...profiles().list,
|
|
654
|
-
[id]: {
|
|
655
|
-
name,
|
|
656
|
-
theme: 'light',
|
|
657
|
-
preferences: {},
|
|
658
|
-
createdAt: new Date().toISOString()
|
|
659
|
-
}
|
|
660
|
-
}
|
|
661
|
-
});
|
|
662
|
-
return id;
|
|
663
|
-
};
|
|
664
|
-
|
|
665
|
-
// Delete profile
|
|
666
|
-
const deleteProfile = (profileId) => {
|
|
667
|
-
if (profileId === 'default') return; // Can't delete default
|
|
668
|
-
|
|
669
|
-
const newList = { ...profiles().list };
|
|
670
|
-
delete newList[profileId];
|
|
671
|
-
|
|
672
|
-
profiles({
|
|
673
|
-
current: 'default',
|
|
674
|
-
list: newList
|
|
675
|
-
});
|
|
676
|
-
};
|
|
677
|
-
|
|
678
|
-
// Get current profile data
|
|
679
|
-
const currentProfile = $(() => {
|
|
680
|
-
const { current, list } = profiles();
|
|
681
|
-
return list[current] || list.default;
|
|
682
|
-
});
|
|
683
|
-
|
|
684
|
-
// Profile-aware settings
|
|
685
|
-
const profileTheme = $(() => currentProfile().theme);
|
|
686
|
-
const profilePreferences = $(() => currentProfile().preferences);
|
|
687
|
-
|
|
688
|
-
// Update profile data
|
|
689
|
-
const updateCurrentProfile = (updates) => {
|
|
690
|
-
const { current, list } = profiles();
|
|
691
|
-
profiles({
|
|
692
|
-
current,
|
|
693
|
-
list: {
|
|
694
|
-
...list,
|
|
695
|
-
[current]: {
|
|
696
|
-
...list[current],
|
|
697
|
-
...updates
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
});
|
|
701
|
-
};
|
|
702
|
-
|
|
703
|
-
// Profile selector component
|
|
704
|
-
const ProfileSelector = () => html`
|
|
705
|
-
<div class="profile-selector">
|
|
706
|
-
<select
|
|
707
|
-
:value=${() => profiles().current}
|
|
708
|
-
@change=${(e) => switchProfile(e.target.value)}
|
|
709
|
-
>
|
|
710
|
-
${Object.entries(profiles().list).map(([id, profile]) => html`
|
|
711
|
-
<option value="${id}">${profile.name}</option>
|
|
712
|
-
`)}
|
|
713
|
-
</select>
|
|
714
|
-
|
|
715
|
-
<button @click=${() => {
|
|
716
|
-
const name = prompt('Enter profile name:');
|
|
717
|
-
if (name) createProfile(name);
|
|
718
|
-
}}>
|
|
719
|
-
New Profile
|
|
720
|
-
</button>
|
|
721
|
-
</div>
|
|
722
|
-
`;
|
|
723
|
-
```
|
|
724
|
-
|
|
725
|
-
## 🛡️ Error Handling
|
|
726
|
-
|
|
727
|
-
### Storage Errors
|
|
728
|
-
|
|
729
|
-
```javascript
|
|
730
|
-
import { $ } from 'sigpro';
|
|
731
|
-
|
|
732
|
-
// Safe storage wrapper
|
|
733
|
-
const safeStorage = (key, initialValue, storage = localStorage) => {
|
|
734
|
-
try {
|
|
735
|
-
return $.storage(key, initialValue, storage);
|
|
736
|
-
} catch (error) {
|
|
737
|
-
console.warn(`Storage failed for ${key}, using in-memory fallback:`, error);
|
|
738
|
-
return $(initialValue);
|
|
739
|
-
}
|
|
740
|
-
};
|
|
741
|
-
|
|
742
|
-
// Usage with fallback
|
|
743
|
-
const theme = safeStorage('theme', 'light');
|
|
744
|
-
const user = safeStorage('user', null);
|
|
745
|
-
```
|
|
746
|
-
|
|
747
|
-
### Quota Exceeded Handling
|
|
748
|
-
|
|
749
|
-
```javascript
|
|
750
|
-
import { $ } from 'sigpro';
|
|
751
|
-
|
|
752
|
-
const createManagedStorage = (key, initialValue, maxSize = 1024 * 100) => { // 100KB limit
|
|
753
|
-
const signal = $.storage(key, initialValue);
|
|
754
|
-
|
|
755
|
-
// Monitor size
|
|
756
|
-
const size = $(0);
|
|
757
|
-
|
|
758
|
-
$.effect(() => {
|
|
759
|
-
try {
|
|
760
|
-
const value = signal();
|
|
761
|
-
const json = JSON.stringify(value);
|
|
762
|
-
const bytes = new Blob([json]).size;
|
|
763
|
-
|
|
764
|
-
size(bytes);
|
|
765
|
-
|
|
766
|
-
if (bytes > maxSize) {
|
|
767
|
-
console.warn(`Storage for ${key} exceeded ${maxSize} bytes`);
|
|
768
|
-
// Could implement cleanup strategy here
|
|
769
|
-
}
|
|
770
|
-
} catch (e) {
|
|
771
|
-
console.error('Size check failed:', e);
|
|
772
|
-
}
|
|
773
|
-
});
|
|
774
|
-
|
|
775
|
-
return { signal, size };
|
|
776
|
-
};
|
|
777
|
-
|
|
778
|
-
// Usage
|
|
779
|
-
const { signal: largeData, size } = createManagedStorage('app-data', {}, 50000);
|
|
780
|
-
```
|
|
781
|
-
|
|
782
|
-
## 📊 Storage Limits
|
|
783
|
-
|
|
784
|
-
| Storage Type | Typical Limit | Notes |
|
|
785
|
-
|--------------|---------------|-------|
|
|
786
|
-
| `localStorage` | 5-10MB | Varies by browser |
|
|
787
|
-
| `sessionStorage` | 5-10MB | Cleared when tab closes |
|
|
788
|
-
| `cookies` | 4KB | Not recommended for SigPro |
|
|
789
|
-
|
|
790
|
-
## 🎯 Best Practices
|
|
791
|
-
|
|
792
|
-
### 1. Validate Stored Data
|
|
793
|
-
|
|
794
|
-
```javascript
|
|
795
|
-
import { $ } from 'sigpro';
|
|
796
|
-
|
|
797
|
-
// Schema validation
|
|
798
|
-
const createValidatedStorage = (key, schema, defaultValue, storage) => {
|
|
799
|
-
const signal = $.storage(key, defaultValue, storage);
|
|
800
|
-
|
|
801
|
-
// Wrap to validate on read/write
|
|
802
|
-
const validated = (...args) => {
|
|
803
|
-
if (args.length) {
|
|
804
|
-
// Validate before writing
|
|
805
|
-
const value = args[0];
|
|
806
|
-
if (typeof value === 'function') {
|
|
807
|
-
// Handle functional updates
|
|
808
|
-
return validated(validated());
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
// Basic validation
|
|
812
|
-
const isValid = Object.keys(schema).every(key => {
|
|
813
|
-
const validator = schema[key];
|
|
814
|
-
return !validator || validator(value[key]);
|
|
815
|
-
});
|
|
816
|
-
|
|
817
|
-
if (!isValid) {
|
|
818
|
-
console.warn('Invalid data, skipping storage write');
|
|
819
|
-
return signal();
|
|
820
|
-
}
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
return signal(...args);
|
|
824
|
-
};
|
|
825
|
-
|
|
826
|
-
return validated;
|
|
827
|
-
};
|
|
828
|
-
|
|
829
|
-
// Usage
|
|
830
|
-
const userSchema = {
|
|
831
|
-
name: v => v && v.length > 0,
|
|
832
|
-
age: v => v >= 18 && v <= 120,
|
|
833
|
-
email: v => /@/.test(v)
|
|
834
|
-
};
|
|
835
|
-
|
|
836
|
-
const user = createValidatedStorage('user', userSchema, {
|
|
837
|
-
name: '',
|
|
838
|
-
age: 25,
|
|
839
|
-
email: ''
|
|
840
|
-
});
|
|
841
|
-
```
|
|
842
|
-
|
|
843
|
-
### 2. Handle Versioning
|
|
844
|
-
|
|
845
|
-
```javascript
|
|
846
|
-
import { $ } from 'sigpro';
|
|
847
|
-
|
|
848
|
-
const VERSION = 2;
|
|
849
|
-
|
|
850
|
-
const createVersionedStorage = (key, migrations, storage) => {
|
|
851
|
-
const raw = $.storage(key, { version: VERSION, data: {} }, storage);
|
|
852
|
-
|
|
853
|
-
const migrate = (data) => {
|
|
854
|
-
let current = data;
|
|
855
|
-
const currentVersion = current.version || 1;
|
|
856
|
-
|
|
857
|
-
for (let v = currentVersion; v < VERSION; v++) {
|
|
858
|
-
const migrator = migrations[v];
|
|
859
|
-
if (migrator) {
|
|
860
|
-
current = migrator(current);
|
|
861
|
-
}
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
return current;
|
|
865
|
-
};
|
|
866
|
-
|
|
867
|
-
// Migrate if needed
|
|
868
|
-
const stored = raw();
|
|
869
|
-
if (stored.version !== VERSION) {
|
|
870
|
-
const migrated = migrate(stored);
|
|
871
|
-
raw(migrated);
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
return raw;
|
|
875
|
-
};
|
|
876
|
-
|
|
877
|
-
// Usage
|
|
878
|
-
const migrations = {
|
|
879
|
-
1: (old) => ({
|
|
880
|
-
version: 2,
|
|
881
|
-
data: {
|
|
882
|
-
...old.data,
|
|
883
|
-
preferences: old.preferences || {}
|
|
884
|
-
}
|
|
885
|
-
})
|
|
886
|
-
};
|
|
887
|
-
|
|
888
|
-
const settings = createVersionedStorage('app-settings', migrations);
|
|
889
|
-
```
|
|
890
|
-
|
|
891
|
-
### 3. Encrypt Sensitive Data
|
|
892
|
-
|
|
893
|
-
```javascript
|
|
894
|
-
import { $ } from 'sigpro';
|
|
895
|
-
|
|
896
|
-
// Simple encryption (use proper crypto in production)
|
|
897
|
-
const encrypt = (text) => {
|
|
898
|
-
return btoa(text); // Base64 - NOT secure, just example
|
|
899
|
-
};
|
|
900
|
-
|
|
901
|
-
const decrypt = (text) => {
|
|
902
|
-
try {
|
|
903
|
-
return atob(text);
|
|
904
|
-
} catch {
|
|
905
|
-
return null;
|
|
906
|
-
}
|
|
907
|
-
};
|
|
908
|
-
|
|
909
|
-
const createSecureStorage = (key, initialValue, storage) => {
|
|
910
|
-
const encryptedKey = `enc_${key}`;
|
|
911
|
-
const signal = $.storage(encryptedKey, null, storage);
|
|
912
|
-
|
|
913
|
-
const secure = (...args) => {
|
|
914
|
-
if (args.length) {
|
|
915
|
-
// Encrypt before storing
|
|
916
|
-
const value = args[0];
|
|
917
|
-
const encrypted = encrypt(JSON.stringify(value));
|
|
918
|
-
return signal(encrypted);
|
|
919
|
-
}
|
|
920
|
-
|
|
921
|
-
// Decrypt when reading
|
|
922
|
-
const encrypted = signal();
|
|
923
|
-
if (!encrypted) return initialValue;
|
|
924
|
-
|
|
925
|
-
try {
|
|
926
|
-
const decrypted = decrypt(encrypted);
|
|
927
|
-
return decrypted ? JSON.parse(decrypted) : initialValue;
|
|
928
|
-
} catch {
|
|
929
|
-
return initialValue;
|
|
930
|
-
}
|
|
931
|
-
};
|
|
932
|
-
|
|
933
|
-
return secure;
|
|
934
|
-
};
|
|
935
|
-
|
|
936
|
-
// Usage
|
|
937
|
-
const secureToken = createSecureStorage('auth-token', null);
|
|
938
|
-
secureToken('sensitive-data-123'); // Stored encrypted
|
|
939
|
-
```
|
|
940
|
-
|
|
941
|
-
## 📈 Performance Considerations
|
|
942
|
-
|
|
943
|
-
| Operation | Cost | Notes |
|
|
944
|
-
|-----------|------|-------|
|
|
945
|
-
| Initial read | O(1) | Single storage read |
|
|
946
|
-
| Write | O(1) + JSON.stringify | Auto-save on change |
|
|
947
|
-
| Large objects | O(n) | Stringify/parse overhead |
|
|
948
|
-
| Multiple keys | O(k) | k = number of keys |
|
|
949
|
-
|
|
950
|
-
---
|
|
951
|
-
|
|
952
|
-
> **Pro Tip:** Use `sessionStorage` for temporary data like form drafts, and `localStorage` for persistent user preferences. Always validate data when reading from storage to handle corrupted values gracefully.
|