svstate 0.0.1 โ†’ 1.0.1

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