svstate 0.0.1 โ†’ 1.0.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.
Files changed (3) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +815 -0
  3. package/package.json +1 -1
package/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2026 BCsabaEngine
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,815 @@
1
+ # ๐Ÿš€ svstate
2
+
3
+ ### Supercharged `$state()` for Svelte 5
4
+
5
+ [![Node.js](https://img.shields.io/badge/Node.js-%3E%3D20-green.svg)](https://nodejs.org/)
6
+ [![Svelte 5](https://img.shields.io/badge/Svelte-5-orange.svg)](https://svelte.dev/)
7
+ [![License: ISC](https://img.shields.io/badge/License-ISC-blue.svg)](https://opensource.org/licenses/ISC)
8
+
9
+ > **Deep reactive proxy with validation, snapshot/undo, and side effects โ€” built for complex, real-world applications.**
10
+
11
+ <p align="center">
12
+ <img src="svstate.png" alt="svstate" />
13
+ </p>
14
+
15
+ <p align="center">
16
+ <a href="https://bcsabaengine.github.io/svstate/"><strong>๐ŸŽฎ Live Demo</strong></a> ยท
17
+ <a href="#-installation">Installation</a> ยท
18
+ <a href="#-core-features">Features</a> ยท
19
+ <a href="#-complete-examples">Examples</a>
20
+ </p>
21
+
22
+ ---
23
+
24
+ ## ๐Ÿค” The Problem
25
+
26
+ Svelte 5's `$state()` is fantastic for simple use cases. A login form? Easy. A settings toggle? Trivial.
27
+
28
+ But what about **real enterprise applications**?
29
+
30
+ ```typescript
31
+ // โŒ A simple user/password form is NOT your problem
32
+ const loginForm = $state({ username: '', password: '' });
33
+
34
+ // โœ… THIS is your problem โ€” a complex ERP customer page
35
+ const customer = $state({
36
+ name: 'Acme Corp',
37
+ taxId: 'US-12345678',
38
+ creditLimit: 50000,
39
+ addresses: [
40
+ { type: 'billing', street: '123 Main St', city: 'New York', zip: '10001' },
41
+ { type: 'shipping', street: '456 Oak Ave', city: 'Boston', zip: '02101' }
42
+ ],
43
+ contacts: [
44
+ { name: 'John Doe', email: 'john@acme.com', phone: '555-1234', isPrimary: true },
45
+ { name: 'Jane Smith', email: 'jane@acme.com', phone: '555-5678', isPrimary: false }
46
+ ],
47
+ billing: {
48
+ paymentTerms: 'NET30',
49
+ currency: 'USD',
50
+ bankAccount: { iban: 'US12345678901234567890', swift: 'BOFA1234' }
51
+ }
52
+ });
53
+ ```
54
+
55
+ **With native Svelte 5, you're missing:**
56
+
57
+ - โŒ No automatic change detection for nested properties
58
+ - โŒ No built-in validation that mirrors your data structure
59
+ - โŒ No way to know _which_ property changed and react to it
60
+ - โŒ No undo/redo for complex editing workflows
61
+ - โŒ No dirty tracking across the entire object tree
62
+
63
+ ---
64
+
65
+ ## โœจ The Solution: svstate
66
+
67
+ **svstate** wraps your state in a deep reactive proxy that:
68
+
69
+ - ๐Ÿ” **Detects changes** at any nesting level (`customer.billing.bankAccount.iban`)
70
+ - โœ… **Validates** with a structure that mirrors your data
71
+ - โšก **Fires effects** when any property changes (with full context)
72
+ - โช **Snapshots & undo** for complex editing workflows
73
+ - ๐ŸŽฏ **Tracks dirty state** automatically
74
+
75
+ ```typescript
76
+ import { createSvState, stringValidator, numberValidator } from 'svstate';
77
+
78
+ const { data, state, rollback, reset, execute } = createSvState(customer, {
79
+ validator: (source) => ({
80
+ /* validation that mirrors your structure */
81
+ }),
82
+ effect: ({ snapshot, property, currentValue, oldValue }) => {
83
+ console.log(`${property} changed from ${oldValue} to ${currentValue}`);
84
+ snapshot(`Changed ${property}`); // Create undo point
85
+ },
86
+ action: async () => {
87
+ /* Save to API */
88
+ }
89
+ });
90
+
91
+ // Deep binding just works!
92
+ data.billing.bankAccount.iban = 'NEW-IBAN'; // โœ… Detected, validated, snapshot created
93
+ ```
94
+
95
+ ---
96
+
97
+ ## ๐Ÿ“ฆ Installation
98
+
99
+ ```bash
100
+ npm install svstate
101
+ ```
102
+
103
+ **Requirements:** Node.js โ‰ฅ20, Svelte 5
104
+
105
+ ---
106
+
107
+ ## ๐ŸŽฏ Core Features
108
+
109
+ ### 1๏ธโƒฃ Validation โ€” Structure-Aware, Real-Time
110
+
111
+ Validation in svstate mirrors your data structure exactly. When you have nested objects, your validation errors have the same shape. No more flattening, no more path strings.
112
+
113
+ **Built-in fluent validators** handle common patterns with chainable methods:
114
+
115
+ ```typescript
116
+ import { createSvState, stringValidator, numberValidator, dateValidator } from 'svstate';
117
+
118
+ const {
119
+ data,
120
+ state: { errors, hasErrors }
121
+ } = createSvState(
122
+ {
123
+ email: '',
124
+ age: 0,
125
+ birthDate: new Date(),
126
+ tags: []
127
+ },
128
+ {
129
+ validator: (source) => ({
130
+ // Fluent API: chain validations, get first error
131
+ email: stringValidator(source.email, 'trim') // 'trim' preprocesses input
132
+ .required()
133
+ .email()
134
+ .maxLength(100)
135
+ .getError(),
136
+
137
+ age: numberValidator(source.age).required().integer().between(18, 120).getError(),
138
+
139
+ birthDate: dateValidator(source.birthDate).required().past().minAge(18).getError(),
140
+
141
+ tags: arrayValidator(source.tags).minLength(1).maxLength(10).unique().getError()
142
+ })
143
+ }
144
+ );
145
+
146
+ // In your template:
147
+ // $errors?.email โ†’ "Required" | "Invalid email format" | ""
148
+ // $hasErrors โ†’ true/false
149
+ ```
150
+
151
+ **Key features:**
152
+
153
+ - ๐Ÿ”„ Automatic re-validation on any change (debounced via microtask)
154
+ - ๐Ÿ“ Error structure matches data structure exactly
155
+ - ๐Ÿงน String preprocessing: `'trim'`, `'normalize'`, `'upper'`, `'lower'`
156
+ - โšก First-error-wins: `getError()` returns the first failure
157
+
158
+ ---
159
+
160
+ ### 2๏ธโƒฃ Effect โ€” React to Every Change
161
+
162
+ JavaScript objects don't have property change events. **svstate fixes this.** The `effect` callback fires whenever _any_ property changes, giving you full context:
163
+
164
+ ```typescript
165
+ const { data } = createSvState(formData, {
166
+ effect: ({ target, property, currentValue, oldValue, snapshot }) => {
167
+ // 'property' is the dot-notation path: "address.city", "contacts.0.email"
168
+ console.log(`${property}: ${oldValue} โ†’ ${currentValue}`);
169
+
170
+ // Create undo point on significant changes
171
+ if (property.startsWith('billing')) {
172
+ snapshot(`Modified billing: ${property}`);
173
+ }
174
+
175
+ // Trigger side effects
176
+ if (property === 'country') {
177
+ loadTaxRates(currentValue);
178
+ }
179
+ }
180
+ });
181
+ ```
182
+
183
+ **Use cases:**
184
+
185
+ - ๐Ÿ“ธ Create snapshots for undo/redo
186
+ - ๐Ÿ“Š Analytics tracking
187
+ - ๐Ÿ”— Cross-field updates (computed fields)
188
+ - ๐ŸŒ Trigger API calls on specific changes
189
+
190
+ ---
191
+
192
+ ### 3๏ธโƒฃ Action โ€” Submit to Backend with Loading States
193
+
194
+ Each svstate instance has **one action** โ€” typically for submitting data to your backend, REST API, or cloud database (Supabase, Firebase, etc.). The `actionInProgress` store lets you show loading spinners and disable UI while waiting for the server response. This is why async support is essential.
195
+
196
+ ```typescript
197
+ const { data, execute, state: { actionInProgress, actionError } } = createSvState(
198
+ formData,
199
+ {
200
+ validator: (source) => ({ /* ... */ }),
201
+ action: async (params) => {
202
+ // Submit to your backend, Supabase, Firebase, etc.
203
+ const response = await fetch('/api/customers', {
204
+ method: 'POST',
205
+ body: JSON.stringify(data)
206
+ });
207
+ if (!response.ok) throw new Error('Save failed');
208
+ },
209
+ actionCompleted: (error) => {
210
+ if (!error) showToast('Saved successfully!');
211
+ }
212
+ }
213
+ );
214
+
215
+ // Show loading state while waiting for server
216
+ <button onclick={() => execute()} disabled={$hasErrors || $actionInProgress}>
217
+ {$actionInProgress ? 'Saving...' : 'Save'}
218
+ </button>
219
+ ```
220
+
221
+ **Action parameters** โ€” When you need different submit behaviors from multiple places (e.g., "Save Draft" vs "Publish", or "Save" vs "Save & Close"), pass parameters to `execute()`:
222
+
223
+ ```typescript
224
+ const { data, execute } = createSvState(articleData, {
225
+ action: async (params?: { draft?: boolean; redirect?: string }) => {
226
+ await supabase.from('articles').upsert({
227
+ ...data,
228
+ status: params?.draft ? 'draft' : 'published',
229
+ published_at: params?.draft ? null : new Date()
230
+ });
231
+
232
+ if (params?.redirect) goto(params.redirect);
233
+ }
234
+ });
235
+
236
+ // Multiple submit buttons with different behaviors
237
+ <button onclick={() => execute({ draft: true })}>
238
+ Save Draft
239
+ </button>
240
+
241
+ <button onclick={() => execute({ draft: false, redirect: '/articles' })}>
242
+ Publish & Go Back
243
+ </button>
244
+
245
+ <button onclick={() => execute()}>
246
+ Publish
247
+ </button>
248
+ ```
249
+
250
+ **Key features:**
251
+
252
+ - ๐ŸŽฏ **One action per state** โ€” focused on data submission
253
+ - โณ **`actionInProgress`** โ€” show spinners, disable inputs while waiting
254
+ - ๐Ÿ”€ **Action parameters** โ€” different behaviors from multiple submit points
255
+ - ๐Ÿ”’ Prevents concurrent execution by default
256
+ - โŒ `actionError` store captures failures
257
+ - ๐Ÿ”„ Successful action resets dirty state and snapshots
258
+
259
+ ---
260
+
261
+ ### 4๏ธโƒฃ Undo โ€” Snapshot-Based Time Travel
262
+
263
+ Complex forms need undo. svstate provides a snapshot system that captures state at meaningful moments:
264
+
265
+ ```typescript
266
+ const {
267
+ data,
268
+ rollback,
269
+ reset,
270
+ state: { snapshots }
271
+ } = createSvState(formData, {
272
+ effect: ({ snapshot, property }) => {
273
+ // Create snapshot on each change
274
+ // Same title = replaces previous (debouncing)
275
+ snapshot(`Changed ${property}`);
276
+
277
+ // Use snapshot(title, false) to always create new
278
+ }
279
+ });
280
+
281
+ // Undo last change
282
+ rollback();
283
+
284
+ // Undo 3 changes
285
+ rollback(3);
286
+
287
+ // Reset to initial state
288
+ reset();
289
+
290
+ // Show history
291
+ $snapshots.forEach((s, i) => console.log(`${i}: ${s.title}`));
292
+ ```
293
+
294
+ **Key features:**
295
+
296
+ - ๐Ÿ“ธ `snapshot(title, replace?)` โ€” create undo points
297
+ - โช `rollback(steps)` โ€” undo N changes
298
+ - ๐Ÿ”„ `reset()` โ€” return to initial state
299
+ - ๐Ÿ“œ `snapshots` store โ€” access full history
300
+ - ๐Ÿ”€ Smart deduplication: same title replaces previous snapshot
301
+
302
+ ---
303
+
304
+ ### 5๏ธโƒฃ Options โ€” Fine-Tune Behavior
305
+
306
+ Customize svstate behavior with options:
307
+
308
+ ```typescript
309
+ const { data } = createSvState(formData, actuators, {
310
+ // Reset isDirty after successful action (default: true)
311
+ resetDirtyOnAction: true,
312
+
313
+ // Debounce validation in ms (default: 0 = microtask)
314
+ debounceValidation: 300,
315
+
316
+ // Allow concurrent action executions (default: false)
317
+ allowConcurrentActions: false,
318
+
319
+ // Keep actionError until next action (default: false)
320
+ persistActionError: false
321
+ });
322
+ ```
323
+
324
+ | Option | Default | Description |
325
+ | ------------------------ | ------- | ---------------------------------------- |
326
+ | `resetDirtyOnAction` | `true` | Clear dirty flag after successful action |
327
+ | `debounceValidation` | `0` | Delay validation (0 = next microtask) |
328
+ | `allowConcurrentActions` | `false` | Block execute() while action runs |
329
+ | `persistActionError` | `false` | Clear error on next change or action |
330
+
331
+ ---
332
+
333
+ ## ๐Ÿ—๏ธ Complete Examples
334
+
335
+ ### Example 1: ERP Customer Form with Nested Addresses
336
+
337
+ A complex customer management form with 3-level nesting, validation, undo, and API save:
338
+
339
+ ```typescript
340
+ <script lang="ts">
341
+ import { createSvState, stringValidator, numberValidator, arrayValidator } from 'svstate';
342
+
343
+ // ๐Ÿ“Š Complex nested data structure
344
+ const initialCustomer = {
345
+ name: '',
346
+ taxId: '',
347
+ creditLimit: 0,
348
+ address: {
349
+ street: '',
350
+ city: '',
351
+ zip: '',
352
+ country: ''
353
+ },
354
+ contacts: [
355
+ { name: '', email: '', phone: '', isPrimary: true }
356
+ ],
357
+ billing: {
358
+ paymentTerms: 'NET30',
359
+ currency: 'USD',
360
+ bankAccount: {
361
+ iban: '',
362
+ swift: ''
363
+ }
364
+ }
365
+ };
366
+
367
+ // ๐Ÿš€ Create supercharged state
368
+ const {
369
+ data, // Deep reactive proxy
370
+ execute, // Trigger async action
371
+ rollback, // Undo changes
372
+ reset, // Reset to initial
373
+ state: {
374
+ errors, // Validation errors (same structure as data)
375
+ hasErrors, // Quick boolean check
376
+ isDirty, // Has anything changed?
377
+ actionInProgress, // Is action running?
378
+ actionError, // Last action error
379
+ snapshots // Undo history
380
+ }
381
+ } = createSvState(initialCustomer, {
382
+
383
+ // โœ… Validator mirrors data structure exactly
384
+ validator: (source) => ({
385
+ name: stringValidator(source.name, 'trim')
386
+ .required()
387
+ .minLength(2)
388
+ .maxLength(100)
389
+ .getError(),
390
+
391
+ taxId: stringValidator(source.taxId, 'trim', 'upper')
392
+ .required()
393
+ .regexp(/^[A-Z]{2}-\d{8}$/, 'Format: XX-12345678')
394
+ .getError(),
395
+
396
+ creditLimit: numberValidator(source.creditLimit)
397
+ .required()
398
+ .min(0)
399
+ .max(1_000_000)
400
+ .getError(),
401
+
402
+ // ๐Ÿ“ Nested address validation
403
+ address: {
404
+ street: stringValidator(source.address.street, 'trim')
405
+ .required()
406
+ .minLength(5)
407
+ .getError(),
408
+ city: stringValidator(source.address.city, 'trim')
409
+ .required()
410
+ .getError(),
411
+ zip: stringValidator(source.address.zip, 'trim')
412
+ .required()
413
+ .minLength(5)
414
+ .getError(),
415
+ country: stringValidator(source.address.country)
416
+ .required()
417
+ .inArray(['US', 'CA', 'UK', 'DE', 'FR'])
418
+ .getError()
419
+ },
420
+
421
+ // ๐Ÿ“‹ Array validation
422
+ contacts: arrayValidator(source.contacts)
423
+ .required()
424
+ .minLength(1)
425
+ .getError(),
426
+
427
+ // ๐Ÿ’ณ 3-level nested billing validation
428
+ billing: {
429
+ paymentTerms: stringValidator(source.billing.paymentTerms)
430
+ .required()
431
+ .inArray(['NET15', 'NET30', 'NET60', 'COD'])
432
+ .getError(),
433
+ currency: stringValidator(source.billing.currency)
434
+ .required()
435
+ .inArray(['USD', 'EUR', 'GBP'])
436
+ .getError(),
437
+ bankAccount: {
438
+ iban: stringValidator(source.billing.bankAccount.iban, 'trim', 'upper')
439
+ .required()
440
+ .minLength(15)
441
+ .maxLength(34)
442
+ .getError(),
443
+ swift: stringValidator(source.billing.bankAccount.swift, 'trim', 'upper')
444
+ .required()
445
+ .minLength(8)
446
+ .maxLength(11)
447
+ .getError()
448
+ }
449
+ }
450
+ }),
451
+
452
+ // โšก Effect fires on every change
453
+ effect: ({ snapshot, property, currentValue, oldValue }) => {
454
+ // Create undo point with descriptive title
455
+ const fieldName = property.split('.').pop();
456
+ snapshot(`Changed ${fieldName}`);
457
+
458
+ // Log for debugging
459
+ console.log(`[svstate] ${property}: "${oldValue}" โ†’ "${currentValue}"`);
460
+ },
461
+
462
+ // ๐ŸŒ Async save action
463
+ action: async () => {
464
+ const response = await fetch('/api/customers', {
465
+ method: 'POST',
466
+ headers: { 'Content-Type': 'application/json' },
467
+ body: JSON.stringify(data)
468
+ });
469
+
470
+ if (!response.ok) {
471
+ const error = await response.json();
472
+ throw new Error(error.message || 'Failed to save customer');
473
+ }
474
+ },
475
+
476
+ // โœ… Called after action (success or failure)
477
+ actionCompleted: (error) => {
478
+ if (error) {
479
+ console.error('Save failed:', error);
480
+ } else {
481
+ console.log('Customer saved successfully!');
482
+ }
483
+ }
484
+ });
485
+ </script>
486
+
487
+ <!-- ๐Ÿ“ Template with deep bindings -->
488
+ <form onsubmit|preventDefault={() => execute()}>
489
+ <!-- Basic fields -->
490
+ <input bind:value={data.name} placeholder="Company Name" />
491
+ {#if $errors?.name}<span class="error">{$errors.name}</span>{/if}
492
+
493
+ <!-- 2-level nested: address.city -->
494
+ <input bind:value={data.address.city} placeholder="City" />
495
+ {#if $errors?.address?.city}<span class="error">{$errors.address.city}</span>{/if}
496
+
497
+ <!-- 3-level nested: billing.bankAccount.iban -->
498
+ <input bind:value={data.billing.bankAccount.iban} placeholder="IBAN" />
499
+ {#if $errors?.billing?.bankAccount?.iban}
500
+ <span class="error">{$errors.billing.bankAccount.iban}</span>
501
+ {/if}
502
+
503
+ <!-- Action buttons -->
504
+ <div class="actions">
505
+ <button type="submit" disabled={$hasErrors || $actionInProgress}>
506
+ {$actionInProgress ? 'Saving...' : 'Save Customer'}
507
+ </button>
508
+
509
+ <button type="button" onclick={() => rollback()} disabled={$snapshots.length <= 1}>
510
+ Undo ({$snapshots.length - 1})
511
+ </button>
512
+
513
+ <button type="button" onclick={reset} disabled={!$isDirty}>
514
+ Reset
515
+ </button>
516
+ </div>
517
+
518
+ {#if $actionError}
519
+ <div class="error-banner">{$actionError.message}</div>
520
+ {/if}
521
+ </form>
522
+ ```
523
+
524
+ ---
525
+
526
+ ### Example 2: Product Inventory with Array Management
527
+
528
+ Managing arrays of items with validation at both array and item level:
529
+
530
+ ```typescript
531
+ <script lang="ts">
532
+ import { createSvState, stringValidator, numberValidator, arrayValidator } from 'svstate';
533
+
534
+ // ๐Ÿ“ฆ Product with inventory items
535
+ const initialProduct = {
536
+ sku: '',
537
+ name: '',
538
+ description: '',
539
+ price: 0,
540
+ inventory: [
541
+ { warehouseId: 'WH-001', quantity: 0, reorderPoint: 10 }
542
+ ],
543
+ tags: [] as string[]
544
+ };
545
+
546
+ const {
547
+ data,
548
+ rollback,
549
+ state: { errors, hasErrors, isDirty, snapshots }
550
+ } = createSvState(initialProduct, {
551
+
552
+ validator: (source) => ({
553
+ sku: stringValidator(source.sku, 'trim', 'upper')
554
+ .required()
555
+ .regexp(/^[A-Z]{3}-\d{4}$/, 'Format: ABC-1234')
556
+ .getError(),
557
+
558
+ name: stringValidator(source.name, 'trim')
559
+ .required()
560
+ .minLength(3)
561
+ .maxLength(100)
562
+ .getError(),
563
+
564
+ description: stringValidator(source.description, 'trim')
565
+ .maxLength(500)
566
+ .getError(),
567
+
568
+ price: numberValidator(source.price)
569
+ .required()
570
+ .positive()
571
+ .decimal(2) // Max 2 decimal places
572
+ .getError(),
573
+
574
+ // ๐Ÿ“‹ Validate the array itself
575
+ inventory: arrayValidator(source.inventory)
576
+ .required()
577
+ .minLength(1)
578
+ .getError(),
579
+
580
+ // ๐Ÿท๏ธ Tags must be unique
581
+ tags: arrayValidator(source.tags)
582
+ .maxLength(10)
583
+ .unique()
584
+ .getError()
585
+ }),
586
+
587
+ effect: ({ snapshot, property, currentValue }) => {
588
+ // Create snapshots for significant changes
589
+ if (property === 'price') {
590
+ snapshot(`Price: $${currentValue}`);
591
+ } else if (property.startsWith('inventory')) {
592
+ snapshot(`Updated inventory`);
593
+ } else {
594
+ snapshot(`Changed ${property}`);
595
+ }
596
+ }
597
+ });
598
+
599
+ // ๐Ÿ”ง Array manipulation functions
600
+ function addWarehouse() {
601
+ data.inventory.push({
602
+ warehouseId: `WH-${String(data.inventory.length + 1).padStart(3, '0')}`,
603
+ quantity: 0,
604
+ reorderPoint: 10
605
+ });
606
+ }
607
+
608
+ function removeWarehouse(index: number) {
609
+ data.inventory.splice(index, 1);
610
+ }
611
+
612
+ function addTag(tag: string) {
613
+ if (tag && !data.tags.includes(tag)) {
614
+ data.tags.push(tag);
615
+ }
616
+ }
617
+
618
+ function removeTag(index: number) {
619
+ data.tags.splice(index, 1);
620
+ }
621
+ </script>
622
+
623
+ <!-- Product form -->
624
+ <div class="product-form">
625
+ <input bind:value={data.sku} placeholder="SKU (ABC-1234)" />
626
+ {#if $errors?.sku}<span class="error">{$errors.sku}</span>{/if}
627
+
628
+ <input bind:value={data.name} placeholder="Product Name" />
629
+ <input type="number" bind:value={data.price} step="0.01" placeholder="Price" />
630
+
631
+ <!-- ๐Ÿ“ฆ Inventory locations (array) -->
632
+ <section class="inventory">
633
+ <h3>Inventory Locations</h3>
634
+ {#if $errors?.inventory}
635
+ <span class="error">{$errors.inventory}</span>
636
+ {/if}
637
+
638
+ {#each data.inventory as item, index}
639
+ <div class="inventory-row">
640
+ <input bind:value={item.warehouseId} placeholder="Warehouse ID" />
641
+ <input type="number" bind:value={item.quantity} placeholder="Qty" />
642
+ <input type="number" bind:value={item.reorderPoint} placeholder="Reorder at" />
643
+ <button onclick={() => removeWarehouse(index)}>Remove</button>
644
+ </div>
645
+ {/each}
646
+
647
+ <button onclick={addWarehouse}>+ Add Warehouse</button>
648
+ </section>
649
+
650
+ <!-- ๐Ÿท๏ธ Tags (simple array) -->
651
+ <section class="tags">
652
+ <h3>Tags</h3>
653
+ {#if $errors?.tags}<span class="error">{$errors.tags}</span>{/if}
654
+
655
+ <div class="tag-list">
656
+ {#each data.tags as tag, index}
657
+ <span class="tag">
658
+ {tag}
659
+ <button onclick={() => removeTag(index)}>ร—</button>
660
+ </span>
661
+ {/each}
662
+ </div>
663
+
664
+ <input
665
+ placeholder="Add tag..."
666
+ onkeydown={(e) => {
667
+ if (e.key === 'Enter') {
668
+ addTag(e.currentTarget.value);
669
+ e.currentTarget.value = '';
670
+ }
671
+ }}
672
+ />
673
+ </section>
674
+
675
+ <!-- Status bar -->
676
+ <div class="status">
677
+ <span class:dirty={$isDirty}>{$isDirty ? 'Modified' : 'Saved'}</span>
678
+ <span>{$snapshots.length} snapshots</span>
679
+ <button onclick={() => rollback()} disabled={$snapshots.length <= 1}>
680
+ Undo
681
+ </button>
682
+ </div>
683
+ </div>
684
+ ```
685
+
686
+ ---
687
+
688
+ ## ๐Ÿงฐ API Reference
689
+
690
+ ### `createSvState(init, actuators?, options?)`
691
+
692
+ Creates a supercharged state object.
693
+
694
+ **Returns:**
695
+ | Property | Type | Description |
696
+ |----------|------|-------------|
697
+ | `data` | `T` | Deep reactive proxy โ€” bind directly |
698
+ | `execute(params?)` | `(P?) => Promise<void>` | Run the configured action |
699
+ | `rollback(steps?)` | `(n?: number) => void` | Undo N changes (default: 1) |
700
+ | `reset()` | `() => void` | Return to initial state |
701
+ | `state.errors` | `Readable<V>` | Validation errors store |
702
+ | `state.hasErrors` | `Readable<boolean>` | Quick error check |
703
+ | `state.isDirty` | `Readable<boolean>` | Has state changed? |
704
+ | `state.actionInProgress` | `Readable<boolean>` | Is action running? |
705
+ | `state.actionError` | `Readable<Error>` | Last action error |
706
+ | `state.snapshots` | `Readable<Snapshot[]>` | Undo history |
707
+
708
+ ### Built-in Validators
709
+
710
+ svstate ships with four fluent validator builders that cover the most common validation scenarios. Each validator uses a chainable API โ€” call validation methods in sequence and finish with `getError()` to retrieve the first error message (or an empty string if valid).
711
+
712
+ String validators support optional preprocessing (`'trim'`, `'normalize'`, `'upper'`, `'lower'`) applied before validation. All validators return descriptive error messages that you can customize or use as-is.
713
+
714
+ | Validator | Methods |
715
+ | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
716
+ | `stringValidator(input, ...prepares)` | `required()`, `minLength(n)`, `maxLength(n)`, `email()`, `regexp(re)`, `inArray(arr)`, `startsWith(s)`, `endsWith(s)`, `contains(s)`, `noSpace()`, `uppercase()`, `lowercase()`, `alphanumeric()`, `numeric()`, `website(mode)` |
717
+ | `numberValidator(input)` | `required()`, `min(n)`, `max(n)`, `between(min, max)`, `integer()`, `positive()`, `negative()`, `nonNegative()`, `multipleOf(n)`, `decimal(places)`, `percentage()` |
718
+ | `arrayValidator(input)` | `required()`, `minLength(n)`, `maxLength(n)`, `unique()` |
719
+ | `dateValidator(input)` | `required()`, `before(date)`, `after(date)`, `between(start, end)`, `past()`, `future()`, `weekday()`, `weekend()`, `minAge(years)`, `maxAge(years)` |
720
+
721
+ ### TypeScript Types
722
+
723
+ svstate exports TypeScript types to help you write type-safe external validator and effect functions. This is useful when you want to define these functions outside the `createSvState` call or reuse them across multiple state instances.
724
+
725
+ ```typescript
726
+ import type { Validator, EffectContext, Snapshot, SnapshotFunction, SvStateOptions } from 'svstate';
727
+ ```
728
+
729
+ | Type | Description |
730
+ | ------------------ | --------------------------------------------------------------------------------------------------- |
731
+ | `Validator` | Nested object type for validation errors โ€” leaf values are error strings (empty = valid) |
732
+ | `EffectContext<T>` | Context object passed to effect callbacks: `{ snapshot, target, property, currentValue, oldValue }` |
733
+ | `SnapshotFunction` | Type for the `snapshot(title, replace?)` function used in effects |
734
+ | `Snapshot<T>` | Shape of a snapshot entry: `{ title: string; data: T }` |
735
+ | `SvStateOptions` | Configuration options type for `createSvState` |
736
+
737
+ **Example: External validator and effect functions**
738
+
739
+ ```typescript
740
+ import { createSvState, stringValidator, type Validator, type EffectContext } from 'svstate';
741
+
742
+ // Define types for your data
743
+ type UserData = {
744
+ name: string;
745
+ email: string;
746
+ };
747
+
748
+ type UserErrors = {
749
+ name: string;
750
+ email: string;
751
+ };
752
+
753
+ // External validator function with proper typing
754
+ const validateUser = (source: UserData): UserErrors => ({
755
+ name: stringValidator(source.name, 'trim').required().minLength(2).getError(),
756
+ email: stringValidator(source.email, 'trim').required().email().getError()
757
+ });
758
+
759
+ // External effect function with proper typing
760
+ const userEffect = ({ snapshot, property, currentValue }: EffectContext<UserData>) => {
761
+ console.log(`${property} changed to ${currentValue}`);
762
+ snapshot(`Updated ${property}`);
763
+ };
764
+
765
+ // Use the external functions
766
+ const { data, state } = createSvState<UserData, UserErrors, object>(
767
+ { name: '', email: '' },
768
+ { validator: validateUser, effect: userEffect }
769
+ );
770
+ ```
771
+
772
+ ---
773
+
774
+ ## ๐ŸŽจ Why svstate?
775
+
776
+ | Feature | Native Svelte 5 | svstate |
777
+ | ---------------------- | ------------------ | --------------- |
778
+ | Simple flat objects | โœ… Great | โœ… Great |
779
+ | Deep nested objects | โš ๏ธ Manual tracking | โœ… Automatic |
780
+ | Property change events | โŒ Not available | โœ… Full context |
781
+ | Structured validation | โŒ DIY | โœ… Mirrors data |
782
+ | Undo/Redo | โŒ DIY | โœ… Built-in |
783
+ | Dirty tracking | โŒ DIY | โœ… Automatic |
784
+ | Action loading states | โŒ DIY | โœ… Built-in |
785
+
786
+ **svstate is for:**
787
+
788
+ - ๐Ÿข Enterprise applications with complex forms
789
+ - ๐Ÿ“Š ERP, CRM, admin dashboards
790
+ - ๐Ÿ“ Multi-step wizards
791
+ - ๐Ÿ”„ Applications needing undo/redo
792
+ - โœ… Any form beyond username/password
793
+
794
+ ---
795
+
796
+ ## ๐Ÿ“š Resources
797
+
798
+ - ๐ŸŽฎ [Live Demo](https://bcsabaengine.github.io/svstate/) โ€” Try it in your browser
799
+ - ๐Ÿ“– [Documentation](https://github.com/BCsabaEngine/svstate)
800
+ - ๐Ÿ› [Report Issues](https://github.com/BCsabaEngine/svstate/issues)
801
+ - ๐Ÿ’ฌ [Discussions](https://github.com/BCsabaEngine/svstate/discussions)
802
+
803
+ ---
804
+
805
+ ## ๐Ÿ“„ License
806
+
807
+ ISC ยฉ [BCsabaEngine](https://github.com/BCsabaEngine)
808
+
809
+ ---
810
+
811
+ <p align="center">
812
+ <b>Stop fighting with state. Start building features.</b>
813
+ <br><br>
814
+ โญ Star us on GitHub if svstate helps your project!
815
+ </p>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svstate",
3
- "version": "0.0.1",
3
+ "version": "1.0.0",
4
4
  "description": "Supercharged $state() for Svelte 5: deep reactive proxy with validation, cross-field rules, computed & side-effects",
5
5
  "author": "BCsabaEngine",
6
6
  "license": "ISC",