Compare commits

...

10 Commits

Author SHA1 Message Date
NicoR
998927f752 Rename form-svelte to TanStack form 2025-07-04 11:24:58 -03:00
NicoR
1da5f9c150 Use TanStack Form with JSON object for draft form state 2025-07-03 12:17:14 -03:00
NicoR
ef242d9a7b Add form example using Svelte + vanilla form 2025-07-03 11:44:46 -03:00
NicoR
ca4551b044 Use TanStack Form with JSON object for draft form state 2025-07-03 10:46:13 -03:00
NicoR
9cd8c407d2 Use React Hook Form with JSON object for draft form state 2025-07-03 10:06:39 -03:00
NicoR
88d393ac91 Keep form's draft state in a JSON object 2025-07-02 15:13:55 -03:00
NicoR
f04b11572d Fix rebase errors 2025-07-02 13:56:56 -03:00
Trisha Lim
3cbd4eae3b fix cotext field 2025-07-02 13:56:56 -03:00
Trisha Lim
63ab6abd4f fix reactivity 2025-07-02 13:56:56 -03:00
Trisha Lim
52243161a3 useForm hook 2025-07-02 13:56:55 -03:00
26 changed files with 1419 additions and 15 deletions

View File

@@ -10,6 +10,7 @@
"format-and-lint:fix": "biome check . --write"
},
"dependencies": {
"@tanstack/react-form": "^0.33.0",
"hash-slash": "workspace:*",
"jazz-tools": "workspace:*",
"react": "19.0.0",
@@ -30,4 +31,4 @@
"typescript": "5.6.2",
"vite": "^6.3.5"
}
}
}

View File

@@ -1,11 +1,13 @@
import { useCoState } from "jazz-tools/react";
import { LinkToHome } from "./LinkToHome.tsx";
import { OrderForm } from "./OrderForm.tsx";
import { OrderFormWithSaveButton } from "./OrderFormWithSaveButton.tsx";
import { OrderThumbnail } from "./OrderThumbnail.tsx";
import { BubbleTeaOrder } from "./schema.ts";
export function EditOrder(props: { id: string }) {
const order = useCoState(BubbleTeaOrder, props.id);
const order = useCoState(BubbleTeaOrder, props.id, {
resolve: { addOns: true, instructions: true },
});
if (!order) return;
@@ -13,13 +15,17 @@ export function EditOrder(props: { id: string }) {
<>
<LinkToHome />
<OrderThumbnail order={order} />
<div>
<p>Saved order:</p>
<OrderThumbnail order={order} />
</div>
<h1 className="text-lg">
<strong>Edit your bubble tea order 🧋</strong>
</h1>
<OrderForm order={order} />
<OrderFormWithSaveButton order={order} />
</>
);
}

View File

@@ -0,0 +1,214 @@
import { CoPlainText, Loaded } from "jazz-tools";
import { useForm } from "@tanstack/react-form";
import {
BubbleTeaAddOnTypes,
BubbleTeaBaseTeaTypes,
BubbleTeaOrder,
} from "./schema.ts";
import { OrderThumbnail } from "./OrderThumbnail.tsx";
type LoadedBubbleTeaOrder = Loaded<
typeof BubbleTeaOrder,
{ addOns: { $each: true }; instructions: true }
>;
// Would be great to derive this type from the CoValue schema
export type OrderFormData = {
id: string;
baseTea: (typeof BubbleTeaBaseTeaTypes)[number];
addOns: (typeof BubbleTeaAddOnTypes)[number][];
deliveryDate: Date;
withMilk: boolean;
instructions?: string;
};
export function OrderFormWithSaveButton({
order: originalOrder,
}: {
order: LoadedBubbleTeaOrder;
}) {
const defaultValues = originalOrder.toJSON();
// Convert timestamp to Date
defaultValues.deliveryDate = new Date(defaultValues.deliveryDate);
const form = useForm<OrderFormData>({
defaultValues,
onSubmit: async ({ value }: { value: OrderFormData }) => {
console.log("submit form", value);
// Apply changes to the original Jazz order
originalOrder.baseTea = value.baseTea;
originalOrder.addOns.applyDiff(value.addOns);
originalOrder.deliveryDate = new Date(value.deliveryDate);
originalOrder.withMilk = value.withMilk;
// `applyDiff` requires nested objects to be CoValues as well
const instructions = originalOrder.instructions ?? CoPlainText.create("");
if (value.instructions) {
instructions.applyDiff(value.instructions);
}
},
});
return (
<form
onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
form.handleSubmit();
}}
className="grid gap-5"
>
{/* TODO refactor OrderThumbnail to support receiving a plain JSON object */}
<div>
<p>Unsaved order preview:</p>
<form.Subscribe
selector={(state) => [state.values]}
children={([values]) => <OrderThumbnail order={values} />}
/>
</div>
<div className="flex flex-col gap-2">
<label htmlFor="baseTea">Base tea</label>
<form.Field
name="baseTea"
validators={{
onChange: ({ value }) =>
!value ? "Please select your preferred base tea" : undefined,
}}
children={(field) => (
<>
<select
id="baseTea"
className="dark:bg-transparent"
value={field.state.value}
onChange={(e) =>
field.handleChange(e.target.value as typeof field.state.value)
}
onBlur={field.handleBlur}
>
<option value="" disabled>
Please select your preferred base tea
</option>
{BubbleTeaBaseTeaTypes.map((teaType) => (
<option key={teaType} value={teaType}>
{teaType}
</option>
))}
</select>
{field.state.meta.errors.length > 0 && (
<span className="text-red-500 text-sm">
{field.state.meta.errors[0]}
</span>
)}
</>
)}
/>
</div>
<fieldset>
<legend className="mb-2">Add-ons</legend>
<form.Field
name="addOns"
mode="array"
children={(field) => (
<>
{BubbleTeaAddOnTypes.map((addOn) => (
<div key={addOn} className="flex items-center gap-2">
<input
type="checkbox"
id={addOn}
checked={field.state.value.includes(addOn)}
onChange={(e) => {
const currentValue = field.state.value;
if (e.target.checked) {
field.handleChange([...currentValue, addOn]);
} else {
field.handleChange(
currentValue.filter((item) => item !== addOn),
);
}
}}
/>
<label htmlFor={addOn}>{addOn}</label>
</div>
))}
</>
)}
/>
</fieldset>
<div className="flex flex-col gap-2">
<label htmlFor="deliveryDate">Delivery date</label>
<form.Field
name="deliveryDate"
validators={{
onChange: ({ value }) =>
!value ? "Delivery date is required" : undefined,
}}
children={(field) => (
<>
<input
type="date"
id="deliveryDate"
className="dark:bg-transparent"
value={field.state.value.toISOString().split("T")[0]}
onChange={(e) => field.handleChange(new Date(e.target.value))}
onBlur={field.handleBlur}
/>
{field.state.meta.errors.length > 0 && (
<span className="text-red-500 text-sm">
{field.state.meta.errors[0]}
</span>
)}
</>
)}
/>
</div>
<div className="flex items-center gap-2">
<form.Field
name="withMilk"
children={(field) => (
<input
type="checkbox"
id="withMilk"
checked={field.state.value}
onChange={(e) => field.handleChange(e.target.checked)}
/>
)}
/>
<label htmlFor="withMilk">With milk?</label>
</div>
<div className="flex flex-col gap-2">
<label htmlFor="instructions">Special instructions</label>
<form.Field
name="instructions"
children={(field) => (
<textarea
id="instructions"
className="dark:bg-transparent"
value={field.state.value || ""}
onChange={(e) => field.handleChange(e.target.value)}
onBlur={field.handleBlur}
/>
)}
/>
</div>
<form.Subscribe
selector={(state) => [state.canSubmit, state.isSubmitting]}
children={([canSubmit, isSubmitting]) => (
<button
type="submit"
disabled={!canSubmit}
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded disabled:bg-gray-400"
>
{isSubmitting ? "Submitting..." : "Submit"}
</button>
)}
/>
</form>
);
}

