@thomasfosterau/effect-forms-svelte 0.1.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +137 -23
- package/dist/ClientValidate.d.ts +3 -3
- package/dist/ClientValidate.js +1 -2
- package/dist/ClientValidate.js.map +1 -1
- package/dist/EnhanceCore.d.ts +41 -10
- package/dist/EnhanceCore.js +107 -40
- package/dist/EnhanceCore.js.map +1 -1
- package/dist/EntityForm.d.ts +17 -18
- package/dist/EntityForm.js +28 -28
- package/dist/EntityForm.js.map +1 -1
- package/dist/FormStore.svelte.d.ts +25 -18
- package/dist/FormStore.svelte.js +95 -62
- package/dist/FormStore.svelte.js.map +1 -1
- package/dist/Listeners.d.ts +6 -6
- package/dist/Listeners.js +3 -2
- package/dist/Listeners.js.map +1 -1
- package/dist/SvelteKit.d.ts +6 -4
- package/dist/SvelteKit.js +5 -2
- package/dist/SvelteKit.js.map +1 -1
- package/dist/Tainted.d.ts +7 -7
- package/dist/Tainted.js +27 -39
- package/dist/Tainted.js.map +1 -1
- package/dist/_virtual/_rolldown/runtime.js +13 -0
- package/dist/kit/ActionRedirect.d.ts +22 -0
- package/dist/kit/ActionRedirect.js +54 -0
- package/dist/kit/ActionRedirect.js.map +1 -0
- package/dist/kit/CommandFailure.d.ts +52 -0
- package/dist/kit/CommandFailure.js +81 -0
- package/dist/kit/CommandFailure.js.map +1 -0
- package/dist/kit/Enhance.d.ts +30 -0
- package/dist/kit/Enhance.js +57 -0
- package/dist/kit/Enhance.js.map +1 -0
- package/dist/kit/FormAction.d.ts +132 -0
- package/dist/kit/FormAction.js +170 -0
- package/dist/kit/FormAction.js.map +1 -0
- package/dist/kit/FormStateReaders.d.ts +58 -0
- package/dist/kit/FormStateReaders.js +64 -0
- package/dist/kit/FormStateReaders.js.map +1 -0
- package/dist/kit/Pointer.d.ts +23 -0
- package/dist/kit/Pointer.js +38 -0
- package/dist/kit/Pointer.js.map +1 -0
- package/dist/kit/index.d.ts +7 -0
- package/dist/kit/index.js +7 -0
- package/package.json +23 -4
package/README.md
CHANGED
|
@@ -1,17 +1,27 @@
|
|
|
1
1
|
# @thomasfosterau/effect-forms-svelte
|
|
2
2
|
|
|
3
|
-
Svelte 5 (runes) bindings for [`@thomasfosterau/effect-forms`](
|
|
3
|
+
Svelte 5 (runes) bindings for [`@thomasfosterau/effect-forms`](https://www.npmjs.com/package/@thomasfosterau/effect-forms): a reactive form store with progressive-enhancement submission, tainted tracking, lifecycle events, and optional client-side validation against the **same Effect Schema the server uses**.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
The root entry depends only on `svelte` and the framework-neutral core, not SvelteKit, so it works in any Svelte 5 app. The [`/kit` entry](#kit) (`@thomasfosterau/effect-forms-svelte/kit`) is the SvelteKit-only half: derived form actions, JSON:API error routing and `$app/forms` glue.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
pnpm add @thomasfosterau/effect-forms-svelte @thomasfosterau/effect-forms effect svelte
|
|
11
|
+
# for the /kit entry
|
|
12
|
+
pnpm add @sveltejs/kit
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
`effect` and `svelte` are required peers. `@sveltejs/kit` is an optional peer, required by `/kit`; `@thomasfosterau/effect-jsonapi` is an optional, type-only peer of `/kit`.
|
|
6
16
|
|
|
7
17
|
## Usage
|
|
8
18
|
|
|
9
|
-
Server (`+page.server.ts` or any action) produces a `FormState` with the core's `
|
|
19
|
+
Server (`+page.server.ts` or any action) produces a `FormState` with the core's `Form.make(schema, …).validate(request)`, and returns it to the page. The component hydrates it:
|
|
10
20
|
|
|
11
21
|
```svelte
|
|
12
22
|
<script lang="ts">
|
|
13
23
|
import { createForm } from "@thomasfosterau/effect-forms-svelte";
|
|
14
|
-
import { Signup } from "
|
|
24
|
+
import { Signup } from "#lib/schemas";
|
|
15
25
|
|
|
16
26
|
let { data } = $props(); // data.form is the server FormState
|
|
17
27
|
|
|
@@ -35,24 +45,24 @@ Server (`+page.server.ts` or any action) produces a `FormState` with the core's
|
|
|
35
45
|
|
|
36
46
|
## The store
|
|
37
47
|
|
|
38
|
-
`createForm(source, options)` returns a [`FormStore`](src/FormStore.svelte.ts). `source` is a server-produced `FormState`, a `{ schema, defaults }` seed, or a [`FormBuilder`](
|
|
48
|
+
`createForm(source, options)` returns a [`FormStore`](https://github.com/thomasfosterau/effect-forms/blob/trunk/packages/svelte/src/FormStore.svelte.ts). `source` is a server-produced `FormState`, a `{ schema, defaults }` seed, or a [`FormBuilder`](https://github.com/thomasfosterau/effect-forms/blob/trunk/packages/forms/src/FormBuilder.ts) — the builder derives its schema, blank defaults, and array-item defaults for you:
|
|
39
49
|
|
|
40
50
|
```ts
|
|
41
51
|
import { createForm } from "@thomasfosterau/effect-forms-svelte";
|
|
42
|
-
import { signup } from "
|
|
52
|
+
import { signup } from "#lib/forms"; // a FormBuilder
|
|
43
53
|
|
|
44
54
|
const form = createForm(signup, { mode: { validation: "onChange", debounce: 300 } });
|
|
45
55
|
```
|
|
46
56
|
|
|
47
57
|
**Reactive state** — `data` (mutable, bind inputs to it), `errors` (merged client + server), `clientErrors` / `serverErrors` (per-source views), `message`, `constraints`, `submitting`, `delayed`, `posted`, `valid`, `canSubmit`, `isValidating`, `isSubmitted`, `isSubmitSuccessful`, `submissionAttempts`, `tainted`, `isTainted`, and (against the last submission) `changedSinceSubmit` / `hasChangedSinceSubmit`.
|
|
48
58
|
|
|
49
|
-
**Methods** — `enhance(node)` (the form action), `validate()` / `validateField(path)` (client-side, needs `schema`; also runs a builder's `refineEffect` refinements), `errorsAt(path)` / `taintedAt(path)` / `isValidatingAt(path)`, `setValue(path, v)` / `valueAt(path)`, `handleBlur(path)` / `setTouched(path)` / `isTouchedAt(path)`, `isDirtyAt(path)` / `isDefaultValueAt(path)` (persistent per-field dirty tracking — see [Field handles](#field-handles)), `setFieldValidators(path, validators)` (register a field's validators dynamically), `shouldShowError(path)` / `visibleErrorsAt(path)` (mode-aware display gating), `constraintAt(path)`, `revertToLastSubmit()`, `reset()`, `capture()` / `restore()` (SvelteKit snapshots), `setServerState(data, errors)` (apply a result received some other way), `toFormData()`.
|
|
59
|
+
**Methods** — `enhance(node)` (the form action), `validate()` / `validateField(path)` (client-side, needs `schema`; also runs a builder's `refineEffect` refinements, on the decoded values as the server does, once they decode), `errorsAt(path)` / `taintedAt(path)` / `isValidatingAt(path)`, `setValue(path, v)` / `valueAt(path)`, `handleBlur(path)` / `setTouched(path)` / `isTouchedAt(path)`, `isDirtyAt(path)` / `isDefaultValueAt(path)` (persistent per-field dirty tracking — see [Field handles](#field-handles)), `setFieldValidators(path, validators)` (register a field's validators dynamically), `shouldShowError(path)` / `visibleErrorsAt(path)` (mode-aware display gating), `constraintAt(path)`, `revertToLastSubmit()`, `reset()`, `capture()` / `restore()` (SvelteKit snapshots), `setServerState(data, errors)` (apply a result received some other way), `toFormData()`.
|
|
50
60
|
|
|
51
61
|
**Field arrays** — `append(path, value?)`, `insertItem(path, i, value?)`, `replaceItem(path, i, value)`, `removeItem(path, i)`, `clearItems(path)`, `swap(path, a, b)`, `move(path, from, to)` mutate the array at `path` immutably. `append` / `insertItem` derive a blank item by walking the bound schema's AST at `path` (any nesting depth, e.g. `authors[0].emails`), falling back to the top-level `FormBuilder` field when no schema is bound. Every index-shifting mutation (`insertItem` / `removeItem` / `swap` / `move` / `clearItems`) remaps per-index touched flags and errors so they keep following their row — `replaceItem` drops only the replaced index's own touched flag / errors.
|
|
52
62
|
|
|
53
63
|
## Field handles
|
|
54
64
|
|
|
55
|
-
`form.field(path)` (modelled on TanStack Form's `FieldApi`) returns a typed, reactive [`FieldHandle`](src/FormStore.svelte.ts) bundling everything one input needs, instead of wiring `bind:value` / `name` / `handleBlur` / `visibleErrorsAt` / `constraints` against the store separately:
|
|
65
|
+
`form.field(path)` (modelled on TanStack Form's `FieldApi`) returns a typed, reactive [`FieldHandle`](https://github.com/thomasfosterau/effect-forms/blob/trunk/packages/svelte/src/FormStore.svelte.ts) bundling everything one input needs, instead of wiring `bind:value` / `name` / `handleBlur` / `visibleErrorsAt` / `constraints` against the store separately:
|
|
56
66
|
|
|
57
67
|
```svelte
|
|
58
68
|
<script lang="ts">
|
|
@@ -68,11 +78,11 @@ const form = createForm(signup, { mode: { validation: "onChange", debounce: 300
|
|
|
68
78
|
{#each name.visibleErrors as message}<small>{message}</small>{/each}
|
|
69
79
|
```
|
|
70
80
|
|
|
71
|
-
`path` is checked against the form data's shape via `Path.FieldPath<A>` — a typo'd segment (`"authr.name"`) or a path into a field that doesn't exist is a compile error, and `field.value` is the leaf's actual type, not `unknown`.
|
|
81
|
+
`path` is checked against the form data's shape via `Path.FieldPath<A>` — a typo'd segment (`"authr.name"`) or a path into a field that doesn't exist is a compile error, and `field.value` is the leaf's actual type, not `unknown`.
|
|
72
82
|
|
|
73
83
|
`field.meta` exposes TanStack-style field state: `isBlurred` (lost focus at least once), `isTouched` (`isBlurred || isDirty`), `isDirty` (changed away from the default at some point — **persistent**, unlike `taintedAt` it does not clear on reverting to the default; only `reset()` clears it), and `isDefaultValue` (current value equals the default — flips back on revert). `isDirty` only tracks writes made through `field.value = …` / `setValue` / the array mutators (`append` / `removeItem` / `swap` / `move`) — a form that still binds `bind:value={form.data.x}` directly, bypassing `field()`, never marks that field dirty.
|
|
74
84
|
|
|
75
|
-
**Validation modes** — the `mode` option (see [`Mode`](
|
|
85
|
+
**Validation modes** — the `mode` option (see [`Mode`](https://github.com/thomasfosterau/effect-forms/blob/trunk/packages/forms/src/Mode.ts)) drives when the form validates: `onSubmit` (default), `onBlur` (call `handleBlur` from an input's blur), or `onChange` (debounced). `onBlur` / `onChange` may `autoSubmit`, turning the form into a live-saving surface. `revalidate` (`"onChange"` default / `"onBlur"` / `"never"`) governs what happens _after_ a failed post in `onSubmit` mode: fixing a field re-validates just that field, clearing its error without a resubmit. Errors are computed for the whole form; gate their display per field with `visibleErrorsAt(path)` (shown only once the field is tainted / touched, or after a submit).
|
|
76
86
|
|
|
77
87
|
**Per-field validators** — the `validators` option (or `setFieldValidators`) attaches TanStack-Form-style `{ onChange?, onBlur?, onChangeAsync?, onBlurAsync?, asyncDebounceMs? }` per field, keyed by `Path.toString` (e.g. `"author.name"`, `"tags[0]"`). Sync validators return a `Schema.FilterOutput`; async ones are `Effect`s, run as a fiber per field — a new value interrupts the field's in-flight fiber before forking the next run, which is what gives the debounce + "latest wins" cancellation (no timer bookkeeping needed). Pass `layer` when a validator (or a builder's `refineEffect` refinement) needs services on its `R` channel:
|
|
78
88
|
|
|
@@ -93,23 +103,127 @@ const form = createForm(
|
|
|
93
103
|
|
|
94
104
|
**Error sources** — a field's server-reported error (from the last submission) survives a client-side revalidation of some _other_ field; it clears once _that_ field's own value changes (or the server clears it on the next submit). `errors` is the merged view; `clientErrors` / `serverErrors` expose the two trees separately for a UI that wants to style them differently.
|
|
95
105
|
|
|
96
|
-
**Lifecycle options** — `onSubmit` (with `cancel()`), `onResult`, `onUpdate`, `onUpdated`, `onError`, `onRedirect`, plus `resetForm`, `taintedMessage`, `delayMs`, and a `submit` transport override (defaults to `fetch` to the form's action, parsing a JSON [`FormActionResult`](
|
|
106
|
+
**Lifecycle options** — `onSubmit` (with `cancel()`), `onResult`, `onUpdate`, `onUpdated`, `onError`, `onRedirect`, plus `resetForm`, `taintedMessage`, `delayMs`, and a `submit` transport override (defaults to `fetch` to the form's action, parsing a JSON [`FormActionResult`](https://github.com/thomasfosterau/effect-forms/blob/trunk/packages/forms/src/Result.ts) or a bare `FormState`).
|
|
97
107
|
|
|
98
|
-
##
|
|
108
|
+
## API
|
|
99
109
|
|
|
100
|
-
|
|
110
|
+
Everything below is exported flat from the root entry (`@thomasfosterau/effect-forms-svelte`).
|
|
101
111
|
|
|
102
|
-
| Module
|
|
103
|
-
|
|
|
104
|
-
| [`
|
|
105
|
-
| [`
|
|
106
|
-
| [`
|
|
112
|
+
| Module | Exports |
|
|
113
|
+
| ------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
114
|
+
| [`FormStore`](https://github.com/thomasfosterau/effect-forms/blob/trunk/packages/svelte/src/FormStore.svelte.ts) | `createForm`, `FormStore`, `FieldHandle`, `FieldMeta`, `FormOptions`, `FormSeed` — the reactive store above. |
|
|
115
|
+
| [`EntityForm`](https://github.com/thomasfosterau/effect-forms/blob/trunk/packages/svelte/src/EntityForm.ts) | `defineEntityForm` — an ordered, key-checked field list over a core `Command.plan`, walked by one generic renderer component; its entry types (`ScalarEntry`, `SelectEntry`, `WidgetEntry`, …). |
|
|
116
|
+
| [`SvelteKit`](https://github.com/thomasfosterau/effect-forms/blob/trunk/packages/svelte/src/SvelteKit.ts) | `applyActionResult` — apply a SvelteKit `ActionResult` (`ServerActionResult`) to a store; returns whether it consumed a validation failure. |
|
|
117
|
+
| [`Listeners`](https://github.com/thomasfosterau/effect-forms/blob/trunk/packages/svelte/src/Listeners.ts) | `changeEvents` / `dispatchChange` and the `FormListeners` / `FieldListener` types — per-field and form-wide change listeners (linked fields). |
|
|
118
|
+
| [`Tainted`](https://github.com/thomasfosterau/effect-forms/blob/trunk/packages/svelte/src/Tainted.ts) | Deep-diff the data to compute the tainted tree. |
|
|
119
|
+
| [`ClientValidate`](https://github.com/thomasfosterau/effect-forms/blob/trunk/packages/svelte/src/ClientValidate.ts) | `validateData` / `validateField` — validate the form, or one field, against the schema. |
|
|
120
|
+
| [`EnhanceCore`](https://github.com/thomasfosterau/effect-forms/blob/trunk/packages/svelte/src/EnhanceCore.ts) | Serialise data → form body, parse the response, compute the store update. |
|
|
107
121
|
|
|
108
|
-
The store itself is
|
|
122
|
+
The reactive store (`FormStore.svelte.ts`, rune-compiled) delegates its logic to the pure, unit-tested modules. The store itself is type-checked (by `vp check` and by `svelte-check`); its submitter-aware submit path (`enhance` → the posted `FormData`) is unit-tested in [`FormStore.svelte.test.ts`](https://github.com/thomasfosterau/effect-forms/blob/trunk/packages/svelte/tests/FormStore.svelte.test.ts).
|
|
109
123
|
|
|
110
|
-
##
|
|
124
|
+
## /kit
|
|
111
125
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
126
|
+
`@thomasfosterau/effect-forms-svelte/kit` holds the SvelteKit form-action glue: the derived page action, the JSON:API error routing, the idempotent create retry, the `$app/forms` enhance, the `PageProps["form"]` readers, and failure routing for action-only routes. The root entry (`@thomasfosterau/effect-forms-svelte`) stays framework-neutral Svelte; only `/kit` imports `@sveltejs/kit` and `$app/forms`.
|
|
127
|
+
|
|
128
|
+
| Export | What it does |
|
|
129
|
+
| ---------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
|
|
130
|
+
| `formAction(form, run, locationFor, options?)` | A page action: validate, run the command, then `redirect(303)`, `fail(status, { form })` or `fail(500, { message })` |
|
|
131
|
+
| `runFormAction(form, request, run, locationFor, options?)` | The same, as a framework-neutral `Result.FormActionResult` |
|
|
132
|
+
| `fieldFromPointer(pointer)` | `/data/attributes/<field>…` → `Option<field>` |
|
|
133
|
+
| `idempotentCreate(create, fetchExisting)` | A 409 on the caller's own client id resolves as success |
|
|
134
|
+
| `CommandFailure.*`, `jsonapiFailure` | Classify a command failure into routed JSON:API errors or a root message |
|
|
135
|
+
| `enhanceForm(store)` | Spreadable `<form>` props on `$app/forms` `enhance` |
|
|
136
|
+
| `formStateOf` / `issuesFor` / `rootIssuesOf` / `valueOf` | Total readers over `PageProps["form"]` |
|
|
137
|
+
| `userFixableMessage` / `withFailureParams` | Redirect-with-query failure routing for routes that have no page |
|
|
138
|
+
|
|
139
|
+
### Peers
|
|
140
|
+
|
|
141
|
+
- `@sveltejs/kit` (`^3.0.0`) is an **optional** peer of the package and **required** by `/kit`. SvelteKit 2 is not supported. The repository's tests run against the SvelteKit version pinned in its `pnpm-workspace.yaml`.
|
|
142
|
+
- `$app/forms` is a SvelteKit virtual module, not a dependency. It resolves because SvelteKit compiles this package (its `exports` carry the `svelte` condition), so `/kit` only works inside a SvelteKit build.
|
|
143
|
+
- `@thomasfosterau/effect-jsonapi` (`>=0.16.0`) is an **optional** peer used **for its types only**. `CommandFailure.ErrorObject` is `Document.ErrorObject.Type`. At runtime a failure is recognised by its `_tag`, so an application's own `JsonapiError` with the same shape is recognised too.
|
|
144
|
+
|
|
145
|
+
### Commands and failures
|
|
146
|
+
|
|
147
|
+
A command is `(input, event) => Promise<O> | Effect<O, E>`. An Effect command must have no requirements left. Provide them first, e.g. with the application's `ManagedRuntime`. `locationFor(output, input)` builds the redirect, and must return a same-origin path such as `/organisations/${id}`. SvelteKit 3's `redirect` rejects an external location unless it is given `{ external }`, which `formAction` never passes, so an absolute URL on another origin (or a protocol-relative `//host/...`) makes the action throw an `Error` after the command has run. To send the user off-site, redirect to a same-origin route that performs the external redirect itself. `runFormAction` returns the location unchecked.
|
|
148
|
+
|
|
149
|
+
`jsonapiFailure`, the default classifier, recognises:
|
|
150
|
+
|
|
151
|
+
- `{ _tag: "JsonapiError", status, errors }`: effect-jsonapi's `ServerErrors.JsonapiError`, or any error of the same shape. Each error object goes to the field its `source.pointer` names, or to the root (`form.errors._errors`), at the cause's own status.
|
|
152
|
+
- `{ _tag: "ErrorResponse", status, message, document }`: effect-jsonapi's client-side decoded error. The document's error objects are routed as above. With no document, the message goes to the root.
|
|
153
|
+
- An `Error` with a numeric `status`, such as a transport's 413: its message goes to the root.
|
|
154
|
+
|
|
155
|
+
Anything else is unexpected. By default it becomes `fail(500, { message })`. Pass `unrecognised: "throw"` to hand it to `handleError` instead. `classify` replaces the classifier; compose with `CommandFailure.jsonapiFailure` to extend it. `redact` runs on every state echoed back to the browser. `idempotent: { clientId, fetchExisting }` turns on the client-id retry.
|
|
156
|
+
|
|
157
|
+
### Worked example: a login action
|
|
158
|
+
|
|
159
|
+
A hand-written sign-in action typically hand-rolls validation, password blanking, 401 → generic message, a rethrow for anything else, and a same-origin redirect. With `/kit` it becomes:
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
// src/routes/login/+page.server.ts
|
|
163
|
+
import { Form, State } from "@thomasfosterau/effect-forms";
|
|
164
|
+
import { CommandFailure, formAction } from "@thomasfosterau/effect-forms-svelte/kit";
|
|
165
|
+
import * as Option from "effect/Option";
|
|
166
|
+
import * as Schema from "effect/Schema";
|
|
167
|
+
import { SessionCallError } from "#lib/server/api/binding";
|
|
168
|
+
import { safeRedirect } from "#lib/redirects";
|
|
169
|
+
|
|
170
|
+
const SignInFields = Schema.Struct({
|
|
171
|
+
email: Schema.String,
|
|
172
|
+
_password: Schema.String,
|
|
173
|
+
redirectTo: Schema.optional(Schema.String),
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const signInForm = Form.make(SignInFields, {
|
|
177
|
+
defaults: { email: "", _password: "", redirectTo: undefined },
|
|
178
|
+
id: "sign-in",
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
type SignIn = typeof SignInFields.Type;
|
|
182
|
+
|
|
183
|
+
/** Blank the password before a state is echoed back to the browser. */
|
|
184
|
+
const blankPassword = (state: State.FormState<SignIn>): State.FormState<SignIn> => ({
|
|
185
|
+
...state,
|
|
186
|
+
data: { ...state.data, _password: "" },
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
/** A 401 → one generic whole-form message (never reveal whether the email exists). */
|
|
190
|
+
const classify: CommandFailure.Classify = (cause) =>
|
|
191
|
+
cause instanceof SessionCallError && cause.status === 401
|
|
192
|
+
? Option.some(CommandFailure.message(401, "Invalid email or password"))
|
|
193
|
+
: Option.none();
|
|
194
|
+
|
|
195
|
+
export const actions = {
|
|
196
|
+
default: formAction(
|
|
197
|
+
signInForm,
|
|
198
|
+
(input, { locals }) =>
|
|
199
|
+
locals.session.signIn({ email: input.email, password: input._password, rememberMe: true }),
|
|
200
|
+
(_, input) => safeRedirect(input.redirectTo, "/"),
|
|
201
|
+
{ classify, redact: blankPassword, unrecognised: "throw" },
|
|
202
|
+
),
|
|
203
|
+
};
|
|
115
204
|
```
|
|
205
|
+
|
|
206
|
+
The runnable copy is [`packages/svelte/tests/kit/examples/login.page.server.ts`](https://github.com/thomasfosterau/effect-forms/blob/trunk/packages/svelte/tests/kit/examples/login.page.server.ts). [`login.example.test.ts`](https://github.com/thomasfosterau/effect-forms/blob/trunk/packages/svelte/tests/kit/login.example.test.ts) runs it against the real `@sveltejs/kit`, covering the success redirect, the off-site `redirectTo` guard, the 401 with a blanked password, and the rethrow.
|
|
207
|
+
|
|
208
|
+
The page renders server state without a client store:
|
|
209
|
+
|
|
210
|
+
```svelte
|
|
211
|
+
<script lang="ts">
|
|
212
|
+
import { formStateOf, issuesFor, rootIssuesOf, valueOf } from "@thomasfosterau/effect-forms-svelte/kit";
|
|
213
|
+
let { form } = $props();
|
|
214
|
+
const state = $derived(formStateOf(form));
|
|
215
|
+
</script>
|
|
216
|
+
|
|
217
|
+
<form method="post">
|
|
218
|
+
{#each rootIssuesOf(state) as message}<p role="alert">{message}</p>{/each}
|
|
219
|
+
<input name="email" value={valueOf(state, "email")} />
|
|
220
|
+
{#each issuesFor(state, "email") as message}<small>{message}</small>{/each}
|
|
221
|
+
<input name="_password" type="password" />
|
|
222
|
+
</form>
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
With a client store, spread `enhanceForm(form)` onto the `<form>` instead.
|
|
226
|
+
|
|
227
|
+
## License
|
|
228
|
+
|
|
229
|
+
[MIT](https://github.com/thomasfosterau/effect-forms/blob/trunk/packages/svelte/LICENSE)
|
package/dist/ClientValidate.d.ts
CHANGED
|
@@ -5,14 +5,14 @@ type FormErrors = Errors.FormErrors;
|
|
|
5
5
|
type FormPath = Issues.FormPath;
|
|
6
6
|
type LeafFormatter = Issues.LeafFormatter;
|
|
7
7
|
/** Validate the whole form's data, returning the nested error tree (empty when valid). */
|
|
8
|
-
declare function validateData<S extends Schema.Top>(schema: S, data: unknown, format?: LeafFormatter): FormErrors;
|
|
8
|
+
export declare function validateData<S extends Schema.Top>(schema: S, data: unknown, format?: LeafFormatter): FormErrors;
|
|
9
9
|
/**
|
|
10
10
|
* Validate the whole form and return just the messages at `path`. Validating
|
|
11
11
|
* the whole object (rather than the leaf in isolation) keeps cross-field rules
|
|
12
12
|
* working; the caller merges the result into its existing error tree for that
|
|
13
13
|
* field only.
|
|
14
14
|
*/
|
|
15
|
-
declare function validateField<S extends Schema.Top>(schema: S, data: unknown, path: FormPath, format?: LeafFormatter): readonly string[];
|
|
15
|
+
export declare function validateField<S extends Schema.Top>(schema: S, data: unknown, path: FormPath, format?: LeafFormatter): readonly string[];
|
|
16
16
|
//#endregion
|
|
17
|
-
export {
|
|
17
|
+
export {};
|
|
18
18
|
//# sourceMappingURL=ClientValidate.d.ts.map
|
package/dist/ClientValidate.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { Errors, Validate } from "@thomasfosterau/effect-forms";
|
|
2
2
|
//#region src/ClientValidate.ts
|
|
3
3
|
/**
|
|
4
|
-
* @copyright Thomas Foster
|
|
5
|
-
*
|
|
6
4
|
* Client-side validation helpers — validate the whole form or a single field
|
|
7
5
|
* against the same Effect Schema the server uses, returning the nested
|
|
8
6
|
* {@link FormErrors} tree (or one field's messages).
|
|
@@ -12,6 +10,7 @@ import { Errors, Validate } from "@thomasfosterau/effect-forms";
|
|
|
12
10
|
* have no decoding-service requirements (the common case for form schemas) so
|
|
13
11
|
* validation can run synchronously in an event handler.
|
|
14
12
|
*
|
|
13
|
+
* @copyright Thomas Foster
|
|
15
14
|
* @module effect-forms-svelte/ClientValidate
|
|
16
15
|
*/
|
|
17
16
|
const { getError, noErrors } = Errors;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ClientValidate.js","names":[],"sources":["../src/ClientValidate.ts"],"sourcesContent":["/**\n *
|
|
1
|
+
{"version":3,"file":"ClientValidate.js","names":[],"sources":["../src/ClientValidate.ts"],"sourcesContent":["/**\n * Client-side validation helpers — validate the whole form or a single field\n * against the same Effect Schema the server uses, returning the nested\n * {@link FormErrors} tree (or one field's messages).\n *\n * Re-running the schema in the browser gives instant feedback; this is\n * the framework-neutral plumbing the Svelte store calls. Schemas used here must\n * have no decoding-service requirements (the common case for form schemas) so\n * validation can run synchronously in an event handler.\n *\n * @copyright Thomas Foster\n * @module effect-forms-svelte/ClientValidate\n */\n\nimport { Errors, Issues, Validate } from \"@thomasfosterau/effect-forms\";\n\n// Terse local aliases onto the namespaced core exports.\ntype FormErrors = Errors.FormErrors;\ntype FormPath = Issues.FormPath;\ntype LeafFormatter = Issues.LeafFormatter;\nconst { getError, noErrors } = Errors;\nconst { validateValueSync } = Validate;\nimport type * as Schema from \"effect/Schema\";\n\n/** Validate the whole form's data, returning the nested error tree (empty when valid). */\nexport function validateData<S extends Schema.Top>(\n schema: S,\n data: unknown,\n format?: LeafFormatter,\n): FormErrors {\n const result = validateValueSync(schema, data, format);\n return result.valid ? noErrors : result.errors;\n}\n\n/**\n * Validate the whole form and return just the messages at `path`. Validating\n * the whole object (rather than the leaf in isolation) keeps cross-field rules\n * working; the caller merges the result into its existing error tree for that\n * field only.\n */\nexport function validateField<S extends Schema.Top>(\n schema: S,\n data: unknown,\n path: FormPath,\n format?: LeafFormatter,\n): readonly string[] {\n const errors = validateData(schema, data, format);\n return getError(errors, path) ?? [];\n}\n"],"mappings":";;;;;;;;;;;;;;;AAoBA,MAAM,EAAE,UAAU,aAAa;AAC/B,MAAM,EAAE,sBAAsB;;AAI9B,SAAgB,aACd,QACA,MACA,QACY;CACZ,MAAM,SAAS,kBAAkB,QAAQ,MAAM,MAAM;CACrD,OAAO,OAAO,QAAQ,WAAW,OAAO;AAC1C;;;;;;;AAQA,SAAgB,cACd,QACA,MACA,MACA,QACmB;CACnB,MAAM,SAAS,aAAa,QAAQ,MAAM,MAAM;CAChD,OAAO,SAAS,QAAQ,IAAI,KAAK,CAAC;AACpC"}
|
package/dist/EnhanceCore.d.ts
CHANGED
|
@@ -2,19 +2,50 @@ import { Errors, Result } from "@thomasfosterau/effect-forms";
|
|
|
2
2
|
//#region src/EnhanceCore.d.ts
|
|
3
3
|
type FormActionResult<A, Message = unknown> = Result.FormActionResult<A, Message>;
|
|
4
4
|
type FormErrors = Errors.FormErrors;
|
|
5
|
-
/**
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Flatten a nested data object into `[name, value]` string entries using path
|
|
7
|
+
* notation.
|
|
8
|
+
*
|
|
9
|
+
* Nested objects become dotted names (`author.name`) and array items indexed
|
|
10
|
+
* names (`tags[0]`). Each leaf kind is handled as follows:
|
|
11
|
+
*
|
|
12
|
+
* - A `string`, `number`, `boolean` or `bigint` is stringified.
|
|
13
|
+
* - `null` becomes `""`.
|
|
14
|
+
* - A `Date` becomes the `YYYY-MM-DD` of its **UTC** calendar day (what
|
|
15
|
+
* `<input type="date">` submits, and what `Schema.DateFromString` decodes
|
|
16
|
+
* back to UTC midnight); the time of day is dropped, so a datetime needs its
|
|
17
|
+
* own string field. An invalid `Date` is omitted.
|
|
18
|
+
* - A `Blob` or `File` is omitted here, since a string entry can't carry it;
|
|
19
|
+
* {@link objectToFormData} keeps it.
|
|
20
|
+
* - An `undefined`, `function` or `symbol` is omitted, as `JSON.stringify`
|
|
21
|
+
* omits such object properties, so a callback or symbol left in form data
|
|
22
|
+
* never reaches the form body as `"Symbol(…)"` or function source.
|
|
23
|
+
* - Any other object contributes its own enumerable fields, so one with none
|
|
24
|
+
* (a `Map`, a `Set`, an empty object) contributes no entries.
|
|
25
|
+
*
|
|
26
|
+
* An omitted array item leaves a gap (where `JSON.stringify` would write
|
|
27
|
+
* `null`), so the remaining items keep their indices (`tags[0]`, `tags[2]`).
|
|
28
|
+
*/
|
|
29
|
+
export declare function objectToEntries(data: unknown, prefix?: string): Array<[string, string]>;
|
|
30
|
+
/**
|
|
31
|
+
* Serialise the store's data into a {@link FormData} body for submission.
|
|
32
|
+
*
|
|
33
|
+
* Names and leaves follow {@link objectToEntries}, except that a `Blob` or
|
|
34
|
+
* `File` leaf is appended as a file (a `File` keeps its name; a bare `Blob`
|
|
35
|
+
* arrives as a `File` named `"blob"`, the `FormData` default), as an
|
|
36
|
+
* `<input type="file">` would submit it, so it reaches the core's
|
|
37
|
+
* `FormData.formDataToObject` (`@thomasfosterau/effect-forms`) unchanged.
|
|
38
|
+
*/
|
|
39
|
+
export declare function objectToFormData(data: unknown): FormData;
|
|
9
40
|
/**
|
|
10
41
|
* Normalise a parsed JSON response body into a {@link FormActionResult}. The
|
|
11
42
|
* server may return a full action result (`{ _tag, … }`) or a bare
|
|
12
43
|
* {@link FormState} — a bare state maps to `Success` / `Failure` by its `valid`
|
|
13
44
|
* flag, using `status` for the failure code.
|
|
14
45
|
*/
|
|
15
|
-
declare function parseActionResult<A, Message>(body: unknown, status?: number): FormActionResult<A, Message>;
|
|
46
|
+
export declare function parseActionResult<A, Message>(body: unknown, status?: number): FormActionResult<A, Message>;
|
|
16
47
|
/** The mutation a {@link FormActionResult} implies for the store. */
|
|
17
|
-
interface FormUpdate<A, Message> {
|
|
48
|
+
export interface FormUpdate<A, Message> {
|
|
18
49
|
readonly data?: A | undefined;
|
|
19
50
|
readonly errors?: FormErrors | undefined;
|
|
20
51
|
readonly message?: Message | undefined;
|
|
@@ -27,9 +58,9 @@ interface FormUpdate<A, Message> {
|
|
|
27
58
|
* caller applies the returned fields to its reactive state and acts on
|
|
28
59
|
* `redirect` / `error`.
|
|
29
60
|
*/
|
|
30
|
-
declare function resultToUpdate<A, Message>(result: FormActionResult<A, Message>): FormUpdate<A, Message>;
|
|
61
|
+
export declare function resultToUpdate<A, Message>(result: FormActionResult<A, Message>): FormUpdate<A, Message>;
|
|
31
62
|
/** How a successful/failed submit's data should land on `data` and the tainted baseline. */
|
|
32
|
-
interface DataApplication<A> {
|
|
63
|
+
export interface DataApplication<A> {
|
|
33
64
|
/** The next value for `data`, or `undefined` to leave it untouched (an auto-submit keeps local edits). */
|
|
34
65
|
readonly data: A | undefined;
|
|
35
66
|
/** The next tainted baseline (`#initial`), or `undefined` when this isn't a successful submit. */
|
|
@@ -48,7 +79,7 @@ interface DataApplication<A> {
|
|
|
48
79
|
* `submittedData` (the snapshot actually sent), not the live value, since the
|
|
49
80
|
* user may have typed further edits that haven't been submitted yet.
|
|
50
81
|
*/
|
|
51
|
-
declare function computeDataApplication<A>(params: {
|
|
82
|
+
export declare function computeDataApplication<A>(params: {
|
|
52
83
|
readonly isSuccess: boolean;
|
|
53
84
|
readonly appliedData: A;
|
|
54
85
|
readonly initial: A;
|
|
@@ -57,5 +88,5 @@ declare function computeDataApplication<A>(params: {
|
|
|
57
88
|
readonly autoSubmit: boolean;
|
|
58
89
|
}): DataApplication<A>;
|
|
59
90
|
//#endregion
|
|
60
|
-
export {
|
|
91
|
+
export {};
|
|
61
92
|
//# sourceMappingURL=EnhanceCore.d.ts.map
|
package/dist/EnhanceCore.js
CHANGED
|
@@ -1,49 +1,119 @@
|
|
|
1
1
|
import { Result } from "@thomasfosterau/effect-forms";
|
|
2
|
+
import * as Arr from "effect/Array";
|
|
3
|
+
import * as Match from "effect/Match";
|
|
4
|
+
import * as Predicate from "effect/Predicate";
|
|
5
|
+
import * as Record from "effect/Record";
|
|
2
6
|
//#region src/EnhanceCore.ts
|
|
3
7
|
/**
|
|
4
|
-
* @copyright Thomas Foster
|
|
5
|
-
*
|
|
6
8
|
* Pure plumbing for the `enhance` action: serialise the store's data back into
|
|
7
9
|
* a form body, interpret a server response into a {@link FormActionResult}, and
|
|
8
10
|
* compute the store update a result implies. Kept framework-free and side-
|
|
9
11
|
* effect-free so it is unit-testable in isolation; the rune store
|
|
10
|
-
* (`
|
|
12
|
+
* (`FormStore.svelte.ts`) wires these to the DOM.
|
|
11
13
|
*
|
|
14
|
+
* @copyright Thomas Foster
|
|
12
15
|
* @module effect-forms-svelte/EnhanceCore
|
|
13
16
|
*/
|
|
14
17
|
const { failure, success } = Result;
|
|
15
|
-
/**
|
|
16
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Whether a value is a `Blob` (a `File` is one). Guards on `globalThis.Blob`
|
|
20
|
+
* so it holds in any runtime, including one without a global `Blob`.
|
|
21
|
+
*/
|
|
22
|
+
const isBlob = (value) => !Predicate.isUndefined(globalThis.Blob) && value instanceof Blob;
|
|
23
|
+
/**
|
|
24
|
+
* Encode a `Date` as the `YYYY-MM-DD` of its UTC calendar day, or `undefined`
|
|
25
|
+
* for an invalid `Date`. The UTC day round-trips: a `YYYY-MM-DD` string
|
|
26
|
+
* decodes to UTC midnight. Taking everything before the `T` keeps
|
|
27
|
+
* `toISOString`'s expanded form for a year outside 0000–9999
|
|
28
|
+
* (`+010000-01-01`), where a fixed 10-character slice would cut it off.
|
|
29
|
+
*/
|
|
30
|
+
const encodeDate = (value) => {
|
|
31
|
+
if (Number.isNaN(value.getTime())) return void 0;
|
|
32
|
+
const iso = value.toISOString();
|
|
33
|
+
return iso.slice(0, iso.indexOf("T"));
|
|
34
|
+
};
|
|
35
|
+
/** Append the entries for one named value to `out`, recursing into containers. */
|
|
36
|
+
function appendEntries(out, key, value) {
|
|
37
|
+
if (value === null) out.push([key, ""]);
|
|
38
|
+
else if (isBlob(value)) out.push([key, value]);
|
|
39
|
+
else if (value instanceof Date) {
|
|
40
|
+
const encoded = encodeDate(value);
|
|
41
|
+
if (encoded !== void 0) out.push([key, encoded]);
|
|
42
|
+
} else if (Array.isArray(value)) value.forEach((item, i) => appendEntries(out, `${key}[${i}]`, item));
|
|
43
|
+
else if (Predicate.isObject(value)) appendFields(out, value, key);
|
|
44
|
+
else if (Predicate.isString(value) || Predicate.isNumber(value) || Predicate.isBoolean(value) || Predicate.isBigInt(value)) out.push([key, String(value)]);
|
|
45
|
+
}
|
|
46
|
+
/** Append an object's own enumerable fields to `out`, as `prefix.key` names. */
|
|
47
|
+
function appendFields(out, data, prefix) {
|
|
48
|
+
Arr.forEach(Record.toEntries(data), ([key, value]) => appendEntries(out, prefix ? `${prefix}.${key}` : key, value));
|
|
49
|
+
}
|
|
50
|
+
/** Flatten `data` into form-body entries, `Blob` / `File` leaves included. */
|
|
51
|
+
function toBodyEntries(data, prefix) {
|
|
17
52
|
const out = [];
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
if (value === null) out.push([key, ""]);
|
|
21
|
-
else if (Array.isArray(value)) value.forEach((item, i) => out.push(...objectToEntries(item, `${key}[${i}]`)));
|
|
22
|
-
else if (typeof value === "object") out.push(...objectToEntries(value, key));
|
|
23
|
-
else out.push([key, String(value)]);
|
|
24
|
-
};
|
|
25
|
-
if (data !== null && typeof data === "object" && !Array.isArray(data)) for (const [key, value] of Object.entries(data)) push(prefix ? `${prefix}.${key}` : key, value);
|
|
26
|
-
else if (prefix) push(prefix, data);
|
|
53
|
+
if (prefix) appendEntries(out, prefix, data);
|
|
54
|
+
else if (Predicate.isObject(data)) appendFields(out, data, "");
|
|
27
55
|
return out;
|
|
28
56
|
}
|
|
29
|
-
/**
|
|
57
|
+
/**
|
|
58
|
+
* Flatten a nested data object into `[name, value]` string entries using path
|
|
59
|
+
* notation.
|
|
60
|
+
*
|
|
61
|
+
* Nested objects become dotted names (`author.name`) and array items indexed
|
|
62
|
+
* names (`tags[0]`). Each leaf kind is handled as follows:
|
|
63
|
+
*
|
|
64
|
+
* - A `string`, `number`, `boolean` or `bigint` is stringified.
|
|
65
|
+
* - `null` becomes `""`.
|
|
66
|
+
* - A `Date` becomes the `YYYY-MM-DD` of its **UTC** calendar day (what
|
|
67
|
+
* `<input type="date">` submits, and what `Schema.DateFromString` decodes
|
|
68
|
+
* back to UTC midnight); the time of day is dropped, so a datetime needs its
|
|
69
|
+
* own string field. An invalid `Date` is omitted.
|
|
70
|
+
* - A `Blob` or `File` is omitted here, since a string entry can't carry it;
|
|
71
|
+
* {@link objectToFormData} keeps it.
|
|
72
|
+
* - An `undefined`, `function` or `symbol` is omitted, as `JSON.stringify`
|
|
73
|
+
* omits such object properties, so a callback or symbol left in form data
|
|
74
|
+
* never reaches the form body as `"Symbol(…)"` or function source.
|
|
75
|
+
* - Any other object contributes its own enumerable fields, so one with none
|
|
76
|
+
* (a `Map`, a `Set`, an empty object) contributes no entries.
|
|
77
|
+
*
|
|
78
|
+
* An omitted array item leaves a gap (where `JSON.stringify` would write
|
|
79
|
+
* `null`), so the remaining items keep their indices (`tags[0]`, `tags[2]`).
|
|
80
|
+
*/
|
|
81
|
+
function objectToEntries(data, prefix = "") {
|
|
82
|
+
return toBodyEntries(data, prefix).filter((entry) => Predicate.isString(entry[1]));
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Serialise the store's data into a {@link FormData} body for submission.
|
|
86
|
+
*
|
|
87
|
+
* Names and leaves follow {@link objectToEntries}, except that a `Blob` or
|
|
88
|
+
* `File` leaf is appended as a file (a `File` keeps its name; a bare `Blob`
|
|
89
|
+
* arrives as a `File` named `"blob"`, the `FormData` default), as an
|
|
90
|
+
* `<input type="file">` would submit it, so it reaches the core's
|
|
91
|
+
* `FormData.formDataToObject` (`@thomasfosterau/effect-forms`) unchanged.
|
|
92
|
+
*/
|
|
30
93
|
function objectToFormData(data) {
|
|
31
94
|
const formData = new FormData();
|
|
32
|
-
|
|
95
|
+
Arr.forEach(toBodyEntries(data, ""), ([key, value]) => formData.append(key, value));
|
|
33
96
|
return formData;
|
|
34
97
|
}
|
|
35
|
-
/**
|
|
98
|
+
/**
|
|
99
|
+
* Whether a value looks like a {@link FormActionResult} (carries a known
|
|
100
|
+
* `_tag`). Only the tag is checked: `A` / `Message` are the caller's declared
|
|
101
|
+
* expectation of the server's response, not verified here.
|
|
102
|
+
*/
|
|
36
103
|
function isActionResult(value) {
|
|
37
|
-
return
|
|
104
|
+
return Predicate.isObjectOrArray(value) && Predicate.hasProperty(value, "_tag") && Predicate.isString(value._tag) && [
|
|
38
105
|
"Success",
|
|
39
106
|
"Failure",
|
|
40
107
|
"Redirect",
|
|
41
108
|
"Error"
|
|
42
109
|
].includes(value._tag);
|
|
43
110
|
}
|
|
44
|
-
/**
|
|
111
|
+
/**
|
|
112
|
+
* Whether a value looks like a bare {@link FormState}. Only the keys are
|
|
113
|
+
* checked: `A` / `Message` are the caller's declared expectation, not verified.
|
|
114
|
+
*/
|
|
45
115
|
function isFormState(value) {
|
|
46
|
-
return
|
|
116
|
+
return Predicate.isObjectOrArray(value) && "valid" in value && "data" in value && "errors" in value;
|
|
47
117
|
}
|
|
48
118
|
/**
|
|
49
119
|
* Normalise a parsed JSON response body into a {@link FormActionResult}. The
|
|
@@ -53,10 +123,7 @@ function isFormState(value) {
|
|
|
53
123
|
*/
|
|
54
124
|
function parseActionResult(body, status = 200) {
|
|
55
125
|
if (isActionResult(body)) return body;
|
|
56
|
-
if (isFormState(body))
|
|
57
|
-
const state = body;
|
|
58
|
-
return state.valid ? success(state, status) : failure(state, status);
|
|
59
|
-
}
|
|
126
|
+
if (isFormState(body)) return body.valid ? success(body, status) : failure(body, status);
|
|
60
127
|
return {
|
|
61
128
|
_tag: "Error",
|
|
62
129
|
status,
|
|
@@ -69,25 +136,25 @@ function parseActionResult(body, status = 200) {
|
|
|
69
136
|
* `redirect` / `error`.
|
|
70
137
|
*/
|
|
71
138
|
function resultToUpdate(result) {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
data:
|
|
75
|
-
errors:
|
|
76
|
-
message:
|
|
139
|
+
return Match.valueTags(result, {
|
|
140
|
+
Success: ({ state }) => ({
|
|
141
|
+
data: state.data,
|
|
142
|
+
errors: state.errors,
|
|
143
|
+
message: state.message,
|
|
77
144
|
valid: true
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
data:
|
|
81
|
-
errors:
|
|
82
|
-
message:
|
|
145
|
+
}),
|
|
146
|
+
Failure: ({ state }) => ({
|
|
147
|
+
data: state.data,
|
|
148
|
+
errors: state.errors,
|
|
149
|
+
message: state.message,
|
|
83
150
|
valid: false
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
error:
|
|
151
|
+
}),
|
|
152
|
+
Redirect: ({ location }) => ({ redirect: location }),
|
|
153
|
+
Error: ({ message }) => ({
|
|
154
|
+
error: message,
|
|
88
155
|
valid: false
|
|
89
|
-
}
|
|
90
|
-
}
|
|
156
|
+
})
|
|
157
|
+
});
|
|
91
158
|
}
|
|
92
159
|
/**
|
|
93
160
|
* Decide how a submit result's data applies to the store, given a defined
|
package/dist/EnhanceCore.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EnhanceCore.js","names":[],"sources":["../src/EnhanceCore.ts"],"sourcesContent":["/**\n * @copyright Thomas Foster\n *\n * Pure plumbing for the `enhance` action: serialise the store's data back into\n * a form body, interpret a server response into a {@link FormActionResult}, and\n * compute the store update a result implies. Kept framework-free and side-\n * effect-free so it is unit-testable in isolation; the rune store\n * (`superForm.svelte.ts`) wires these to the DOM.\n *\n * @module effect-forms-svelte/EnhanceCore\n */\n\nimport { Errors, Result, State } from \"@thomasfosterau/effect-forms\";\n\n// Terse local aliases onto the namespaced core exports.\ntype FormActionResult<A, Message = unknown> = Result.FormActionResult<A, Message>;\ntype FormErrors = Errors.FormErrors;\ntype FormState<A, Message = unknown> = State.FormState<A, Message>;\nconst { failure, success } = Result;\n\n/** Flatten a nested data object into `[name, value]` entries using path notation. */\nexport function objectToEntries(data: unknown, prefix = \"\"): Array<[string, string]> {\n const out: Array<[string, string]> = [];\n const push = (key: string, value: unknown): void => {\n if (value === undefined) return;\n if (value === null) {\n out.push([key, \"\"]);\n } else if (Array.isArray(value)) {\n value.forEach((item, i) => out.push(...objectToEntries(item, `${key}[${i}]`)));\n } else if (typeof value === \"object\") {\n out.push(...objectToEntries(value, key));\n } else {\n out.push([key, String(value)]);\n }\n };\n\n if (data !== null && typeof data === \"object\" && !Array.isArray(data)) {\n for (const [key, value] of Object.entries(data as Record<string, unknown>)) {\n push(prefix ? `${prefix}.${key}` : key, value);\n }\n } else if (prefix) {\n push(prefix, data);\n }\n return out;\n}\n\n/** Serialise the store's data into a {@link FormData} body for submission. */\nexport function objectToFormData(data: unknown): FormData {\n const formData = new FormData();\n for (const [key, value] of objectToEntries(data)) formData.append(key, value);\n return formData;\n}\n\n/** Whether a value looks like a {@link FormActionResult} (carries a known `_tag`). */\nfunction isActionResult(value: unknown): value is FormActionResult<unknown, unknown> {\n return (\n typeof value === \"object\" &&\n value !== null &&\n typeof (value as { _tag?: unknown })._tag === \"string\" &&\n [\"Success\", \"Failure\", \"Redirect\", \"Error\"].includes((value as { _tag: string })._tag)\n );\n}\n\n/** Whether a value looks like a bare {@link FormState}. */\nfunction isFormState(value: unknown): value is FormState<unknown, unknown> {\n return (\n typeof value === \"object\" &&\n value !== null &&\n \"valid\" in value &&\n \"data\" in value &&\n \"errors\" in value\n );\n}\n\n/**\n * Normalise a parsed JSON response body into a {@link FormActionResult}. The\n * server may return a full action result (`{ _tag, … }`) or a bare\n * {@link FormState} — a bare state maps to `Success` / `Failure` by its `valid`\n * flag, using `status` for the failure code.\n */\nexport function parseActionResult<A, Message>(\n body: unknown,\n status = 200,\n): FormActionResult<A, Message> {\n if (isActionResult(body)) return body as FormActionResult<A, Message>;\n if (isFormState(body)) {\n const state = body as FormState<A, Message>;\n return state.valid ? success(state, status) : failure(state, status);\n }\n return { _tag: \"Error\", status, message: \"Unrecognised form response\" };\n}\n\n/** The mutation a {@link FormActionResult} implies for the store. */\nexport interface FormUpdate<A, Message> {\n readonly data?: A | undefined;\n readonly errors?: FormErrors | undefined;\n readonly message?: Message | undefined;\n readonly redirect?: string | undefined;\n readonly error?: string | undefined;\n readonly valid?: boolean | undefined;\n}\n\n/**\n * Compute the store update implied by a {@link FormActionResult}. Pure: the\n * caller applies the returned fields to its reactive state and acts on\n * `redirect` / `error`.\n */\nexport function resultToUpdate<A, Message>(\n result: FormActionResult<A, Message>,\n): FormUpdate<A, Message> {\n switch (result._tag) {\n case \"Success\":\n return {\n data: result.state.data,\n errors: result.state.errors,\n message: result.state.message,\n valid: true,\n };\n case \"Failure\":\n return {\n data: result.state.data,\n errors: result.state.errors,\n message: result.state.message,\n valid: false,\n };\n case \"Redirect\":\n return { redirect: result.location };\n case \"Error\":\n return { error: result.message, valid: false };\n }\n}\n\n/** How a successful/failed submit's data should land on `data` and the tainted baseline. */\nexport interface DataApplication<A> {\n /** The next value for `data`, or `undefined` to leave it untouched (an auto-submit keeps local edits). */\n readonly data: A | undefined;\n /** The next tainted baseline (`#initial`), or `undefined` when this isn't a successful submit. */\n readonly rebaseTo: A | undefined;\n}\n\n/**\n * Decide how a submit result's data applies to the store, given a defined\n * `update.data` (redirects/errors never reach this — see {@link resultToUpdate}).\n * Pure so the save-then-navigate baseline rebase is unit-testable without\n * compiling the rune store: on a successful submit the tainted baseline\n * rebases to whatever was actually persisted, so `isTainted` goes back to\n * `false` for values that were just saved, not just the original seed. An\n * auto-submitting (live-save) form keeps its local `data` as the source of\n * truth while a request is in flight — adopting the server's echo would\n * discard keystrokes made during the round-trip — but on success rebases to\n * `submittedData` (the snapshot actually sent), not the live value, since the\n * user may have typed further edits that haven't been submitted yet.\n */\nexport function computeDataApplication<A>(params: {\n readonly isSuccess: boolean;\n readonly appliedData: A;\n readonly initial: A;\n readonly submittedData: A;\n readonly resetForm: boolean;\n readonly autoSubmit: boolean;\n}): DataApplication<A> {\n const { isSuccess, appliedData, initial, submittedData, resetForm, autoSubmit } = params;\n if (resetForm && isSuccess) return { data: initial, rebaseTo: initial };\n if (autoSubmit) return { data: undefined, rebaseTo: isSuccess ? submittedData : undefined };\n return { data: appliedData, rebaseTo: isSuccess ? appliedData : undefined };\n}\n"],"mappings":";;;;;;;;;;;;;AAkBA,MAAM,EAAE,SAAS,YAAY;;AAG7B,SAAgB,gBAAgB,MAAe,SAAS,IAA6B;CACnF,MAAM,MAA+B,CAAC;CACtC,MAAM,QAAQ,KAAa,UAAyB;EAClD,IAAI,UAAU,KAAA,GAAW;EACzB,IAAI,UAAU,MACZ,IAAI,KAAK,CAAC,KAAK,EAAE,CAAC;OACb,IAAI,MAAM,QAAQ,KAAK,GAC5B,MAAM,SAAS,MAAM,MAAM,IAAI,KAAK,GAAG,gBAAgB,MAAM,GAAG,IAAI,GAAG,EAAE,EAAE,CAAC,CAAC;OACxE,IAAI,OAAO,UAAU,UAC1B,IAAI,KAAK,GAAG,gBAAgB,OAAO,GAAG,CAAC;OAEvC,IAAI,KAAK,CAAC,KAAK,OAAO,KAAK,CAAC,CAAC;CAEjC;CAEA,IAAI,SAAS,QAAQ,OAAO,SAAS,YAAY,CAAC,MAAM,QAAQ,IAAI,GAClE,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,IAA+B,GACvE,KAAK,SAAS,GAAG,OAAO,GAAG,QAAQ,KAAK,KAAK;MAE1C,IAAI,QACT,KAAK,QAAQ,IAAI;CAEnB,OAAO;AACT;;AAGA,SAAgB,iBAAiB,MAAyB;CACxD,MAAM,WAAW,IAAI,SAAS;CAC9B,KAAK,MAAM,CAAC,KAAK,UAAU,gBAAgB,IAAI,GAAG,SAAS,OAAO,KAAK,KAAK;CAC5E,OAAO;AACT;;AAGA,SAAS,eAAe,OAA6D;CACnF,OACE,OAAO,UAAU,YACjB,UAAU,QACV,OAAQ,MAA6B,SAAS,YAC9C;EAAC;EAAW;EAAW;EAAY;CAAO,CAAC,CAAC,SAAU,MAA2B,IAAI;AAEzF;;AAGA,SAAS,YAAY,OAAsD;CACzE,OACE,OAAO,UAAU,YACjB,UAAU,QACV,WAAW,SACX,UAAU,SACV,YAAY;AAEhB;;;;;;;AAQA,SAAgB,kBACd,MACA,SAAS,KACqB;CAC9B,IAAI,eAAe,IAAI,GAAG,OAAO;CACjC,IAAI,YAAY,IAAI,GAAG;EACrB,MAAM,QAAQ;EACd,OAAO,MAAM,QAAQ,QAAQ,OAAO,MAAM,IAAI,QAAQ,OAAO,MAAM;CACrE;CACA,OAAO;EAAE,MAAM;EAAS;EAAQ,SAAS;CAA6B;AACxE;;;;;;AAiBA,SAAgB,eACd,QACwB;CACxB,QAAQ,OAAO,MAAf;EACE,KAAK,WACH,OAAO;GACL,MAAM,OAAO,MAAM;GACnB,QAAQ,OAAO,MAAM;GACrB,SAAS,OAAO,MAAM;GACtB,OAAO;EACT;EACF,KAAK,WACH,OAAO;GACL,MAAM,OAAO,MAAM;GACnB,QAAQ,OAAO,MAAM;GACrB,SAAS,OAAO,MAAM;GACtB,OAAO;EACT;EACF,KAAK,YACH,OAAO,EAAE,UAAU,OAAO,SAAS;EACrC,KAAK,SACH,OAAO;GAAE,OAAO,OAAO;GAAS,OAAO;EAAM;CACjD;AACF;;;;;;;;;;;;;;AAuBA,SAAgB,uBAA0B,QAOnB;CACrB,MAAM,EAAE,WAAW,aAAa,SAAS,eAAe,WAAW,eAAe;CAClF,IAAI,aAAa,WAAW,OAAO;EAAE,MAAM;EAAS,UAAU;CAAQ;CACtE,IAAI,YAAY,OAAO;EAAE,MAAM,KAAA;EAAW,UAAU,YAAY,gBAAgB,KAAA;CAAU;CAC1F,OAAO;EAAE,MAAM;EAAa,UAAU,YAAY,cAAc,KAAA;CAAU;AAC5E"}
|
|
1
|
+
{"version":3,"file":"EnhanceCore.js","names":[],"sources":["../src/EnhanceCore.ts"],"sourcesContent":["/**\n * Pure plumbing for the `enhance` action: serialise the store's data back into\n * a form body, interpret a server response into a {@link FormActionResult}, and\n * compute the store update a result implies. Kept framework-free and side-\n * effect-free so it is unit-testable in isolation; the rune store\n * (`FormStore.svelte.ts`) wires these to the DOM.\n *\n * @copyright Thomas Foster\n * @module effect-forms-svelte/EnhanceCore\n */\n\nimport { Errors, Result, State } from \"@thomasfosterau/effect-forms\";\nimport * as Arr from \"effect/Array\";\nimport * as Match from \"effect/Match\";\nimport * as Predicate from \"effect/Predicate\";\nimport * as Record from \"effect/Record\";\n\n// Terse local aliases onto the namespaced core exports.\ntype FormActionResult<A, Message = unknown> = Result.FormActionResult<A, Message>;\ntype FormErrors = Errors.FormErrors;\ntype FormState<A, Message = unknown> = State.FormState<A, Message>;\nconst { failure, success } = Result;\n\n/** A form-body entry: a stringified leaf, or a `Blob` / `File` passed through. */\ntype BodyEntry = [string, string | Blob];\n\n/**\n * Whether a value is a `Blob` (a `File` is one). Guards on `globalThis.Blob`\n * so it holds in any runtime, including one without a global `Blob`.\n */\nconst isBlob = (value: unknown): value is Blob =>\n !Predicate.isUndefined(globalThis.Blob) && value instanceof Blob;\n\n/**\n * Encode a `Date` as the `YYYY-MM-DD` of its UTC calendar day, or `undefined`\n * for an invalid `Date`. The UTC day round-trips: a `YYYY-MM-DD` string\n * decodes to UTC midnight. Taking everything before the `T` keeps\n * `toISOString`'s expanded form for a year outside 0000–9999\n * (`+010000-01-01`), where a fixed 10-character slice would cut it off.\n */\nconst encodeDate = (value: Date): string | undefined => {\n if (Number.isNaN(value.getTime())) return undefined;\n const iso = value.toISOString();\n return iso.slice(0, iso.indexOf(\"T\"));\n};\n\n/** Append the entries for one named value to `out`, recursing into containers. */\nfunction appendEntries(out: Array<BodyEntry>, key: string, value: unknown): void {\n if (value === null) {\n out.push([key, \"\"]);\n } else if (isBlob(value)) {\n out.push([key, value]);\n } else if (value instanceof Date) {\n const encoded = encodeDate(value);\n if (encoded !== undefined) out.push([key, encoded]);\n } else if (Array.isArray(value)) {\n value.forEach((item, i) => appendEntries(out, `${key}[${i}]`, item));\n } else if (Predicate.isObject(value)) {\n appendFields(out, value, key);\n } else if (\n Predicate.isString(value) ||\n Predicate.isNumber(value) ||\n Predicate.isBoolean(value) ||\n Predicate.isBigInt(value)\n ) {\n out.push([key, String(value)]);\n }\n // `undefined`, `function` and `symbol` leaves are omitted.\n}\n\n/** Append an object's own enumerable fields to `out`, as `prefix.key` names. */\nfunction appendFields<V>(\n out: Array<BodyEntry>,\n data: Record.ReadonlyRecord<string, V>,\n prefix: string,\n): void {\n Arr.forEach(Record.toEntries(data), ([key, value]) =>\n appendEntries(out, prefix ? `${prefix}.${key}` : key, value),\n );\n}\n\n/** Flatten `data` into form-body entries, `Blob` / `File` leaves included. */\nfunction toBodyEntries(data: unknown, prefix: string): Array<BodyEntry> {\n const out: Array<BodyEntry> = [];\n if (prefix) appendEntries(out, prefix, data);\n else if (Predicate.isObject(data)) {\n appendFields(out, data, \"\");\n }\n return out;\n}\n\n/**\n * Flatten a nested data object into `[name, value]` string entries using path\n * notation.\n *\n * Nested objects become dotted names (`author.name`) and array items indexed\n * names (`tags[0]`). Each leaf kind is handled as follows:\n *\n * - A `string`, `number`, `boolean` or `bigint` is stringified.\n * - `null` becomes `\"\"`.\n * - A `Date` becomes the `YYYY-MM-DD` of its **UTC** calendar day (what\n * `<input type=\"date\">` submits, and what `Schema.DateFromString` decodes\n * back to UTC midnight); the time of day is dropped, so a datetime needs its\n * own string field. An invalid `Date` is omitted.\n * - A `Blob` or `File` is omitted here, since a string entry can't carry it;\n * {@link objectToFormData} keeps it.\n * - An `undefined`, `function` or `symbol` is omitted, as `JSON.stringify`\n * omits such object properties, so a callback or symbol left in form data\n * never reaches the form body as `\"Symbol(…)\"` or function source.\n * - Any other object contributes its own enumerable fields, so one with none\n * (a `Map`, a `Set`, an empty object) contributes no entries.\n *\n * An omitted array item leaves a gap (where `JSON.stringify` would write\n * `null`), so the remaining items keep their indices (`tags[0]`, `tags[2]`).\n */\nexport function objectToEntries(data: unknown, prefix = \"\"): Array<[string, string]> {\n return toBodyEntries(data, prefix).filter((entry): entry is [string, string] =>\n Predicate.isString(entry[1]),\n );\n}\n\n/**\n * Serialise the store's data into a {@link FormData} body for submission.\n *\n * Names and leaves follow {@link objectToEntries}, except that a `Blob` or\n * `File` leaf is appended as a file (a `File` keeps its name; a bare `Blob`\n * arrives as a `File` named `\"blob\"`, the `FormData` default), as an\n * `<input type=\"file\">` would submit it, so it reaches the core's\n * `FormData.formDataToObject` (`@thomasfosterau/effect-forms`) unchanged.\n */\nexport function objectToFormData(data: unknown): FormData {\n const formData = new FormData();\n Arr.forEach(toBodyEntries(data, \"\"), ([key, value]) => formData.append(key, value));\n return formData;\n}\n\n/**\n * Whether a value looks like a {@link FormActionResult} (carries a known\n * `_tag`). Only the tag is checked: `A` / `Message` are the caller's declared\n * expectation of the server's response, not verified here.\n */\nfunction isActionResult<A, Message>(value: unknown): value is FormActionResult<A, Message> {\n return (\n Predicate.isObjectOrArray(value) &&\n Predicate.hasProperty(value, \"_tag\") &&\n Predicate.isString(value._tag) &&\n [\"Success\", \"Failure\", \"Redirect\", \"Error\"].includes(value._tag)\n );\n}\n\n/**\n * Whether a value looks like a bare {@link FormState}. Only the keys are\n * checked: `A` / `Message` are the caller's declared expectation, not verified.\n */\nfunction isFormState<A, Message>(value: unknown): value is FormState<A, Message> {\n return (\n Predicate.isObjectOrArray(value) && \"valid\" in value && \"data\" in value && \"errors\" in value\n );\n}\n\n/**\n * Normalise a parsed JSON response body into a {@link FormActionResult}. The\n * server may return a full action result (`{ _tag, … }`) or a bare\n * {@link FormState} — a bare state maps to `Success` / `Failure` by its `valid`\n * flag, using `status` for the failure code.\n */\nexport function parseActionResult<A, Message>(\n body: unknown,\n status = 200,\n): FormActionResult<A, Message> {\n if (isActionResult<A, Message>(body)) return body;\n if (isFormState<A, Message>(body)) {\n return body.valid ? success(body, status) : failure(body, status);\n }\n return { _tag: \"Error\", status, message: \"Unrecognised form response\" };\n}\n\n/** The mutation a {@link FormActionResult} implies for the store. */\nexport interface FormUpdate<A, Message> {\n readonly data?: A | undefined;\n readonly errors?: FormErrors | undefined;\n readonly message?: Message | undefined;\n readonly redirect?: string | undefined;\n readonly error?: string | undefined;\n readonly valid?: boolean | undefined;\n}\n\n/**\n * Compute the store update implied by a {@link FormActionResult}. Pure: the\n * caller applies the returned fields to its reactive state and acts on\n * `redirect` / `error`.\n */\nexport function resultToUpdate<A, Message>(\n result: FormActionResult<A, Message>,\n): FormUpdate<A, Message> {\n return Match.valueTags(result, {\n Success: ({ state }): FormUpdate<A, Message> => ({\n data: state.data,\n errors: state.errors,\n message: state.message,\n valid: true,\n }),\n Failure: ({ state }): FormUpdate<A, Message> => ({\n data: state.data,\n errors: state.errors,\n message: state.message,\n valid: false,\n }),\n Redirect: ({ location }): FormUpdate<A, Message> => ({ redirect: location }),\n Error: ({ message }): FormUpdate<A, Message> => ({ error: message, valid: false }),\n });\n}\n\n/** How a successful/failed submit's data should land on `data` and the tainted baseline. */\nexport interface DataApplication<A> {\n /** The next value for `data`, or `undefined` to leave it untouched (an auto-submit keeps local edits). */\n readonly data: A | undefined;\n /** The next tainted baseline (`#initial`), or `undefined` when this isn't a successful submit. */\n readonly rebaseTo: A | undefined;\n}\n\n/**\n * Decide how a submit result's data applies to the store, given a defined\n * `update.data` (redirects/errors never reach this — see {@link resultToUpdate}).\n * Pure so the save-then-navigate baseline rebase is unit-testable without\n * compiling the rune store: on a successful submit the tainted baseline\n * rebases to whatever was actually persisted, so `isTainted` goes back to\n * `false` for values that were just saved, not just the original seed. An\n * auto-submitting (live-save) form keeps its local `data` as the source of\n * truth while a request is in flight — adopting the server's echo would\n * discard keystrokes made during the round-trip — but on success rebases to\n * `submittedData` (the snapshot actually sent), not the live value, since the\n * user may have typed further edits that haven't been submitted yet.\n */\nexport function computeDataApplication<A>(params: {\n readonly isSuccess: boolean;\n readonly appliedData: A;\n readonly initial: A;\n readonly submittedData: A;\n readonly resetForm: boolean;\n readonly autoSubmit: boolean;\n}): DataApplication<A> {\n const { isSuccess, appliedData, initial, submittedData, resetForm, autoSubmit } = params;\n if (resetForm && isSuccess) return { data: initial, rebaseTo: initial };\n if (autoSubmit) return { data: undefined, rebaseTo: isSuccess ? submittedData : undefined };\n return { data: appliedData, rebaseTo: isSuccess ? appliedData : undefined };\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAqBA,MAAM,EAAE,SAAS,YAAY;;;;;AAS7B,MAAM,UAAU,UACd,CAAC,UAAU,YAAY,WAAW,IAAI,KAAK,iBAAiB;;;;;;;;AAS9D,MAAM,cAAc,UAAoC;CACtD,IAAI,OAAO,MAAM,MAAM,QAAQ,CAAC,GAAG,OAAO,KAAA;CAC1C,MAAM,MAAM,MAAM,YAAY;CAC9B,OAAO,IAAI,MAAM,GAAG,IAAI,QAAQ,GAAG,CAAC;AACtC;;AAGA,SAAS,cAAc,KAAuB,KAAa,OAAsB;CAC/E,IAAI,UAAU,MACZ,IAAI,KAAK,CAAC,KAAK,EAAE,CAAC;MACb,IAAI,OAAO,KAAK,GACrB,IAAI,KAAK,CAAC,KAAK,KAAK,CAAC;MAChB,IAAI,iBAAiB,MAAM;EAChC,MAAM,UAAU,WAAW,KAAK;EAChC,IAAI,YAAY,KAAA,GAAW,IAAI,KAAK,CAAC,KAAK,OAAO,CAAC;CACpD,OAAO,IAAI,MAAM,QAAQ,KAAK,GAC5B,MAAM,SAAS,MAAM,MAAM,cAAc,KAAK,GAAG,IAAI,GAAG,EAAE,IAAI,IAAI,CAAC;MAC9D,IAAI,UAAU,SAAS,KAAK,GACjC,aAAa,KAAK,OAAO,GAAG;MACvB,IACL,UAAU,SAAS,KAAK,KACxB,UAAU,SAAS,KAAK,KACxB,UAAU,UAAU,KAAK,KACzB,UAAU,SAAS,KAAK,GAExB,IAAI,KAAK,CAAC,KAAK,OAAO,KAAK,CAAC,CAAC;AAGjC;;AAGA,SAAS,aACP,KACA,MACA,QACM;CACN,IAAI,QAAQ,OAAO,UAAU,IAAI,IAAI,CAAC,KAAK,WACzC,cAAc,KAAK,SAAS,GAAG,OAAO,GAAG,QAAQ,KAAK,KAAK,CAC7D;AACF;;AAGA,SAAS,cAAc,MAAe,QAAkC;CACtE,MAAM,MAAwB,CAAC;CAC/B,IAAI,QAAQ,cAAc,KAAK,QAAQ,IAAI;MACtC,IAAI,UAAU,SAAS,IAAI,GAC9B,aAAa,KAAK,MAAM,EAAE;CAE5B,OAAO;AACT;;;;;;;;;;;;;;;;;;;;;;;;;AA0BA,SAAgB,gBAAgB,MAAe,SAAS,IAA6B;CACnF,OAAO,cAAc,MAAM,MAAM,CAAC,CAAC,QAAQ,UACzC,UAAU,SAAS,MAAM,EAAE,CAC7B;AACF;;;;;;;;;;AAWA,SAAgB,iBAAiB,MAAyB;CACxD,MAAM,WAAW,IAAI,SAAS;CAC9B,IAAI,QAAQ,cAAc,MAAM,EAAE,IAAI,CAAC,KAAK,WAAW,SAAS,OAAO,KAAK,KAAK,CAAC;CAClF,OAAO;AACT;;;;;;AAOA,SAAS,eAA2B,OAAuD;CACzF,OACE,UAAU,gBAAgB,KAAK,KAC/B,UAAU,YAAY,OAAO,MAAM,KACnC,UAAU,SAAS,MAAM,IAAI,KAC7B;EAAC;EAAW;EAAW;EAAY;CAAO,CAAC,CAAC,SAAS,MAAM,IAAI;AAEnE;;;;;AAMA,SAAS,YAAwB,OAAgD;CAC/E,OACE,UAAU,gBAAgB,KAAK,KAAK,WAAW,SAAS,UAAU,SAAS,YAAY;AAE3F;;;;;;;AAQA,SAAgB,kBACd,MACA,SAAS,KACqB;CAC9B,IAAI,eAA2B,IAAI,GAAG,OAAO;CAC7C,IAAI,YAAwB,IAAI,GAC9B,OAAO,KAAK,QAAQ,QAAQ,MAAM,MAAM,IAAI,QAAQ,MAAM,MAAM;CAElE,OAAO;EAAE,MAAM;EAAS;EAAQ,SAAS;CAA6B;AACxE;;;;;;AAiBA,SAAgB,eACd,QACwB;CACxB,OAAO,MAAM,UAAU,QAAQ;EAC7B,UAAU,EAAE,aAAqC;GAC/C,MAAM,MAAM;GACZ,QAAQ,MAAM;GACd,SAAS,MAAM;GACf,OAAO;EACT;EACA,UAAU,EAAE,aAAqC;GAC/C,MAAM,MAAM;GACZ,QAAQ,MAAM;GACd,SAAS,MAAM;GACf,OAAO;EACT;EACA,WAAW,EAAE,gBAAwC,EAAE,UAAU,SAAS;EAC1E,QAAQ,EAAE,eAAuC;GAAE,OAAO;GAAS,OAAO;EAAM;CAClF,CAAC;AACH;;;;;;;;;;;;;;AAuBA,SAAgB,uBAA0B,QAOnB;CACrB,MAAM,EAAE,WAAW,aAAa,SAAS,eAAe,WAAW,eAAe;CAClF,IAAI,aAAa,WAAW,OAAO;EAAE,MAAM;EAAS,UAAU;CAAQ;CACtE,IAAI,YAAY,OAAO;EAAE,MAAM,KAAA;EAAW,UAAU,YAAY,gBAAgB,KAAA;CAAU;CAC1F,OAAO;EAAE,MAAM;EAAa,UAAU,YAAY,cAAc,KAAA;CAAU;AAC5E"}
|