From bd9240a88dbd20649319594faf790e9e548dfff2 Mon Sep 17 00:00:00 2001 From: Oluwawunmi Bewaji Date: Sat, 4 Jul 2026 01:39:56 +0000 Subject: [PATCH 01/16] Blog: Add dynamic invoice form guide with Svelte 5 and Formisch --- .../dynamic-invoice-form-svelte/index.mdx | 510 ++++++++++++++++++ 1 file changed, 510 insertions(+) create mode 100644 website/src/routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx diff --git a/website/src/routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx b/website/src/routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx new file mode 100644 index 00000000..61104f9a --- /dev/null +++ b/website/src/routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx @@ -0,0 +1,510 @@ +--- +title: 'Building a Dynamic Invoice Form in Svelte 5 with Formisch' +description: Build a dynamic invoice form in Svelte 5 using Formisch, Valibot, FieldArray, and typed schema validation. +authors: + - flySewa +--- + + + +# Building a Dynamic Invoice Form in Svelte 5 with Formisch + +Svelte 5 gives us powerful primitives for managing state with runes, but forms still have their own set of challenges. + +A field is not only a value. It has validation state, errors, transformations, and a relationship with the rest of the form. When a form grows beyond a handful of inputs, things like nested data, dynamic fields, and derived values all need to stay in sync. + +At this point, form state management moves away from handling individual inputs and towards managing the entire form model. + +Formisch approaches this by treating the form as a connected structure. It's a headless form library designed to stay lightweight while keeping the schema, state, validation, and submitted output connected. The schema, state, validation, and submitted output all come from the same source, while still letting you control the UI. + +To demonstrate this, we'll build a dynamic invoice form and look at how Formisch works with Svelte 5's rune-based reactivity to manage: + +- Invoice details +- Client information +- Dynamic line items +- Validation +- Live totals +- Typed submission output + +By the end, you'll have a working invoice form that handles nested data, dynamic fields, validation, and typed output. + +The focus here is on the form architecture: how state, validation, and dynamic fields fit together. The styling and project setup are kept minimal on purpose. + +**Prerequisites:** This tutorial assumes you already have a Svelte 5 project set up and are familiar with Svelte components and runes. + +We'll be using: + +- Svelte 5 +- [Formisch](https://formisch.dev) +- [Valibot](https://valibot.dev) for schema validation + +You can follow along here, or view the complete example on [Stackblitz](https://stackblitz.com/edit/sveltejs-kit-template-default-vbklpioo?file=src%2Froutes%2F%2Bpage.svelte) + +--- + +## Step 1: Install dependencies + +We'll start by installing Formisch and Valibot: + +```bash +npm install @formisch/svelte valibot +``` + +--- + +## Step 2: Define the form model + +Before connecting inputs, we need to define what our invoice form looks like. + +The form model describes the structure of our data: the fields the user can edit, the validation rules for each field, and the shape of the final output after submission. + +Instead of defining validation separately and then recreating the same structure as TypeScript types or default values, Formisch can use a schema as the source of truth. + +Our invoice needs basic metadata, client details, and a list of line items. We'll define those requirements with Valibot: + +```javascript +import * as v from 'valibot'; + +const moneyInput = (message) => + v.pipe( + v.string(), + v.nonEmpty(message), + v.toNumber(), + v.minValue(0, 'Amount cannot be negative') + ); + +const positiveNumberInput = (message) => + v.pipe( + v.string(), + v.nonEmpty(message), + v.toNumber(), + v.minValue(1, 'Value must be at least 1') + ); + +const InvoiceSchema = v.object({ + invoiceNumber: v.pipe(v.string(), v.nonEmpty('Invoice number is required')), + + issueDate: v.pipe(v.string(), v.nonEmpty('Issue date is required')), + + dueDate: v.pipe(v.string(), v.nonEmpty('Due date is required')), + + client: v.object({ + name: v.pipe(v.string(), v.nonEmpty('Client name is required')), + + email: v.pipe( + v.string(), + v.nonEmpty('Client email is required'), + v.email('Enter a valid email address') + ) + }), + + lineItems: v.pipe( + v.array( + v.object({ + description: v.pipe( + v.string(), + v.nonEmpty('Description is required') + ), + + quantity: positiveNumberInput('Quantity is required'), + + unitPrice: moneyInput('Unit price is required') + }) + ), + + v.minLength(1, 'Add at least one invoice item'), + v.maxLength(10, 'You can only add up to 10 invoice items') + ), + + taxRate: v.pipe( + v.string(), + v.nonEmpty('Tax rate is required'), + v.toNumber(), + v.minValue(0, 'Tax cannot be negative'), + v.maxValue(100, 'Tax cannot be more than 100%') + ), + + discount: moneyInput('Discount is required'), + + notes: v.optional(v.string()) +}); +``` + +The invoice form has fields like `quantity`, `unitPrice`, and `taxRate` that represent numbers in the final invoice data, but the values coming from the inputs are still part of the form's input state. + +Instead of converting those values in separate event handlers or before submission, we keep that logic with the field definition. The schema handles the transition from input values to the validated data shape we actually want to submit. + +The `lineItems` field is modeled as an array because an invoice can contain a changing number of items. Each item has its own fields and validation rules, while the array itself has rules like the minimum and maximum number of items allowed. + +This keeps the form model as the place where the shape of the data is defined. When a field changes, gets added, or gets removed, the validation rules and submitted output stay connected to that same structure. + +--- + +## Step 3: Create the form + +With the form model defined, we can create the form state from that schema. + +`createForm` connects the schema to the reactive form state. From this point, Formisch can use the same structure for fields, validation, and submission. + +```javascript +import { createForm } from '@formisch/svelte'; + +const invoiceForm = createForm({ + schema: InvoiceSchema, + + initialInput: { + invoiceNumber: 'INV-001', + + issueDate: new Date() + .toISOString() + .slice(0, 10), + + dueDate: '', + + client: { + name: '', + email: '' + }, + + lineItems: [ + { + description: '', + quantity: '1', + unitPrice: '0' + } + ], + + taxRate: '7.5', + discount: '0', + notes: '' + } +}); +``` + +The initial values represent the input state of the form, not the final invoice object. That is why numeric values are still strings here — the user is editing input values, and the schema transformation handles converting them when the form is validated. + +Starting with one `lineItem` also matches the validation rule from the schema. Since the invoice requires at least one item, the form begins in a state that already satisfies that constraint. + +With the form created, the next step is to connect individual fields to that state. + +--- + +## Step 4: Connect fields + +Formisch is headless by design. It does not decide what your inputs should look like. Instead, it gives you the state and bindings needed to connect your own markup. + +A field is connected through the `Field` component: + +```svelte + + {#snippet children(field)} + + + {#if field.errors} +

{field.errors[0]}

+ {/if} + {/snippet} +
+``` + +The `path` tells Formisch where this field exists in the form model. For nested values, the path follows the same structure as the schema: + +```svelte + +``` + +This means the component structure and the data structure stay aligned. The field already knows how to read its value, update the form state, and expose validation results. + +You don't need separate bindings or error state for every input. Each field remains connected to the form model it belongs to. + +--- + +## Step 5: Add dynamic line items + +Line items are where forms usually become more complex. The number of rows is not fixed, and each row has its own fields and validation state. + +Instead of manually keeping track of indexes, values, and errors when items are added or removed, Formisch provides `FieldArray` to keep the collection connected to the form model. + +```svelte + + {#snippet children(fieldArray)} + {#each fieldArray.items as item, index (item)} +
+ Item {index + 1} + + + + + {#snippet children(field)} + + {#if field.errors} +

{field.errors[0]}

+ {/if} + {/snippet} +
+ + + {#snippet children(field)} + + {#if field.errors} +

{field.errors[0]}

+ {/if} + {/snippet} +
+ + + {#snippet children(field)} + + {#if field.errors} +

{field.errors[0]}

+ {/if} + {/snippet} +
+
+ {/each} + + {#if fieldArray.errors} +

{fieldArray.errors[0]}

+ {/if} + {/snippet} +
+``` + +`FieldArray.items` represents the rows currently tracked by the form. When an item is inserted or removed, Formisch updates the related field state along with it. + +The field paths follow the same structure as the schema: + +```javascript +['lineItems', index, 'description'] +``` + +That means the UI structure mirrors the data structure. A row in the interface maps directly to an item in the form state, so validation and updates stay attached to the correct fields. + +To add and remove items: + +```javascript +import { insert, remove } from '@formisch/svelte'; + +function addLineItem() { + insert(invoiceForm, { + path: ['lineItems'], + initialInput: { + description: '', + quantity: '1', + unitPrice: '0' + } + }); +} + +function removeLineItem(index) { + remove(invoiceForm, { + path: ['lineItems'], + at: index + }); +} +``` + +The remove action is disabled when only one item remains because the schema requires at least one line item. The UI behavior and validation rules are coming from the same model. + +--- + +## Step 6: Add reactive totals + +The invoice total depends on several values in the form: quantities, prices, tax, and discounts. + +Instead of updating totals manually inside every input handler, we can derive them from the current form state. + +Svelte 5's `$derived` works well here because the calculation automatically stays connected to the values it depends on. + +```javascript +import { getInput } from '@formisch/svelte'; + +let invoiceInput = $derived.by(() => { + const taxRate = getInput(invoiceForm, { + path: ['taxRate'] + }); + + const discount = getInput(invoiceForm, { + path: ['discount'] + }); + + const lineItems = getInput(invoiceForm, { + path: ['lineItems'] + }); + + return { + taxRate, + discount, + lineItems + }; +}); + +let totals = $derived(calculateTotals(invoiceInput)); + +function calculateTotals(input) { + const subtotal = input.lineItems.reduce((sum, item) => { + return sum + Number(item.quantity) * Number(item.unitPrice); + }, 0); + + const tax = subtotal * (Number(input.taxRate) / 100); + + const discount = Number(input.discount); + + const total = Math.max(subtotal + tax - discount, 0); + + return { + subtotal, + tax, + discount, + total + }; +} +``` + +`getInput` lets us derive only the parts of the form we need. Using `$derived.by()`, the totals stay connected to the relevant fields without recreating the entire form input object whenever an unrelated field changes. + +We still derive the invoice totals from those values, but the calculation now depends only on the fields it actually uses. That keeps the totals reactive without adding extra synchronization between the form fields and the invoice summary. + +For individual row totals, we can use the same input state: + +```javascript +function getLineTotal(index) { + const item = invoiceInput.lineItems[index]; + + if (!item) return 0; + + return Number(item.quantity) * Number(item.unitPrice); +} +``` + +Because `invoiceInput` is connected to the form, any dependent values update when the form changes. + +--- + +## Step 7: Submit the form + +At this point, the form state, validation, and fields are all connected. The final step is handling the transition from editable input values to the validated invoice data your application can use. + +By default, Formisch runs validation on submission and provides the parsed output from the schema. This behavior can be configured if your application needs a different validation strategy. + +```javascript +type InvoiceOutput = v.InferOutput; + +let submittedInvoice = $state(null); + +const submitInvoice = async (output) => { + submittedInvoice = output; + + console.log('Invoice submitted:', output); +}; +``` + +Then connect the submit handler to the form: + +```svelte +
+ +
+``` + +The value received here is the validated schema output, not just the raw values from the inputs. + +That means fields like `quantity`, `unitPrice`, `taxRate`, and `discount` have already gone through their transformations. The form moves from input state to application data at the validation boundary. + +The form state also exposes submission and validation status, so the UI can react without maintaining separate loading flags. + +To reset the form: + +```javascript +import { reset } from '@formisch/svelte'; + +function resetInvoice() { + submittedInvoice = null; + reset(invoiceForm); +} +``` + +Resetting restores the initial state and clears the form state in one operation. + +--- + +## What we built + +The invoice form now includes: + +- A schema that defines the form structure and validation rules +- Field-level state management through `Field` +- Dynamic collections through `FieldArray` +- Derived values using Svelte 5 reactivity +- Validated and transformed output on submission + +The important part is that these pieces are not separate systems. The schema defines the shape, fields connect the UI, and submission produces the validated result from the same model. + +Instead of manually syncing inputs, errors, and derived values, the form stays connected throughout its lifecycle. + +--- + +## When to use Formisch + +Formisch is a good fit when your form logic mostly lives on the client and the form itself becomes part of your application state. + +If you have nested data, dynamic fields, complex validation rules, or values that need to update as the user types, keeping the form model connected can remove a lot of manual synchronization. + +It is also intentionally headless. Formisch does not provide pre-built inputs or decide how your UI should look. You control the markup and connect it to the form state. + +This works well when you already have a component system or design language and want the form layer to stay separate from your UI. + +Formisch keeps form state, validation, and UI connected, but still gives you full control over your markup. Because of that, it works really well for interactive, browser-first forms in both Svelte and SvelteKit apps. + +Native SvelteKit support is also planned, including support for progressively enhanced forms. If your main requirement today is a server-first workflow, a library built specifically for server actions may be a better fit until those capabilities arrive in Formisch. +--- + +## What's next for Formisch + +Formisch is currently in [RC](https://formisch.dev/blog/formisch-v1-release-candidate/), with v1 approaching. + +As we prepare for v1, feedback from developers building real forms is especially useful. If you try Formisch in your own projects, let us know what works well, what feels unclear, and what you'd like improved. + +For a deeper look at the ideas behind Formisch and how it works internally, you can read the [architecture post](https://formisch.dev/blog/one-core-six-frameworks/). + +If you're comparing form libraries, our [comparison guide](https://formisch.dev/svelte/guides/comparison/) looks at how Formisch differs from other approaches in the Svelte ecosystem, including Superforms and TanStack Form. \ No newline at end of file From 6f1425a2d58f002d1a36e0017452d500e9de6c7f Mon Sep 17 00:00:00 2001 From: Oluwawunmi Bewaji Date: Sat, 4 Jul 2026 01:59:29 +0000 Subject: [PATCH 02/16] Fix blog post formatting and add missing blog metadata --- .../dynamic-invoice-form-svelte/index.mdx | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/website/src/routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx b/website/src/routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx index 61104f9a..a6859919 100644 --- a/website/src/routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx +++ b/website/src/routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx @@ -1,12 +1,12 @@ --- +cover: Dynamic invoice form title: 'Building a Dynamic Invoice Form in Svelte 5 with Formisch' description: Build a dynamic invoice form in Svelte 5 using Formisch, Valibot, FieldArray, and typed schema validation. +published: 2026-07-04 authors: - flySewa --- - - # Building a Dynamic Invoice Form in Svelte 5 with Formisch Svelte 5 gives us powerful primitives for managing state with runes, but forms still have their own set of challenges. @@ -40,7 +40,6 @@ We'll be using: You can follow along here, or view the complete example on [Stackblitz](https://stackblitz.com/edit/sveltejs-kit-template-default-vbklpioo?file=src%2Froutes%2F%2Bpage.svelte) ---- ## Step 1: Install dependencies @@ -50,7 +49,6 @@ We'll start by installing Formisch and Valibot: npm install @formisch/svelte valibot ``` ---- ## Step 2: Define the form model @@ -138,7 +136,6 @@ The `lineItems` field is modeled as an array because an invoice can contain a ch This keeps the form model as the place where the shape of the data is defined. When a field changes, gets added, or gets removed, the validation rules and submitted output stay connected to that same structure. ---- ## Step 3: Create the form @@ -181,13 +178,12 @@ const invoiceForm = createForm({ }); ``` -The initial values represent the input state of the form, not the final invoice object. That is why numeric values are still strings here — the user is editing input values, and the schema transformation handles converting them when the form is validated. +The initial values represent the input state of the form, not the final invoice object. That is why numeric values are still strings here — the user is editing input values, and the schema transformation handles converting them when the form is validated. Starting with one `lineItem` also matches the validation rule from the schema. Since the invoice requires at least one item, the form begins in a state that already satisfies that constraint. With the form created, the next step is to connect individual fields to that state. ---- ## Step 4: Connect fields @@ -227,7 +223,6 @@ This means the component structure and the data structure stay aligned. The fiel You don't need separate bindings or error state for every input. Each field remains connected to the form model it belongs to. ---- ## Step 5: Add dynamic line items @@ -331,7 +326,6 @@ function removeLineItem(index) { The remove action is disabled when only one item remains because the schema requires at least one line item. The UI behavior and validation rules are coming from the same model. ---- ## Step 6: Add reactive totals @@ -404,7 +398,6 @@ function getLineTotal(index) { Because `invoiceInput` is connected to the form, any dependent values update when the form changes. ---- ## Step 7: Submit the form @@ -466,7 +459,6 @@ function resetInvoice() { Resetting restores the initial state and clears the form state in one operation. ---- ## What we built @@ -482,7 +474,6 @@ The important part is that these pieces are not separate systems. The schema def Instead of manually syncing inputs, errors, and derived values, the form stays connected throughout its lifecycle. ---- ## When to use Formisch @@ -497,7 +488,6 @@ This works well when you already have a component system or design language and Formisch keeps form state, validation, and UI connected, but still gives you full control over your markup. Because of that, it works really well for interactive, browser-first forms in both Svelte and SvelteKit apps. Native SvelteKit support is also planned, including support for progressively enhanced forms. If your main requirement today is a server-first workflow, a library built specifically for server actions may be a better fit until those capabilities arrive in Formisch. ---- ## What's next for Formisch @@ -507,4 +497,4 @@ As we prepare for v1, feedback from developers building real forms is especially For a deeper look at the ideas behind Formisch and how it works internally, you can read the [architecture post](https://formisch.dev/blog/one-core-six-frameworks/). -If you're comparing form libraries, our [comparison guide](https://formisch.dev/svelte/guides/comparison/) looks at how Formisch differs from other approaches in the Svelte ecosystem, including Superforms and TanStack Form. \ No newline at end of file +If you're comparing form libraries, our [comparison guide](https://formisch.dev/svelte/guides/comparison/) looks at how Formisch differs from other approaches in the Svelte ecosystem, including Superforms and TanStack Form. From 16cac43c86c05a8a81425a9bbf6c5052020e16b1 Mon Sep 17 00:00:00 2001 From: Oluwawunmi Bewaji Date: Sat, 4 Jul 2026 03:12:58 +0100 Subject: [PATCH 03/16] Improve clarity and conciseness in dynamic invoice form guide Refactor content for clarity and conciseness, removing redundant explanations about form model and validation logic. --- .../dynamic-invoice-form-svelte/index.mdx | 58 +++++-------------- 1 file changed, 14 insertions(+), 44 deletions(-) diff --git a/website/src/routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx b/website/src/routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx index a6859919..b150e1d0 100644 --- a/website/src/routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx +++ b/website/src/routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx @@ -54,9 +54,7 @@ npm install @formisch/svelte valibot Before connecting inputs, we need to define what our invoice form looks like. -The form model describes the structure of our data: the fields the user can edit, the validation rules for each field, and the shape of the final output after submission. - -Instead of defining validation separately and then recreating the same structure as TypeScript types or default values, Formisch can use a schema as the source of truth. +The form model describes the structure of our data: the fields the user can edit, the validation rules for each field, and the shape of the final output after submission. Instead of defining validation separately and then recreating the same structure as TypeScript types or default values, Formisch can use a schema as the source of truth. Our invoice needs basic metadata, client details, and a list of line items. We'll define those requirements with Valibot: @@ -128,14 +126,9 @@ const InvoiceSchema = v.object({ }); ``` -The invoice form has fields like `quantity`, `unitPrice`, and `taxRate` that represent numbers in the final invoice data, but the values coming from the inputs are still part of the form's input state. - -Instead of converting those values in separate event handlers or before submission, we keep that logic with the field definition. The schema handles the transition from input values to the validated data shape we actually want to submit. - -The `lineItems` field is modeled as an array because an invoice can contain a changing number of items. Each item has its own fields and validation rules, while the array itself has rules like the minimum and maximum number of items allowed. - -This keeps the form model as the place where the shape of the data is defined. When a field changes, gets added, or gets removed, the validation rules and submitted output stay connected to that same structure. +The invoice form has fields like `quantity`, `unitPrice`, and `taxRate` that represent numbers in the final invoice data, but the values coming from the inputs are still part of the form's input state. Instead of converting those values in separate event handlers or before submission, we keep that logic with the field definition. The schema handles the transition from input values to the validated data shape we actually want to submit. +The `lineItems` field is modeled as an array because an invoice can contain a changing number of items. Each item has its own fields and validation rules, while the array itself has rules like the minimum and maximum number of items allowed. This keeps the form model as the place where the shape of the data is defined. When a field changes, gets added, or gets removed, the validation rules and submitted output stay connected to that same structure. ## Step 3: Create the form @@ -178,9 +171,7 @@ const invoiceForm = createForm({ }); ``` -The initial values represent the input state of the form, not the final invoice object. That is why numeric values are still strings here — the user is editing input values, and the schema transformation handles converting them when the form is validated. - -Starting with one `lineItem` also matches the validation rule from the schema. Since the invoice requires at least one item, the form begins in a state that already satisfies that constraint. +The initial values represent the input state of the form, not the final invoice object. That is why numeric values are still strings here — the user is editing input values, and the schema transformation handles converting them when the form is validated. Starting with one `lineItem` also matches the validation rule from the schema. Since the invoice requires at least one item, the form begins in a state that already satisfies that constraint. With the form created, the next step is to connect individual fields to that state. @@ -219,16 +210,11 @@ The `path` tells Formisch where this field exists in the form model. For nested > ``` -This means the component structure and the data structure stay aligned. The field already knows how to read its value, update the form state, and expose validation results. - -You don't need separate bindings or error state for every input. Each field remains connected to the form model it belongs to. - +This means the component structure and the data structure stay aligned. The field already knows how to read its value, update the form state, and expose validation results. You don't need separate bindings or error state for every input. Each field remains connected to the form model it belongs to. ## Step 5: Add dynamic line items -Line items are where forms usually become more complex. The number of rows is not fixed, and each row has its own fields and validation state. - -Instead of manually keeping track of indexes, values, and errors when items are added or removed, Formisch provides `FieldArray` to keep the collection connected to the form model. +Line items are where forms usually become more complex. The number of rows is not fixed, and each row has its own fields and validation state. Instead of manually keeping track of indexes, values, and errors when items are added or removed, Formisch provides `FieldArray` to keep the collection connected to the form model. ```svelte @@ -329,9 +315,7 @@ The remove action is disabled when only one item remains because the schema requ ## Step 6: Add reactive totals -The invoice total depends on several values in the form: quantities, prices, tax, and discounts. - -Instead of updating totals manually inside every input handler, we can derive them from the current form state. +The invoice total depends on several values in the form: quantities, prices, tax, and discounts. Instead of updating totals manually inside every input handler, we can derive them from the current form state. Svelte 5's `$derived` works well here because the calculation automatically stays connected to the values it depends on. @@ -380,9 +364,7 @@ function calculateTotals(input) { } ``` -`getInput` lets us derive only the parts of the form we need. Using `$derived.by()`, the totals stay connected to the relevant fields without recreating the entire form input object whenever an unrelated field changes. - -We still derive the invoice totals from those values, but the calculation now depends only on the fields it actually uses. That keeps the totals reactive without adding extra synchronization between the form fields and the invoice summary. +`getInput` lets us derive only the parts of the form we need. Using `$derived.by()`, the totals stay connected to the relevant fields without recreating the entire form input object whenever an unrelated field changes. We still derive the invoice totals from those values, but the calculation now depends only on the fields it actually uses. That keeps the totals reactive without adding extra synchronization between the form fields and the invoice summary. For individual row totals, we can use the same input state: @@ -401,9 +383,7 @@ Because `invoiceInput` is connected to the form, any dependent values update whe ## Step 7: Submit the form -At this point, the form state, validation, and fields are all connected. The final step is handling the transition from editable input values to the validated invoice data your application can use. - -By default, Formisch runs validation on submission and provides the parsed output from the schema. This behavior can be configured if your application needs a different validation strategy. +At this point, the form state, validation, and fields are all connected. The final step is handling the transition from editable input values to the validated invoice data your application can use. By default, Formisch runs validation on submission and provides the parsed output from the schema. This behavior can be configured if your application needs a different validation strategy. ```javascript type InvoiceOutput = v.InferOutput; @@ -440,9 +420,7 @@ Then connect the submit handler to the form: ``` -The value received here is the validated schema output, not just the raw values from the inputs. - -That means fields like `quantity`, `unitPrice`, `taxRate`, and `discount` have already gone through their transformations. The form moves from input state to application data at the validation boundary. +The value received here is the validated schema output, not just the raw values from the inputs. That means fields like `quantity`, `unitPrice`, `taxRate`, and `discount` have already gone through their transformations. The form moves from input state to application data at the validation boundary. The form state also exposes submission and validation status, so the UI can react without maintaining separate loading flags. @@ -470,20 +448,14 @@ The invoice form now includes: - Derived values using Svelte 5 reactivity - Validated and transformed output on submission -The important part is that these pieces are not separate systems. The schema defines the shape, fields connect the UI, and submission produces the validated result from the same model. - -Instead of manually syncing inputs, errors, and derived values, the form stays connected throughout its lifecycle. +The important part is that these pieces are not separate systems. The schema defines the shape, fields connect the UI, and submission produces the validated result from the same model. Instead of manually syncing inputs, errors, and derived values, the form stays connected throughout its lifecycle. ## When to use Formisch -Formisch is a good fit when your form logic mostly lives on the client and the form itself becomes part of your application state. - -If you have nested data, dynamic fields, complex validation rules, or values that need to update as the user types, keeping the form model connected can remove a lot of manual synchronization. +Formisch is a good fit when your form logic mostly lives on the client and the form itself becomes part of your application state. If you have nested data, dynamic fields, complex validation rules, or values that need to update as the user types, keeping the form model connected can remove a lot of manual synchronization. -It is also intentionally headless. Formisch does not provide pre-built inputs or decide how your UI should look. You control the markup and connect it to the form state. - -This works well when you already have a component system or design language and want the form layer to stay separate from your UI. +It is also intentionally headless. Formisch does not provide pre-built inputs or decide how your UI should look. You control the markup and connect it to the form state. This works well when you already have a component system or design language and want the form layer to stay separate from your UI. Formisch keeps form state, validation, and UI connected, but still gives you full control over your markup. Because of that, it works really well for interactive, browser-first forms in both Svelte and SvelteKit apps. @@ -491,9 +463,7 @@ Native SvelteKit support is also planned, including support for progressively en ## What's next for Formisch -Formisch is currently in [RC](https://formisch.dev/blog/formisch-v1-release-candidate/), with v1 approaching. - -As we prepare for v1, feedback from developers building real forms is especially useful. If you try Formisch in your own projects, let us know what works well, what feels unclear, and what you'd like improved. +Formisch is currently in [RC](https://formisch.dev/blog/formisch-v1-release-candidate/), with v1 approaching. As we prepare for v1, feedback from developers building real forms is especially useful. If you try Formisch in your own projects, let us know what works well, what feels unclear, and what you'd like improved. For a deeper look at the ideas behind Formisch and how it works internally, you can read the [architecture post](https://formisch.dev/blog/one-core-six-frameworks/). From b587264a9f00d4506823034fec45728aa85c328c Mon Sep 17 00:00:00 2001 From: Oluwawunmi Bewaji Date: Sat, 4 Jul 2026 19:08:48 +0100 Subject: [PATCH 04/16] Add type annotation to submitInvoice function --- .../routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx b/website/src/routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx index b150e1d0..9f41a184 100644 --- a/website/src/routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx +++ b/website/src/routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx @@ -390,7 +390,7 @@ type InvoiceOutput = v.InferOutput; let submittedInvoice = $state(null); -const submitInvoice = async (output) => { +const submitInvoice = async (output: InvoiceOutput) => { submittedInvoice = output; console.log('Invoice submitted:', output); From e0088f0a23d91d889bf2e7ea116d6b69d8a4378a Mon Sep 17 00:00:00 2001 From: Oluwawunmi Bewaji Date: Sat, 4 Jul 2026 19:30:10 +0100 Subject: [PATCH 05/16] Enhance Formisch documentation with detailed benefits Expanded the explanation of Formisch to emphasize its schema-native approach, benefits for complex forms, and integration with Svelte 5 reactivity. Clarified its headless nature and future support for SvelteKit. --- .../blog/(posts)/dynamic-invoice-form-svelte/index.mdx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/website/src/routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx b/website/src/routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx index 9f41a184..4cdbe397 100644 --- a/website/src/routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx +++ b/website/src/routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx @@ -453,11 +453,13 @@ The important part is that these pieces are not separate systems. The schema def ## When to use Formisch -Formisch is a good fit when your form logic mostly lives on the client and the form itself becomes part of your application state. If you have nested data, dynamic fields, complex validation rules, or values that need to update as the user types, keeping the form model connected can remove a lot of manual synchronization. +Formisch is a good fit when your form logic mostly lives on the client and you want a **schema-native** approach to building forms. Instead of defining your data shape, validation rules, transformations, and submitted output separately, the schema becomes the source of truth for the entire form. Fields, validation, and typed output all stay connected to that same model, reducing duplication and the amount of manual synchronization needed as your forms grow. -It is also intentionally headless. Formisch does not provide pre-built inputs or decide how your UI should look. You control the markup and connect it to the form state. This works well when you already have a component system or design language and want the form layer to stay separate from your UI. +This becomes especially useful when working with nested objects, dynamic collections, complex validation rules, or derived values that update as the user types. Rather than wiring those pieces together yourself, they continue to follow the same schema-driven structure. + +Because Formisch is built for Svelte 5, it also fits naturally with rune-based reactivity. Form state can participate in `$derived` calculations and other reactive logic without introducing another state management layer. -Formisch keeps form state, validation, and UI connected, but still gives you full control over your markup. Because of that, it works really well for interactive, browser-first forms in both Svelte and SvelteKit apps. +It is also intentionally headless. Formisch does not provide pre-built inputs or decide how your UI should look. You control the markup and connect it to the form state. This works well when you already have a component system or design language and want the form layer to stay separate from your UI. Native SvelteKit support is also planned, including support for progressively enhanced forms. If your main requirement today is a server-first workflow, a library built specifically for server actions may be a better fit until those capabilities arrive in Formisch. From c8447d13e80f624f1e1e981cc0e3d8302bfc9856 Mon Sep 17 00:00:00 2001 From: Fabian Hiller Date: Mon, 13 Jul 2026 15:16:05 -0400 Subject: [PATCH 06/16] Add Svelte invoice example to repo --- examples/svelte-invoice-form/.gitignore | 4 + examples/svelte-invoice-form/README.md | 39 + examples/svelte-invoice-form/index.html | 12 + .../svelte-invoice-form/package-lock.json | 1558 +++++++++++++++++ examples/svelte-invoice-form/package.json | 24 + examples/svelte-invoice-form/src/App.svelte | 389 ++++ examples/svelte-invoice-form/src/app.css | 205 +++ examples/svelte-invoice-form/src/main.ts | 9 + examples/svelte-invoice-form/src/schema.ts | 69 + .../svelte-invoice-form/src/vite-env.d.ts | 2 + examples/svelte-invoice-form/svelte.config.js | 5 + examples/svelte-invoice-form/tsconfig.json | 18 + examples/svelte-invoice-form/vite.config.ts | 6 + .../dynamic-invoice-form-svelte/index.mdx | 2 +- 14 files changed, 2341 insertions(+), 1 deletion(-) create mode 100644 examples/svelte-invoice-form/.gitignore create mode 100644 examples/svelte-invoice-form/README.md create mode 100644 examples/svelte-invoice-form/index.html create mode 100644 examples/svelte-invoice-form/package-lock.json create mode 100644 examples/svelte-invoice-form/package.json create mode 100644 examples/svelte-invoice-form/src/App.svelte create mode 100644 examples/svelte-invoice-form/src/app.css create mode 100644 examples/svelte-invoice-form/src/main.ts create mode 100644 examples/svelte-invoice-form/src/schema.ts create mode 100644 examples/svelte-invoice-form/src/vite-env.d.ts create mode 100644 examples/svelte-invoice-form/svelte.config.js create mode 100644 examples/svelte-invoice-form/tsconfig.json create mode 100644 examples/svelte-invoice-form/vite.config.ts diff --git a/examples/svelte-invoice-form/.gitignore b/examples/svelte-invoice-form/.gitignore new file mode 100644 index 00000000..d70bb9cc --- /dev/null +++ b/examples/svelte-invoice-form/.gitignore @@ -0,0 +1,4 @@ +node_modules +dist +.DS_Store +*.local diff --git a/examples/svelte-invoice-form/README.md b/examples/svelte-invoice-form/README.md new file mode 100644 index 00000000..860e7930 --- /dev/null +++ b/examples/svelte-invoice-form/README.md @@ -0,0 +1,39 @@ +# Dynamic Invoice Form — Formisch + Svelte 5 + +A minimal, standalone [Vite](https://vite.dev) + [Svelte 5](https://svelte.dev) example that builds a dynamic invoice form with [Formisch](https://formisch.dev) and [Valibot](https://valibot.dev). + +It demonstrates the ideas from the guide [_Building a Dynamic Invoice Form in Svelte 5 with Formisch_](https://formisch.dev/blog/dynamic-invoice-form-svelte/): + +- A single Valibot schema as the source of truth for structure, validation and typed output +- Nested fields (`client.name`, `client.email`) wired with `Field` +- Dynamic line items with `FieldArray`, plus `insert` / `remove` +- Live totals derived from the form input with Svelte 5's `$derived` +- Typed, validated output on submit and one-call `reset` + +## Getting started + +```bash +npm install +npm run dev +``` + +Then open the printed local URL. + +## Scripts + +| Command | Description | +| ----------------- | ---------------------------------------- | +| `npm run dev` | Start the Vite dev server | +| `npm run build` | Build for production into `dist/` | +| `npm run preview` | Preview the production build | +| `npm run check` | Type-check the project with svelte-check | + +## Project structure + +``` +src/ +├── App.svelte # The invoice form (fields, array, totals, submit) +├── lib/schema.ts # Valibot InvoiceSchema + inferred InvoiceOutput type +├── main.ts # App entry point +└── app.css # Minimal styling +``` diff --git a/examples/svelte-invoice-form/index.html b/examples/svelte-invoice-form/index.html new file mode 100644 index 00000000..880414fc --- /dev/null +++ b/examples/svelte-invoice-form/index.html @@ -0,0 +1,12 @@ + + + + + + Dynamic Invoice Form · Formisch + Svelte 5 + + +
+ + + diff --git a/examples/svelte-invoice-form/package-lock.json b/examples/svelte-invoice-form/package-lock.json new file mode 100644 index 00000000..6f5a85a7 --- /dev/null +++ b/examples/svelte-invoice-form/package-lock.json @@ -0,0 +1,1558 @@ +{ + "name": "svelte-invoice-form", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "svelte-invoice-form", + "version": "0.0.0", + "dependencies": { + "@formisch/svelte": "^1.0.0-rc.0", + "valibot": "^1.4.1" + }, + "devDependencies": { + "@sveltejs/vite-plugin-svelte": "^6.2.0", + "@tsconfig/svelte": "^5.0.0", + "svelte": "^5.38.8", + "svelte-check": "^4.3.1", + "typescript": "^5.9.2", + "vite": "^7.1.5" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@formisch/svelte": { + "version": "1.0.0-rc.0", + "resolved": "https://registry.npmjs.org/@formisch/svelte/-/svelte-1.0.0-rc.0.tgz", + "integrity": "sha512-lTH/ayophfj2FtXMLY/GoteXEfqkVefksiIKmjmEEalJEauNdajvi4iTITyEbdnlAJxqfS7pQMel5CXMfPLMZA==", + "license": "MIT", + "peerDependencies": { + "svelte": "^5.29.0", + "typescript": ">=5", + "valibot": "^1.4.1" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sveltejs/acorn-typescript": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.11.tgz", + "integrity": "sha512-LFuZUkjJ9iF7JZye/aG5XM0SFcQ5VyL0oVX4WJ9dc0Va3R3s0OauX1BESVCb+YN/ol8TAfqGDDAQsTG627Y5kw==", + "license": "MIT", + "peerDependencies": { + "acorn": "^8.9.0" + } + }, + "node_modules/@sveltejs/load-config": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@sveltejs/load-config/-/load-config-0.2.0.tgz", + "integrity": "sha512-1LgZ/qUqSoq+QorD83lk2hka79Px0wXNW2q5V1nZlxGhQgw1jrsIbVz5YiCeucVLo4XvFLjXukUaQjIiqowkcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18.0.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte": { + "version": "6.2.4", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-6.2.4.tgz", + "integrity": "sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", + "deepmerge": "^4.3.1", + "magic-string": "^0.30.21", + "obug": "^2.1.0", + "vitefu": "^1.1.1" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24" + }, + "peerDependencies": { + "svelte": "^5.0.0", + "vite": "^6.3.0 || ^7.0.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte-inspector": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-5.0.2.tgz", + "integrity": "sha512-TZzRTcEtZffICSAoZGkPSl6Etsj2torOVrx6Uw0KpXxrec9Gg6jFWQ60Q3+LmNGfZSxHRCZL7vXVZIWmuV50Ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "obug": "^2.1.0" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24" + }, + "peerDependencies": { + "@sveltejs/vite-plugin-svelte": "^6.0.0-next.0", + "svelte": "^5.0.0", + "vite": "^6.3.0 || ^7.0.0" + } + }, + "node_modules/@tsconfig/svelte": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/@tsconfig/svelte/-/svelte-5.0.8.tgz", + "integrity": "sha512-UkNnw1/oFEfecR8ypyHIQuWYdkPvHiwcQ78sh+ymIiYoF+uc5H1UBetbjyqT+vgGJ3qQN6nhucJviX6HesWtKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "license": "MIT" + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT" + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/aria-query": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz", + "integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/devalue": { + "version": "5.8.1", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.8.1.tgz", + "integrity": "sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw==", + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/esm-env": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", + "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", + "license": "MIT" + }, + "node_modules/esrap": { + "version": "2.2.13", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.13.tgz", + "integrity": "sha512-m8jH5hZgJE2RRUK/jjkGPcJEDAV+dYnZYFkosQaPTcE+Yw4xynXHOo6FUdwaWBtdR3b1MMa7wEDTSHeR2VWsGA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "peerDependencies": { + "@typescript-eslint/types": "^8.2.0" + }, + "peerDependenciesMeta": { + "@typescript-eslint/types": { + "optional": true + } + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/is-reference": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", + "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.6" + } + }, + "node_modules/locate-character": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/obug": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz", + "integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.19", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.19.tgz", + "integrity": "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mri": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/svelte": { + "version": "5.56.4", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.56.4.tgz", + "integrity": "sha512-/d0QHehmRuJW8gVz395MTkPcPozxzdjBMBE8oEYGz8O3b9KTMzzQ9ZHJQLuFKOHOPQbU6kx/X4iid/EBBzH7iw==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "@jridgewell/sourcemap-codec": "^1.5.0", + "@sveltejs/acorn-typescript": "^1.0.10", + "@types/estree": "^1.0.5", + "@types/trusted-types": "^2.0.7", + "acorn": "^8.12.1", + "aria-query": "5.3.1", + "axobject-query": "^4.1.0", + "clsx": "^2.1.1", + "devalue": "^5.8.1", + "esm-env": "^1.2.1", + "esrap": "^2.2.12", + "is-reference": "^3.0.3", + "locate-character": "^3.0.0", + "magic-string": "^0.30.11", + "zimmerframe": "^1.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/svelte-check": { + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.7.2.tgz", + "integrity": "sha512-GoS4XJdGswlq0rIT1vtFLzJY1bvHtY37McY9H9Gkm1Ggw/ICdZYn8J/Z8Yi0BEL0i3R4+jtaWVePjyppMlij/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "@sveltejs/load-config": "^0.2.0", + "chokidar": "^4.0.1", + "fdir": "^6.2.0", + "picocolors": "^1.0.0", + "sade": "^1.7.4" + }, + "bin": { + "svelte-check": "bin/svelte-check" + }, + "engines": { + "node": ">= 18.0.0" + }, + "peerDependencies": { + "svelte": "^4.0.0 || ^5.0.0-next.0", + "typescript": ">=5.0.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/valibot": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.4.2.tgz", + "integrity": "sha512-gjdCvJ6d3RyHAneqxMYMW9QMCwYMb3jpOO0IyHZV1bnRHFBHrX3VkIILt5XYR0WhwHiH7Mty8ovuPZ/O3gamrg==", + "license": "MIT", + "peerDependencies": { + "typescript": ">=5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vite": { + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", + "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0 || ^0.28.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitefu": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.3.tgz", + "integrity": "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==", + "dev": true, + "license": "MIT", + "workspaces": [ + "tests/deps/*", + "tests/projects/*", + "tests/projects/workspace/packages/*" + ], + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/zimmerframe": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", + "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==", + "license": "MIT" + } + } +} diff --git a/examples/svelte-invoice-form/package.json b/examples/svelte-invoice-form/package.json new file mode 100644 index 00000000..0705b078 --- /dev/null +++ b/examples/svelte-invoice-form/package.json @@ -0,0 +1,24 @@ +{ + "name": "svelte-invoice-form", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "check": "svelte-check --tsconfig ./tsconfig.json" + }, + "dependencies": { + "@formisch/svelte": "^1.0.0-rc.0", + "valibot": "^1.4.1" + }, + "devDependencies": { + "@sveltejs/vite-plugin-svelte": "^6.2.0", + "@tsconfig/svelte": "^5.0.0", + "svelte": "^5.38.8", + "svelte-check": "^4.3.1", + "typescript": "^5.9.2", + "vite": "^7.1.5" + } +} diff --git a/examples/svelte-invoice-form/src/App.svelte b/examples/svelte-invoice-form/src/App.svelte new file mode 100644 index 00000000..5f07cb4c --- /dev/null +++ b/examples/svelte-invoice-form/src/App.svelte @@ -0,0 +1,389 @@ + + +
+

Dynamic Invoice Form

+

Built with Formisch, Valibot and Svelte 5 runes.

+ +
+
+ Invoice details +
+ + {#snippet children(field)} +
+ + {#if field.errors} +

{field.errors[0]}

+ {/if} +
+ {/snippet} +
+ +
+ + + {#snippet children(field)} +
+ + {#if field.errors} +

{field.errors[0]}

+ {/if} +
+ {/snippet} +
+ + + {#snippet children(field)} +
+ + {#if field.errors} +

{field.errors[0]}

+ {/if} +
+ {/snippet} +
+
+
+ +
+ Client +
+ + {#snippet children(field)} +
+ + {#if field.errors} +

{field.errors[0]}

+ {/if} +
+ {/snippet} +
+ + + {#snippet children(field)} +
+ + {#if field.errors} +

{field.errors[0]}

+ {/if} +
+ {/snippet} +
+
+
+ +
+ Line items + + {#snippet children(fieldArray)} + {#each fieldArray.items as item, index (item)} +
+
+ Item {index + 1} + +
+ +
+ + {#snippet children(field)} +
+ + {#if field.errors} +

{field.errors[0]}

+ {/if} +
+ {/snippet} +
+ + + {#snippet children(field)} +
+ + {#if field.errors} +

{field.errors[0]}

+ {/if} +
+ {/snippet} +
+ + + {#snippet children(field)} +
+ + {#if field.errors} +

{field.errors[0]}

+ {/if} +
+ {/snippet} +
+
+ +

+ Line total: {currency.format(getLineTotal(index))} +

+
+ {/each} + + {#if fieldArray.errors} +

{fieldArray.errors[0]}

+ {/if} + + + {/snippet} +
+
+ +
+ Summary +
+ + {#snippet children(field)} +
+ + {#if field.errors} +

{field.errors[0]}

+ {/if} +
+ {/snippet} +
+ + + {#snippet children(field)} +
+ + {#if field.errors} +

{field.errors[0]}

+ {/if} +
+ {/snippet} +
+
+ +
+
+ Subtotal{currency.format(totals.subtotal)} +
+
+ Tax{currency.format(totals.tax)} +
+
+ Discount-{currency.format(totals.discount)} +
+
+ Total{currency.format(totals.total)} +
+
+
+ +
+ Notes + + {#snippet children(field)} +
+ + {#if field.errors} +

{field.errors[0]}

+ {/if} +
+ {/snippet} +
+
+ +
+ + +
+
+ + {#if submittedInvoice} + + {/if} +
diff --git a/examples/svelte-invoice-form/src/app.css b/examples/svelte-invoice-form/src/app.css new file mode 100644 index 00000000..29b20151 --- /dev/null +++ b/examples/svelte-invoice-form/src/app.css @@ -0,0 +1,205 @@ +:root { + --border: #e2e8f0; + --muted: #64748b; + --text: #0f172a; + --primary: #6d28d9; + --error: #dc2626; + --bg: #f8fafc; + --surface: #ffffff; + + color-scheme: light; + font-family: + system-ui, + -apple-system, + 'Segoe UI', + Roboto, + sans-serif; + color: var(--text); + background-color: var(--bg); +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + padding: 2rem 1rem; +} + +.page { + max-width: 720px; + margin: 0 auto; +} + +h1 { + font-size: 1.5rem; + margin: 0 0 0.25rem; +} + +.subtitle { + margin: 0 0 2rem; + color: var(--muted); +} + +fieldset { + border: 1px solid var(--border); + border-radius: 12px; + padding: 1.25rem; + margin: 0 0 1.5rem; + background: var(--surface); +} + +legend { + font-weight: 600; + padding: 0 0.5rem; +} + +.grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1rem; +} + +.field { + display: flex; + flex-direction: column; + min-width: 0; +} + +label { + display: flex; + flex-direction: column; + gap: 0.35rem; + font-size: 0.875rem; + font-weight: 500; +} + +input, +textarea { + font: inherit; + padding: 0.55rem 0.7rem; + border: 1px solid var(--border); + border-radius: 8px; + background: var(--surface); + color: inherit; +} + +input:focus, +textarea:focus { + outline: 2px solid var(--primary); + outline-offset: -1px; +} + +.error { + color: var(--error); + font-size: 0.8rem; + margin: 0.25rem 0 0; +} + +.line-item { + border: 1px solid var(--border); + border-radius: 10px; + padding: 1rem; + margin-bottom: 1rem; +} + +.line-item-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 0.75rem; +} + +.line-item-header strong { + font-size: 0.95rem; +} + +.line-item-fields { + display: grid; + grid-template-columns: 2fr 1fr 1fr; + gap: 0.75rem; +} + +.line-total { + text-align: right; + color: var(--muted); + font-size: 0.85rem; + margin: 0.5rem 0 0; +} + +.totals { + display: grid; + gap: 0.35rem; + margin-left: auto; + margin-top: 1.5rem; + max-width: 260px; +} + +.totals-row { + display: flex; + justify-content: space-between; +} + +.totals-row.grand { + font-weight: 700; + font-size: 1.05rem; + border-top: 1px solid var(--border); + padding-top: 0.5rem; + margin-top: 0.25rem; +} + +button { + font: inherit; + cursor: pointer; + border-radius: 8px; + border: 1px solid transparent; + padding: 0.55rem 1rem; + font-weight: 600; +} + +button:disabled { + cursor: not-allowed; + opacity: 0.55; +} + +.btn-primary { + background: var(--primary); + color: white; +} + +.btn-secondary { + background: var(--surface); + border-color: var(--border); + color: var(--text); +} + +.btn-ghost { + background: transparent; + color: var(--error); + padding: 0.35rem 0.6rem; + font-size: 0.85rem; +} + +.actions { + display: flex; + gap: 0.75rem; + margin-top: 1rem; +} + +.submitted { + margin-top: 2rem; + border: 1px solid var(--border); + border-radius: 12px; + padding: 1.25rem; + background: var(--surface); +} + +pre { + overflow-x: auto; + background: #0f172a; + color: #e2e8f0; + padding: 1rem; + border-radius: 8px; + font-size: 0.8rem; +} diff --git a/examples/svelte-invoice-form/src/main.ts b/examples/svelte-invoice-form/src/main.ts new file mode 100644 index 00000000..c049299c --- /dev/null +++ b/examples/svelte-invoice-form/src/main.ts @@ -0,0 +1,9 @@ +import { mount } from 'svelte'; +import './app.css'; +import App from './App.svelte'; + +const app = mount(App, { + target: document.getElementById('app')!, +}); + +export default app; diff --git a/examples/svelte-invoice-form/src/schema.ts b/examples/svelte-invoice-form/src/schema.ts new file mode 100644 index 00000000..71bb987f --- /dev/null +++ b/examples/svelte-invoice-form/src/schema.ts @@ -0,0 +1,69 @@ +import * as v from 'valibot'; + +/** + * Input pipe for a monetary amount. The value comes from the input as a string, + * gets transformed to a number and must not be negative. + */ +const moneyInput = (message: string) => + v.pipe( + v.string(), + v.nonEmpty(message), + v.toNumber(), + v.minValue(0, 'Amount cannot be negative') + ); + +/** + * Input pipe for a positive number (e.g. a quantity). The value comes from the + * input as a string, gets transformed to a number and must be at least 1. + */ +const positiveNumberInput = (message: string) => + v.pipe( + v.string(), + v.nonEmpty(message), + v.toNumber(), + v.minValue(1, 'Value must be at least 1') + ); + +/** + * The invoice form model. This single schema is the source of truth for the + * form structure, its validation rules and the typed output produced on submit. + */ +export const InvoiceSchema = v.object({ + invoiceNumber: v.pipe(v.string(), v.nonEmpty('Invoice number is required')), + issueDate: v.pipe(v.string(), v.nonEmpty('Issue date is required')), + dueDate: v.pipe(v.string(), v.nonEmpty('Due date is required')), + client: v.object({ + name: v.pipe(v.string(), v.nonEmpty('Client name is required')), + email: v.pipe( + v.string(), + v.nonEmpty('Client email is required'), + v.email('Enter a valid email address') + ), + }), + lineItems: v.pipe( + v.array( + v.object({ + description: v.pipe(v.string(), v.nonEmpty('Description is required')), + quantity: positiveNumberInput('Quantity is required'), + unitPrice: moneyInput('Unit price is required'), + }) + ), + v.minLength(1, 'Add at least one invoice item'), + v.maxLength(10, 'You can only add up to 10 invoice items') + ), + taxRate: v.pipe( + v.string(), + v.nonEmpty('Tax rate is required'), + v.toNumber(), + v.minValue(0, 'Tax cannot be negative'), + v.maxValue(100, 'Tax cannot be more than 100%') + ), + discount: moneyInput('Discount is required'), + notes: v.optional(v.string()), +}); + +/** + * The validated and transformed invoice data produced when the form is + * submitted. Numeric fields have already been converted from strings to numbers. + */ +export type InvoiceOutput = v.InferOutput; diff --git a/examples/svelte-invoice-form/src/vite-env.d.ts b/examples/svelte-invoice-form/src/vite-env.d.ts new file mode 100644 index 00000000..4078e747 --- /dev/null +++ b/examples/svelte-invoice-form/src/vite-env.d.ts @@ -0,0 +1,2 @@ +/// +/// diff --git a/examples/svelte-invoice-form/svelte.config.js b/examples/svelte-invoice-form/svelte.config.js new file mode 100644 index 00000000..4c6b24b1 --- /dev/null +++ b/examples/svelte-invoice-form/svelte.config.js @@ -0,0 +1,5 @@ +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; + +export default { + preprocess: vitePreprocess(), +}; diff --git a/examples/svelte-invoice-form/tsconfig.json b/examples/svelte-invoice-form/tsconfig.json new file mode 100644 index 00000000..0d70a67b --- /dev/null +++ b/examples/svelte-invoice-form/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "@tsconfig/svelte/tsconfig.json", + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "module": "ESNext", + "resolveJsonModule": true, + "allowJs": true, + "checkJs": true, + "isolatedModules": true, + "moduleDetection": "force", + "skipLibCheck": true, + "verbatimModuleSyntax": true, + "moduleResolution": "bundler", + "types": ["svelte", "vite/client"] + }, + "include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte", "vite.config.ts"] +} diff --git a/examples/svelte-invoice-form/vite.config.ts b/examples/svelte-invoice-form/vite.config.ts new file mode 100644 index 00000000..0cf4613a --- /dev/null +++ b/examples/svelte-invoice-form/vite.config.ts @@ -0,0 +1,6 @@ +import { defineConfig } from 'vite'; +import { svelte } from '@sveltejs/vite-plugin-svelte'; + +export default defineConfig({ + plugins: [svelte()], +}); diff --git a/website/src/routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx b/website/src/routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx index 4cdbe397..776f66b0 100644 --- a/website/src/routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx +++ b/website/src/routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx @@ -38,7 +38,7 @@ We'll be using: - [Formisch](https://formisch.dev) - [Valibot](https://valibot.dev) for schema validation -You can follow along here, or view the complete example on [Stackblitz](https://stackblitz.com/edit/sveltejs-kit-template-default-vbklpioo?file=src%2Froutes%2F%2Bpage.svelte) +You can follow along here, view the complete example on [Stackblitz](https://stackblitz.com/edit/sveltejs-kit-template-default-vbklpioo?file=src%2Froutes%2F%2Bpage.svelte), or clone the [runnable example](https://github.com/open-circle/formisch/tree/main/examples/svelte-invoice-form) from the repo. ## Step 1: Install dependencies From 8956b5fe2ee6079c4c9a26abff6765b65db84f53 Mon Sep 17 00:00:00 2001 From: Fabian Hiller Date: Mon, 13 Jul 2026 15:56:57 -0400 Subject: [PATCH 07/16] Update a few things in dynamic invoice form post --- .../dynamic-invoice-form-svelte/index.mdx | 134 +++++++----------- 1 file changed, 52 insertions(+), 82 deletions(-) diff --git a/website/src/routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx b/website/src/routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx index 776f66b0..1535b1cc 100644 --- a/website/src/routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx +++ b/website/src/routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx @@ -1,23 +1,21 @@ --- -cover: Dynamic invoice form +cover: Invoice Form title: 'Building a Dynamic Invoice Form in Svelte 5 with Formisch' description: Build a dynamic invoice form in Svelte 5 using Formisch, Valibot, FieldArray, and typed schema validation. -published: 2026-07-04 +published: 2026-07-15 authors: - flySewa --- -# Building a Dynamic Invoice Form in Svelte 5 with Formisch - Svelte 5 gives us powerful primitives for managing state with runes, but forms still have their own set of challenges. A field is not only a value. It has validation state, errors, transformations, and a relationship with the rest of the form. When a form grows beyond a handful of inputs, things like nested data, dynamic fields, and derived values all need to stay in sync. At this point, form state management moves away from handling individual inputs and towards managing the entire form model. -Formisch approaches this by treating the form as a connected structure. It's a headless form library designed to stay lightweight while keeping the schema, state, validation, and submitted output connected. The schema, state, validation, and submitted output all come from the same source, while still letting you control the UI. +Formisch approaches this by treating the form as a single connected structure. It's a headless, lightweight library where the schema, state, validation, and submitted output all come from one source, while you stay in full control of the UI. -To demonstrate this, we'll build a dynamic invoice form and look at how Formisch works with Svelte 5's rune-based reactivity to manage: +To see this in practice, we'll build a dynamic invoice form and walk through how Formisch works with Svelte 5's rune-based reactivity to manage: - Invoice details - Client information @@ -40,7 +38,6 @@ We'll be using: You can follow along here, view the complete example on [Stackblitz](https://stackblitz.com/edit/sveltejs-kit-template-default-vbklpioo?file=src%2Froutes%2F%2Bpage.svelte), or clone the [runnable example](https://github.com/open-circle/formisch/tree/main/examples/svelte-invoice-form) from the repo. - ## Step 1: Install dependencies We'll start by installing Formisch and Valibot: @@ -49,7 +46,6 @@ We'll start by installing Formisch and Valibot: npm install @formisch/svelte valibot ``` - ## Step 2: Define the form model Before connecting inputs, we need to define what our invoice form looks like. @@ -58,7 +54,7 @@ The form model describes the structure of our data: the fields the user can edit Our invoice needs basic metadata, client details, and a list of line items. We'll define those requirements with Valibot: -```javascript +```ts import * as v from 'valibot'; const moneyInput = (message) => @@ -91,20 +87,17 @@ const InvoiceSchema = v.object({ v.string(), v.nonEmpty('Client email is required'), v.email('Enter a valid email address') - ) + ), }), lineItems: v.pipe( v.array( v.object({ - description: v.pipe( - v.string(), - v.nonEmpty('Description is required') - ), + description: v.pipe(v.string(), v.nonEmpty('Description is required')), quantity: positiveNumberInput('Quantity is required'), - unitPrice: moneyInput('Unit price is required') + unitPrice: moneyInput('Unit price is required'), }) ), @@ -122,7 +115,7 @@ const InvoiceSchema = v.object({ discount: moneyInput('Discount is required'), - notes: v.optional(v.string()) + notes: v.optional(v.string()), }); ``` @@ -136,7 +129,7 @@ With the form model defined, we can create the form state from that schema. `createForm` connects the schema to the reactive form state. From this point, Formisch can use the same structure for fields, validation, and submission. -```javascript +```ts import { createForm } from '@formisch/svelte'; const invoiceForm = createForm({ @@ -145,29 +138,27 @@ const invoiceForm = createForm({ initialInput: { invoiceNumber: 'INV-001', - issueDate: new Date() - .toISOString() - .slice(0, 10), + issueDate: new Date().toISOString().slice(0, 10), dueDate: '', client: { name: '', - email: '' + email: '', }, lineItems: [ { description: '', quantity: '1', - unitPrice: '0' - } + unitPrice: '0', + }, ], taxRate: '7.5', discount: '0', - notes: '' - } + notes: '', + }, }); ``` @@ -175,7 +166,6 @@ The initial values represent the input state of the form, not the final invoice With the form created, the next step is to connect individual fields to that state. - ## Step 4: Connect fields Formisch is headless by design. It does not decide what your inputs should look like. Instead, it gives you the state and bindings needed to connect your own markup. @@ -187,11 +177,7 @@ A field is connected through the `Field` component: {#snippet children(field)} {#if field.errors} @@ -247,7 +233,12 @@ Line items are where forms usually become more complex. The number of rows is no {#snippet children(field)} {#if field.errors}

{field.errors[0]}

@@ -259,7 +250,13 @@ Line items are where forms usually become more complex. The number of rows is no {#snippet children(field)} {#if field.errors}

{field.errors[0]}

@@ -280,7 +277,8 @@ Line items are where forms usually become more complex. The number of rows is no The field paths follow the same structure as the schema: -```javascript +{/* prettier-ignore */} +```ts ['lineItems', index, 'description'] ``` @@ -288,7 +286,7 @@ That means the UI structure mirrors the data structure. A row in the interface m To add and remove items: -```javascript +```ts import { insert, remove } from '@formisch/svelte'; function addLineItem() { @@ -297,52 +295,35 @@ function addLineItem() { initialInput: { description: '', quantity: '1', - unitPrice: '0' - } + unitPrice: '0', + }, }); } function removeLineItem(index) { remove(invoiceForm, { path: ['lineItems'], - at: index + at: index, }); } ``` The remove action is disabled when only one item remains because the schema requires at least one line item. The UI behavior and validation rules are coming from the same model. - ## Step 6: Add reactive totals The invoice total depends on several values in the form: quantities, prices, tax, and discounts. Instead of updating totals manually inside every input handler, we can derive them from the current form state. Svelte 5's `$derived` works well here because the calculation automatically stays connected to the values it depends on. -```javascript +```ts import { getInput } from '@formisch/svelte'; -let invoiceInput = $derived.by(() => { - const taxRate = getInput(invoiceForm, { - path: ['taxRate'] - }); - - const discount = getInput(invoiceForm, { - path: ['discount'] - }); - - const lineItems = getInput(invoiceForm, { - path: ['lineItems'] - }); - - return { - taxRate, - discount, - lineItems - }; -}); - -let totals = $derived(calculateTotals(invoiceInput)); +const invoiceInput = $derived.by(() => ({ + taxRate: getInput(invoiceForm, { path: ['taxRate'] }), + discount: getInput(invoiceForm, { path: ['discount'] }), + lineItems: getInput(invoiceForm, { path: ['lineItems'] }), +})); function calculateTotals(input) { const subtotal = input.lineItems.reduce((sum, item) => { @@ -359,16 +340,18 @@ function calculateTotals(input) { subtotal, tax, discount, - total + total, }; } + +const totals = $derived(calculateTotals(invoiceInput)); ``` -`getInput` lets us derive only the parts of the form we need. Using `$derived.by()`, the totals stay connected to the relevant fields without recreating the entire form input object whenever an unrelated field changes. We still derive the invoice totals from those values, but the calculation now depends only on the fields it actually uses. That keeps the totals reactive without adding extra synchronization between the form fields and the invoice summary. +`getInput` lets us read only the parts of the form we need. Wrapping those reads in `$derived.by()` keeps the totals connected to just the tax rate, discount, and line items, so they recalculate when those fields change and ignore unrelated updates. The totals stay reactive without any manual synchronization between the form fields and the invoice summary. For individual row totals, we can use the same input state: -```javascript +```ts function getLineTotal(index) { const item = invoiceInput.lineItems[index]; @@ -380,12 +363,11 @@ function getLineTotal(index) { Because `invoiceInput` is connected to the form, any dependent values update when the form changes. - ## Step 7: Submit the form At this point, the form state, validation, and fields are all connected. The final step is handling the transition from editable input values to the validated invoice data your application can use. By default, Formisch runs validation on submission and provides the parsed output from the schema. This behavior can be configured if your application needs a different validation strategy. -```javascript +```ts type InvoiceOutput = v.InferOutput; let submittedInvoice = $state(null); @@ -400,22 +382,12 @@ const submitInvoice = async (output: InvoiceOutput) => { Then connect the submit handler to the form: ```svelte -
+
``` @@ -426,7 +398,7 @@ The form state also exposes submission and validation status, so the UI can reac To reset the form: -```javascript +```ts import { reset } from '@formisch/svelte'; function resetInvoice() { @@ -437,7 +409,6 @@ function resetInvoice() { Resetting restores the initial state and clears the form state in one operation. - ## What we built The invoice form now includes: @@ -450,7 +421,6 @@ The invoice form now includes: The important part is that these pieces are not separate systems. The schema defines the shape, fields connect the UI, and submission produces the validated result from the same model. Instead of manually syncing inputs, errors, and derived values, the form stays connected throughout its lifecycle. - ## When to use Formisch Formisch is a good fit when your form logic mostly lives on the client and you want a **schema-native** approach to building forms. Instead of defining your data shape, validation rules, transformations, and submitted output separately, the schema becomes the source of truth for the entire form. Fields, validation, and typed output all stay connected to that same model, reducing duplication and the amount of manual synchronization needed as your forms grow. From 0324da50bc34d236c2009f86a87971f8c3053b1c Mon Sep 17 00:00:00 2001 From: Fabian Hiller Date: Mon, 13 Jul 2026 16:22:06 -0400 Subject: [PATCH 08/16] Address PR review feedback and futhre improve post and example --- examples/svelte-invoice-form/README.md | 2 +- examples/svelte-invoice-form/package.json | 3 + examples/svelte-invoice-form/src/App.svelte | 24 ++++---- examples/svelte-invoice-form/src/app.css | 8 +++ .../dynamic-invoice-form-svelte/index.mdx | 61 +++++++++---------- 5 files changed, 51 insertions(+), 47 deletions(-) diff --git a/examples/svelte-invoice-form/README.md b/examples/svelte-invoice-form/README.md index 860e7930..282f21c5 100644 --- a/examples/svelte-invoice-form/README.md +++ b/examples/svelte-invoice-form/README.md @@ -33,7 +33,7 @@ Then open the printed local URL. ``` src/ ├── App.svelte # The invoice form (fields, array, totals, submit) -├── lib/schema.ts # Valibot InvoiceSchema + inferred InvoiceOutput type +├── schema.ts # Valibot InvoiceSchema + inferred InvoiceOutput type ├── main.ts # App entry point └── app.css # Minimal styling ``` diff --git a/examples/svelte-invoice-form/package.json b/examples/svelte-invoice-form/package.json index 0705b078..c1e3d078 100644 --- a/examples/svelte-invoice-form/package.json +++ b/examples/svelte-invoice-form/package.json @@ -3,6 +3,9 @@ "private": true, "version": "0.0.0", "type": "module", + "engines": { + "node": ">=22.12" + }, "scripts": { "dev": "vite", "build": "vite build", diff --git a/examples/svelte-invoice-form/src/App.svelte b/examples/svelte-invoice-form/src/App.svelte index 5f07cb4c..d1421944 100644 --- a/examples/svelte-invoice-form/src/App.svelte +++ b/examples/svelte-invoice-form/src/App.svelte @@ -48,27 +48,25 @@ return Number.isFinite(parsed) ? parsed : 0; } - // Derive only the parts of the form the totals depend on, so the summary - // stays reactive without recreating the whole input on every keystroke. - const invoiceInput = $derived.by(() => ({ - taxRate: getInput(invoiceForm, { path: ['taxRate'] }), - discount: getInput(invoiceForm, { path: ['discount'] }), - lineItems: getInput(invoiceForm, { path: ['lineItems'] }), - })); + // Derive each input the totals depend on individually, so every value stays + // reactive and only recomputes when its own field changes. + const taxRate = $derived(getInput(invoiceForm, { path: ['taxRate'] })); + const discount = $derived(getInput(invoiceForm, { path: ['discount'] })); + const lineItems = $derived(getInput(invoiceForm, { path: ['lineItems'] })); const totals = $derived.by(() => { - const subtotal = invoiceInput.lineItems.reduce( + const subtotal = lineItems.reduce( (sum, item) => sum + toNumber(item.quantity) * toNumber(item.unitPrice), 0 ); - const tax = subtotal * (toNumber(invoiceInput.taxRate) / 100); - const discount = toNumber(invoiceInput.discount); - const total = Math.max(subtotal + tax - discount, 0); - return { subtotal, tax, discount, total }; + const tax = subtotal * (toNumber(taxRate) / 100); + const discountAmount = toNumber(discount); + const total = Math.max(subtotal + tax - discountAmount, 0); + return { subtotal, tax, discount: discountAmount, total }; }); function getLineTotal(index: number): number { - const item = invoiceInput.lineItems[index]; + const item = lineItems[index]; if (!item) return 0; return toNumber(item.quantity) * toNumber(item.unitPrice); } diff --git a/examples/svelte-invoice-form/src/app.css b/examples/svelte-invoice-form/src/app.css index 29b20151..fe8c6e42 100644 --- a/examples/svelte-invoice-form/src/app.css +++ b/examples/svelte-invoice-form/src/app.css @@ -121,6 +121,14 @@ textarea:focus { gap: 0.75rem; } +/* Stack the multi-column sections on narrow screens so inputs stay usable. */ +@media (max-width: 640px) { + .grid, + .line-item-fields { + grid-template-columns: 1fr; + } +} + .line-total { text-align: right; color: var(--muted); diff --git a/website/src/routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx b/website/src/routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx index 1535b1cc..3602c456 100644 --- a/website/src/routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx +++ b/website/src/routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx @@ -57,7 +57,7 @@ Our invoice needs basic metadata, client details, and a list of line items. We'l ```ts import * as v from 'valibot'; -const moneyInput = (message) => +const moneyInput = (message: string) => v.pipe( v.string(), v.nonEmpty(message), @@ -65,7 +65,7 @@ const moneyInput = (message) => v.minValue(0, 'Amount cannot be negative') ); -const positiveNumberInput = (message) => +const positiveNumberInput = (message: string) => v.pipe( v.string(), v.nonEmpty(message), @@ -300,7 +300,7 @@ function addLineItem() { }); } -function removeLineItem(index) { +function removeLineItem(index: number) { remove(invoiceForm, { path: ['lineItems'], at: index, @@ -319,49 +319,44 @@ Svelte 5's `$derived` works well here because the calculation automatically stay ```ts import { getInput } from '@formisch/svelte'; -const invoiceInput = $derived.by(() => ({ - taxRate: getInput(invoiceForm, { path: ['taxRate'] }), - discount: getInput(invoiceForm, { path: ['discount'] }), - lineItems: getInput(invoiceForm, { path: ['lineItems'] }), -})); - -function calculateTotals(input) { - const subtotal = input.lineItems.reduce((sum, item) => { - return sum + Number(item.quantity) * Number(item.unitPrice); - }, 0); - - const tax = subtotal * (Number(input.taxRate) / 100); - - const discount = Number(input.discount); - - const total = Math.max(subtotal + tax - discount, 0); - - return { - subtotal, - tax, - discount, - total, - }; +// Coerce a raw input string to a finite number, falling back to 0 while a +// field is empty or mid-edit so the totals never flash "NaN". +function toNumber(value: unknown): number { + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : 0; } -const totals = $derived(calculateTotals(invoiceInput)); +const taxRate = $derived(getInput(invoiceForm, { path: ['taxRate'] })); +const discount = $derived(getInput(invoiceForm, { path: ['discount'] })); +const lineItems = $derived(getInput(invoiceForm, { path: ['lineItems'] })); + +const totals = $derived.by(() => { + const subtotal = lineItems.reduce( + (sum, item) => sum + toNumber(item.quantity) * toNumber(item.unitPrice), + 0 + ); + const tax = subtotal * (toNumber(taxRate) / 100); + const discountAmount = toNumber(discount); + const total = Math.max(subtotal + tax - discountAmount, 0); + return { subtotal, tax, discount: discountAmount, total }; +}); ``` -`getInput` lets us read only the parts of the form we need. Wrapping those reads in `$derived.by()` keeps the totals connected to just the tax rate, discount, and line items, so they recalculate when those fields change and ignore unrelated updates. The totals stay reactive without any manual synchronization between the form fields and the invoice summary. +`getInput` reads a single field from the form, and wrapping each read in `$derived` gives us reactive values for just the tax rate, discount, and line items. The `totals` derived then combines them, recalculating whenever one of those fields changes while ignoring unrelated updates. Everything stays reactive without any manual synchronization between the form fields and the invoice summary. -For individual row totals, we can use the same input state: +For individual row totals, we can reuse the derived `lineItems`: ```ts -function getLineTotal(index) { - const item = invoiceInput.lineItems[index]; +function getLineTotal(index: number): number { + const item = lineItems[index]; if (!item) return 0; - return Number(item.quantity) * Number(item.unitPrice); + return toNumber(item.quantity) * toNumber(item.unitPrice); } ``` -Because `invoiceInput` is connected to the form, any dependent values update when the form changes. +Because these derived values are connected to the form, anything computed from them updates when the form changes. ## Step 7: Submit the form From 97aa41aa2714974bf0b9059d9304c17438e324fb Mon Sep 17 00:00:00 2001 From: Oluwawunmi Bewaji Date: Wed, 15 Jul 2026 11:11:05 +0100 Subject: [PATCH 09/16] Refactor invoice total calculation with $derived Refactor invoice total calculation to use Svelte 5's $derived for reactive updates. Improve clarity and performance by ensuring totals update automatically based on relevant form fields. --- .../dynamic-invoice-form-svelte/index.mdx | 74 ++++++++++++------- 1 file changed, 46 insertions(+), 28 deletions(-) diff --git a/website/src/routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx b/website/src/routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx index 3602c456..1eaffc66 100644 --- a/website/src/routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx +++ b/website/src/routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx @@ -312,62 +312,80 @@ The remove action is disabled when only one item remains because the schema requ ## Step 6: Add reactive totals -The invoice total depends on several values in the form: quantities, prices, tax, and discounts. Instead of updating totals manually inside every input handler, we can derive them from the current form state. - -Svelte 5's `$derived` works well here because the calculation automatically stays connected to the values it depends on. +The invoice total depends on several values in the form: quantities, prices, tax, and discounts. Instead of updating totals manually inside every input handler, we can derive only the fields the calculation depends on. Svelte 5's $derived keeps those values connected to the form state, so the totals update automatically whenever one of the relevant fields changes. ```ts -import { getInput } from '@formisch/svelte'; +import { getInput } from '@formisch/svelte';import { getInput } from '@formisch/svelte'; + +const taxRate = $derived( + getInput(invoiceForm, { + path: ['taxRate'] + }) +); + +const discount = $derived( + getInput(invoiceForm, { + path: ['discount'] + }) +); + +const lineItems = $derived( + getInput(invoiceForm, { + path: ['lineItems'] + }) +); -// Coerce a raw input string to a finite number, falling back to 0 while a -// field is empty or mid-edit so the totals never flash "NaN". -function toNumber(value: unknown): number { - const parsed = Number(value); - return Number.isFinite(parsed) ? parsed : 0; -} +const totals = $derived.by(() => { + const subtotal = lineItems.reduce((sum, item) => { + return sum + Number(item.quantity) * Number(item.unitPrice); + }, 0); -const taxRate = $derived(getInput(invoiceForm, { path: ['taxRate'] })); -const discount = $derived(getInput(invoiceForm, { path: ['discount'] })); -const lineItems = $derived(getInput(invoiceForm, { path: ['lineItems'] })); + const tax = subtotal * (Number(taxRate) / 100); + + const discountAmount = Number(discount); -const totals = $derived.by(() => { - const subtotal = lineItems.reduce( - (sum, item) => sum + toNumber(item.quantity) * toNumber(item.unitPrice), - 0 - ); - const tax = subtotal * (toNumber(taxRate) / 100); - const discountAmount = toNumber(discount); const total = Math.max(subtotal + tax - discountAmount, 0); - return { subtotal, tax, discount: discountAmount, total }; + + return { + subtotal, + tax, + discount: discountAmount, + total + }; }); ``` -`getInput` reads a single field from the form, and wrapping each read in `$derived` gives us reactive values for just the tax rate, discount, and line items. The `totals` derived then combines them, recalculating whenever one of those fields changes while ignoring unrelated updates. Everything stays reactive without any manual synchronization between the form fields and the invoice summary. +`getInput`() lets us derive individual fields from the form state. Here we derive taxRate, discount, and lineItems separately, then use `$derived.by`() to calculate the invoice totals. Because each derived value stays connected only to the field it represents, the `totals` only recompute when one of those values changes. Changes to unrelated fields elsewhere in the form won't trigger the calculation, keeping the derived state focused on exactly the data it needs. -For individual row totals, we can reuse the derived `lineItems`: +For individual row totals, we can reuse the same derived lineItems state: ```ts -function getLineTotal(index: number): number { +function getLineTotal(index) { const item = lineItems[index]; if (!item) return 0; - return toNumber(item.quantity) * toNumber(item.unitPrice); + return Number(item.quantity) * Number(item.unitPrice); } ``` - -Because these derived values are connected to the form, anything computed from them updates when the form changes. +Because lineItems stays connected to the form state, each row total updates automatically whenever that item's quantity or unit price changes. ## Step 7: Submit the form At this point, the form state, validation, and fields are all connected. The final step is handling the transition from editable input values to the validated invoice data your application can use. By default, Formisch runs validation on submission and provides the parsed output from the schema. This behavior can be configured if your application needs a different validation strategy. ```ts +import { + createForm, + reset, + type SubmitHandler +} from '@formisch/svelte'; + type InvoiceOutput = v.InferOutput; let submittedInvoice = $state(null); -const submitInvoice = async (output: InvoiceOutput) => { +const submitInvoice: SubmitHandler = (output) => { submittedInvoice = output; console.log('Invoice submitted:', output); From a023d6da5654b3e1fc66e1bc3d61e2885eca2ccb Mon Sep 17 00:00:00 2001 From: Oluwawunmi Bewaji Date: Wed, 15 Jul 2026 11:20:37 +0100 Subject: [PATCH 10/16] Improve line total calculation in dynamic invoice form Refactor line total calculation to use toNumber function for better type handling. --- .../(posts)/dynamic-invoice-form-svelte/index.mdx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/website/src/routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx b/website/src/routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx index 1eaffc66..e645a642 100644 --- a/website/src/routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx +++ b/website/src/routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx @@ -315,7 +315,7 @@ The remove action is disabled when only one item remains because the schema requ The invoice total depends on several values in the form: quantities, prices, tax, and discounts. Instead of updating totals manually inside every input handler, we can derive only the fields the calculation depends on. Svelte 5's $derived keeps those values connected to the form state, so the totals update automatically whenever one of the relevant fields changes. ```ts -import { getInput } from '@formisch/svelte';import { getInput } from '@formisch/svelte'; +import { getInput } from '@formisch/svelte'; const taxRate = $derived( getInput(invoiceForm, { @@ -360,14 +360,20 @@ const totals = $derived.by(() => { For individual row totals, we can reuse the same derived lineItems state: ```ts -function getLineTotal(index) { +function toNumber(value: unknown): number { + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : 0; +} + +function getLineTotal(index: number): number { const item = lineItems[index]; if (!item) return 0; - return Number(item.quantity) * Number(item.unitPrice); + return toNumber(item.quantity) * toNumber(item.unitPrice); } ``` + Because lineItems stays connected to the form state, each row total updates automatically whenever that item's quantity or unit price changes. ## Step 7: Submit the form From 7cc37e766fd8962ac930b22267c5f5ab2eeab258 Mon Sep 17 00:00:00 2001 From: Oluwawunmi Bewaji Date: Wed, 15 Jul 2026 12:11:10 +0100 Subject: [PATCH 11/16] Refine language and explanations in invoice form docs Updated terminology for clarity and improved consistency in the dynamic invoice form documentation. Enhanced explanations of form state management and derived calculations. --- .../dynamic-invoice-form-svelte/index.mdx | 43 ++++++++++--------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/website/src/routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx b/website/src/routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx index e645a642..1f246d8e 100644 --- a/website/src/routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx +++ b/website/src/routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx @@ -13,7 +13,7 @@ A field is not only a value. It has validation state, errors, transformations, a At this point, form state management moves away from handling individual inputs and towards managing the entire form model. -Formisch approaches this by treating the form as a single connected structure. It's a headless, lightweight library where the schema, state, validation, and submitted output all come from one source, while you stay in full control of the UI. +Formisch approaches this by treating the form as a single connected model. It's a headless, lightweight library where the schema, state, validation, and submitted output all come from one source, while you stay in full control of the UI. To see this in practice, we'll build a dynamic invoice form and walk through how Formisch works with Svelte 5's rune-based reactivity to manage: @@ -168,7 +168,7 @@ With the form created, the next step is to connect individual fields to that sta ## Step 4: Connect fields -Formisch is headless by design. It does not decide what your inputs should look like. Instead, it gives you the state and bindings needed to connect your own markup. +Formisch is headless by design and does not decide what your inputs should look like. Instead, it gives you the state and bindings needed to connect your own markup. A field is connected through the `Field` component: @@ -312,37 +312,44 @@ The remove action is disabled when only one item remains because the schema requ ## Step 6: Add reactive totals -The invoice total depends on several values in the form: quantities, prices, tax, and discounts. Instead of updating totals manually inside every input handler, we can derive only the fields the calculation depends on. Svelte 5's $derived keeps those values connected to the form state, so the totals update automatically whenever one of the relevant fields changes. +The invoice total depends on several values in the form: `quantities`, `prices`, `tax`, and `discounts`. Instead of updating totals manually inside every input handler, we can derive only the fields the calculation depends on. Svelte 5's `$derived` keeps those values connected to the form state, so the totals update automatically whenever one of the relevant fields changes. ```ts import { getInput } from '@formisch/svelte'; +function toNumber(value: unknown): number { + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : 0; +} + const taxRate = $derived( getInput(invoiceForm, { - path: ['taxRate'] + path: ['taxRate'], }) ); const discount = $derived( getInput(invoiceForm, { - path: ['discount'] + path: ['discount'], }) ); const lineItems = $derived( getInput(invoiceForm, { - path: ['lineItems'] + path: ['lineItems'], }) ); const totals = $derived.by(() => { - const subtotal = lineItems.reduce((sum, item) => { - return sum + Number(item.quantity) * Number(item.unitPrice); - }, 0); + const subtotal = lineItems.reduce( + (sum, item) => + sum + toNumber(item.quantity) * toNumber(item.unitPrice), + 0 + ); - const tax = subtotal * (Number(taxRate) / 100); + const tax = subtotal * (toNumber(taxRate) / 100); - const discountAmount = Number(discount); + const discountAmount = toNumber(discount); const total = Math.max(subtotal + tax - discountAmount, 0); @@ -350,21 +357,16 @@ const totals = $derived.by(() => { subtotal, tax, discount: discountAmount, - total + total, }; }); ``` -`getInput`() lets us derive individual fields from the form state. Here we derive taxRate, discount, and lineItems separately, then use `$derived.by`() to calculate the invoice totals. Because each derived value stays connected only to the field it represents, the `totals` only recompute when one of those values changes. Changes to unrelated fields elsewhere in the form won't trigger the calculation, keeping the derived state focused on exactly the data it needs. +`getInput` lets us derive individual fields from the form state. Here we derive `taxRate`, `discount`, and `lineItems` separately, then use `$derived.by` to calculate the invoice totals. The small `toNumber` helper keeps the calculation stable while a user is editing numeric fields, so the UI never briefly displays `NaN` if an input is temporarily empty. Because each derived value stays connected only to the field it represents, the totals only recompute when one of those values changes. Changes to unrelated fields elsewhere in the form won't trigger the calculation, keeping the derived state focused on exactly the data it needs. -For individual row totals, we can reuse the same derived lineItems state: +For individual row totals, we can reuse the same derived `lineItems` state: ```ts -function toNumber(value: unknown): number { - const parsed = Number(value); - return Number.isFinite(parsed) ? parsed : 0; -} - function getLineTotal(index: number): number { const item = lineItems[index]; @@ -374,7 +376,7 @@ function getLineTotal(index: number): number { } ``` -Because lineItems stays connected to the form state, each row total updates automatically whenever that item's quantity or unit price changes. +Because `lineItems` stays connected to the form state, each row total updates automatically whenever that item's quantity or unit price changes. ## Step 7: Submit the form @@ -382,7 +384,6 @@ At this point, the form state, validation, and fields are all connected. The fin ```ts import { - createForm, reset, type SubmitHandler } from '@formisch/svelte'; From f7fffefd1563531b04c5cfb495211b2fb8bd3ab1 Mon Sep 17 00:00:00 2001 From: Oluwawunmi Bewaji Date: Wed, 15 Jul 2026 12:53:55 +0100 Subject: [PATCH 12/16] Clarify invoice total explanation in documentation --- .../routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx b/website/src/routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx index 1f246d8e..636b9e15 100644 --- a/website/src/routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx +++ b/website/src/routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx @@ -312,7 +312,7 @@ The remove action is disabled when only one item remains because the schema requ ## Step 6: Add reactive totals -The invoice total depends on several values in the form: `quantities`, `prices`, `tax`, and `discounts`. Instead of updating totals manually inside every input handler, we can derive only the fields the calculation depends on. Svelte 5's `$derived` keeps those values connected to the form state, so the totals update automatically whenever one of the relevant fields changes. +The invoice total depends on several values in the form: quantities, prices, tax, and discounts. Instead of updating totals manually inside every input handler, we can derive only the fields the calculation depends on. Svelte 5's `$derived` keeps those values connected to the form state, so the totals update automatically whenever one of the relevant fields changes. ```ts import { getInput } from '@formisch/svelte'; From d2569660a0255b0d0e8e88831a5eb0c8ac84f9af Mon Sep 17 00:00:00 2001 From: Oluwawunmi Bewaji Date: Thu, 16 Jul 2026 01:37:25 +0100 Subject: [PATCH 13/16] Update publication date --- .../blog/(posts)/dynamic-invoice-form-svelte/index.mdx | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/website/src/routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx b/website/src/routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx index 636b9e15..7c605978 100644 --- a/website/src/routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx +++ b/website/src/routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx @@ -2,7 +2,7 @@ cover: Invoice Form title: 'Building a Dynamic Invoice Form in Svelte 5 with Formisch' description: Build a dynamic invoice form in Svelte 5 using Formisch, Valibot, FieldArray, and typed schema validation. -published: 2026-07-15 +published: 2026-07-16 authors: - flySewa --- @@ -36,7 +36,7 @@ We'll be using: - [Formisch](https://formisch.dev) - [Valibot](https://valibot.dev) for schema validation -You can follow along here, view the complete example on [Stackblitz](https://stackblitz.com/edit/sveltejs-kit-template-default-vbklpioo?file=src%2Froutes%2F%2Bpage.svelte), or clone the [runnable example](https://github.com/open-circle/formisch/tree/main/examples/svelte-invoice-form) from the repo. +You can follow along here, view the complete example on [Stackblitz](https://stackblitz.com/edit/sveltejs-kit-template-default-umydieqm?file=src%2Froutes%2F%2Bpage.svelte), or clone the [runnable example](https://github.com/open-circle/formisch/tree/main/examples/svelte-invoice-form) from the repo. ## Step 1: Install dependencies @@ -383,10 +383,7 @@ Because `lineItems` stays connected to the form state, each row total updates au At this point, the form state, validation, and fields are all connected. The final step is handling the transition from editable input values to the validated invoice data your application can use. By default, Formisch runs validation on submission and provides the parsed output from the schema. This behavior can be configured if your application needs a different validation strategy. ```ts -import { - reset, - type SubmitHandler -} from '@formisch/svelte'; +import { type SubmitHandler } from '@formisch/svelte'; type InvoiceOutput = v.InferOutput; From f708f2f3ea847dedbc9055c802fb0ed381b5ad54 Mon Sep 17 00:00:00 2001 From: Oluwawunmi Bewaji Date: Thu, 16 Jul 2026 01:44:07 +0100 Subject: [PATCH 14/16] Enhance discussion on form state management Expanded explanation of form state management in Svelte 5. --- .../routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/website/src/routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx b/website/src/routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx index 7c605978..a2dbb780 100644 --- a/website/src/routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx +++ b/website/src/routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx @@ -9,9 +9,7 @@ authors: Svelte 5 gives us powerful primitives for managing state with runes, but forms still have their own set of challenges. -A field is not only a value. It has validation state, errors, transformations, and a relationship with the rest of the form. When a form grows beyond a handful of inputs, things like nested data, dynamic fields, and derived values all need to stay in sync. - -At this point, form state management moves away from handling individual inputs and towards managing the entire form model. +A field is not only a value. It has validation state, errors, transformations, and a relationship with the rest of the form. When a form grows beyond a handful of inputs, things like nested data, dynamic fields, and derived values all need to stay in sync. At this point, form state management moves away from handling individual inputs and towards managing the entire form model. Formisch approaches this by treating the form as a single connected model. It's a headless, lightweight library where the schema, state, validation, and submitted output all come from one source, while you stay in full control of the UI. From 707e55ad82b1ef61f686a46fcb1bfef55bdeb4ec Mon Sep 17 00:00:00 2001 From: Oluwawunmi Bewaji Date: Thu, 16 Jul 2026 02:08:00 +0100 Subject: [PATCH 15/16] Fix errors --- .../routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx b/website/src/routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx index a2dbb780..5d783eb9 100644 --- a/website/src/routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx +++ b/website/src/routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx @@ -34,7 +34,7 @@ We'll be using: - [Formisch](https://formisch.dev) - [Valibot](https://valibot.dev) for schema validation -You can follow along here, view the complete example on [Stackblitz](https://stackblitz.com/edit/sveltejs-kit-template-default-umydieqm?file=src%2Froutes%2F%2Bpage.svelte), or clone the [runnable example](https://github.com/open-circle/formisch/tree/main/examples/svelte-invoice-form) from the repo. +You can follow along here, view the complete example on [Stackblitz](https://stackblitz.com/edit/sveltejs-kit-template-default-g5kpabcj?file=src%2Froutes%2F%2Bpage.svelte) , or clone the [runnable example](https://github.com/open-circle/formisch/tree/main/examples/svelte-invoice-form) from the repo. ## Step 1: Install dependencies From f794eedbab0b087e029fb63e01bbabdfc71e76d9 Mon Sep 17 00:00:00 2001 From: Fabian Hiller Date: Thu, 16 Jul 2026 01:14:03 -0400 Subject: [PATCH 16/16] Fix formatting of code example in blog post --- .../routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/website/src/routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx b/website/src/routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx index 5d783eb9..119d302c 100644 --- a/website/src/routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx +++ b/website/src/routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx @@ -340,8 +340,7 @@ const lineItems = $derived( const totals = $derived.by(() => { const subtotal = lineItems.reduce( - (sum, item) => - sum + toNumber(item.quantity) * toNumber(item.unitPrice), + (sum, item) => sum + toNumber(item.quantity) * toNumber(item.unitPrice), 0 );