@menapo/forms 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 menapo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,46 @@
1
- # Temporary Holding Version
1
+ # @menapo/forms
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Form state for kit apps: `form("PATCH /projects/:id")` is a form typed and
4
+ validated by the app's own `@menapo/api` operation, submitted through
5
+ `@menapo/query`'s mutation, with the server's field issues on their fields;
6
+ `form(schema, submit)` is one for anything else. Controls are uncontrolled
7
+ and the `<form>` listens once, so typing re-renders nothing that doesn't
8
+ show the change; every control gets its ids and `aria-*` from its parts,
9
+ and a failed submit moves focus. Read with `useForm` in React, Preact, Vue,
10
+ Svelte, Solid and plain code.
11
+
12
+ **Status: built, private until 1.0.** Definitions, validation timing,
13
+ server issues, defaults that follow a query, rows, uploads, checks, the
14
+ plugin (the browser's copy of each operation's input schema, cut from its
15
+ module; constraint attributes from its JSON Schema; glue for six
16
+ frameworks), `/server`'s `action()` and the DevTools panel are implemented
17
+ and tested; `examples/forms-react` and `examples/forms-svelte` run in
18
+ Chromium with axe. The release waits on the owner's review.
19
+
20
+ ```ts
21
+ // forms.ts: no framework, no second schema
22
+ export const newOrg = form("POST /orgs");
23
+ export const contact = form(Contact, (values, { signal }) => send(values, signal));
24
+ ```
25
+
26
+ ```tsx
27
+ const f = useForm(newOrg, { onSuccess: (org) => navigate(`/${org.slug}`) });
28
+ const { name, slug } = f.fields;
29
+ <form {...f.form}>
30
+ <label {...name.label}>Name</label>
31
+ <input {...name.control} />
32
+ <p {...name.error} />
33
+ <label {...slug.label}>Address</label>
34
+ <input {...slug.control} />
35
+ <p {...slug.hint}>Lowercase letters and dashes</p>
36
+ <p {...slug.error} />
37
+ <button {...f.submit}>Create</button>
38
+ </form>;
39
+ ```
40
+
41
+ - [Guide](docs/guide/README.md): getting started and the reference.
42
+ - [Plan](docs/PLAN.md): the design, the decisions and the measurements.
43
+ - Entries: `@menapo/forms` (`form`, `useForm`, `inspect`, types),
44
+ `@menapo/forms/server` (`action`, `decode`), `@menapo/forms/vite`.
45
+
46
+ MIT licensed.
@@ -0,0 +1,5 @@
1
+ import { n as Shape, t as FormViewImpl } from "./view-5_kyMWP3.js";
2
+ //#region src/array/index.d.ts
3
+ /** The field of the array at `path` (`Meta.a`): a field, and its items. */
4
+ export declare function arrayField(form: FormViewImpl, path: string, item: Shape): object;
5
+ //#endregion
package/dist/array.js ADDED
@@ -0,0 +1,101 @@
1
+ import { n as get, s as set } from "./paths-DqNafpnl.js";
2
+ import { i as fieldAt, n as at, t as FieldViewImpl } from "./view--JGPLHEo.js";
3
+ //#region src/array/index.ts
4
+ /**
5
+ * Field arrays (D14): `f.fields.invites` is a field with stable `keys` to
6
+ * render rows by, its `items`' fields, and `append`, `insert`, `remove`
7
+ * and `move`. A field's state (error, touched, dirty) follows its item,
8
+ * not its index: the records under a moved item are renamed with it, and
9
+ * its row keeps its key, so the uncontrolled controls keep their text. A
10
+ * module of its own, which the plugin hands only a form whose input has an
11
+ * array, so a form without one ships none of it (AGENTS.md: features are
12
+ * modules).
13
+ */
14
+ let next = 0;
15
+ /** The keys of the array at `path`, as many as it has items. */
16
+ function keysOf(i, path) {
17
+ const all = i.ak ??= /* @__PURE__ */ new Map();
18
+ let keys = all.get(path);
19
+ if (!keys) all.set(path, keys = []);
20
+ const length = get(i.v, path)?.length ?? 0;
21
+ while (keys.length < length) keys.push(`k${++next}`);
22
+ keys.length = length;
23
+ return keys;
24
+ }
25
+ /** Renames what lives under `path.<index>` to where `order` now has that index; gone, it drops. */
26
+ function remap(i, path, order) {
27
+ const prefix = `${path}.`;
28
+ const rename = (p) => {
29
+ if (!p.startsWith(prefix)) return p;
30
+ const [, index, rest] = /^(\d+)(.*)$/s.exec(p.slice(prefix.length)) ?? [];
31
+ const moved = order.indexOf(Number(index));
32
+ return moved < 0 ? void 0 : prefix + moved + rest;
33
+ };
34
+ const records = [...i.r.values()];
35
+ i.r.clear();
36
+ for (const rec of records) {
37
+ const p = rename(rec.p);
38
+ if (p === void 0) continue;
39
+ if (p !== rec.p) {
40
+ rec.p = p;
41
+ rec.ver++;
42
+ }
43
+ i.r.set(p, rec);
44
+ }
45
+ const dirty = [...i.dp];
46
+ i.dp.clear();
47
+ for (const p of dirty) {
48
+ const renamed = rename(p);
49
+ if (renamed !== void 0) i.dp.add(renamed);
50
+ }
51
+ i.dirty(path);
52
+ i.ord = i.ord.map(rename).filter((p) => p !== void 0);
53
+ }
54
+ /**
55
+ * Applies `change` to the array (with `item`), to its keys (with a new key)
56
+ * and to the old indexes, which then say where each record moves: one
57
+ * rule for the four edits rather than an index formula per edit.
58
+ */
59
+ function edit(i, path, item, change) {
60
+ const list = [...get(i.v, path) ?? []];
61
+ const order = list.map((_, index) => index);
62
+ change(list, item);
63
+ change(order, -1);
64
+ change(keysOf(i, path), `k${++next}`);
65
+ i.v = set(i.v, path, list);
66
+ remap(i, path, order);
67
+ i.all();
68
+ }
69
+ var ArrayFieldImpl = class extends FieldViewImpl {
70
+ /** The form view, for its items' cached views, and the item's shape. */
71
+ f = void 0;
72
+ s = {};
73
+ get keys() {
74
+ this.$.read(this.r.p, 64);
75
+ return keysOf(this.$.i, this.r.p);
76
+ }
77
+ get items() {
78
+ return this.keys.map((_, n) => at(this.f, `${this.r.p}.${n}`, this.s));
79
+ }
80
+ append(item) {
81
+ edit(this.$.i, this.r.p, item, (list, it) => void list.push(it));
82
+ }
83
+ insert(index, item) {
84
+ edit(this.$.i, this.r.p, item, (list, it) => void list.splice(index, 0, it));
85
+ }
86
+ remove(index) {
87
+ edit(this.$.i, this.r.p, void 0, (list) => void list.splice(index, 1));
88
+ }
89
+ move(from, to) {
90
+ edit(this.$.i, this.r.p, void 0, (list) => void list.splice(to, 0, ...list.splice(from, 1)));
91
+ }
92
+ };
93
+ /** The field of the array at `path` (`Meta.a`): a field, and its items. */
94
+ function arrayField(form, path, item) {
95
+ const field = fieldAt(form, path, ArrayFieldImpl);
96
+ field.f = form;
97
+ field.s = item;
98
+ return field;
99
+ }
100
+ //#endregion
101
+ export { arrayField };
@@ -0,0 +1,4 @@
1
+ import { r as Checker } from "./view-5_kyMWP3.js";
2
+ //#region src/checks/index.d.ts
3
+ export declare const checks: Checker;
4
+ //#endregion
package/dist/checks.js ADDED
@@ -0,0 +1,53 @@
1
+ //#region src/checks/index.ts
2
+ const DEBOUNCE = 300;
3
+ function start(i, path, run) {
4
+ run.t = void 0;
5
+ const controller = run.c = new AbortController();
6
+ const rec = i.rec(path);
7
+ const done = (message) => {
8
+ if (controller.signal.aborted) return;
9
+ run.c = void 0;
10
+ rec.vd--;
11
+ const changed = i.error_(rec, typeof message === "string" ? message : void 0, 1);
12
+ i.tell([path], 16 | (changed ? 34 : 0), changed ? 4 : 0);
13
+ };
14
+ run.p = Promise.resolve().then(() => run.k(run.v, { signal: controller.signal })).then(done, () => done(void 0));
15
+ }
16
+ const checks = {
17
+ run(instance, path, value) {
18
+ const i = instance;
19
+ const check = i.F.o.checks?.[path];
20
+ if (!check) return;
21
+ const all = i.ck ??= /* @__PURE__ */ new Map();
22
+ let run = all.get(path);
23
+ if (!run) all.set(path, run = { k: check });
24
+ if (!run.t && !run.c) {
25
+ i.rec(path).vd++;
26
+ i.tell([path], 16, 0);
27
+ }
28
+ clearTimeout(run.t);
29
+ run.c?.abort();
30
+ run.v = value;
31
+ const due = run;
32
+ run.t = setTimeout(() => start(i, path, due), DEBOUNCE);
33
+ },
34
+ async settle(instance) {
35
+ const waiting = [];
36
+ for (const [path, run] of instance.ck ?? []) {
37
+ if (run.t !== void 0) {
38
+ clearTimeout(run.t);
39
+ start(instance, path, run);
40
+ }
41
+ if (run.c) waiting.push(run.p);
42
+ }
43
+ await Promise.all(waiting);
44
+ },
45
+ stop(instance) {
46
+ for (const run of instance.ck?.values() ?? []) {
47
+ clearTimeout(run.t);
48
+ run.c?.abort();
49
+ }
50
+ }
51
+ };
52
+ //#endregion
53
+ export { checks };
@@ -0,0 +1,8 @@
1
+ import { a as Meta, i as Form } from "../view-5_kyMWP3.js";
2
+ import { d as FormOptions } from "../types-CdCFdlFp.js";
3
+ import { StandardSchemaV1 } from "@standard-schema/spec";
4
+ //#region src/factories/fn.d.ts
5
+ type Submit = ConstructorParameters<typeof Form>[0];
6
+ export declare const fnForm: (schema: StandardSchemaV1, submit: Submit, options?: FormOptions<never>, meta?: Meta) => Form;
7
+ //#endregion
8
+ export {};
@@ -0,0 +1,5 @@
1
+ import { n as fnKind, t as Form } from "../form-BZ2OH06z.js";
2
+ //#region src/factories/fn.ts
3
+ const fnForm = (schema, submit, options, meta) => new Form(submit, schema, options, meta, fnKind);
4
+ //#endregion
5
+ export { fnForm };
@@ -0,0 +1,5 @@
1
+ import { a as Meta, i as Form } from "../view-5_kyMWP3.js";
2
+ import { d as FormOptions } from "../types-CdCFdlFp.js";
3
+ //#region src/factories/route.d.ts
4
+ export declare const routeForm: (route: string, options?: FormOptions<never>, meta?: Meta) => Form;
5
+ //#endregion
@@ -0,0 +1,31 @@
1
+ import { t as Form } from "../form-BZ2OH06z.js";
2
+ import * as query from "@menapo/query";
3
+ //#region src/route/index.ts
4
+ /**
5
+ * Route forms (D2): `form("PATCH /projects/:id")` submits through
6
+ * `@menapo/query`'s `mutation()`, so the convention's refresh, the
7
+ * optimistic journal, upload progress, `ApiError` and the request's client
8
+ * on a server (`withClient`) are query's, not a second copy here. The raw
9
+ * input is sent; the server validates it (D10). Only route forms import
10
+ * this module, so a function form ships nothing of query (D17).
11
+ */
12
+ /** Makes the definition's mutation, with `optimistic` and `refresh` passed through unchanged. */
13
+ function routeInit(form) {
14
+ const { optimistic, refresh } = form.o;
15
+ form.m = query.mutation(form.r, {
16
+ ...optimistic && { optimistic },
17
+ ...refresh && { refresh }
18
+ });
19
+ }
20
+ const routeKind = (form, input, _output, call) => form.m(input, call);
21
+ //#endregion
22
+ //#region src/factories/route.ts
23
+ /**
24
+ * The route form's factory (D17): what the plugin points a top-level
25
+ * `form("METHOD /path")` at, with its hidden last argument (the name, the
26
+ * operation's browser schema, D4, and the field spec, D13). Internal:
27
+ * resolved by file, never a package entry.
28
+ */
29
+ const routeForm = (route, options, meta) => new Form(route, void 0, options, meta, routeKind, routeInit);
30
+ //#endregion
31
+ export { routeForm };
@@ -0,0 +1,48 @@
1
+ import { i as fail } from "./instance-C1aijv06.js";
2
+ import { arrayField } from "./array.js";
3
+ import { checks } from "./checks.js";
4
+ //#region src/core/form.ts
5
+ var Form = class {
6
+ /** The name, for DevTools and errors. */
7
+ n;
8
+ s;
9
+ f;
10
+ o;
11
+ c;
12
+ a;
13
+ x;
14
+ /** A route form's route, or a function form's submit. */
15
+ r;
16
+ /** The input's shape, for `fields`; each instance's values extend it (view.ts). */
17
+ sh = void 0;
18
+ /** A route form's mutation, made by its kind. */
19
+ m = void 0;
20
+ constructor(source, schema, options, meta, kind, init) {
21
+ this.r = source;
22
+ this.s = meta?.s ?? schema;
23
+ this.f = meta?.f ?? {};
24
+ this.o = options ?? {};
25
+ this.c = meta?.c;
26
+ this.a = meta?.a;
27
+ this.x = kind;
28
+ this.n = meta?.n ?? (typeof source === "string" ? `form("${source}")` : "form(schema, submit)");
29
+ init?.(this);
30
+ }
31
+ get name() {
32
+ return this.n;
33
+ }
34
+ };
35
+ /** A function form's kind: `submit(output, { signal })` (D10). */
36
+ const fnKind = async (form, _input, output, call) => form.r(output, { signal: call.signal });
37
+ /** A route form without the plugin: its kind lives in `route/`, which only the plugin points it at. */
38
+ const noRoute = (form) => Promise.reject(fail("NO_PLUGIN", `${form.n} submits through @menapo/query, which the Vite plugin wires; add forms() from "@menapo/forms/vite" to vite.config.ts.`));
39
+ function form(a, b, c) {
40
+ const options = typeof a === "string" ? b : c;
41
+ const meta = options?.checks ? {
42
+ a: arrayField,
43
+ c: checks
44
+ } : { a: arrayField };
45
+ return typeof a === "string" ? new Form(a, void 0, options, meta, noRoute) : new Form(b, a, options, meta, fnKind);
46
+ }
47
+ //#endregion
48
+ export { fnKind as n, form as r, Form as t };
@@ -0,0 +1,4 @@
1
+ import { i as Form, t as FormViewImpl } from "../view-5_kyMWP3.js";
2
+ //#region src/glue/preact.d.ts
3
+ export declare function useForm(source: Form | FormViewImpl, options?: object): FormViewImpl;
4
+ //#endregion
@@ -0,0 +1,59 @@
1
+ import { a as objectOptions, r as create } from "../view--JGPLHEo.js";
2
+ import { n as jsx, t as names } from "../names-D_TE3BpM.js";
3
+ import { n as same } from "../source-DdFTPjID.js";
4
+ import { useId, useLayoutEffect, useState } from "preact/hooks";
5
+ //#region ../glue/src/preact.ts
6
+ /**
7
+ * Preact (machine PLAN §5.5): React's shape on `preact/hooks`, which has no
8
+ * `useSyncExternalStore`: a state update when the snapshot moved, checked
9
+ * again once subscribed so a change between render and subscription is not
10
+ * lost. Side effects start in a layout effect, props are taken after each
11
+ * commit.
12
+ */
13
+ function useSource(create, props) {
14
+ const id = useId();
15
+ const [binding] = useState(() => ({
16
+ source: create(id, props),
17
+ seen: 0,
18
+ props
19
+ }));
20
+ const [, render] = useState(0);
21
+ const { source } = binding;
22
+ binding.seen = source.snapshot();
23
+ useLayoutEffect(() => {
24
+ const check = () => {
25
+ const now = source.snapshot();
26
+ if (now !== binding.seen) render(now);
27
+ };
28
+ const off = source.subscribe(check);
29
+ source.mount();
30
+ check();
31
+ return () => {
32
+ off();
33
+ source.unmount();
34
+ };
35
+ }, [source]);
36
+ useLayoutEffect(() => {
37
+ if (props && !same(props, binding.props)) source.setProps(binding.props = props);
38
+ source.commit();
39
+ });
40
+ return source;
41
+ }
42
+ //#endregion
43
+ //#region src/glue/preact.ts
44
+ /**
45
+ * `useForm` in Preact (PLAN §5.2): the shared glue's hooks
46
+ * adapter. Reads are tracked again on each render, so the component renders
47
+ * only when something it read changed; the view object is new per version
48
+ * (as in React). Options are taken after each
49
+ * commit, so a render that never commits changes nothing.
50
+ */
51
+ const N = names(jsx, false, true);
52
+ function useForm(source, options) {
53
+ objectOptions(options, "Preact");
54
+ const view = useSource(create(source, N), options);
55
+ view.restart();
56
+ return view.current();
57
+ }
58
+ //#endregion
59
+ export { useForm };
@@ -0,0 +1,4 @@
1
+ import { i as Form, t as FormViewImpl } from "../view-5_kyMWP3.js";
2
+ //#region src/glue/react.d.ts
3
+ export declare function useForm(source: Form | FormViewImpl, options?: object): FormViewImpl;
4
+ //#endregion
@@ -0,0 +1,55 @@
1
+ import { a as objectOptions, r as create } from "../view--JGPLHEo.js";
2
+ import { i as react, t as names } from "../names-D_TE3BpM.js";
3
+ import { n as same } from "../source-DdFTPjID.js";
4
+ import { useId, useLayoutEffect, useState, useSyncExternalStore } from "react";
5
+ //#region ../glue/src/react.ts
6
+ /**
7
+ * React (machine PLAN §5.5, spike S3): the source lives in `useState`, so a
8
+ * render React throws away (StrictMode, a concurrent render) never mutates
9
+ * a live one; `useSyncExternalStore` renders once per snapshot move and
10
+ * never tears; side effects start in a layout effect, after the DOM is
11
+ * committed, and stop on unmount (StrictMode's mount, unmount and mount
12
+ * leave one running). Props are taken after each commit, so a render that
13
+ * never commits never changes the source. No `flushSync`.
14
+ */
15
+ function useSource(create, props) {
16
+ const id = useId();
17
+ const [binding] = useState(() => {
18
+ const source = create(id, props);
19
+ return {
20
+ source,
21
+ subscribe: (listener) => source.subscribe(listener),
22
+ snapshot: () => source.snapshot(),
23
+ props
24
+ };
25
+ });
26
+ const { source } = binding;
27
+ useSyncExternalStore(binding.subscribe, binding.snapshot, binding.snapshot);
28
+ useLayoutEffect(() => {
29
+ source.mount();
30
+ return () => source.unmount();
31
+ }, [source]);
32
+ useLayoutEffect(() => {
33
+ if (props && !same(props, binding.props)) source.setProps(binding.props = props);
34
+ source.commit();
35
+ });
36
+ return source;
37
+ }
38
+ //#endregion
39
+ //#region src/glue/react.ts
40
+ /**
41
+ * `useForm` in React (PLAN §5.2): the shared glue's `useSyncExternalStore`
42
+ * adapter. Reads are tracked again on each render, so the component renders
43
+ * only when something it read changed; the view object is new per version
44
+ * (React's Compiler compares by identity). Options are taken after each
45
+ * commit, so a render that never commits changes nothing.
46
+ */
47
+ const N = names(react, true);
48
+ function useForm(source, options) {
49
+ objectOptions(options, "React");
50
+ const view = useSource(create(source, N), options);
51
+ view.restart();
52
+ return view.current();
53
+ }
54
+ //#endregion
55
+ export { useForm };
@@ -0,0 +1,4 @@
1
+ import { i as Form, t as FormViewImpl } from "../view-5_kyMWP3.js";
2
+ //#region src/glue/solid.d.ts
3
+ export declare function useForm(source: Form | FormViewImpl, options?: object | (() => object)): FormViewImpl;
4
+ //#endregion
@@ -0,0 +1,48 @@
1
+ import { r as create } from "../view--JGPLHEo.js";
2
+ import { n as jsx, t as names } from "../names-D_TE3BpM.js";
3
+ import { createComputed, createSignal, createUniqueId, onCleanup, onMount, untrack } from "solid-js";
4
+ //#region ../glue/src/solid.ts
5
+ /**
6
+ * Solid (machine PLAN §5.5): a signal holding the snapshot, read through
7
+ * the returned tracker, so only the computations that read it re-run, and
8
+ * Solid's own spread applies only the attributes that changed. Props given
9
+ * as a function are read in a computation; side effects start in `onMount`
10
+ * and run after each change, once Solid has updated the DOM.
11
+ */
12
+ function useSource(create, props) {
13
+ const read = typeof props === "function" ? props : void 0;
14
+ const source = create(createUniqueId(), read ? untrack(read) : props);
15
+ const [version, set] = createSignal(source.snapshot());
16
+ const commit = () => source.commit();
17
+ const off = source.subscribe(() => {
18
+ set(source.snapshot());
19
+ queueMicrotask(commit);
20
+ });
21
+ if (read) createComputed(() => {
22
+ const next = read();
23
+ untrack(() => source.setProps(next));
24
+ });
25
+ onMount(() => source.mount());
26
+ onCleanup(() => {
27
+ off();
28
+ source.unmount();
29
+ });
30
+ return [source, version];
31
+ }
32
+ //#endregion
33
+ //#region src/glue/solid.ts
34
+ /**
35
+ * `useForm` in Solid (PLAN §5.2): the shared glue's signal adapter;
36
+ * every read calls the tracker, and options given as a getter are watched,
37
+ * so pristine fields follow new defaults (D11). Handlers are named
38
+ * `onInput`, `onFocusOut`, which Solid lowercases.
39
+ */
40
+ const N = names(jsx, false, true);
41
+ function useForm(source, options) {
42
+ const [view, track] = useSource(create(source, N), options);
43
+ view.k = track;
44
+ view.keep = true;
45
+ return view.current();
46
+ }
47
+ //#endregion
48
+ export { useForm };
@@ -0,0 +1,4 @@
1
+ import { i as Form, t as FormViewImpl } from "../view-5_kyMWP3.js";
2
+ //#region src/glue/svelte.svelte.d.ts
3
+ export declare function useForm(source: Form | FormViewImpl, options?: object | (() => object)): FormViewImpl;
4
+ //#endregion
@@ -0,0 +1,62 @@
1
+ import { r as create } from "../view--JGPLHEo.js";
2
+ import { r as lower, t as names } from "../names-D_TE3BpM.js";
3
+ import { t as nextId } from "../source-DdFTPjID.js";
4
+ import { onMount, tick, untrack } from "svelte";
5
+ import { createSubscriber } from "svelte/reactivity";
6
+ //#region ../glue/src/svelte.svelte.ts
7
+ /**
8
+ * Svelte 5 (machine PLAN §5.5): `createSubscriber` re-runs the effects and
9
+ * deriveds that read the tracker when the snapshot moves; props given as a
10
+ * getter are taken in `$effect.pre`, before the DOM updates. Side effects
11
+ * start in `onMount` and run after each update that follows a change.
12
+ * Svelte gives a module no id of its own, so ids come from a counter (or
13
+ * the owner's `id`, which SSR needs).
14
+ */
15
+ function useSource(create, props) {
16
+ const read = typeof props === "function" ? props : void 0;
17
+ const source = create(nextId(), read ? read() : props);
18
+ let update;
19
+ let seen = source.snapshot();
20
+ const track = createSubscriber((u) => {
21
+ update = u;
22
+ return () => update = void 0;
23
+ });
24
+ const commit = () => source.commit();
25
+ const off = source.subscribe(() => {
26
+ const now = source.snapshot();
27
+ if (now !== seen) {
28
+ seen = now;
29
+ update?.();
30
+ }
31
+ tick().then(commit);
32
+ });
33
+ if (read) $effect.pre(() => {
34
+ const next = read();
35
+ untrack(() => source.setProps(next));
36
+ });
37
+ onMount(() => {
38
+ source.mount();
39
+ return () => {
40
+ off();
41
+ source.unmount();
42
+ };
43
+ });
44
+ return [source, track];
45
+ }
46
+ //#endregion
47
+ //#region src/glue/svelte.svelte.ts
48
+ /**
49
+ * `useForm` in Svelte 5 (PLAN §5.2): the shared glue's `createSubscriber` adapter;
50
+ * every read calls the tracker, and options given as a getter are watched,
51
+ * so pristine fields follow new defaults (D11). Handlers are named
52
+ * `oninput`, `onfocusout` (glue's naming).
53
+ */
54
+ const N = names(lower, false);
55
+ function useForm(source, options) {
56
+ const [view, track] = useSource(create(source, N), options);
57
+ view.k = track;
58
+ view.keep = true;
59
+ return view.current();
60
+ }
61
+ //#endregion
62
+ export { useForm };
@@ -0,0 +1,4 @@
1
+ import { i as Form, t as FormViewImpl } from "../view-5_kyMWP3.js";
2
+ //#region src/glue/vanilla.d.ts
3
+ export declare function useForm(source: Form | FormViewImpl, options?: object): FormViewImpl;
4
+ //#endregion