View File

@@ -1,10 +1,11 @@
import { Loaded } from "jazz-tools";
import { BubbleTeaOrder } from "./schema.ts";
import { OrderFormData } from "./OrderFormWithSaveButton.tsx";
export function OrderThumbnail({
order,
}: {
order: Loaded<typeof BubbleTeaOrder>;
order: Loaded<typeof BubbleTeaOrder> | OrderFormData;
}) {
const { id, baseTea, addOns, instructions, deliveryDate, withMilk } = order;
const date = deliveryDate.toLocaleDateString();

View File

@@ -0,0 +1,8 @@
{
"useTabs": true,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte"],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
}

View File

@@ -0,0 +1,35 @@
# Changelog
## v0.1.0 - Initial Svelte Migration
### Added
- Complete Svelte implementation of the form example
- Native Svelte reactive state management (replacing TanStack Form)
- SvelteKit routing with hash router
- Tailwind CSS styling
- Jazz tools integration for Svelte
### Changes from React Version
- **Form Management**: Replaced TanStack Form with native Svelte `$state` and `$derived` reactives
- **Components**: Converted all React components to Svelte components
- **Event Handling**: Used Svelte's native event handling instead of React event handlers
- **State Management**: Leveraged Svelte's built-in reactivity instead of external form libraries
- **Validation**: Implemented custom validation using Svelte reactive patterns
### Features
- ✅ Create new bubble tea orders
- ✅ Edit existing orders
- ✅ Real-time form preview
- ✅ Form validation with error messages
- ✅ Date input handling with proper format conversion
- ✅ Multiple selection for add-ons
- ✅ Jazz CoValue synchronization
- ✅ Responsive design with Tailwind CSS
### Technical Implementation
- **Framework**: SvelteKit 2.x with Svelte 5.x
- **State**: Native Svelte `$state` runes for reactive state
- **Derived State**: `$derived` for computed values like form validity
- **Effects**: `$effect` for side effects and lifecycle management
- **TypeScript**: Full type safety maintained
- **Styling**: Tailwind CSS for consistent design

View File

@@ -0,0 +1,126 @@
# Jazz Form Example - Svelte
A comprehensive form example built with Svelte and Jazz, demonstrating reactive form handling, validation, and real-time data synchronization.
## Overview
This example showcases a bubble tea ordering system with the following features:
- **Create Orders**: Build new bubble tea orders with base tea, add-ons, delivery date, and special instructions
- **Edit Orders**: Modify existing orders with real-time preview
- **Form Validation**: Client-side validation with error messages
- **Real-time Sync**: Jazz CoValues for real-time data synchronization
- **Responsive Design**: Modern UI with Tailwind CSS
## Key Technologies
- **Svelte 5.x**: Modern reactive UI framework with runes
- **SvelteKit**: Full-stack Svelte framework
- **Jazz Tools**: Real-time collaborative data layer
- **TypeScript**: Full type safety
- **Tailwind CSS**: Utility-first CSS framework
## Architecture
### State Management
Instead of external form libraries, this example uses Svelte's native reactivity:
- `$state` - For mutable reactive state
- `$derived` - For computed values (like form validity)
- `$effect` - For side effects and lifecycle management
### Form Pattern
```svelte
<script>
// Reactive form state
let formData = $state({
baseTea: '',
addOns: [],
deliveryDate: new Date(),
withMilk: false,
instructions: ''
});
// Derived validation
const isValid = $derived(
formData.baseTea && formData.deliveryDate
);
// Event handlers
function handleSubmit(event) {
// Handle form submission
}
</script>
```
### Jazz Integration
The example demonstrates Jazz CoValues for:
- **Account Management**: User authentication and profile
- **Data Persistence**: Bubble tea orders stored as CoMaps
- **Real-time Sync**: Changes sync across sessions
- **Collaborative Features**: Shared data structures
## Getting Started
```bash
# Install dependencies
pnpm install
# Start development server
pnpm dev
# Build for production
pnpm build
```
## Project Structure
```
src/
├── lib/
│ ├── components/
│ │ ├── CreateOrder.svelte # New order creation
│ │ ├── EditOrder.svelte # Edit existing orders
│ │ ├── Orders.svelte # Order list view
│ │ ├── OrderFormWithSaveButton.svelte # Main form component
│ │ ├── OrderThumbnail.svelte # Order preview
│ │ └── LinkToHome.svelte # Navigation
│ ├── schema.ts # Jazz CoValue definitions
│ └── apiKey.ts # Jazz API configuration
├── routes/
│ ├── +layout.svelte # App layout with Jazz provider
│ └── +page.svelte # Main app with routing
└── app.html # HTML template
```
## Migration from React
This Svelte version was migrated from a React + TanStack Form implementation. Key differences:
| React | Svelte |
|-------|--------|
| `useForm` hook | `$state` runes |
| `form.Field` components | Native form elements |
| `form.Subscribe` | `$derived` reactives |
| Event handlers via props | Native Svelte event handling |
| TanStack Form validation | Custom validation functions |
The Svelte implementation is more concise while maintaining the same functionality and type safety.
## Features Demonstrated
-**Form State Management**: Reactive state with validation
-**Real-time Preview**: Live updates as user types
-**Date Handling**: Proper Date ↔ string conversion for inputs
-**Array Fields**: Dynamic add-on selection
-**Error Handling**: Field-level validation messages
-**Submit State**: Loading states and form submission
-**Jazz Integration**: CoValue creation and updates
-**TypeScript**: Full type safety throughout
## Learn More
- [Svelte Documentation](https://svelte.dev/docs)
- [SvelteKit Documentation](https://kit.svelte.dev/docs)
- [Jazz Documentation](https://jazz.tools/docs)
- [Tailwind CSS](https://tailwindcss.com/docs)

View File

@@ -0,0 +1,32 @@
import eslint from "@eslint/js";
import prettier from "eslint-config-prettier";
import svelte from "eslint-plugin-svelte";
import globals from "globals";
import tseslint from "typescript-eslint";
export default tseslint.config(
eslint.configs.recommended,
...tseslint.configs.recommended,
...svelte.configs["flat/recommended"],
prettier,
...svelte.configs["flat/prettier"],
{
languageOptions: {
globals: {
...globals.browser,
...globals.node,
},
},
},
{
files: ["**/*.svelte"],
languageOptions: {
parserOptions: {
parser: tseslint.parser,
},
},
},
{
ignores: ["build/", ".svelte-kit/", "dist/"],
},
);

View File

@@ -0,0 +1,43 @@
{
"name": "tanstack-form",
"private": true,
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "pnpm run check && vite build",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"format": "prettier --write .",
"lint": "prettier --check . && eslint .",
"format-and-lint": "pnpm run format && pnpm run lint",
"format-and-lint:fix": "pnpm run format --write && pnpm run lint --fix",
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui"
},
"devDependencies": {
"@sveltejs/adapter-auto": "^3.3.1",
"@sveltejs/kit": "^2.21.1",
"@sveltejs/vite-plugin-svelte": "^5.0.3",
"@tailwindcss/postcss": "^4.1.10",
"@types/eslint": "^9.6.1",
"eslint": "^9.27.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.46.1",
"globals": "^15.15.0",
"prettier": "^3.5.3",
"prettier-plugin-svelte": "^3.4.0",
"svelte": "^5.31.1",
"svelte-check": "^4.2.1",
"tailwindcss": "^4.1.7",
"typescript": "5.6.2",
"typescript-eslint": "^8.32.1",
"vite": "6.0.11"
},
"dependencies": {
"@tailwindcss/vite": "^4.1.7",
"@tanstack/svelte-form": "^1.12.4",
"jazz-tools": "workspace:*",
"tailwindcss": "^4.1.7"
}
}

View File

@@ -0,0 +1 @@
@import 'tailwindcss';

13
examples/tanstack-form/src/app.d.ts vendored Normal file
View File

@@ -0,0 +1,13 @@
// See https://svelte.dev/docs/kit/types#app.d.ts
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
export {};

View File

@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

View File

@@ -0,0 +1 @@
export const apiKey = 'tanstack-form-example_bYRMYEZmBNwE1qKQ';

View File

@@ -0,0 +1,50 @@
<script lang="ts">
import { AccountCoState } from 'jazz-tools/svelte';
import { JazzAccount, BubbleTeaOrder, BubbleTeaBaseTeaTypes, ListOfBubbleTeaAddOns } from '$lib/schema';
import OrderFormWithSaveButton from './OrderFormWithSaveButton.svelte';
import LinkToHome from './LinkToHome.svelte';
import { CoPlainText } from 'jazz-tools';
const me = new AccountCoState(JazzAccount, {
resolve: {
profile: true,
root: {
orders: true
}
}
});
const orders = $derived(me.current?.root.orders);
// Create a new order when component loads
let newOrder = $state<any>(null);
$effect(() => {
if (orders && !newOrder) {
const order = BubbleTeaOrder.create({
baseTea: BubbleTeaBaseTeaTypes[0],
addOns: ListOfBubbleTeaAddOns.create([], orders._owner),
deliveryDate: new Date(),
withMilk: false,
instructions: CoPlainText.create(''),
}, orders._owner);
orders.push(order);
newOrder = order;
}
});
</script>
<div>
<LinkToHome />
<h1 class="text-lg mb-4">
<strong>Create your bubble tea order 🧋</strong>
</h1>
{#if newOrder}
<OrderFormWithSaveButton order={newOrder} />
{:else}
<p>Creating order...</p>
{/if}
</div>

View File

@@ -0,0 +1,39 @@
<script lang="ts">
import { CoState } from 'jazz-tools/svelte';
import LinkToHome from './LinkToHome.svelte';
import OrderFormWithSaveButton from './OrderFormWithSaveButton.svelte';
import OrderThumbnail from './OrderThumbnail.svelte';
import { BubbleTeaOrder } from '$lib/schema';
type Props = {
id: string;
};
let { id }: Props = $props();
const order = new CoState(BubbleTeaOrder, id, {
resolve: { addOns: true, instructions: true },
});
</script>
{#if order.current}
<div>
<LinkToHome />
<div class="mb-6">
<p>Saved order:</p>
<OrderThumbnail order={order.current} />
</div>
<h1 class="text-lg mb-4">
<strong>Edit your bubble tea order 🧋</strong>
</h1>
<OrderFormWithSaveButton order={order.current} />
</div>
{:else}
<div>
<LinkToHome />
<p>Loading order...</p>
</div>
{/if}

View File

@@ -0,0 +1,14 @@
<script lang="ts">
import { goto } from '$app/navigation';
function navigateHome() {
goto('#/');
}
</script>
<button
onclick={navigateHome}
class="mb-4 text-blue-500 hover:text-blue-700 underline"
>
← Back to orders
</button>

View File

@@ -0,0 +1,194 @@
<script lang="ts">
import { CoPlainText, type Loaded } from 'jazz-tools';
import {
BubbleTeaAddOnTypes,
BubbleTeaBaseTeaTypes,
type BubbleTeaOrder
} from '$lib/schema';
import { createForm } from '@tanstack/svelte-form';
import OrderThumbnail from './OrderThumbnail.svelte';
type LoadedBubbleTeaOrder = Loaded<
typeof BubbleTeaOrder,
{ addOns: { $each: true }; instructions: true }
>;
type OrderFormData = {
id: string;
baseTea: (typeof BubbleTeaBaseTeaTypes)[number];
addOns: (typeof BubbleTeaAddOnTypes)[number][];
deliveryDate: Date;
withMilk: boolean;
instructions?: string;
};
type Props = {
order: LoadedBubbleTeaOrder;
};
let { order: originalOrder }: Props = $props();
const defaultValues: OrderFormData = originalOrder.toJSON();
defaultValues.deliveryDate = new Date(defaultValues.deliveryDate);
const form = createForm(() => ({
defaultValues,
onSubmit: async ({ value }) => {
console.log('submit form', value);
// Apply changes to the original Jazz order
originalOrder.baseTea = value.baseTea;
originalOrder.addOns.applyDiff(value.addOns);
originalOrder.deliveryDate = value.deliveryDate;
originalOrder.withMilk = value.withMilk;
// Handle instructions (CoPlainText)
const instructions = originalOrder.instructions ?? CoPlainText.create('');
if (value.instructions) {
instructions.applyDiff(value.instructions);
}
},
}));
function handleSubmit(event: Event) {
event.preventDefault();
event.stopPropagation();
form.handleSubmit();
}
</script>
<form onsubmit={handleSubmit} class="grid gap-5">
<form.Subscribe selector={(state) => state.values}>
{#snippet children(values)}
<p>Unsaved order preview:</p>
<OrderThumbnail order={values} />
{/snippet}
</form.Subscribe>
<div class="flex flex-col gap-2">
<label for="baseTea">Base tea</label>
<form.Field
name="baseTea"
validators={{
onChange: ({ value }) => {
if (!value) {
return 'Please select your preferred base tea';
}
return undefined;
},
}}
>
{#snippet children(field)}
<select
id="baseTea"
class="dark:bg-transparent"
value={field.state.value}
onchange={(e) => field.handleChange(e.currentTarget.value as typeof BubbleTeaBaseTeaTypes[number])}
onblur={field.handleBlur}
>
<option value="" disabled>
Please select your preferred base tea
</option>
{#each BubbleTeaBaseTeaTypes as teaType}
<option value={teaType}>
{teaType}
</option>
{/each}
</select>
{#if field.state.meta.errors.length > 0}
<span class="text-red-500 text-sm">{field.state.meta.errors[0]}</span>
{/if}
{/snippet}
</form.Field>
</div>
<fieldset>
<legend class="mb-2">Add-ons</legend>
<form.Field name="addOns">
{#snippet children(field)}
{#each BubbleTeaAddOnTypes as addOn}
<div class="flex items-center gap-2">
<input
type="checkbox"
id={addOn}
checked={field.state.value.includes(addOn)}
onchange={(e) => {
const updatedAddons = (e.currentTarget.checked) ? [...field.state.value, addOn] : field.state.value.filter((item: string) => item !== addOn)
field.handleChange(updatedAddons)}}
/>
<label for={addOn}>{addOn}</label>
</div>
{/each}
{/snippet}
</form.Field>
</fieldset>
<div class="flex flex-col gap-2">
<label for="deliveryDate">Delivery date</label>
<form.Field
name="deliveryDate"
validators={{
onChange: ({ value }) => {
if (!value) {
return 'Delivery date is required';
}
return undefined;
},
}}
>
{#snippet children(field)}
<input
type="date"
id="deliveryDate"
class="dark:bg-transparent"
value={field.state.value.toISOString().split('T')[0]}
onchange={(e) => field.handleChange(new Date(e.currentTarget.value))}
onblur={field.handleBlur}
/>
{#if field.state.meta.errors.length > 0}
<span class="text-red-500 text-sm">{field.state.meta.errors[0]}</span>
{/if}
{/snippet}
</form.Field>
</div>
<div class="flex items-center gap-2">
<form.Field name="withMilk">
{#snippet children(field)}
<input
type="checkbox"
id="withMilk"
checked={field.state.value}
onchange={(e) => field.handleChange(e.currentTarget.checked)}
/>
<label for="withMilk">With milk?</label>
{/snippet}
</form.Field>
</div>
<div class="flex flex-col gap-2">
<label for="instructions">Special instructions</label>
<form.Field name="instructions">
{#snippet children(field)}
<textarea
id="instructions"
class="dark:bg-transparent"
value={field.state.value || ''}
onchange={(e) => field.handleChange(e.currentTarget.value)}
></textarea>
{/snippet}
</form.Field>
</div>
<form.Subscribe selector={(state) => [state.canSubmit, state.isSubmitting]}>
{#snippet children([canSubmit, isSubmitting])}
<button
type="submit"
disabled={!canSubmit}
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded disabled:bg-gray-400"
>
{isSubmitting ? 'Submitting...' : 'Submit'}
</button>
{/snippet}
</form.Subscribe>
</form>

View File

@@ -0,0 +1,39 @@
<script lang="ts">
import { type Loaded } from 'jazz-tools';
import { type BubbleTeaOrder } from '$lib/schema';
type Props = {
order: Loaded<typeof BubbleTeaOrder> | {
baseTea: string;
addOns: string[];
deliveryDate: Date;
withMilk: boolean;
instructions?: string;
};
};
let { order }: Props = $props();
</script>
<div class="border p-3 bg-gray-50 dark:bg-gray-800 rounded">
<strong>
{order.baseTea || "(No tea selected)"}
{order.withMilk ? " milk " : " "}
tea
</strong>
{#if order.addOns && order.addOns.length > 0}
<p class="text-sm text-stone-600">
with {order.addOns.join(", ").toLowerCase()}
</p>
{/if}
{#if order.instructions}
<p class="text-sm text-stone-600 italic">
{order.instructions}
</p>
{/if}
{#if order.deliveryDate}
<p class="text-sm text-stone-600">
Delivery: {order.deliveryDate.toLocaleDateString()}
</p>
{/if}
</div>

View File

@@ -0,0 +1,59 @@
<script lang="ts">
import { AccountCoState } from 'jazz-tools/svelte';
import { JazzAccount, type BubbleTeaOrder } from '$lib/schema';
import { type Loaded } from 'jazz-tools';
import OrderThumbnail from './OrderThumbnail.svelte';
import { goto } from '$app/navigation';
const me = new AccountCoState(JazzAccount, {
resolve: {
profile: true,
root: {
orders: {
$each: true
}
}
}
});
const orders = $derived(me.current?.root.orders);
function navigateToCreateOrder() {
goto('#/order');
}
function navigateToEditOrder(id: string) {
goto(`#/order/${id}`);
}
</script>
<div>
<h1 class="text-lg mb-4">
<strong>Your bubble tea orders 🧋</strong>
</h1>
{#if orders && orders.length > 0}
<div class="space-y-4">
{#each orders as order}
{#if order}
<button
type="button"
class="border rounded-lg p-4 cursor-pointer hover:bg-gray-50 text-left w-full"
onclick={() => navigateToEditOrder(order.id)}
>
<OrderThumbnail {order} />
</button>
{/if}
{/each}
</div>
{:else}
<p class="text-gray-500">No orders yet. Create your first order!</p>
{/if}
<button
onclick={navigateToCreateOrder}
class="mt-6 bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
>
Create New Order
</button>
</div>

View File

@@ -0,0 +1,86 @@
import { type Loaded, co, z } from 'jazz-tools';
export const BubbleTeaAddOnTypes = [
'Pearl',
'Lychee jelly',
'Red bean',
'Brown sugar',
'Taro'
] as const;
export const BubbleTeaBaseTeaTypes = ['Black', 'Oolong', 'Jasmine', 'Thai'] as const;
export const ListOfBubbleTeaAddOns = co
.list(z.literal([...BubbleTeaAddOnTypes]))
// eslint-disable-next-line @typescript-eslint/no-unused-vars
.withHelpers((Self) => ({
hasChanges(list?: Loaded<typeof Self> | null) {
return list && Object.entries(list._raw.insertions).length > 0;
}
}));
export const BubbleTeaOrder = co.map({
baseTea: z.literal([...BubbleTeaBaseTeaTypes]),
addOns: ListOfBubbleTeaAddOns,
deliveryDate: z.date(),
withMilk: z.boolean(),
instructions: z.optional(co.plainText())
});
export const DraftBubbleTeaOrder = co
.map({
baseTea: z.optional(z.literal([...BubbleTeaBaseTeaTypes])),
addOns: z.optional(ListOfBubbleTeaAddOns),
deliveryDate: z.optional(z.date()),
withMilk: z.optional(z.boolean()),
instructions: z.optional(co.plainText())
})
// eslint-disable-next-line @typescript-eslint/no-unused-vars
.withHelpers((Self) => ({
hasChanges(order: Loaded<typeof Self> | undefined) {
return (
!!order &&
(Object.keys(order._edits).length > 1 || ListOfBubbleTeaAddOns.hasChanges(order.addOns))
);
},
validate(order: Loaded<typeof Self>) {
const errors: string[] = [];
if (!order.baseTea) {
errors.push('Please select your preferred base tea.');
}
if (!order.deliveryDate) {
errors.push('Plese select a delivery date.');
}
return { errors };
}
}));
/** The root is an app-specific per-user private `CoMap`
* where you can store top-level objects for that user */
export const AccountRoot = co.map({
draft: DraftBubbleTeaOrder,
orders: co.list(BubbleTeaOrder)
});
export const JazzAccount = co
.account({
root: AccountRoot,
profile: co.profile()
})
.withMigration((account) => {
if (!account.root) {
const orders = co.list(BubbleTeaOrder).create([], account);
const draft = DraftBubbleTeaOrder.create(
{
addOns: ListOfBubbleTeaAddOns.create([], account),
instructions: co.plainText().create('', account)
},
account
);
account.root = AccountRoot.create({ draft, orders }, account);
}
});

View File

@@ -0,0 +1,28 @@
<script lang="ts">
import { JazzSvelteProvider } from 'jazz-tools/svelte';
import 'jazz-tools/inspector/register-custom-element';
import { PasskeyAuthBasicUI } from 'jazz-tools/svelte';
import { apiKey } from '$lib/apiKey';
import { JazzAccount } from '$lib/schema';
import '../app.css';
let { children } = $props();
</script>
<svelte:head>
<title>Form Example</title>
</svelte:head>
<JazzSvelteProvider
AccountSchema={JazzAccount}
sync={{
peer: `wss://cloud.jazz.tools/?key=${apiKey}`,
}}
>
<jazz-inspector></jazz-inspector>
<PasskeyAuthBasicUI appName="Form Example">
<div class="min-h-screen bg-gray-100">
{@render children()}
</div>
</PasskeyAuthBasicUI>
</JazzSvelteProvider>

View File

@@ -0,0 +1,22 @@
<script lang="ts">
import { page } from '$app/stores';
import CreateOrder from '$lib/components/CreateOrder.svelte';
import EditOrder from '$lib/components/EditOrder.svelte';
import Orders from '$lib/components/Orders.svelte';
// Get current route from URL hash or default to '/'
const currentRoute = $derived($page.url.hash || '#/');
const route = $derived(currentRoute.replace('#', '') || '/');
</script>
<main class="max-w-xl mx-auto px-3 py-8 space-y-8">
{#if route === '/'}
<Orders />
{:else if route === '/order'}
<CreateOrder />
{:else if route.startsWith('/order/')}
{#if route.split('/').length === 3}
<EditOrder id={route.split('/')[2]} />
{/if}
{/if}
</main>

View File

@@ -0,0 +1,13 @@
import adapter from "@sveltejs/adapter-auto";
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
/** @type {import('@sveltejs/kit').Config} */
const config = {
preprocess: vitePreprocess(),
kit: {
adapter: adapter(),
},
};
export default config;

View File

@@ -0,0 +1,14 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"moduleResolution": "bundler"
}
}

View File

@@ -0,0 +1,10 @@
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
import tailwindcss from '@tailwindcss/vite';
export default defineConfig({
plugins: [sveltekit(), tailwindcss()],
optimizeDeps: {
exclude: ['jazz-tools']
}
});

361
pnpm-lock.yaml generated
View File

@@ -129,7 +129,7 @@ importers:
version: 0.510.0(react@19.0.0)
next:
specifier: 15.3.2
version: 15.3.2(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
version: 15.3.2(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
react:
specifier: 19.0.0
version: 19.0.0
@@ -169,7 +169,7 @@ importers:
version: 19.0.0
react-email:
specifier: ^4.0.11
version: 4.0.13(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
version: 4.0.13(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
tailwindcss:
specifier: ^4
version: 4.1.10
@@ -667,6 +667,9 @@ importers:
examples/form:
dependencies:
'@tanstack/react-form':
specifier: ^0.33.0
version: 0.33.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.6.2)
hash-slash:
specifier: workspace:*
version: link:../../packages/hash-slash
@@ -720,6 +723,70 @@ importers:
specifier: 6.3.5
version: 6.3.5(@types/node@22.15.18)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.19.3)(yaml@2.6.1)
examples/form-svelte:
dependencies:
'@tailwindcss/vite':
specifier: ^4.1.7
version: 4.1.10(vite@6.3.5(@types/node@22.15.18)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.19.3)(yaml@2.6.1))
'@tanstack/svelte-form':
specifier: ^1.12.4
version: 1.12.4(svelte@5.34.6)
jazz-tools:
specifier: workspace:*
version: link:../../packages/jazz-tools
tailwindcss:
specifier: ^4.1.7
version: 4.1.10
devDependencies:
'@sveltejs/adapter-auto':
specifier: ^3.3.1
version: 3.3.1(@sveltejs/kit@2.21.5(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.34.6)(vite@6.3.5(@types/node@22.15.18)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.19.3)(yaml@2.6.1)))(svelte@5.34.6)(vite@6.3.5(@types/node@22.15.18)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.19.3)(yaml@2.6.1)))
'@sveltejs/kit':
specifier: ^2.21.1
version: 2.21.5(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.34.6)(vite@6.3.5(@types/node@22.15.18)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.19.3)(yaml@2.6.1)))(svelte@5.34.6)(vite@6.3.5(@types/node@22.15.18)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.19.3)(yaml@2.6.1))
'@sveltejs/vite-plugin-svelte':
specifier: ^5.0.3
version: 5.1.0(svelte@5.34.6)(vite@6.3.5(@types/node@22.15.18)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.19.3)(yaml@2.6.1))
'@tailwindcss/postcss':
specifier: ^4.1.10
version: 4.1.10
'@types/eslint':
specifier: ^9.6.1
version: 9.6.1
eslint:
specifier: ^9.27.0
version: 9.28.0(jiti@2.4.2)
eslint-config-prettier:
specifier: ^9.1.0
version: 9.1.0(eslint@9.28.0(jiti@2.4.2))
eslint-plugin-svelte:
specifier: ^2.46.1
version: 2.46.1(eslint@9.28.0(jiti@2.4.2))(svelte@5.34.6)(ts-node@10.9.2(@swc/core@1.11.29(@swc/helpers@0.5.17))(@types/node@22.15.18)(typescript@5.6.2))
globals:
specifier: ^15.15.0
version: 15.15.0
prettier:
specifier: ^3.5.3
version: 3.5.3
prettier-plugin-svelte:
specifier: ^3.4.0
version: 3.4.0(prettier@3.5.3)(svelte@5.34.6)
svelte:
specifier: ^5.31.1
version: 5.34.6
svelte-check:
specifier: ^4.2.1
version: 4.2.1(picomatch@4.0.2)(svelte@5.34.6)(typescript@5.6.2)
typescript:
specifier: 5.6.2
version: 5.6.2
typescript-eslint:
specifier: ^8.32.1
version: 8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.6.2)
vite:
specifier: 6.3.5
version: 6.3.5(@types/node@22.15.18)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.19.3)(yaml@2.6.1)
examples/image-upload:
dependencies:
jazz-tools:
@@ -825,7 +892,7 @@ importers:
version: link:../../packages/jazz-tools
next:
specifier: 15.3.2
version: 15.3.2(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
version: 15.3.2(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
react:
specifier: 19.0.0
version: 19.0.0
@@ -5282,10 +5349,48 @@ packages:
'@remirror/core-constants@3.0.0':
resolution: {integrity: sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==}
'@remix-run/node@2.16.8':
resolution: {integrity: sha512-foeYXU3mdaBJZnbtGbM8mNdHowz2+QnVGDRo7P3zgFkmsccMEflArGZNbkACGKd9xwDguTxxMJ6cuXBC4jIfgQ==}
engines: {node: '>=18.0.0'}
peerDependencies:
typescript: ^5.1.0
peerDependenciesMeta:
typescript:
optional: true
'@remix-run/router@1.21.0':
resolution: {integrity: sha512-xfSkCAchbdG5PnbrKqFWwia4Bi61nH+wm8wLEqfHDyp7Y3dZzgqS2itV8i4gAq9pC2HsTpwyBC6Ds8VHZ96JlA==}
engines: {node: '>=14.0.0'}
'@remix-run/router@1.23.0':
resolution: {integrity: sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==}
engines: {node: '>=14.0.0'}
'@remix-run/server-runtime@2.16.8':
resolution: {integrity: sha512-ZwWOam4GAQTx10t+wK09YuYctd2Koz5Xy/klDgUN3lmTXmwbV0tZU0baiXEqZXrvyD+WDZ4b0ADDW9Df3+dpzA==}
engines: {node: '>=18.0.0'}
peerDependencies:
typescript: ^5.1.0
peerDependenciesMeta:
typescript:
optional: true
'@remix-run/web-blob@3.1.0':
resolution: {integrity: sha512-owGzFLbqPH9PlKb8KvpNJ0NO74HWE2euAn61eEiyCXX/oteoVzTVSN8mpLgDjaxBf2btj5/nUllSUgpyd6IH6g==}
'@remix-run/web-fetch@4.4.2':
resolution: {integrity: sha512-jgKfzA713/4kAW/oZ4bC3MoLWyjModOVDjFPNseVqcJKSafgIscrYL9G50SurEYLswPuoU3HzSbO0jQCMYWHhA==}
engines: {node: ^10.17 || >=12.3}
'@remix-run/web-file@3.1.0':
resolution: {integrity: sha512-dW2MNGwoiEYhlspOAXFBasmLeYshyAyhIdrlXBi06Duex5tDr3ut2LFKVj7tyHLmn8nnNwFf1BjNbkQpygC2aQ==}
'@remix-run/web-form-data@3.1.0':
resolution: {integrity: sha512-NdeohLMdrb+pHxMQ/Geuzdp0eqPbea+Ieo8M8Jx2lGC6TBHsgHzYcBvr0LyPdPVycNRDEpWpiDdCOdCryo3f9A==}
'@remix-run/web-stream@1.1.0':
resolution: {integrity: sha512-KRJtwrjRV5Bb+pM7zxcTJkhIqWWSy+MYsIxHK+0m5atcznsf15YwUBWHWulZerV2+vvHH1Lp1DD7pw6qKW8SgA==}
'@rnx-kit/console@2.0.0':
resolution: {integrity: sha512-8kyM0TUWpawWlaypHKWLgJjPoF5HcsjIxx9LSnYM8VgwBW1gOl2cCDhPYaeS8IG9m0fi4rMtQD8OJqmwpJ2jEg==}
engines: {node: '>=16.17'}
@@ -5776,10 +5881,25 @@ packages:
peerDependencies:
vite: 6.3.5
'@tanstack/form-core@0.33.0':
resolution: {integrity: sha512-ouu1JVwLZgfPkIdIq3TWIZs++KpLOo8Bx4tXYOc/1KwA9qUQ0Iv/+Y6GfiC4wMcUzGCtLKOww84eZ8Qdge3ZmQ==}
'@tanstack/form-core@1.12.4':
resolution: {integrity: sha512-BhfNI5sEjI68Im1Vqezf9w68fJL4EB80cqW5w0zb/MV1erHHsXNRwLGmljF88VnCx1t/xd4fmF0D08wNajBauQ==}
'@tanstack/history@1.115.0':
resolution: {integrity: sha512-K7JJNrRVvyjAVnbXOH2XLRhFXDkeP54Kt2P4FR1Kl2KDGlIbkua5VqZQD2rot3qaDrpufyUa63nuLai1kOLTsQ==}
engines: {node: '>=12'}
'@tanstack/react-form@0.33.0':
resolution: {integrity: sha512-+292gWfP67uAu3zYeIQAQYEk6T7IvQUiuKwanCyATROG92FsxQrYeJdqDk4Bkh0MB8uy13CilrdxZOoEMdEKSQ==}
peerDependencies:
'@tanstack/start': ^1.43.13
react: 19.0.0
peerDependenciesMeta:
'@tanstack/start':
optional: true
'@tanstack/react-router-devtools@1.116.0':
resolution: {integrity: sha512-PsJZWPjcmwZGe71kUvH4bI1ozkv1FgBuBEE0hTYlTCSJ3uG+qv3ndGEI+AiFyuF5OStrbfg0otW1OxeNq5vdGQ==}
engines: {node: '>=12'}
@@ -5795,6 +5915,12 @@ packages:
react: 19.0.0
react-dom: 19.0.0
'@tanstack/react-store@0.5.8':
resolution: {integrity: sha512-G8TFpT/QJv2B2vro4QOUypvMjjSrbLRR8VKNoBByr6gpXAhU7y7mkrsn55Ra6svplVuvF+rAPTvHcUJKtU6geQ==}
peerDependencies:
react: 19.0.0
react-dom: 19.0.0
'@tanstack/react-store@0.7.0':
resolution: {integrity: sha512-S/Rq17HaGOk+tQHV/yrePMnG1xbsKZIl/VsNWnNXt4XW+tTY8dTlvpJH2ZQ3GRALsusG5K6Q3unAGJ2pd9W/Ng==}
peerDependencies:
@@ -5851,9 +5977,25 @@ packages:
resolution: {integrity: sha512-Dng4y+uLR9b5zPGg7dHReHOTHQa6x+G6nCoZshsDtWrYsrdCcJEtLyhwZ5wG8OyYS6dVr/Cn+E5Bd2b6BhJ89w==}
engines: {node: '>=12'}
'@tanstack/store@0.5.5':
resolution: {integrity: sha512-EOSrgdDAJExbvRZEQ/Xhh9iZchXpMN+ga1Bnk8Nmygzs8TfiE6hbzThF+Pr2G19uHL6+DTDTHhJ8VQiOd7l4tA==}
'@tanstack/store@0.7.0':
resolution: {integrity: sha512-CNIhdoUsmD2NolYuaIs8VfWM467RK6oIBAW4nPEKZhg1smZ+/CwtCdpURgp7nxSqOaV9oKkzdWD80+bC66F/Jg==}
'@tanstack/store@0.7.1':
resolution: {integrity: sha512-PjUQKXEXhLYj2X5/6c1Xn/0/qKY0IVFxTJweopRfF26xfjVyb14yALydJrHupDh3/d+1WKmfEgZPBVCmDkzzwg==}
'@tanstack/svelte-form@1.12.4':
resolution: {integrity: sha512-as8J3wu+GZUIeG1DZSEibF81D8GoalCJvag/OiOjeO28aHZEL3fExtCltzJkCE5odQCrL0QmNUreQ/P3gE7rEA==}
peerDependencies:
svelte: ^5.0.0
'@tanstack/svelte-store@0.7.1':
resolution: {integrity: sha512-09GSf96YCmwquWVqLbbX0VpO+bXdJFqzxTC7gVR6Q/uqd+3mwZnC3ORVMi25tFQDKNrbAtAzhpEwzOgSUobeyQ==}
peerDependencies:
svelte: ^5.0.0
'@tanstack/virtual-file-routes@1.115.0':
resolution: {integrity: sha512-XLUh1Py3AftcERrxkxC5Y5m5mfllRH3YR6YVlyjFgI2Tc2Ssy2NKmQFQIafoxfW459UJ8Dn81nWKETEIJifE4g==}
engines: {node: '>=12'}
@@ -6598,6 +6740,9 @@ packages:
resolution: {integrity: sha512-0TcTjBiax1VxtJQ/iQA0ZyYOSHjjX2ARVmEI0AMo9+AuIq+xBfnY561+v8k9GqOMPKsiH/HrK3xwjx8xCVS03g==}
engines: {node: ^16.13 || >=18}
'@web3-storage/multipart-parser@1.0.0':
resolution: {integrity: sha512-BEO6al7BYqcnfX15W2cnGR+Q566ACXAT9UQykORCWW80lmkpWsnEob6zJS1ZVBKsSJC8+7vJkHwlp+lXG1UCdw==}
'@xmldom/xmldom@0.7.13':
resolution: {integrity: sha512-lm2GW5PkosIzccsaZIz7tp8cPADSIlIHWDFTR1N0SzfinhhYgeIQjFMz4rYzanCScr3DqQLeomUDArp6MWKm+g==}
engines: {node: '>=10.0.0'}
@@ -6635,6 +6780,9 @@ packages:
'@zxcvbn-ts/language-common@3.0.4':
resolution: {integrity: sha512-viSNNnRYtc7ULXzxrQIVUNwHAPSXRtoIwy/Tq4XQQdIknBzw4vz36lQLF6mvhMlTIlpjoN/Z1GFu/fwiAlUSsw==}
'@zxing/text-encoding@0.9.0':
resolution: {integrity: sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==}
abbrev@2.0.0:
resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
@@ -7394,6 +7542,10 @@ packages:
convert-source-map@2.0.0:
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
cookie-signature@1.2.2:
resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==}
engines: {node: '>=6.6.0'}
cookie@0.5.0:
resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==}
engines: {node: '>= 0.6'}
@@ -7511,6 +7663,10 @@ packages:
data-uri-to-buffer@2.0.2:
resolution: {integrity: sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==}
data-uri-to-buffer@3.0.1:
resolution: {integrity: sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og==}
engines: {node: '>= 6'}
data-uri-to-buffer@4.0.1:
resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==}
engines: {node: '>= 12'}
@@ -7587,6 +7743,9 @@ packages:
decimal.js@10.4.3:
resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==}
decode-formdata@0.8.0:
resolution: {integrity: sha512-iUzDgnWsw5ToSkFY7VPFA5Gfph6ROoOxOB7Ybna4miUSzLZ4KaSJk6IAB2AdW6+C9vCVWhjjNA4gjT6wF3eZHQ==}
decode-uri-component@0.2.2:
resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==}
engines: {node: '>=0.10'}
@@ -10012,6 +10171,10 @@ packages:
resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==}
engines: {node: '>=4'}
mrmime@1.0.1:
resolution: {integrity: sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==}
engines: {node: '>=10'}
mrmime@2.0.0:
resolution: {integrity: sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==}
engines: {node: '>=10'}
@@ -11747,6 +11910,10 @@ packages:
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
engines: {node: '>=0.10.0'}
source-map@0.7.4:
resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==}
engines: {node: '>= 8'}
source-map@0.8.0-beta.0:
resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==}
engines: {node: '>= 8'}
@@ -11847,6 +12014,9 @@ packages:
resolution: {integrity: sha512-uyQK/mx5QjHun80FLJTfaWE7JtwfRMKBLkMne6udYOmvH0CawotVa7TfgYHzAnpphn4+TweIx1QKMnRIbipmUg==}
engines: {node: '>= 0.10.0'}
stream-slice@0.1.2:
resolution: {integrity: sha512-QzQxpoacatkreL6jsxnVb7X5R/pGw9OUv2qWTYWnmLpg4NdN31snPy/f3TdQE1ZUXaThRvj1Zw4/OGg0ZkaLMA==}
streamsearch@1.1.0:
resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==}
engines: {node: '>=10.0.0'}
@@ -12349,6 +12519,9 @@ packages:
cpu: [arm64]
os: [linux]
turbo-stream@2.4.1:
resolution: {integrity: sha512-v8kOJXpG3WoTN/+at8vK7erSzo6nW6CIaeOvNOkHQVDajfz1ZVeSxCbc6tOH4hrGZW7VUCV0TOXd8CPzYnYkrw==}
turbo-windows-64@2.3.3:
resolution: {integrity: sha512-O2+BS4QqjK3dOERscXqv7N2GXNcqHr9hXumkMxDj/oGx9oCatIwnnwx34UmzodloSnJpgSqjl8iRWiY65SmYoQ==}
cpu: [x64]
@@ -12481,6 +12654,10 @@ packages:
resolution: {integrity: sha512-BUgJXc752Kou3oOIuU1i+yZZypyZRqNPW0vqoMPl8VaoalSfeR0D8/t4iAS3yirs79SSMTxTag+ZC86uswv+Cw==}
engines: {node: '>=18.17'}
undici@6.21.3:
resolution: {integrity: sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==}
engines: {node: '>=18.17'}
unenv@2.0.0-rc.1:
resolution: {integrity: sha512-PU5fb40H8X149s117aB4ytbORcCvlASdtF97tfls4BPIyj4PeVxvpSuy1jAptqYHqB0vb2w2sHvzM0XWcp2OKg==}
@@ -12814,6 +12991,9 @@ packages:
wcwidth@1.0.1:
resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==}
web-encoding@1.1.5:
resolution: {integrity: sha512-HYLeVCdJ0+lBYV2FvNZmv3HJ2Nt0QYXqZojk3d9FJOLkwnuhzM9tmamh8d7HPM8QqjKH8DeHkFTx+CFlWpZZDA==}
web-streams-polyfill@3.3.3:
resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
engines: {node: '>= 8'}
@@ -17302,8 +17482,62 @@ snapshots:
'@remirror/core-constants@3.0.0': {}
'@remix-run/node@2.16.8(typescript@5.6.2)':
dependencies:
'@remix-run/server-runtime': 2.16.8(typescript@5.6.2)
'@remix-run/web-fetch': 4.4.2
'@web3-storage/multipart-parser': 1.0.0
cookie-signature: 1.2.2
source-map-support: 0.5.21
stream-slice: 0.1.2
undici: 6.21.3
optionalDependencies:
typescript: 5.6.2
'@remix-run/router@1.21.0': {}
'@remix-run/router@1.23.0': {}
'@remix-run/server-runtime@2.16.8(typescript@5.6.2)':
dependencies:
'@remix-run/router': 1.23.0
'@types/cookie': 0.6.0
'@web3-storage/multipart-parser': 1.0.0
cookie: 0.7.2
set-cookie-parser: 2.7.1
source-map: 0.7.4
turbo-stream: 2.4.1
optionalDependencies:
typescript: 5.6.2
'@remix-run/web-blob@3.1.0':
dependencies:
'@remix-run/web-stream': 1.1.0
web-encoding: 1.1.5
'@remix-run/web-fetch@4.4.2':
dependencies:
'@remix-run/web-blob': 3.1.0
'@remix-run/web-file': 3.1.0
'@remix-run/web-form-data': 3.1.0
'@remix-run/web-stream': 1.1.0
'@web3-storage/multipart-parser': 1.0.0
abort-controller: 3.0.0
data-uri-to-buffer: 3.0.1
mrmime: 1.0.1
'@remix-run/web-file@3.1.0':
dependencies:
'@remix-run/web-blob': 3.1.0
'@remix-run/web-form-data@3.1.0':
dependencies:
web-encoding: 1.1.5
'@remix-run/web-stream@1.1.0':
dependencies:
web-streams-polyfill: 3.3.3
'@rnx-kit/console@2.0.0': {}
'@rnx-kit/metro-config@2.0.1(@react-native/metro-config@0.80.0(@babel/core@7.27.1))(react-native@0.80.0(@babel/core@7.27.1)(@react-native-community/cli@19.0.0(typescript@5.6.2))(@types/react@19.0.0)(react@19.0.0))(react@19.0.0)':
@@ -17545,6 +17779,11 @@ snapshots:
'@sveltejs/kit': 2.21.5(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.34.1)(vite@6.3.5(@types/node@22.15.18)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.19.3)(yaml@2.6.1)))(svelte@5.34.1)(vite@6.3.5(@types/node@22.15.18)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.19.3)(yaml@2.6.1))
import-meta-resolve: 4.1.0
'@sveltejs/adapter-auto@3.3.1(@sveltejs/kit@2.21.5(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.34.6)(vite@6.3.5(@types/node@22.15.18)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.19.3)(yaml@2.6.1)))(svelte@5.34.6)(vite@6.3.5(@types/node@22.15.18)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.19.3)(yaml@2.6.1)))':
dependencies:
'@sveltejs/kit': 2.21.5(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.34.6)(vite@6.3.5(@types/node@22.15.18)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.19.3)(yaml@2.6.1)))(svelte@5.34.6)(vite@6.3.5(@types/node@22.15.18)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.19.3)(yaml@2.6.1))
import-meta-resolve: 4.1.0
'@sveltejs/adapter-auto@6.0.1(@sveltejs/kit@2.21.5(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.34.1)(vite@6.3.5(@types/node@22.15.18)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.19.3)(yaml@2.6.1)))(svelte@5.34.1)(vite@6.3.5(@types/node@22.15.18)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.19.3)(yaml@2.6.1)))':
dependencies:
'@sveltejs/kit': 2.21.5(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.34.1)(vite@6.3.5(@types/node@22.15.18)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.19.3)(yaml@2.6.1)))(svelte@5.34.1)(vite@6.3.5(@types/node@22.15.18)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.19.3)(yaml@2.6.1))
@@ -17819,8 +18058,27 @@ snapshots:
tailwindcss: 4.1.10
vite: 6.3.5(@types/node@22.15.18)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.19.3)(yaml@2.6.1)
'@tanstack/form-core@0.33.0':
dependencies:
'@tanstack/store': 0.5.5
'@tanstack/form-core@1.12.4':
dependencies:
'@tanstack/store': 0.7.1
'@tanstack/history@1.115.0': {}
'@tanstack/react-form@0.33.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.6.2)':
dependencies:
'@remix-run/node': 2.16.8(typescript@5.6.2)
'@tanstack/form-core': 0.33.0
'@tanstack/react-store': 0.5.8(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
decode-formdata: 0.8.0
react: 19.0.0
transitivePeerDependencies:
- react-dom
- typescript
'@tanstack/react-router-devtools@1.116.0(@tanstack/react-router@1.116.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@tanstack/router-core@1.115.3)(csstype@3.1.3)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(tiny-invariant@1.3.3)':
dependencies:
'@tanstack/react-router': 1.116.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
@@ -17844,6 +18102,13 @@ snapshots:
tiny-invariant: 1.3.3
tiny-warning: 1.0.3
'@tanstack/react-store@0.5.8(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
dependencies:
'@tanstack/store': 0.5.5
react: 19.0.0
react-dom: 19.0.0(react@19.0.0)
use-sync-external-store: 1.5.0(react@19.0.0)
'@tanstack/react-store@0.7.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
dependencies:
'@tanstack/store': 0.7.0
@@ -17908,8 +18173,23 @@ snapshots:
ansis: 3.17.0
diff: 7.0.0
'@tanstack/store@0.5.5': {}
'@tanstack/store@0.7.0': {}
'@tanstack/store@0.7.1': {}
'@tanstack/svelte-form@1.12.4(svelte@5.34.6)':
dependencies:
'@tanstack/form-core': 1.12.4
'@tanstack/svelte-store': 0.7.1(svelte@5.34.6)
svelte: 5.34.6
'@tanstack/svelte-store@0.7.1(svelte@5.34.6)':
dependencies:
'@tanstack/store': 0.7.1
svelte: 5.34.6
'@tanstack/virtual-file-routes@1.115.0': {}
'@testing-library/dom@10.4.0':
@@ -18820,7 +19100,7 @@ snapshots:
sirv: 3.0.1
tinyglobby: 0.2.14
tinyrainbow: 2.0.0
vitest: 3.1.3(@types/node@22.15.18)(@vitest/browser@3.1.3)(@vitest/ui@3.1.3)(happy-dom@17.4.4)(jiti@2.4.2)(jsdom@25.0.1)(lightningcss@1.30.1)(msw@2.7.0(@types/node@22.15.18)(typescript@5.6.2))(terser@5.37.0)(tsx@4.19.3)(yaml@2.6.1)
vitest: 3.1.3(@types/node@22.15.18)(@vitest/browser@3.1.3)(@vitest/ui@3.1.3)(happy-dom@17.4.4)(jiti@2.4.2)(jsdom@25.0.1)(lightningcss@1.30.1)(msw@2.7.0(@types/node@22.15.18)(typescript@5.8.3))(terser@5.37.0)(tsx@4.19.3)(yaml@2.6.1)
'@vitest/utils@3.1.1':
dependencies:
@@ -18952,6 +19232,8 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@web3-storage/multipart-parser@1.0.0': {}
'@xmldom/xmldom@0.7.13': {}
'@xmldom/xmldom@0.8.10': {}
@@ -18987,6 +19269,9 @@ snapshots:
'@zxcvbn-ts/language-common@3.0.4': {}
'@zxing/text-encoding@0.9.0':
optional: true
abbrev@2.0.0: {}
abort-controller@3.0.0:
@@ -19853,6 +20138,8 @@ snapshots:
convert-source-map@2.0.0: {}
cookie-signature@1.2.2: {}
cookie@0.5.0: {}
cookie@0.6.0: {}
@@ -19989,6 +20276,8 @@ snapshots:
data-uri-to-buffer@2.0.2: {}
data-uri-to-buffer@3.0.1: {}
data-uri-to-buffer@4.0.1: {}
data-uri-to-buffer@6.0.2: {}
@@ -20042,6 +20331,8 @@ snapshots:
decimal.js@10.4.3: {}
decode-formdata@0.8.0: {}
decode-uri-component@0.2.2: {}
decode-uri-component@0.4.1: {}
@@ -20505,6 +20796,25 @@ snapshots:
transitivePeerDependencies:
- ts-node
eslint-plugin-svelte@2.46.1(eslint@9.28.0(jiti@2.4.2))(svelte@5.34.6)(ts-node@10.9.2(@swc/core@1.11.29(@swc/helpers@0.5.17))(@types/node@22.15.18)(typescript@5.6.2)):
dependencies:
'@eslint-community/eslint-utils': 4.7.0(eslint@9.28.0(jiti@2.4.2))
'@jridgewell/sourcemap-codec': 1.5.0
eslint: 9.28.0(jiti@2.4.2)
eslint-compat-utils: 0.5.1(eslint@9.28.0(jiti@2.4.2))
esutils: 2.0.3
known-css-properties: 0.35.0
postcss: 8.5.4
postcss-load-config: 3.1.4(postcss@8.5.4)(ts-node@10.9.2(@swc/core@1.11.29(@swc/helpers@0.5.17))(@types/node@22.15.18)(typescript@5.6.2))
postcss-safe-parser: 6.0.0(postcss@8.5.4)
postcss-selector-parser: 6.1.2
semver: 7.7.2
svelte-eslint-parser: 0.43.0(svelte@5.34.6)
optionalDependencies:
svelte: 5.34.6
transitivePeerDependencies:
- ts-node
eslint-scope@5.1.1:
dependencies:
esrecurse: 4.3.0
@@ -22958,6 +23268,8 @@ snapshots:
mri@1.2.0: {}
mrmime@1.0.1: {}
mrmime@2.0.0: {}
ms@2.0.0: {}
@@ -23071,7 +23383,7 @@ snapshots:
neverthrow@7.2.0: {}
next@15.3.2(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
next@15.3.2(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
dependencies:
'@next/env': 15.3.2
'@swc/counter': 0.1.3
@@ -23081,7 +23393,7 @@ snapshots:
postcss: 8.4.31
react: 19.0.0
react-dom: 19.0.0(react@19.0.0)
styled-jsx: 5.1.6(react@19.0.0)
styled-jsx: 5.1.6(@babel/core@7.27.1)(react@19.0.0)
optionalDependencies:
'@next/swc-darwin-arm64': 15.3.2
'@next/swc-darwin-x64': 15.3.2
@@ -23650,6 +23962,11 @@ snapshots:
prettier: 3.5.3
svelte: 5.34.1
prettier-plugin-svelte@3.4.0(prettier@3.5.3)(svelte@5.34.6):
dependencies:
prettier: 3.5.3
svelte: 5.34.6
prettier-plugin-tailwindcss@0.6.9(prettier-plugin-svelte@3.4.0(prettier@3.5.3)(svelte@5.34.1))(prettier@3.5.3):
dependencies:
prettier: 3.5.3
@@ -24040,7 +24357,7 @@ snapshots:
react: 19.0.0
scheduler: 0.25.0
react-email@4.0.13(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
react-email@4.0.13(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
dependencies:
'@babel/parser': 7.27.2
'@babel/traverse': 7.27.1
@@ -24052,7 +24369,7 @@ snapshots:
glob: 11.0.2
log-symbols: 7.0.0
mime-types: 3.0.1
next: 15.3.2(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
next: 15.3.2(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
normalize-path: 3.0.0
ora: 8.2.0
socket.io: 4.8.1
@@ -25040,6 +25357,8 @@ snapshots:
source-map@0.6.1: {}
source-map@0.7.4: {}
source-map@0.8.0-beta.0:
dependencies:
whatwg-url: 7.1.0
@@ -25127,6 +25446,8 @@ snapshots:
stream-buffers@2.2.0: {}
stream-slice@0.1.2: {}
streamsearch@1.1.0: {}
streamx@2.21.1:
@@ -25272,10 +25593,12 @@ snapshots:
structured-headers@0.4.1: {}
styled-jsx@5.1.6(react@19.0.0):
styled-jsx@5.1.6(@babel/core@7.27.1)(react@19.0.0):
dependencies:
client-only: 0.0.1
react: 19.0.0
optionalDependencies:
'@babel/core': 7.27.1
stylis@4.2.0: {}
@@ -25346,6 +25669,16 @@ snapshots:
optionalDependencies:
svelte: 5.34.1
svelte-eslint-parser@0.43.0(svelte@5.34.6):
dependencies:
eslint-scope: 7.2.2
eslint-visitor-keys: 3.4.3
espree: 9.6.1
postcss: 8.5.4
postcss-scss: 4.0.9(postcss@8.5.4)
optionalDependencies:
svelte: 5.34.6
svelte-sonner@0.3.28(svelte@5.34.1):
dependencies:
svelte: 5.34.1
@@ -25714,6 +26047,8 @@ snapshots:
turbo-linux-arm64@2.3.3:
optional: true
turbo-stream@2.4.1: {}
turbo-windows-64@2.3.3:
optional: true
@@ -25842,6 +26177,8 @@ snapshots:
undici@6.21.0: {}
undici@6.21.3: {}
unenv@2.0.0-rc.1:
dependencies:
defu: 6.1.4
@@ -26293,6 +26630,12 @@ snapshots:
dependencies:
defaults: 1.0.4
web-encoding@1.1.5:
dependencies:
util: 0.12.5
optionalDependencies:
'@zxing/text-encoding': 0.9.0
web-streams-polyfill@3.3.3: {}
webdriver@8.41.0: