@wcstack/storage 1.9.1 → 1.10.4

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,515 @@
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 [CSBC](https://github.com/csbc-dev/arch/blob/main/README.md) (Core / Shell / Binding Contract) 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 CSBC 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 CSBC 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
+ The declaration follows the full wc-bindable interface model — three independent surfaces:
269
+
270
+ - **`properties`** — observable outputs that `bind()` subscribes to (`value`, `loading`, `error`, and the Shell's `trigger`)
271
+ - **`inputs`** the settable surface (`key`, `type`, …); declarative metadata that tooling, codegen, and remote proxying read
272
+ - **`commands`** — invocable methods (`load`, `save`, `remove`); a binding system such as `@wcstack/state` can invoke them by name
273
+
274
+ Per the protocol, only `properties` is interpreted by core `bind()`; `inputs` / `commands` (and the `attribute` / `async` hints) are descriptive. They do **not** create implicit two-way data flow.
275
+
276
+ ### Core (`StorageCore`)
277
+
278
+ `StorageCore` declares the bindable state any runtime can subscribe to, plus its portable input/command surface:
279
+
280
+ ```typescript
281
+ static wcBindable = {
282
+ protocol: "wc-bindable",
283
+ version: 1,
284
+ properties: [
285
+ { name: "value", event: "wcs-storage:value-changed",
286
+ getter: (e) => e.detail },
287
+ { name: "loading", event: "wcs-storage:loading-changed" },
288
+ { name: "error", event: "wcs-storage:error" },
289
+ ],
290
+ inputs: [
291
+ { name: "key" },
292
+ { name: "type" },
293
+ ],
294
+ commands: [
295
+ { name: "load" },
296
+ { name: "save" },
297
+ { name: "remove" },
298
+ ],
299
+ };
300
+ ```
301
+
302
+ Headless consumers call `core.load()` / `core.save(value)` directly — no `trigger` needed.
303
+
304
+ ### Shell (`<wcs-storage>`)
305
+
306
+ The Shell extends the Core declaration with the `trigger` output and the DOM-driven input surface; `commands` (`load` / `save` / `remove`) are inherited unchanged:
307
+
308
+ ```typescript
309
+ static wcBindable = {
310
+ ...StorageCore.wcBindable,
311
+ properties: [
312
+ ...StorageCore.wcBindable.properties,
313
+ { name: "trigger", event: "wcs-storage:trigger-changed" },
314
+ ],
315
+ inputs: [
316
+ { name: "key" },
317
+ { name: "type" },
318
+ { name: "value" },
319
+ { name: "manual" },
320
+ { name: "trigger" },
321
+ ],
322
+ };
323
+ ```
324
+
325
+ The Shell's inputs intentionally carry no `attribute` hint: the `key` / `type` / `manual` setters already reflect to their attributes, so a binding system that mirrors `inputs[].attribute` would set the attribute twice.
326
+
327
+ ## TypeScript Types
328
+
329
+ ```typescript
330
+ import type {
331
+ WcsStorageError, WcsStorageCoreValues, WcsStorageValues, StorageType
332
+ } from "@wcstack/storage";
333
+ ```
334
+
335
+ ```typescript
336
+ type StorageType = "local" | "session";
337
+
338
+ // Storage operation error
339
+ interface WcsStorageError {
340
+ operation: "load" | "save" | "remove";
341
+ message: string;
342
+ }
343
+
344
+ // Core (headless) — 3 state properties
345
+ interface WcsStorageCoreValues<T = unknown> {
346
+ value: T;
347
+ loading: boolean;
348
+ error: WcsStorageError | Error | null;
349
+ }
350
+
351
+ // Shell (<wcs-storage>) — extends Core with trigger
352
+ interface WcsStorageValues<T = unknown> extends WcsStorageCoreValues<T> {
353
+ trigger: boolean;
354
+ }
355
+ ```
356
+
357
+ ## Why it works well with `@wcstack/state`
358
+
359
+ `@wcstack/state` uses path strings as the sole contract between UI and state.
360
+ `<wcs-storage>` fits naturally into this model:
361
+
362
+ - `<wcs-storage>` auto-loads from storage on connection
363
+ - `value` is bound to a state path, reflected in the UI
364
+ - User interactions update state, which auto-saves back to storage
365
+ - State survives page reloads
366
+
367
+ Persistence looks just like any other state update.
368
+
369
+ ## Framework Integration
370
+
371
+ `<wcs-storage>` is CSBC + `wc-bindable-protocol`, so it works in any framework via thin `@wc-bindable/*` adapters.
372
+
373
+ ### React
374
+
375
+ ```tsx
376
+ import { useWcBindable } from "@wc-bindable/react";
377
+ import type { WcsStorageValues } from "@wcstack/storage";
378
+
379
+ interface Settings { theme: string; lang: string; }
380
+
381
+ function SettingsPanel() {
382
+ const [ref, { value: settings, loading, error }] =
383
+ useWcBindable<HTMLElement, WcsStorageValues<Settings>>();
384
+
385
+ return (
386
+ <>
387
+ <wcs-storage ref={ref} key="app-settings" />
388
+ {loading && <p>Loading...</p>}
389
+ {settings && <p>Theme: {settings.theme}</p>}
390
+ </>
391
+ );
392
+ }
393
+ ```
394
+
395
+ ### Vue
396
+
397
+ ```vue
398
+ <script setup lang="ts">
399
+ import { useWcBindable } from "@wc-bindable/vue";
400
+ import type { WcsStorageValues } from "@wcstack/storage";
401
+
402
+ interface Settings { theme: string; lang: string; }
403
+
404
+ const { ref, values } = useWcBindable<HTMLElement, WcsStorageValues<Settings>>();
405
+ </script>
406
+
407
+ <template>
408
+ <wcs-storage :ref="ref" key="app-settings" />
409
+ <p v-if="values.loading">Loading...</p>
410
+ <p v-else-if="values.value">Theme: {{ values.value.theme }}</p>
411
+ </template>
412
+ ```
413
+
414
+ ### Svelte
415
+
416
+ ```svelte
417
+ <script>
418
+ import { wcBindable } from "@wc-bindable/svelte";
419
+
420
+ let settings = $state(null);
421
+ let loading = $state(false);
422
+ </script>
423
+
424
+ <wcs-storage key="app-settings"
425
+ use:wcBindable={{ onUpdate: (name, v) => {
426
+ if (name === "value") settings = v;
427
+ if (name === "loading") loading = v;
428
+ }}} />
429
+
430
+ {#if loading}
431
+ <p>Loading...</p>
432
+ {:else if settings}
433
+ <p>Theme: {settings.theme}</p>
434
+ {/if}
435
+ ```
436
+
437
+ ### Solid
438
+
439
+ ```tsx
440
+ import { createWcBindable } from "@wc-bindable/solid";
441
+ import type { WcsStorageValues } from "@wcstack/storage";
442
+
443
+ interface Settings { theme: string; lang: string; }
444
+
445
+ function SettingsPanel() {
446
+ const [values, directive] = createWcBindable<WcsStorageValues<Settings>>();
447
+
448
+ return (
449
+ <>
450
+ <wcs-storage ref={directive} key="app-settings" />
451
+ <Show when={!values.loading} fallback={<p>Loading...</p>}>
452
+ <p>Theme: {values.value?.theme}</p>
453
+ </Show>
454
+ </>
455
+ );
456
+ }
457
+ ```
458
+
459
+ ### Vanilla — direct `bind()`
460
+
461
+ ```javascript
462
+ import { bind } from "@wc-bindable/core";
463
+
464
+ const storageEl = document.querySelector("wcs-storage");
465
+
466
+ bind(storageEl, (name, value) => {
467
+ console.log(`${name} changed:`, value);
468
+ });
469
+ ```
470
+
471
+ ## Optional DOM Triggering
472
+
473
+ When `autoTrigger` is enabled (default), clicking an element with a `data-storagetarget` attribute executes `save()` on the corresponding `<wcs-storage>`:
474
+
475
+ ```html
476
+ <button data-storagetarget="settings-store">Save Settings</button>
477
+ <wcs-storage id="settings-store" key="settings" manual
478
+ data-wcs="value: settings"></wcs-storage>
479
+ ```
480
+
481
+ ## Configuration
482
+
483
+ ```javascript
484
+ import { bootstrapStorage } from "@wcstack/storage";
485
+
486
+ bootstrapStorage({
487
+ autoTrigger: true,
488
+ triggerAttribute: "data-storagetarget",
489
+ tagNames: {
490
+ storage: "wcs-storage",
491
+ },
492
+ });
493
+ ```
494
+
495
+ ## Design Notes
496
+
497
+ - `value`, `loading`, `error` are **output state**
498
+ - `key`, `type`, `trigger` are **input / command surface**
499
+ - `trigger` is intentionally one-way: writing `true` saves, reset signals completion. If the underlying `save()` throws (e.g. `key` is unset), `trigger` is still reset to `false` and the completion event still fires, so it never gets stuck in the `true` state.
500
+ - The `value` setter auto-saves when not in `manual` mode
501
+ - **`value` setter vs `save()` / `trigger`**: assigning `value` (non-manual) persists the *assigned* argument (write-through). `save()` and `trigger`, by contrast, persist the *current* `value` — which a prior `load()` or a cross-tab `storage` event may have updated. This means `trigger`/`save()` can write back a value that arrived from another tab.
502
+ - **`value` in `manual` mode**: the `value` setter **stages** the value (no storage write) instead of persisting it. `el.value = x` updates the readable value (`el.value === x`) but does **not** touch storage; the actual write happens only via `save()` / `trigger`. This is what makes the `value: …` + `trigger: …` binding pair work — the bound value is staged, then committed on trigger.
503
+ - **No echo guard on the non-manual `value` path**: only the *staging* path (the Core `value` setter, used in `manual` mode) skips a same-value `value-changed` re-dispatch. The main non-manual path (`value` setter → `save()`) is deliberately write-through: every assignment persists and re-emits `value-changed`, even when the assigned value equals the current one. This is intentional — the write-through contract above must hold, and same-tab `storage` events do not re-fire, so there is no feedback loop. In a `data-wcs="value: x"` two-way binding the echoed `value-changed` is harmless: `@wcstack/state` dedups the round-trip on its side.
504
+ - **`save` command arity**: the headless Core takes `save(value)`, while the Shell exposes `save()` (persists the current value). Both appear under the same `commands` name `save`; the protocol's `commands` metadata is descriptive and arity-less, so this is contractual, not a protocol violation.
505
+ - **Invalid `type`**: any `type` attribute other than `"session"` is treated as `"local"`. An invalid value (e.g. `type="foo"`) silently falls back to `local` rather than throwing.
506
+ - **Runtime `type` change**: changing the `type` attribute after connection updates the Core's storage area for subsequent operations but does **not** re-load from the new area (only `key` changes auto-reload in non-manual mode). Re-load explicitly with `load()` if you need the value from the newly selected area.
507
+ - **`error` shape**: on a storage failure, `error` is set to a `WcsStorageError` (`{ operation, message }`) identifying which operation (`load` / `save` / `remove`) failed. `key is required` (calling an operation with no key) is thrown synchronously, not surfaced via `error`. In practice `error` is therefore always either a `WcsStorageError` or `null`; the wider `WcsStorageError | Error | null` type is kept for forward compatibility and consistency with sibling packages.
508
+ - JSON auto-serialization handles objects, arrays, and primitives transparently
509
+ - Saving `null` / `undefined` removes the key from storage
510
+ - Cross-tab sync via `storage` event works only with localStorage. The Shell binds the watcher to its current `key` / `type` on connect (and re-binds on re-attach), so cross-tab sync works even in `manual` mode where no auto-load runs. Changing the `key` attribute after connection always re-syncs the Core key, so cross-tab sync follows the new key even in `manual` mode or when the key is cleared. A successful cross-tab update also clears any stale `error` (just like `load()` / `save()` / `remove()` do at the start of a successful operation), so a fresh value never coexists with a leftover error from an earlier failure.
511
+ - `manual` is useful when you want explicit control over save timing
512
+
513
+ ## License
514
+
515
+ MIT