@wcstack/storage 1.9.1 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,478 +1,478 @@
1
- # @wcstack/storage
2
-
3
- `@wcstack/storage` is a headless storage component for the wcstack ecosystem.
4
-
5
- It is not a visual UI widget.
6
- It is an **I/O node** that connects browser storage (localStorage / sessionStorage) to reactive state.
7
-
8
- When combined with `@wcstack/state`, `<wcs-storage>` can be bound directly through a path contract:
9
-
10
- - **Input / Command Surface**: `key`, `type`, `trigger`
11
- - **Output State Surface**: `value`, `loading`, `error`
12
-
13
- This means you can express browser storage persistence declaratively in HTML, without writing `localStorage.getItem()`, `JSON.parse()`, or serialization glue code in the UI layer.
14
-
15
- `@wcstack/storage` follows the [HAWC](https://github.com/wc-bindable-protocol/wc-bindable-protocol/blob/main/docs/articles/HAWC.md) architecture:
16
-
17
- - **Core** (`StorageCore`) handles storage read/write and cross-tab sync
18
- - **Shell** (`<wcs-storage>`) connects that state to the DOM
19
- - Frameworks and binding systems consume it via the [wc-bindable-protocol](https://github.com/wc-bindable-protocol/wc-bindable-protocol)
20
-
21
- ## Why this exists
22
-
23
- Frontend applications frequently use localStorage / sessionStorage for persisting user settings and session data. Yet the glue code — reading, JSON parsing, saving, error handling — follows the same pattern every time.
24
-
25
- `@wcstack/storage` moves that glue code into a reusable component and exposes the stored value as bindable state.
26
-
27
- The flow with `@wcstack/state`:
28
-
29
- 1. `<wcs-storage>` auto-loads from storage on connection
30
- 2. `value` is bound to the UI via `data-wcs`
31
- 3. State changes are automatically written back to storage
32
- 4. Changes from other tabs are automatically detected
33
-
34
- Persistence becomes a **state transition**, not imperative glue code.
35
-
36
- ## Install
37
-
38
- ```bash
39
- npm install @wcstack/storage
40
- ```
41
-
42
- ## Quick Start
43
-
44
- ### 1. Primitive value auto-save
45
-
46
- Primitive values (strings, numbers, booleans) work with just a `value` binding for two-way persistence.
47
-
48
- ```html
49
- <script type="module" src="https://esm.run/@wcstack/state/auto"></script>
50
- <script type="module" src="https://esm.run/@wcstack/storage/auto"></script>
51
-
52
- <wcs-state>
53
- <script type="module">
54
- export default { username: "" };
55
- </script>
56
- </wcs-state>
57
-
58
- <wcs-storage key="username" data-wcs="value: username"></wcs-storage>
59
-
60
- <input data-wcs="value: username" placeholder="Username">
61
- <p>Saved: <span data-wcs="textContent: username"></span></p>
62
- ```
63
-
64
- This is the default mode:
65
-
66
- - Set a `key` to auto-load on connection
67
- - Bind to `value` for two-way persistence
68
- - Optionally bind `loading` and `error` as well
69
-
70
- ### 2. Persisting objects with `$trackDependency`
71
-
72
- When sub-properties of an object (e.g. `settings.theme`) change, the parent path `settings` binding does **not** fire.
73
- This is because `@wcstack/state`'s dependency walk is **parent → child only**.
74
-
75
- In this case, use `$trackDependency` to explicitly list the sub-properties to watch, and save via `trigger`:
76
-
77
- ```html
78
- <wcs-state>
79
- <script type="module">
80
- export default defineState({
81
- settings: { theme: "light", lang: "en" },
82
-
83
- get settingsChanged() {
84
- this.$trackDependency("settings.theme");
85
- this.$trackDependency("settings.lang");
86
- return true;
87
- },
88
- });
89
- </script>
90
- </wcs-state>
91
-
92
- <wcs-storage key="app-settings" manual
93
- data-wcs="value: settings; trigger: settingsChanged">
94
- </wcs-storage>
95
-
96
- <select data-wcs="value: settings.theme">
97
- <option value="light">Light</option>
98
- <option value="dark">Dark</option>
99
- </select>
100
-
101
- <select data-wcs="value: settings.lang">
102
- <option value="en">English</option>
103
- <option value="ja">日本語</option>
104
- </select>
105
- ```
106
-
107
- **Flow:**
108
-
109
- 1. User changes theme → `settings.theme` updates
110
- 2. Dynamic dependency triggers `settingsChanged` re-evaluation → returns `true`
111
- 3. `trigger: settingsChanged` binding fires → `save()` executes
112
- 4. The entire `settings` object is saved to localStorage
113
-
114
- ### 3. Using sessionStorage
115
-
116
- Use `type="session"` for sessionStorage:
117
-
118
- ```html
119
- <wcs-state>
120
- <script type="module">
121
- export default { sessionData: null };
122
- </script>
123
- </wcs-state>
124
-
125
- <wcs-storage key="session-data" type="session"
126
- data-wcs="value: sessionData">
127
- </wcs-storage>
128
-
129
- <p data-wcs="textContent: sessionData"></p>
130
- ```
131
-
132
- ### 4. Cross-tab sync
133
-
134
- localStorage changes are automatically detected from other tabs:
135
-
136
- ```html
137
- <wcs-state>
138
- <script type="module">
139
- export default { sharedCounter: 0 };
140
- </script>
141
- </wcs-state>
142
-
143
- <wcs-storage key="shared-counter"
144
- data-wcs="value: sharedCounter">
145
- </wcs-storage>
146
-
147
- <!-- Changes from other tabs update this value automatically -->
148
- <p data-wcs="textContent: sharedCounter"></p>
149
- ```
150
-
151
- > **Note**: The `storage` event only fires for changes made in other tabs of the same origin. Since sessionStorage is not shared across tabs, cross-tab sync only works with localStorage.
152
-
153
- ## State Surface vs Command Surface
154
-
155
- `<wcs-storage>` exposes two kinds of properties.
156
-
157
- ### Output State (bindable state)
158
-
159
- Represents the current storage value and is the HAWC main surface:
160
-
161
- | Property | Type | Description |
162
- |----------|------|-------------|
163
- | `value` | `any` | Value stored in storage |
164
- | `loading` | `boolean` | `true` during read/write |
165
- | `error` | `WcsStorageError \| Error \| null` | Storage operation error |
166
-
167
- ### Input / Command Surface
168
-
169
- Controls storage operations from HTML, JS, or `@wcstack/state` bindings:
170
-
171
- | Property | Type | Description |
172
- |----------|------|-------------|
173
- | `key` | `string` | Storage key |
174
- | `type` | `"local" \| "session"` | Storage type |
175
- | `value` | `any` | Setting this auto-saves (when not `manual`) |
176
- | `trigger` | `boolean` | One-way save trigger |
177
- | `manual` | `boolean` | Disables auto-load and auto-save |
178
-
179
- ## Architecture
180
-
181
- `@wcstack/storage` follows the HAWC architecture.
182
-
183
- ### Core: `StorageCore`
184
-
185
- `StorageCore` is a pure `EventTarget` class. It encapsulates:
186
-
187
- - Storage read, write, and remove
188
- - Automatic JSON serialization / deserialization
189
- - Cross-tab sync via `storage` event
190
- - `wc-bindable-protocol` declaration
191
-
192
- It works headlessly in any runtime that supports `EventTarget` and `localStorage` / `sessionStorage`.
193
-
194
- ### Shell: `<wcs-storage>`
195
-
196
- `<wcs-storage>` is a thin `HTMLElement` wrapper around `StorageCore`. It adds:
197
-
198
- - Attribute / property mapping
199
- - DOM lifecycle integration (auto-load on connect, cleanup on disconnect)
200
- - Auto-save via the `value` setter
201
- - Declarative execution helpers like `trigger`
202
-
203
- This separation keeps storage logic portable while enabling natural integration with DOM-based binding systems like `@wcstack/state`.
204
-
205
- ### Target injection
206
-
207
- Core uses **target injection** to fire events directly on the Shell, avoiding event re-dispatch.
208
-
209
- ## Headless usage (Core only)
210
-
211
- `StorageCore` can be used standalone without DOM. It declares `static wcBindable`, so you can subscribe to state with `bind()` from `@wc-bindable/core` — the same mechanism used by framework adapters:
212
-
213
- ```typescript
214
- import { StorageCore } from "@wcstack/storage";
215
- import { bind } from "@wc-bindable/core";
216
-
217
- const core = new StorageCore();
218
-
219
- const unbind = bind(core, (name, value) => {
220
- console.log(`${name}:`, value);
221
- });
222
-
223
- core.key = "my-data";
224
- core.load();
225
-
226
- unbind();
227
- ```
228
-
229
- ### Auto JSON serialization
230
-
231
- `StorageCore` automatically serializes and deserializes based on data type:
232
-
233
- | Type on save | Format in storage | Type on load |
234
- |-------------|-------------------|-------------|
235
- | Object / Array | `JSON.stringify()` result | `JSON.parse()` result |
236
- | String | As-is | Parsed if valid JSON, otherwise the raw string |
237
- | Number / boolean | `JSON.stringify()` result | `JSON.parse()` result |
238
- | `null` / `undefined` | Key removed | `null` |
239
-
240
- ## Element Reference
241
-
242
- ### `<wcs-storage>`
243
-
244
- | Attribute | Type | Default | Description |
245
- |-----------|------|---------|-------------|
246
- | `key` | `string` | — | Storage key |
247
- | `type` | `"local" \| "session"` | `local` | Storage type |
248
- | `manual` | `boolean` | `false` | Disables auto-load and auto-save |
249
-
250
- | Property | Type | Description |
251
- |----------|------|-------------|
252
- | `value` | `any` | Storage value (auto-saves on set) |
253
- | `loading` | `boolean` | `true` during read/write |
254
- | `error` | `WcsStorageError \| Error \| null` | Error info |
255
- | `trigger` | `boolean` | Set `true` to execute save |
256
- | `manual` | `boolean` | Manual mode |
257
-
258
- | Method | Description |
259
- |--------|-------------|
260
- | `load()` | Load value from storage |
261
- | `save()` | Save current value to storage |
262
- | `remove()` | Remove key from storage |
263
-
264
- ## wc-bindable-protocol
265
-
266
- Both `StorageCore` and `<wcs-storage>` conform to the wc-bindable-protocol, enabling interop with any protocol-aware framework or component.
267
-
268
- ### Core (`StorageCore`)
269
-
270
- ```typescript
271
- static wcBindable = {
272
- protocol: "wc-bindable",
273
- version: 1,
274
- properties: [
275
- { name: "value", event: "wcs-storage:value-changed",
276
- getter: (e) => e.detail },
277
- { name: "loading", event: "wcs-storage:loading-changed" },
278
- { name: "error", event: "wcs-storage:error" },
279
- ],
280
- };
281
- ```
282
-
283
- ### Shell (`<wcs-storage>`)
284
-
285
- The Shell extends the Core declaration, adding trigger support for declarative storage operations from binding systems:
286
-
287
- ```typescript
288
- static wcBindable = {
289
- ...StorageCore.wcBindable,
290
- properties: [
291
- ...StorageCore.wcBindable.properties,
292
- { name: "trigger", event: "wcs-storage:trigger-changed" },
293
- ],
294
- };
295
- ```
296
-
297
- ## TypeScript Types
298
-
299
- ```typescript
300
- import type {
301
- WcsStorageError, WcsStorageCoreValues, WcsStorageValues, StorageType
302
- } from "@wcstack/storage";
303
- ```
304
-
305
- ```typescript
306
- type StorageType = "local" | "session";
307
-
308
- // Storage operation error
309
- interface WcsStorageError {
310
- operation: "load" | "save" | "remove";
311
- message: string;
312
- }
313
-
314
- // Core (headless) — 3 state properties
315
- interface WcsStorageCoreValues<T = unknown> {
316
- value: T;
317
- loading: boolean;
318
- error: WcsStorageError | Error | null;
319
- }
320
-
321
- // Shell (<wcs-storage>) — extends Core with trigger
322
- interface WcsStorageValues<T = unknown> extends WcsStorageCoreValues<T> {
323
- trigger: boolean;
324
- }
325
- ```
326
-
327
- ## Why it works well with `@wcstack/state`
328
-
329
- `@wcstack/state` uses path strings as the sole contract between UI and state.
330
- `<wcs-storage>` fits naturally into this model:
331
-
332
- - `<wcs-storage>` auto-loads from storage on connection
333
- - `value` is bound to a state path, reflected in the UI
334
- - User interactions update state, which auto-saves back to storage
335
- - State survives page reloads
336
-
337
- Persistence looks just like any other state update.
338
-
339
- ## Framework Integration
340
-
341
- `<wcs-storage>` is HAWC + `wc-bindable-protocol`, so it works in any framework via thin `@wc-bindable/*` adapters.
342
-
343
- ### React
344
-
345
- ```tsx
346
- import { useWcBindable } from "@wc-bindable/react";
347
- import type { WcsStorageValues } from "@wcstack/storage";
348
-
349
- interface Settings { theme: string; lang: string; }
350
-
351
- function SettingsPanel() {
352
- const [ref, { value: settings, loading, error }] =
353
- useWcBindable<HTMLElement, WcsStorageValues<Settings>>();
354
-
355
- return (
356
- <>
357
- <wcs-storage ref={ref} key="app-settings" />
358
- {loading && <p>Loading...</p>}
359
- {settings && <p>Theme: {settings.theme}</p>}
360
- </>
361
- );
362
- }
363
- ```
364
-
365
- ### Vue
366
-
367
- ```vue
368
- <script setup lang="ts">
369
- import { useWcBindable } from "@wc-bindable/vue";
370
- import type { WcsStorageValues } from "@wcstack/storage";
371
-
372
- interface Settings { theme: string; lang: string; }
373
-
374
- const { ref, values } = useWcBindable<HTMLElement, WcsStorageValues<Settings>>();
375
- </script>
376
-
377
- <template>
378
- <wcs-storage :ref="ref" key="app-settings" />
379
- <p v-if="values.loading">Loading...</p>
380
- <p v-else-if="values.value">Theme: {{ values.value.theme }}</p>
381
- </template>
382
- ```
383
-
384
- ### Svelte
385
-
386
- ```svelte
387
- <script>
388
- import { wcBindable } from "@wc-bindable/svelte";
389
-
390
- let settings = $state(null);
391
- let loading = $state(false);
392
- </script>
393
-
394
- <wcs-storage key="app-settings"
395
- use:wcBindable={{ onUpdate: (name, v) => {
396
- if (name === "value") settings = v;
397
- if (name === "loading") loading = v;
398
- }}} />
399
-
400
- {#if loading}
401
- <p>Loading...</p>
402
- {:else if settings}
403
- <p>Theme: {settings.theme}</p>
404
- {/if}
405
- ```
406
-
407
- ### Solid
408
-
409
- ```tsx
410
- import { createWcBindable } from "@wc-bindable/solid";
411
- import type { WcsStorageValues } from "@wcstack/storage";
412
-
413
- interface Settings { theme: string; lang: string; }
414
-
415
- function SettingsPanel() {
416
- const [values, directive] = createWcBindable<WcsStorageValues<Settings>>();
417
-
418
- return (
419
- <>
420
- <wcs-storage ref={directive} key="app-settings" />
421
- <Show when={!values.loading} fallback={<p>Loading...</p>}>
422
- <p>Theme: {values.value?.theme}</p>
423
- </Show>
424
- </>
425
- );
426
- }
427
- ```
428
-
429
- ### Vanilla — direct `bind()`
430
-
431
- ```javascript
432
- import { bind } from "@wc-bindable/core";
433
-
434
- const storageEl = document.querySelector("wcs-storage");
435
-
436
- bind(storageEl, (name, value) => {
437
- console.log(`${name} changed:`, value);
438
- });
439
- ```
440
-
441
- ## Optional DOM Triggering
442
-
443
- When `autoTrigger` is enabled (default), clicking an element with a `data-storagetarget` attribute executes `save()` on the corresponding `<wcs-storage>`:
444
-
445
- ```html
446
- <button data-storagetarget="settings-store">Save Settings</button>
447
- <wcs-storage id="settings-store" key="settings" manual
448
- data-wcs="value: settings"></wcs-storage>
449
- ```
450
-
451
- ## Configuration
452
-
453
- ```javascript
454
- import { bootstrapStorage } from "@wcstack/storage";
455
-
456
- bootstrapStorage({
457
- autoTrigger: true,
458
- triggerAttribute: "data-storagetarget",
459
- tagNames: {
460
- storage: "wcs-storage",
461
- },
462
- });
463
- ```
464
-
465
- ## Design Notes
466
-
467
- - `value`, `loading`, `error` are **output state**
468
- - `key`, `type`, `trigger` are **input / command surface**
469
- - `trigger` is intentionally one-way: writing `true` saves, reset signals completion
470
- - The `value` setter auto-saves when not in `manual` mode
471
- - JSON auto-serialization handles objects, arrays, and primitives transparently
472
- - Saving `null` / `undefined` removes the key from storage
473
- - Cross-tab sync via `storage` event works only with localStorage
474
- - `manual` is useful when you want explicit control over save timing
475
-
476
- ## License
477
-
478
- MIT
1
+ # @wcstack/storage
2
+
3
+ `@wcstack/storage` is a headless storage component for the wcstack ecosystem.
4
+
5
+ It is not a visual UI widget.
6
+ It is an **I/O node** that connects browser storage (localStorage / sessionStorage) to reactive state.
7
+
8
+ When combined with `@wcstack/state`, `<wcs-storage>` can be bound directly through a path contract:
9
+
10
+ - **Input / Command Surface**: `key`, `type`, `trigger`
11
+ - **Output State Surface**: `value`, `loading`, `error`
12
+
13
+ This means you can express browser storage persistence declaratively in HTML, without writing `localStorage.getItem()`, `JSON.parse()`, or serialization glue code in the UI layer.
14
+
15
+ `@wcstack/storage` follows the [HAWC](https://github.com/wc-bindable-protocol/wc-bindable-protocol/blob/main/docs/articles/HAWC.md) architecture:
16
+
17
+ - **Core** (`StorageCore`) handles storage read/write and cross-tab sync
18
+ - **Shell** (`<wcs-storage>`) connects that state to the DOM
19
+ - Frameworks and binding systems consume it via the [wc-bindable-protocol](https://github.com/wc-bindable-protocol/wc-bindable-protocol)
20
+
21
+ ## Why this exists
22
+
23
+ Frontend applications frequently use localStorage / sessionStorage for persisting user settings and session data. Yet the glue code — reading, JSON parsing, saving, error handling — follows the same pattern every time.
24
+
25
+ `@wcstack/storage` moves that glue code into a reusable component and exposes the stored value as bindable state.
26
+
27
+ The flow with `@wcstack/state`:
28
+
29
+ 1. `<wcs-storage>` auto-loads from storage on connection
30
+ 2. `value` is bound to the UI via `data-wcs`
31
+ 3. State changes are automatically written back to storage
32
+ 4. Changes from other tabs are automatically detected
33
+
34
+ Persistence becomes a **state transition**, not imperative glue code.
35
+
36
+ ## Install
37
+
38
+ ```bash
39
+ npm install @wcstack/storage
40
+ ```
41
+
42
+ ## Quick Start
43
+
44
+ ### 1. Primitive value auto-save
45
+
46
+ Primitive values (strings, numbers, booleans) work with just a `value` binding for two-way persistence.
47
+
48
+ ```html
49
+ <script type="module" src="https://esm.run/@wcstack/state/auto"></script>
50
+ <script type="module" src="https://esm.run/@wcstack/storage/auto"></script>
51
+
52
+ <wcs-state>
53
+ <script type="module">
54
+ export default { username: "" };
55
+ </script>
56
+ </wcs-state>
57
+
58
+ <wcs-storage key="username" data-wcs="value: username"></wcs-storage>
59
+
60
+ <input data-wcs="value: username" placeholder="Username">
61
+ <p>Saved: <span data-wcs="textContent: username"></span></p>
62
+ ```
63
+
64
+ This is the default mode:
65
+
66
+ - Set a `key` to auto-load on connection
67
+ - Bind to `value` for two-way persistence
68
+ - Optionally bind `loading` and `error` as well
69
+
70
+ ### 2. Persisting objects with `$trackDependency`
71
+
72
+ When sub-properties of an object (e.g. `settings.theme`) change, the parent path `settings` binding does **not** fire.
73
+ This is because `@wcstack/state`'s dependency walk is **parent → child only**.
74
+
75
+ In this case, use `$trackDependency` to explicitly list the sub-properties to watch, and save via `trigger`:
76
+
77
+ ```html
78
+ <wcs-state>
79
+ <script type="module">
80
+ export default defineState({
81
+ settings: { theme: "light", lang: "en" },
82
+
83
+ get settingsChanged() {
84
+ this.$trackDependency("settings.theme");
85
+ this.$trackDependency("settings.lang");
86
+ return true;
87
+ },
88
+ });
89
+ </script>
90
+ </wcs-state>
91
+
92
+ <wcs-storage key="app-settings" manual
93
+ data-wcs="value: settings; trigger: settingsChanged">
94
+ </wcs-storage>
95
+
96
+ <select data-wcs="value: settings.theme">
97
+ <option value="light">Light</option>
98
+ <option value="dark">Dark</option>
99
+ </select>
100
+
101
+ <select data-wcs="value: settings.lang">
102
+ <option value="en">English</option>
103
+ <option value="ja">日本語</option>
104
+ </select>
105
+ ```
106
+
107
+ **Flow:**
108
+
109
+ 1. User changes theme → `settings.theme` updates
110
+ 2. Dynamic dependency triggers `settingsChanged` re-evaluation → returns `true`
111
+ 3. `trigger: settingsChanged` binding fires → `save()` executes
112
+ 4. The entire `settings` object is saved to localStorage
113
+
114
+ ### 3. Using sessionStorage
115
+
116
+ Use `type="session"` for sessionStorage:
117
+
118
+ ```html
119
+ <wcs-state>
120
+ <script type="module">
121
+ export default { sessionData: null };
122
+ </script>
123
+ </wcs-state>
124
+
125
+ <wcs-storage key="session-data" type="session"
126
+ data-wcs="value: sessionData">
127
+ </wcs-storage>
128
+
129
+ <p data-wcs="textContent: sessionData"></p>
130
+ ```
131
+
132
+ ### 4. Cross-tab sync
133
+
134
+ localStorage changes are automatically detected from other tabs:
135
+
136
+ ```html
137
+ <wcs-state>
138
+ <script type="module">
139
+ export default { sharedCounter: 0 };
140
+ </script>
141
+ </wcs-state>
142
+
143
+ <wcs-storage key="shared-counter"
144
+ data-wcs="value: sharedCounter">
145
+ </wcs-storage>
146
+
147
+ <!-- Changes from other tabs update this value automatically -->
148
+ <p data-wcs="textContent: sharedCounter"></p>
149
+ ```
150
+
151
+ > **Note**: The `storage` event only fires for changes made in other tabs of the same origin. Since sessionStorage is not shared across tabs, cross-tab sync only works with localStorage.
152
+
153
+ ## State Surface vs Command Surface
154
+
155
+ `<wcs-storage>` exposes two kinds of properties.
156
+
157
+ ### Output State (bindable state)
158
+
159
+ Represents the current storage value and is the HAWC main surface:
160
+
161
+ | Property | Type | Description |
162
+ |----------|------|-------------|
163
+ | `value` | `any` | Value stored in storage |
164
+ | `loading` | `boolean` | `true` during read/write |
165
+ | `error` | `WcsStorageError \| Error \| null` | Storage operation error |
166
+
167
+ ### Input / Command Surface
168
+
169
+ Controls storage operations from HTML, JS, or `@wcstack/state` bindings:
170
+
171
+ | Property | Type | Description |
172
+ |----------|------|-------------|
173
+ | `key` | `string` | Storage key |
174
+ | `type` | `"local" \| "session"` | Storage type |
175
+ | `value` | `any` | Setting this auto-saves (when not `manual`) |
176
+ | `trigger` | `boolean` | One-way save trigger |
177
+ | `manual` | `boolean` | Disables auto-load and auto-save |
178
+
179
+ ## Architecture
180
+
181
+ `@wcstack/storage` follows the HAWC architecture.
182
+
183
+ ### Core: `StorageCore`
184
+
185
+ `StorageCore` is a pure `EventTarget` class. It encapsulates:
186
+
187
+ - Storage read, write, and remove
188
+ - Automatic JSON serialization / deserialization
189
+ - Cross-tab sync via `storage` event
190
+ - `wc-bindable-protocol` declaration
191
+
192
+ It works headlessly in any runtime that supports `EventTarget` and `localStorage` / `sessionStorage`.
193
+
194
+ ### Shell: `<wcs-storage>`
195
+
196
+ `<wcs-storage>` is a thin `HTMLElement` wrapper around `StorageCore`. It adds:
197
+
198
+ - Attribute / property mapping
199
+ - DOM lifecycle integration (auto-load on connect, cleanup on disconnect)
200
+ - Auto-save via the `value` setter
201
+ - Declarative execution helpers like `trigger`
202
+
203
+ This separation keeps storage logic portable while enabling natural integration with DOM-based binding systems like `@wcstack/state`.
204
+
205
+ ### Target injection
206
+
207
+ Core uses **target injection** to fire events directly on the Shell, avoiding event re-dispatch.
208
+
209
+ ## Headless usage (Core only)
210
+
211
+ `StorageCore` can be used standalone without DOM. It declares `static wcBindable`, so you can subscribe to state with `bind()` from `@wc-bindable/core` — the same mechanism used by framework adapters:
212
+
213
+ ```typescript
214
+ import { StorageCore } from "@wcstack/storage";
215
+ import { bind } from "@wc-bindable/core";
216
+
217
+ const core = new StorageCore();
218
+
219
+ const unbind = bind(core, (name, value) => {
220
+ console.log(`${name}:`, value);
221
+ });
222
+
223
+ core.key = "my-data";
224
+ core.load();
225
+
226
+ unbind();
227
+ ```
228
+
229
+ ### Auto JSON serialization
230
+
231
+ `StorageCore` automatically serializes and deserializes based on data type:
232
+
233
+ | Type on save | Format in storage | Type on load |
234
+ |-------------|-------------------|-------------|
235
+ | Object / Array | `JSON.stringify()` result | `JSON.parse()` result |
236
+ | String | As-is | Parsed if valid JSON, otherwise the raw string |
237
+ | Number / boolean | `JSON.stringify()` result | `JSON.parse()` result |
238
+ | `null` / `undefined` | Key removed | `null` |
239
+
240
+ ## Element Reference
241
+
242
+ ### `<wcs-storage>`
243
+
244
+ | Attribute | Type | Default | Description |
245
+ |-----------|------|---------|-------------|
246
+ | `key` | `string` | — | Storage key |
247
+ | `type` | `"local" \| "session"` | `local` | Storage type |
248
+ | `manual` | `boolean` | `false` | Disables auto-load and auto-save |
249
+
250
+ | Property | Type | Description |
251
+ |----------|------|-------------|
252
+ | `value` | `any` | Storage value (auto-saves on set) |
253
+ | `loading` | `boolean` | `true` during read/write |
254
+ | `error` | `WcsStorageError \| Error \| null` | Error info |
255
+ | `trigger` | `boolean` | Set `true` to execute save |
256
+ | `manual` | `boolean` | Manual mode |
257
+
258
+ | Method | Description |
259
+ |--------|-------------|
260
+ | `load()` | Load value from storage |
261
+ | `save()` | Save current value to storage |
262
+ | `remove()` | Remove key from storage |
263
+
264
+ ## wc-bindable-protocol
265
+
266
+ Both `StorageCore` and `<wcs-storage>` conform to the wc-bindable-protocol, enabling interop with any protocol-aware framework or component.
267
+
268
+ ### Core (`StorageCore`)
269
+
270
+ ```typescript
271
+ static wcBindable = {
272
+ protocol: "wc-bindable",
273
+ version: 1,
274
+ properties: [
275
+ { name: "value", event: "wcs-storage:value-changed",
276
+ getter: (e) => e.detail },
277
+ { name: "loading", event: "wcs-storage:loading-changed" },
278
+ { name: "error", event: "wcs-storage:error" },
279
+ ],
280
+ };
281
+ ```
282
+
283
+ ### Shell (`<wcs-storage>`)
284
+
285
+ The Shell extends the Core declaration, adding trigger support for declarative storage operations from binding systems:
286
+
287
+ ```typescript
288
+ static wcBindable = {
289
+ ...StorageCore.wcBindable,
290
+ properties: [
291
+ ...StorageCore.wcBindable.properties,
292
+ { name: "trigger", event: "wcs-storage:trigger-changed" },
293
+ ],
294
+ };
295
+ ```
296
+
297
+ ## TypeScript Types
298
+
299
+ ```typescript
300
+ import type {
301
+ WcsStorageError, WcsStorageCoreValues, WcsStorageValues, StorageType
302
+ } from "@wcstack/storage";
303
+ ```
304
+
305
+ ```typescript
306
+ type StorageType = "local" | "session";
307
+
308
+ // Storage operation error
309
+ interface WcsStorageError {
310
+ operation: "load" | "save" | "remove";
311
+ message: string;
312
+ }
313
+
314
+ // Core (headless) — 3 state properties
315
+ interface WcsStorageCoreValues<T = unknown> {
316
+ value: T;
317
+ loading: boolean;
318
+ error: WcsStorageError | Error | null;
319
+ }
320
+
321
+ // Shell (<wcs-storage>) — extends Core with trigger
322
+ interface WcsStorageValues<T = unknown> extends WcsStorageCoreValues<T> {
323
+ trigger: boolean;
324
+ }
325
+ ```
326
+
327
+ ## Why it works well with `@wcstack/state`
328
+
329
+ `@wcstack/state` uses path strings as the sole contract between UI and state.
330
+ `<wcs-storage>` fits naturally into this model:
331
+
332
+ - `<wcs-storage>` auto-loads from storage on connection
333
+ - `value` is bound to a state path, reflected in the UI
334
+ - User interactions update state, which auto-saves back to storage
335
+ - State survives page reloads
336
+
337
+ Persistence looks just like any other state update.
338
+
339
+ ## Framework Integration
340
+
341
+ `<wcs-storage>` is HAWC + `wc-bindable-protocol`, so it works in any framework via thin `@wc-bindable/*` adapters.
342
+
343
+ ### React
344
+
345
+ ```tsx
346
+ import { useWcBindable } from "@wc-bindable/react";
347
+ import type { WcsStorageValues } from "@wcstack/storage";
348
+
349
+ interface Settings { theme: string; lang: string; }
350
+
351
+ function SettingsPanel() {
352
+ const [ref, { value: settings, loading, error }] =
353
+ useWcBindable<HTMLElement, WcsStorageValues<Settings>>();
354
+
355
+ return (
356
+ <>
357
+ <wcs-storage ref={ref} key="app-settings" />
358
+ {loading && <p>Loading...</p>}
359
+ {settings && <p>Theme: {settings.theme}</p>}
360
+ </>
361
+ );
362
+ }
363
+ ```
364
+
365
+ ### Vue
366
+
367
+ ```vue
368
+ <script setup lang="ts">
369
+ import { useWcBindable } from "@wc-bindable/vue";
370
+ import type { WcsStorageValues } from "@wcstack/storage";
371
+
372
+ interface Settings { theme: string; lang: string; }
373
+
374
+ const { ref, values } = useWcBindable<HTMLElement, WcsStorageValues<Settings>>();
375
+ </script>
376
+
377
+ <template>
378
+ <wcs-storage :ref="ref" key="app-settings" />
379
+ <p v-if="values.loading">Loading...</p>
380
+ <p v-else-if="values.value">Theme: {{ values.value.theme }}</p>
381
+ </template>
382
+ ```
383
+
384
+ ### Svelte
385
+
386
+ ```svelte
387
+ <script>
388
+ import { wcBindable } from "@wc-bindable/svelte";
389
+
390
+ let settings = $state(null);
391
+ let loading = $state(false);
392
+ </script>
393
+
394
+ <wcs-storage key="app-settings"
395
+ use:wcBindable={{ onUpdate: (name, v) => {
396
+ if (name === "value") settings = v;
397
+ if (name === "loading") loading = v;
398
+ }}} />
399
+
400
+ {#if loading}
401
+ <p>Loading...</p>
402
+ {:else if settings}
403
+ <p>Theme: {settings.theme}</p>
404
+ {/if}
405
+ ```
406
+
407
+ ### Solid
408
+
409
+ ```tsx
410
+ import { createWcBindable } from "@wc-bindable/solid";
411
+ import type { WcsStorageValues } from "@wcstack/storage";
412
+
413
+ interface Settings { theme: string; lang: string; }
414
+
415
+ function SettingsPanel() {
416
+ const [values, directive] = createWcBindable<WcsStorageValues<Settings>>();
417
+
418
+ return (
419
+ <>
420
+ <wcs-storage ref={directive} key="app-settings" />
421
+ <Show when={!values.loading} fallback={<p>Loading...</p>}>
422
+ <p>Theme: {values.value?.theme}</p>
423
+ </Show>
424
+ </>
425
+ );
426
+ }
427
+ ```
428
+
429
+ ### Vanilla — direct `bind()`
430
+
431
+ ```javascript
432
+ import { bind } from "@wc-bindable/core";
433
+
434
+ const storageEl = document.querySelector("wcs-storage");
435
+
436
+ bind(storageEl, (name, value) => {
437
+ console.log(`${name} changed:`, value);
438
+ });
439
+ ```
440
+
441
+ ## Optional DOM Triggering
442
+
443
+ When `autoTrigger` is enabled (default), clicking an element with a `data-storagetarget` attribute executes `save()` on the corresponding `<wcs-storage>`:
444
+
445
+ ```html
446
+ <button data-storagetarget="settings-store">Save Settings</button>
447
+ <wcs-storage id="settings-store" key="settings" manual
448
+ data-wcs="value: settings"></wcs-storage>
449
+ ```
450
+
451
+ ## Configuration
452
+
453
+ ```javascript
454
+ import { bootstrapStorage } from "@wcstack/storage";
455
+
456
+ bootstrapStorage({
457
+ autoTrigger: true,
458
+ triggerAttribute: "data-storagetarget",
459
+ tagNames: {
460
+ storage: "wcs-storage",
461
+ },
462
+ });
463
+ ```
464
+
465
+ ## Design Notes
466
+
467
+ - `value`, `loading`, `error` are **output state**
468
+ - `key`, `type`, `trigger` are **input / command surface**
469
+ - `trigger` is intentionally one-way: writing `true` saves, reset signals completion
470
+ - The `value` setter auto-saves when not in `manual` mode
471
+ - JSON auto-serialization handles objects, arrays, and primitives transparently
472
+ - Saving `null` / `undefined` removes the key from storage
473
+ - Cross-tab sync via `storage` event works only with localStorage
474
+ - `manual` is useful when you want explicit control over save timing
475
+
476
+ ## License
477
+
478
+ MIT