@shivaedev/form 0.13.1 → 0.15.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/CHANGELOG.md +22 -0
- package/README.md +83 -10
- package/dist/adapters/browserDraftStorage.d.ts.map +1 -1
- package/dist/adapters/browserDraftStorage.js +27 -2
- package/dist/adapters/browserDraftStorage.js.map +1 -1
- package/dist/adapters/draftCodec.d.ts +7 -0
- package/dist/adapters/draftCodec.d.ts.map +1 -0
- package/dist/adapters/draftCodec.js +40 -0
- package/dist/adapters/draftCodec.js.map +1 -0
- package/dist/adapters/numberInput.d.ts +15 -0
- package/dist/adapters/numberInput.d.ts.map +1 -0
- package/dist/adapters/numberInput.js +11 -0
- package/dist/adapters/numberInput.js.map +1 -0
- package/dist/adapters/persistence.d.ts +1 -1
- package/dist/adapters/persistence.d.ts.map +1 -1
- package/dist/adapters/persistence.js +91 -61
- package/dist/adapters/persistence.js.map +1 -1
- package/dist/adapters/react.d.ts.map +1 -1
- package/dist/adapters/react.js +3 -1
- package/dist/adapters/react.js.map +1 -1
- package/dist/adapters/useList.d.ts +23 -0
- package/dist/adapters/useList.d.ts.map +1 -0
- package/dist/adapters/useList.js +27 -0
- package/dist/adapters/useList.js.map +1 -0
- package/dist/draft.d.ts +2 -2
- package/dist/draft.d.ts.map +1 -1
- package/dist/draft.js +67 -45
- package/dist/draft.js.map +1 -1
- package/dist/draftValues.d.ts +13 -0
- package/dist/draftValues.d.ts.map +1 -0
- package/dist/draftValues.js +47 -0
- package/dist/draftValues.js.map +1 -0
- package/dist/field.d.ts +17 -0
- package/dist/field.d.ts.map +1 -0
- package/dist/field.js +20 -0
- package/dist/field.js.map +1 -0
- package/dist/form.d.ts.map +1 -1
- package/dist/form.js +17 -9
- package/dist/form.js.map +1 -1
- package/dist/persistence.d.ts +15 -11
- package/dist/persistence.d.ts.map +1 -1
- package/dist/persistence.js.map +1 -1
- package/package.json +4 -4
- package/src/adapters/browserDraftStorage.ts +30 -2
- package/src/adapters/draftCodec.ts +43 -0
- package/src/adapters/numberInput.ts +17 -0
- package/src/adapters/persistence.ts +97 -63
- package/src/adapters/react.ts +3 -1
- package/src/adapters/useList.ts +39 -0
- package/src/draft.ts +70 -62
- package/src/draftValues.ts +65 -0
- package/src/field.ts +34 -0
- package/src/form.ts +12 -6
- package/src/persistence.ts +17 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.15.0 - 2026-11-04
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- `Field.number({ optional, min, max, integer })` supplies a finite numeric string codec with inclusive bounds and blank-as-null optional values. `numberInput` binds the encoded text to an input without losing invalid input.
|
|
8
|
+
- `Field.list(item, { min, max })` bounds arrays with native Schema checks. The React `useList` hook adds, edits and removes items and exposes feedback for each current item.
|
|
9
|
+
|
|
10
|
+
## 0.14.0 - 2026-11-03
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Persist drafts through asynchronous native Effect storage operations, with optional observation across editors. Observed changes preserve local edits and adopt untouched fields.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- Clear only the submitted draft revision after a successful save, keeping newer stored input from another editor. Serialize asynchronous writes and clears, and condition metadata writes on the owned revision.
|
|
19
|
+
|
|
20
|
+
### Breaking
|
|
21
|
+
|
|
22
|
+
- Custom `DraftStorage.removeItem` implementations must accept `(key, expected)` and atomically remove only when the stored serialized value equals `expected`. Custom `setItem(key, value, expected?)` implementations must also compare atomically when `expected` is supplied, so rebasing accepted fields cannot overwrite newer stored input. Replace unconditional mutations with conditional mutations at the storage owner; writes without `expected` still store new local input. The browser adapter handles this contract; providers can keep synchronous reads and writes or return native Effects.
|
|
23
|
+
- Lower-level `Persistence` integrations use `observe` and `revision` in place of `read`; mount the draft's atoms to own observation. Core form consumers mount `form.dirty` before reading restored input; asynchronous restoration arrives through the existing field refs. Form's React hooks attach this lifetime automatically.
|
|
24
|
+
|
|
3
25
|
## 0.13.1 - 2026-11-03
|
|
4
26
|
|
|
5
27
|
### Changed
|
package/README.md
CHANGED
|
@@ -220,7 +220,11 @@ function OrderEditor({ persistence }: { readonly persistence: Omit<PersistedDraf
|
|
|
220
220
|
|
|
221
221
|
Each stored record defaults to at most 256 KiB of UTF-8 JSON and seven days since its last write. Set `maxBytes` and `maxAgeMs` to finite positive limits to change those bounds. Oversized, expired, malformed or incompatible records are dropped when encountered. Persistence requires JSON-compatible encoded input; invalid input such as an empty required string or an unfinished numeric string still persists. Storage access, serialization and quota failures leave the form usable.
|
|
222
222
|
|
|
223
|
-
Edits and received values update the stored draft. Revert removes
|
|
223
|
+
Edits and received values update the stored draft. Revert conditionally removes the form's owned revision. A successful submit clears only the revision associated with its submitted input, including refreshes of its untouched fields; newer edits made locally or in another editor remain persisted. A failed submit keeps the draft.
|
|
224
|
+
|
|
225
|
+
A custom `DraftStorage` implements `getItem(key)`, `setItem(key, value, expected?)` and `removeItem(key, expected)`. Each operation returns its synchronous result or a native `Effect` with no remaining services; acquire application services when constructing the provider. `expected` is the exact serialized value previously read or written, containing an opaque revision. The storage owner must compare and mutate atomically: remove the key, or replace it when `setItem` supplies `expected`, only if its current value equals `expected`. A `setItem` without `expected` stores a new local edit; a conditional write rebases accepted or refreshed fields without replacing another editor's newer draft. For an Electron bridge, perform these comparisons in the process that owns the shared store. Writes and clears from one form run in order even when storage is asynchronous. Storage failures remain best-effort and do not fail the form's save.
|
|
226
|
+
|
|
227
|
+
Optional `subscribe(key, listener)` reports the current serialized value, or `null` when cleared, and returns an unsubscribe function. Report changes in storage order. Observation starts while the form's atoms are mounted and ends with their lifetime. Form's React hooks mount that lifetime automatically; direct atom consumers mount `form.dirty` or its validation/submission atoms. An untouched editor follows observed changes. Locally edited fields keep their current input while untouched fields adopt the observed draft. Receiving a storage event does not echo a write. The browser adapter observes other windows through storage events and serializes mutations with Web Locks where available; without Web Locks its compare and remove run synchronously.
|
|
224
228
|
|
|
225
229
|
Outside React, pass a `session` from `draftSession(id)` in `@shivaedev/form/draftSession.ts` and call its `drop()` when the application replaces that session. Core forms without a session owner use only the application's key. `DraftSessionContext` supplies the owner to React integrations.
|
|
226
230
|
|
|
@@ -289,6 +293,70 @@ Read the message at the element's **current** index: in React, `useAtomValue(ans
|
|
|
289
293
|
|
|
290
294
|
Without a selector for an array, handler errors fall back to index paths. They remain at that index while its input value matches, even if a different element with the same value moves there. Arrays nested inside an element also use index paths. Use immutable value updates and stable, unique keys for top-level arrays whose errors must follow elements.
|
|
291
295
|
|
|
296
|
+
### Numeric inputs
|
|
297
|
+
|
|
298
|
+
```tsx
|
|
299
|
+
import * as Field from "@shivaedev/form/field.ts";
|
|
300
|
+
import { numberInput } from "@shivaedev/form/adapters/numberInput.ts";
|
|
301
|
+
import { useField } from "@shivaedev/form/adapters/react.ts";
|
|
302
|
+
|
|
303
|
+
const numbers = form(Schema.Struct({ amount: Field.number({ optional: true, min: 0, max: 100, integer: true }) }), {
|
|
304
|
+
initialValues: { amount: "" },
|
|
305
|
+
onSubmit: Effect.succeed,
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
function Amount() {
|
|
309
|
+
const amount = useField(numbers, "amount");
|
|
310
|
+
return (
|
|
311
|
+
<>
|
|
312
|
+
<input {...numberInput(amount)} />
|
|
313
|
+
<span role="alert">{amount.error}</span>
|
|
314
|
+
</>
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
`Field.number` holds a string and decodes to a finite number. `optional: true` decodes empty or whitespace-only input to `null`; encoding `null` gives `""`. Without it, blank input is required. `min` and `max` are inclusive; `integer: true` rejects fractions. Omit `integer` to accept fractions and omit either bound for an open range. Invalid, non-finite and out-of-range values prevent submission.
|
|
320
|
+
|
|
321
|
+
`numberInput(field)` supplies `name`, `value`, `onInput`, `onBlur`, `type: "text"` and `inputMode: "decimal"` for a string-valued field. A text input preserves invalid text so it can display Schema feedback; a native number input can erase that text before the form receives it. Wording and layout remain application choices.
|
|
322
|
+
|
|
323
|
+
### Bounded lists
|
|
324
|
+
|
|
325
|
+
```tsx
|
|
326
|
+
import * as Field from "@shivaedev/form/field.ts";
|
|
327
|
+
import { useList } from "@shivaedev/form/adapters/useList.ts";
|
|
328
|
+
|
|
329
|
+
const quickAmounts = form(Schema.Struct({ amounts: Field.list(Field.number({ min: 1 }), { max: 5 }) }), {
|
|
330
|
+
initialValues: { amounts: ["1", "2"] },
|
|
331
|
+
onSubmit: Effect.succeed,
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
function QuickAmounts() {
|
|
335
|
+
const amounts = useList(quickAmounts, "amounts");
|
|
336
|
+
return (
|
|
337
|
+
<>
|
|
338
|
+
{amounts.items.map((item, index) => (
|
|
339
|
+
<div key={index}>
|
|
340
|
+
<input {...numberInput({ name: String(index), value: item.value, onChange: item.onChange, onBlur: amounts.onBlur })} />
|
|
341
|
+
<span role="alert">{item.error}</span>
|
|
342
|
+
<button type="button" onClick={item.onRemove}>
|
|
343
|
+
Remove
|
|
344
|
+
</button>
|
|
345
|
+
</div>
|
|
346
|
+
))}
|
|
347
|
+
<button type="button" onClick={() => amounts.add("")}>
|
|
348
|
+
Add amount
|
|
349
|
+
</button>
|
|
350
|
+
<span role="alert">{amounts.error}</span>
|
|
351
|
+
</>
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
`Field.list(item, { max, min? })` returns a native array schema with a required maximum count and optional minimum count. An omitted minimum permits an empty list. Bounds validate the current array, including direct edits: adding beyond `max` or removing below `min` displays list feedback and prevents submission. The binding keeps the edit available for correction.
|
|
357
|
+
|
|
358
|
+
`useList(form, name)` edits a top-level array field through the existing form session. `add(encodedItem)` appends an item; each entry of `items` exposes its encoded `value`, `onChange(encodedItem)`, `onRemove()` and `error`. `onBlur()` touches the list. Item errors use `errorAt([name, index])`, and `error` retains the list summary. Schema errors track current positions. For object items, supply stable keys through `arrayKeys` and use the same keys in React; handler rejections then follow the existing keyed-array rules described above. Nested objects remain whole-item edits.
|
|
359
|
+
|
|
292
360
|
### Choices and nullable inputs
|
|
293
361
|
|
|
294
362
|
```ts
|
|
@@ -307,14 +375,17 @@ For a form made with these fields, `order.choices("visibility")` returns the enc
|
|
|
307
375
|
|
|
308
376
|
Import the module that defines a name; there is no root entry.
|
|
309
377
|
|
|
310
|
-
| Module
|
|
311
|
-
|
|
|
312
|
-
| `@shivaedev/form/form.ts`
|
|
313
|
-
| `@shivaedev/form/shape.ts`
|
|
314
|
-
| `@shivaedev/form/fieldRejection.ts`
|
|
315
|
-
| `@shivaedev/form/adapters/react.ts`
|
|
316
|
-
| `@shivaedev/form/draftSession.ts`
|
|
317
|
-
| `@shivaedev/form/
|
|
378
|
+
| Module | Application-facing exports |
|
|
379
|
+
| ----------------------------------------- | ----------------------------------------------------------------------------------------- |
|
|
380
|
+
| `@shivaedev/form/form.ts` | `form(schema, config)` |
|
|
381
|
+
| `@shivaedev/form/shape.ts` | `Form`, `Submitter`, `FieldFailure`, `Invalid` and the field types they use |
|
|
382
|
+
| `@shivaedev/form/fieldRejection.ts` | `rejecting`, `fieldRejectionOf`, `FieldRejection`, `RejectField`, `FieldRejectionMapping` |
|
|
383
|
+
| `@shivaedev/form/adapters/react.ts` | `useField`, `useSubmit`, `useDirty`, `usePersistedDraft`, `DraftSessionContext` |
|
|
384
|
+
| `@shivaedev/form/draftSession.ts` | `draftSession(id)` |
|
|
385
|
+
| `@shivaedev/form/field.ts` | `number`, `list`, `NumberOptions`, `ListOptions` |
|
|
386
|
+
| `@shivaedev/form/adapters/numberInput.ts` | `numberInput` |
|
|
387
|
+
| `@shivaedev/form/adapters/useList.ts` | `useList` |
|
|
388
|
+
| `@shivaedev/form/emptyAsNull.ts` | `emptyAsNull` |
|
|
318
389
|
|
|
319
390
|
`form` takes a `Schema.Struct` and this configuration:
|
|
320
391
|
|
|
@@ -352,6 +423,8 @@ The returned form exposes:
|
|
|
352
423
|
|
|
353
424
|
The lower-level `draft.ts` (`draft`), `refs.ts` (`fromRef`, `propertyRef`) and `statusOf.ts` (`statusOf`) modules hold the machinery the form composes; the examples use the form API instead.
|
|
354
425
|
|
|
426
|
+
For a custom lower-level `Persistence`, `observe(receive)` calls `receive` with decoded stored input (or `undefined` after removal) and returns a cleanup function. The receiver returns `true` only when it adopted the complete incoming input; `false` preserves the local revision when local fields remain. `revision()` captures a revision reader for a submission; `write(..., sameInput)` can keep that captured reader current when only untouched server fields refresh.
|
|
427
|
+
|
|
355
428
|
### Install and limits
|
|
356
429
|
|
|
357
430
|
```sh
|
|
@@ -365,7 +438,7 @@ pnpm add @effect/atom-react react
|
|
|
365
438
|
```
|
|
366
439
|
|
|
367
440
|
The React mounting example also uses the application's `react-dom`. React and `@effect/atom-react` are optional peers; the core modules do not import them. The package needs Node 24 or newer.
|
|
368
|
-
- Field names are top-level schema keys.
|
|
441
|
+
- Field names are top-level schema keys. `useList` edits top-level arrays; nested items remain whole-item edits.
|
|
369
442
|
- Replace encoded values rather than mutating objects in place; use `change` for ordinary UI edits.
|
|
370
443
|
- Keep the form and registry for their intended session, and clean up the registry when that session ends.
|
|
371
444
|
- Decide in the application when to receive normalized rows and how to render general failures. Attach a draft owner when persisted input must end with the session.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"browserDraftStorage.d.ts","sourceRoot":"","sources":["../../src/adapters/browserDraftStorage.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"browserDraftStorage.d.ts","sourceRoot":"","sources":["../../src/adapters/browserDraftStorage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,KAAK,EAAE,MAAM,QAAQ,CAAC;AACvC,OAAO,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAOvD,eAAO,MAAM,mBAAmB,iDAuB7B,CAAC"}
|
|
@@ -1,4 +1,29 @@
|
|
|
1
|
-
import { Layer } from "effect";
|
|
1
|
+
import { Effect, Layer } from "effect";
|
|
2
2
|
import { DraftStorageProvider } from "#persistence.ts";
|
|
3
|
-
|
|
3
|
+
function mutate(key, run) {
|
|
4
|
+
const locks = globalThis.navigator?.locks;
|
|
5
|
+
return locks ? Effect.promise(() => locks.request(`@shivaedev/form/${key}`, run)) : run();
|
|
6
|
+
}
|
|
7
|
+
export const browserDraftStorage = Layer.succeed(DraftStorageProvider, () => ({
|
|
8
|
+
getItem: (key) => globalThis.localStorage.getItem(key),
|
|
9
|
+
removeItem: (key, expected) => mutate(key, () => {
|
|
10
|
+
if (globalThis.localStorage.getItem(key) === expected) {
|
|
11
|
+
globalThis.localStorage.removeItem(key);
|
|
12
|
+
}
|
|
13
|
+
}),
|
|
14
|
+
setItem: (key, value, expected) => mutate(key, () => {
|
|
15
|
+
if (expected === undefined || globalThis.localStorage.getItem(key) === expected) {
|
|
16
|
+
globalThis.localStorage.setItem(key, value);
|
|
17
|
+
}
|
|
18
|
+
}),
|
|
19
|
+
subscribe: (key, listener) => {
|
|
20
|
+
function changed(event) {
|
|
21
|
+
if (event.storageArea === globalThis.localStorage && (event.key === key || event.key === null)) {
|
|
22
|
+
listener(event.newValue);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
globalThis.addEventListener("storage", changed);
|
|
26
|
+
return () => globalThis.removeEventListener("storage", changed);
|
|
27
|
+
},
|
|
28
|
+
}));
|
|
4
29
|
//# sourceMappingURL=browserDraftStorage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"browserDraftStorage.js","sourceRoot":"","sources":["../../src/adapters/browserDraftStorage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;
|
|
1
|
+
{"version":3,"file":"browserDraftStorage.js","sourceRoot":"","sources":["../../src/adapters/browserDraftStorage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AACvC,OAAO,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAEvD,SAAS,MAAM,CAAC,GAAW,EAAE,GAAe;IAC3C,MAAM,KAAK,GAAG,UAAU,CAAC,SAAS,EAAE,KAAK,CAAC;IAC1C,OAAO,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,mBAAmB,GAAG,EAAE,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC;AAC3F,CAAC;AAED,MAAM,CAAC,MAAM,mBAAmB,GAAG,KAAK,CAAC,OAAO,CAAC,oBAAoB,EAAE,GAAG,EAAE,CAAC,CAAC;IAC7E,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,UAAU,CAAC,YAAY,CAAC,OAAO,CAAC,GAAG,CAAC;IACtD,UAAU,EAAE,CAAC,GAAG,EAAE,QAAQ,EAAE,EAAE,CAC7B,MAAM,CAAC,GAAG,EAAE,GAAG,EAAE;QAChB,IAAI,UAAU,CAAC,YAAY,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,QAAQ,EAAE,CAAC;YACvD,UAAU,CAAC,YAAY,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;QACzC,CAAC;IACF,CAAC,CAAC;IACH,OAAO,EAAE,CAAC,GAAG,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,CACjC,MAAM,CAAC,GAAG,EAAE,GAAG,EAAE;QAChB,IAAI,QAAQ,KAAK,SAAS,IAAI,UAAU,CAAC,YAAY,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,QAAQ,EAAE,CAAC;YACjF,UAAU,CAAC,YAAY,CAAC,OAAO,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;QAC7C,CAAC;IACF,CAAC,CAAC;IACH,SAAS,EAAE,CAAC,GAAG,EAAE,QAAQ,EAAE,EAAE;QAC5B,SAAS,OAAO,CAAC,KAAmB;YACnC,IAAI,KAAK,CAAC,WAAW,KAAK,UAAU,CAAC,YAAY,IAAI,CAAC,KAAK,CAAC,GAAG,KAAK,GAAG,IAAI,KAAK,CAAC,GAAG,KAAK,IAAI,CAAC,EAAE,CAAC;gBAChG,QAAQ,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;YAC1B,CAAC;QACF,CAAC;QACD,UAAU,CAAC,gBAAgB,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;QAChD,OAAO,GAAG,EAAE,CAAC,UAAU,CAAC,mBAAmB,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;IACjE,CAAC;CACD,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
import type { PersistedDraftConfig, StoredValues } from "#persistence.ts";
|
|
3
|
+
export declare function draftCodec<TSchema extends Schema.Constraint>(schema: TSchema, config: PersistedDraftConfig): {
|
|
4
|
+
read: (raw: string) => StoredValues<TSchema["Encoded"]>;
|
|
5
|
+
write: (baseline: TSchema["Encoded"], values: TSchema["Encoded"], edited: readonly string[]) => string;
|
|
6
|
+
};
|
|
7
|
+
//# sourceMappingURL=draftCodec.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"draftCodec.d.ts","sourceRoot":"","sources":["../../src/adapters/draftCodec.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAgB,MAAM,QAAQ,CAAC;AAC9C,OAAO,KAAK,EAAE,oBAAoB,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAE1E,wBAAgB,UAAU,CAAC,OAAO,SAAS,MAAM,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,oBAAoB;gBAcvF,MAAM,KAAG,YAAY,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;sBAmBzC,OAAO,CAAC,SAAS,CAAC,UAAU,OAAO,CAAC,SAAS,CAAC,UAAU,SAAS,MAAM,EAAE;EAM5F"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Schema, SchemaParser } from "effect";
|
|
2
|
+
export function draftCodec(schema, config) {
|
|
3
|
+
const StoredDraft = Schema.Struct({
|
|
4
|
+
baseline: Schema.Unknown,
|
|
5
|
+
edited: Schema.Array(Schema.String),
|
|
6
|
+
session: Schema.String,
|
|
7
|
+
updatedAt: Schema.Number,
|
|
8
|
+
values: Schema.Unknown,
|
|
9
|
+
version: Schema.Literal(1),
|
|
10
|
+
});
|
|
11
|
+
const valid = Schema.is(StoredDraft);
|
|
12
|
+
const decode = SchemaParser.decodeUnknownSync(Schema.toEncoded(schema), { disableChecks: true });
|
|
13
|
+
const maxAgeMs = config.maxAgeMs ?? 7 * 24 * 60 * 60 * 1000;
|
|
14
|
+
const maxBytes = config.maxBytes ?? 256 * 1024;
|
|
15
|
+
const session = config.session?.id ?? "";
|
|
16
|
+
function read(raw) {
|
|
17
|
+
const value = new TextEncoder().encode(raw).byteLength <= maxBytes ? JSON.parse(raw) : undefined;
|
|
18
|
+
const now = config.now();
|
|
19
|
+
if (!valid(value)
|
|
20
|
+
|| value.session !== session
|
|
21
|
+
|| !Number.isFinite(maxAgeMs)
|
|
22
|
+
|| !Number.isFinite(maxBytes)
|
|
23
|
+
|| maxAgeMs <= 0
|
|
24
|
+
|| maxBytes <= 0
|
|
25
|
+
|| now - value.updatedAt > maxAgeMs
|
|
26
|
+
|| value.updatedAt > now) {
|
|
27
|
+
throw new Error("Invalid stored draft");
|
|
28
|
+
}
|
|
29
|
+
return { baseline: decode(value.baseline), edited: value.edited, values: decode(value.values) };
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
read,
|
|
33
|
+
write: (baseline, values, edited) => {
|
|
34
|
+
const raw = JSON.stringify({ baseline, edited, revision: crypto.randomUUID(), session, updatedAt: config.now(), values, version: 1 });
|
|
35
|
+
read(raw);
|
|
36
|
+
return raw;
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=draftCodec.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"draftCodec.js","sourceRoot":"","sources":["../../src/adapters/draftCodec.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAG9C,MAAM,UAAU,UAAU,CAAoC,MAAe,EAAE,MAA4B;IAC1G,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,CAAC;QACjC,QAAQ,EAAE,MAAM,CAAC,OAAO;QACxB,MAAM,EAAE,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC;QACnC,OAAO,EAAE,MAAM,CAAC,MAAM;QACtB,SAAS,EAAE,MAAM,CAAC,MAAM;QACxB,MAAM,EAAE,MAAM,CAAC,OAAO;QACtB,OAAO,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC;KAC1B,CAAC,CAAC;IACH,MAAM,KAAK,GAAG,MAAM,CAAC,EAAE,CAAC,WAAW,CAAC,CAAC;IACrC,MAAM,MAAM,GAAG,YAAY,CAAC,iBAAiB,CAAC,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;IACjG,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;IAC5D,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,IAAI,GAAG,GAAG,IAAI,CAAC;IAC/C,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,EAAE,EAAE,IAAI,EAAE,CAAC;IACzC,SAAS,IAAI,CAAC,GAAW;QACxB,MAAM,KAAK,GAAY,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,UAAU,IAAI,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QAC1G,MAAM,GAAG,GAAG,MAAM,CAAC,GAAG,EAAE,CAAC;QACzB,IACC,CAAC,KAAK,CAAC,KAAK,CAAC;eACV,KAAK,CAAC,OAAO,KAAK,OAAO;eACzB,CAAC,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC;eAC1B,CAAC,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC;eAC1B,QAAQ,IAAI,CAAC;eACb,QAAQ,IAAI,CAAC;eACb,GAAG,GAAG,KAAK,CAAC,SAAS,GAAG,QAAQ;eAChC,KAAK,CAAC,SAAS,GAAG,GAAG,EACvB,CAAC;YACF,MAAM,IAAI,KAAK,CAAC,sBAAsB,CAAC,CAAC;QACzC,CAAC;QACD,OAAO,EAAE,QAAQ,EAAE,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC;IACjG,CAAC;IACD,OAAO;QACN,IAAI;QACJ,KAAK,EAAE,CAAC,QAA4B,EAAE,MAA0B,EAAE,MAAyB,EAAE,EAAE;YAC9F,MAAM,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,CAAC,UAAU,EAAE,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,CAAC,GAAG,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC;YACtI,IAAI,CAAC,GAAG,CAAC,CAAC;YACV,OAAO,GAAG,CAAC;QACZ,CAAC;KACD,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { InputEvent } from "react";
|
|
2
|
+
export declare function numberInput(field: {
|
|
3
|
+
readonly name: string;
|
|
4
|
+
readonly value: string;
|
|
5
|
+
readonly onChange: (value: string) => void;
|
|
6
|
+
readonly onBlur: () => void;
|
|
7
|
+
}): {
|
|
8
|
+
inputMode: "decimal";
|
|
9
|
+
name: string;
|
|
10
|
+
onBlur: () => void;
|
|
11
|
+
onInput: (event: InputEvent<HTMLInputElement>) => void;
|
|
12
|
+
type: "text";
|
|
13
|
+
value: string;
|
|
14
|
+
};
|
|
15
|
+
//# sourceMappingURL=numberInput.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"numberInput.d.ts","sourceRoot":"","sources":["../../src/adapters/numberInput.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,OAAO,CAAC;AAExC,wBAAgB,WAAW,CAAC,KAAK,EAAE;IAClC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3C,QAAQ,CAAC,MAAM,EAAE,MAAM,IAAI,CAAC;CAC5B;IAEC,SAAS,EAAE,SAAS;IACpB,IAAI;IACJ,MAAM,QALgB,IAAI;IAM1B,OAAO,UAAU,UAAU,CAAC,gBAAgB,CAAC;IAC7C,IAAI,EAAE,MAAM;IACZ,KAAK;EAEN"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function numberInput(field) {
|
|
2
|
+
return {
|
|
3
|
+
inputMode: "decimal",
|
|
4
|
+
name: field.name,
|
|
5
|
+
onBlur: field.onBlur,
|
|
6
|
+
onInput: (event) => field.onChange(event.currentTarget.value),
|
|
7
|
+
type: "text",
|
|
8
|
+
value: field.value,
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=numberInput.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"numberInput.js","sourceRoot":"","sources":["../../src/adapters/numberInput.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,WAAW,CAAC,KAK3B;IACA,OAAO;QACN,SAAS,EAAE,SAAkB;QAC7B,IAAI,EAAE,KAAK,CAAC,IAAI;QAChB,MAAM,EAAE,KAAK,CAAC,MAAM;QACpB,OAAO,EAAE,CAAC,KAAmC,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,aAAa,CAAC,KAAK,CAAC;QAC3F,IAAI,EAAE,MAAe;QACrB,KAAK,EAAE,KAAK,CAAC,KAAK;KAClB,CAAC;AACH,CAAC"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Schema } from "effect";
|
|
1
|
+
import { type Schema } from "effect";
|
|
2
2
|
import type { PersistedDraftConfig, Persistence } from "#persistence.ts";
|
|
3
3
|
export declare function persistence<TSchema extends Schema.Constraint>(schema: TSchema, config: PersistedDraftConfig): Persistence<TSchema["Encoded"]>;
|
|
4
4
|
//# sourceMappingURL=persistence.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"persistence.d.ts","sourceRoot":"","sources":["../../src/adapters/persistence.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"persistence.d.ts","sourceRoot":"","sources":["../../src/adapters/persistence.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,KAAK,MAAM,EAAa,MAAM,QAAQ,CAAC;AAC9D,OAAO,KAAK,EAAkB,oBAAoB,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAYzF,wBAAgB,WAAW,CAAC,OAAO,SAAS,MAAM,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,oBAAoB,GAAG,WAAW,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CA2G7I"}
|
|
@@ -1,83 +1,113 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Data, Effect, Semaphore } from "effect";
|
|
2
|
+
import { draftCodec } from "./draftCodec.js";
|
|
3
|
+
class DraftStorageUnavailable extends Data.TaggedError("DraftStorageUnavailable") {
|
|
4
|
+
}
|
|
5
|
+
function operation(run) {
|
|
6
|
+
return Effect.suspend(() => {
|
|
7
|
+
const value = run();
|
|
8
|
+
return Effect.isEffect(value) ? value : Effect.succeed(value);
|
|
9
|
+
}).pipe(Effect.catchCause((cause) => Effect.fail(new DraftStorageUnavailable({ cause }))));
|
|
10
|
+
}
|
|
2
11
|
export function persistence(schema, config) {
|
|
3
|
-
const
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
edited: Schema.Array(Schema.String),
|
|
7
|
-
session: Schema.String,
|
|
8
|
-
updatedAt: Schema.Number,
|
|
9
|
-
values: Schema.Unknown,
|
|
10
|
-
version: Schema.Literal(1),
|
|
11
|
-
});
|
|
12
|
-
const valid = Schema.is(StoredDraft);
|
|
13
|
-
const decode = SchemaParser.decodeUnknownSync(encoded, { disableChecks: true });
|
|
14
|
-
const maxAgeMs = config.maxAgeMs ?? 7 * 24 * 60 * 60 * 1000;
|
|
15
|
-
const maxBytes = config.maxBytes ?? 256 * 1024;
|
|
16
|
-
function bytes(value) {
|
|
17
|
-
return new TextEncoder().encode(value).byteLength;
|
|
18
|
-
}
|
|
19
|
-
const storage = config.storage;
|
|
20
|
-
const session = config.session?.id ?? "";
|
|
12
|
+
const codec = draftCodec(schema, config);
|
|
13
|
+
const serial = Semaphore.makeUnsafe(1);
|
|
14
|
+
let revision = { value: undefined };
|
|
21
15
|
function active() {
|
|
22
16
|
return config.session?.active !== false;
|
|
23
17
|
}
|
|
24
|
-
function
|
|
18
|
+
function run(effect) {
|
|
19
|
+
return Effect.runCallback(serial.withPermit(effect).pipe(Effect.catchTag("DraftStorageUnavailable", () => Effect.void)));
|
|
20
|
+
}
|
|
21
|
+
function clear(expected = revision.value) {
|
|
22
|
+
if (expected !== undefined) {
|
|
23
|
+
run(operation(() => config.storage().removeItem(config.key, expected)));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function read(raw) {
|
|
25
27
|
try {
|
|
26
|
-
return
|
|
28
|
+
return { values: raw === null ? undefined : codec.read(raw) };
|
|
27
29
|
}
|
|
28
30
|
catch {
|
|
29
|
-
|
|
31
|
+
if (raw !== null) {
|
|
32
|
+
clear(raw);
|
|
33
|
+
}
|
|
30
34
|
return undefined;
|
|
31
35
|
}
|
|
32
36
|
}
|
|
33
|
-
function
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
+
config.session?.track(config.key, () => run(Effect.gen(function* () {
|
|
38
|
+
const storage = yield* operation(config.storage);
|
|
39
|
+
const raw = yield* operation(() => storage.getItem(config.key));
|
|
40
|
+
if (raw !== null) {
|
|
41
|
+
yield* operation(() => codec.read(raw));
|
|
42
|
+
yield* operation(() => storage.removeItem(config.key, raw));
|
|
43
|
+
}
|
|
44
|
+
})));
|
|
37
45
|
return {
|
|
38
|
-
clear: () => {
|
|
46
|
+
clear: (expected = revision.value) => {
|
|
39
47
|
if (active()) {
|
|
40
|
-
clear();
|
|
48
|
+
clear(expected);
|
|
41
49
|
}
|
|
42
50
|
},
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
&&
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
&& maxAgeMs > 0
|
|
58
|
-
&& maxBytes > 0
|
|
59
|
-
&& now - value.updatedAt <= maxAgeMs
|
|
60
|
-
&& value.updatedAt <= now) {
|
|
61
|
-
return { baseline: decode(value.baseline), edited: value.edited, values: decode(value.values) };
|
|
51
|
+
observe: (receive) => {
|
|
52
|
+
let stopped = false;
|
|
53
|
+
let changes = 0;
|
|
54
|
+
function changed(raw, loaded = false) {
|
|
55
|
+
if (stopped || !active()) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (!loaded && revision.value === (raw ?? undefined)) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const parsed = read(raw);
|
|
62
|
+
if (parsed && receive(parsed.values)) {
|
|
63
|
+
revision = { value: raw ?? undefined };
|
|
64
|
+
}
|
|
62
65
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
66
|
+
let unsubscribe;
|
|
67
|
+
const stop = Effect.runCallback(Effect.gen(function* () {
|
|
68
|
+
if (!active()) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const storage = yield* operation(config.storage);
|
|
72
|
+
unsubscribe = yield* operation(() => storage.subscribe?.(config.key, (incoming) => {
|
|
73
|
+
changes += 1;
|
|
74
|
+
changed(incoming);
|
|
75
|
+
}));
|
|
76
|
+
const before = changes;
|
|
77
|
+
const captured = revision;
|
|
78
|
+
const raw = yield* operation(() => storage.getItem(config.key));
|
|
79
|
+
if (before === changes && captured === revision) {
|
|
80
|
+
changed(raw, true);
|
|
81
|
+
}
|
|
82
|
+
}).pipe(Effect.catchTag("DraftStorageUnavailable", () => Effect.void)));
|
|
83
|
+
return () => {
|
|
84
|
+
stopped = true;
|
|
85
|
+
unsubscribe?.();
|
|
86
|
+
stop();
|
|
87
|
+
};
|
|
88
|
+
},
|
|
89
|
+
revision: () => {
|
|
90
|
+
const captured = revision;
|
|
91
|
+
return () => captured.value;
|
|
92
|
+
},
|
|
93
|
+
write: (baseline, values, edited, sameInput) => {
|
|
67
94
|
if (!active()) {
|
|
68
95
|
return;
|
|
69
96
|
}
|
|
70
|
-
|
|
71
|
-
const
|
|
72
|
-
const
|
|
73
|
-
if (
|
|
74
|
-
|
|
75
|
-
|
|
97
|
+
try {
|
|
98
|
+
const expected = sameInput ? revision.value : undefined;
|
|
99
|
+
const raw = codec.write(baseline, values, edited);
|
|
100
|
+
if (sameInput) {
|
|
101
|
+
revision.value = raw;
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
revision = { value: raw };
|
|
76
105
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
106
|
+
run(operation(() => (active() ? config.storage().setItem(config.key, raw, expected) : undefined)));
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
clear();
|
|
110
|
+
}
|
|
81
111
|
},
|
|
82
112
|
};
|
|
83
113
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"persistence.js","sourceRoot":"","sources":["../../src/adapters/persistence.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"persistence.js","sourceRoot":"","sources":["../../src/adapters/persistence.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,EAAe,SAAS,EAAE,MAAM,QAAQ,CAAC;AAE9D,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAE7C,MAAM,uBAAwB,SAAQ,IAAI,CAAC,WAAW,CAAC,yBAAyB,CAA8B;CAAG;AAEjH,SAAS,SAAS,CAAI,GAA4B;IACjD,OAAO,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE;QAC1B,MAAM,KAAK,GAAG,GAAG,EAAE,CAAC;QACpB,OAAO,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;IAC/D,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,uBAAuB,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;AAC5F,CAAC;AAED,MAAM,UAAU,WAAW,CAAoC,MAAe,EAAE,MAA4B;IAC3G,MAAM,KAAK,GAAG,UAAU,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACzC,MAAM,MAAM,GAAG,SAAS,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;IACvC,IAAI,QAAQ,GAAkC,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC;IACnE,SAAS,MAAM;QACd,OAAO,MAAM,CAAC,OAAO,EAAE,MAAM,KAAK,KAAK,CAAC;IACzC,CAAC;IACD,SAAS,GAAG,CAAC,MAAuD;QACnE,OAAO,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,yBAAyB,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC1H,CAAC;IACD,SAAS,KAAK,CAAC,QAAQ,GAAG,QAAQ,CAAC,KAAK;QACvC,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;YAC5B,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC,UAAU,CAAC,MAAM,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC;QACzE,CAAC;IACF,CAAC;IACD,SAAS,IAAI,CAAC,GAAkB;QAC/B,IAAI,CAAC;YACJ,OAAO,EAAE,MAAM,EAAE,GAAG,KAAK,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;QAC/D,CAAC;QAAC,MAAM,CAAC;YACR,IAAI,GAAG,KAAK,IAAI,EAAE,CAAC;gBAClB,KAAK,CAAC,GAAG,CAAC,CAAC;YACZ,CAAC;YACD,OAAO,SAAS,CAAC;QAClB,CAAC;IACF,CAAC;IACD,MAAM,CAAC,OAAO,EAAE,KAAK,CAAC,MAAM,CAAC,GAAG,EAAE,GAAG,EAAE,CACtC,GAAG,CACF,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;QACnB,MAAM,OAAO,GAAG,KAAK,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QACjD,MAAM,GAAG,GAAG,KAAK,CAAC,CAAC,SAAS,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;QAChE,IAAI,GAAG,KAAK,IAAI,EAAE,CAAC;YAClB,KAAK,CAAC,CAAC,SAAS,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;YACxC,KAAK,CAAC,CAAC,SAAS,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,UAAU,CAAC,MAAM,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC;QAC7D,CAAC;IACF,CAAC,CAAC,CACF,CACD,CAAC;IACF,OAAO;QACN,KAAK,EAAE,CAAC,QAAQ,GAAG,QAAQ,CAAC,KAAK,EAAE,EAAE;YACpC,IAAI,MAAM,EAAE,EAAE,CAAC;gBACd,KAAK,CAAC,QAAQ,CAAC,CAAC;YACjB,CAAC;QACF,CAAC;QACD,OAAO,EAAE,CAAC,OAAO,EAAE,EAAE;YACpB,IAAI,OAAO,GAAG,KAAK,CAAC;YACpB,IAAI,OAAO,GAAG,CAAC,CAAC;YAChB,SAAS,OAAO,CAAC,GAAkB,EAAE,MAAM,GAAG,KAAK;gBAClD,IAAI,OAAO,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC;oBAC1B,OAAO;gBACR,CAAC;gBACD,IAAI,CAAC,MAAM,IAAI,QAAQ,CAAC,KAAK,KAAK,CAAC,GAAG,IAAI,SAAS,CAAC,EAAE,CAAC;oBACtD,OAAO;gBACR,CAAC;gBACD,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC;gBACzB,IAAI,MAAM,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC;oBACtC,QAAQ,GAAG,EAAE,KAAK,EAAE,GAAG,IAAI,SAAS,EAAE,CAAC;gBACxC,CAAC;YACF,CAAC;YACD,IAAI,WAAqC,CAAC;YAC1C,MAAM,IAAI,GAAG,MAAM,CAAC,WAAW,CAC9B,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;gBACnB,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC;oBACf,OAAO;gBACR,CAAC;gBACD,MAAM,OAAO,GAAG,KAAK,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;gBACjD,WAAW,GAAG,KAAK,CAAC,CAAC,SAAS,CAAC,GAAG,EAAE,CACnC,OAAO,CAAC,SAAS,EAAE,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,EAAE;oBAC5C,OAAO,IAAI,CAAC,CAAC;oBACb,OAAO,CAAC,QAAQ,CAAC,CAAC;gBACnB,CAAC,CAAC,CACF,CAAC;gBACF,MAAM,MAAM,GAAG,OAAO,CAAC;gBACvB,MAAM,QAAQ,GAAG,QAAQ,CAAC;gBAC1B,MAAM,GAAG,GAAG,KAAK,CAAC,CAAC,SAAS,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;gBAChE,IAAI,MAAM,KAAK,OAAO,IAAI,QAAQ,KAAK,QAAQ,EAAE,CAAC;oBACjD,OAAO,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;gBACpB,CAAC;YACF,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,yBAAyB,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CACtE,CAAC;YACF,OAAO,GAAG,EAAE;gBACX,OAAO,GAAG,IAAI,CAAC;gBACf,WAAW,EAAE,EAAE,CAAC;gBAChB,IAAI,EAAE,CAAC;YACR,CAAC,CAAC;QACH,CAAC;QACD,QAAQ,EAAE,GAAG,EAAE;YACd,MAAM,QAAQ,GAAG,QAAQ,CAAC;YAC1B,OAAO,GAAG,EAAE,CAAC,QAAQ,CAAC,KAAK,CAAC;QAC7B,CAAC;QACD,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,EAAE;YAC9C,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC;gBACf,OAAO;YACR,CAAC;YACD,IAAI,CAAC;gBACJ,MAAM,QAAQ,GAAG,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;gBACxD,MAAM,GAAG,GAAG,KAAK,CAAC,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;gBAClD,IAAI,SAAS,EAAE,CAAC;oBACf,QAAQ,CAAC,KAAK,GAAG,GAAG,CAAC;gBACtB,CAAC;qBAAM,CAAC;oBACP,QAAQ,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC;gBAC3B,CAAC;gBACD,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,EAAE,GAAG,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;YACpG,CAAC;YAAC,MAAM,CAAC;gBACR,KAAK,EAAE,CAAC;YACT,CAAC;QACF,CAAC;KACD,CAAC;AACH,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../../src/adapters/react.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,WAAW,MAAM,+BAA+B,CAAC;AAElE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAC5D,OAAO,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACpF,eAAO,MAAM,mBAAmB,mDAAqD,CAAC;AACtF,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,oBAAoB,GAAG,oBAAoB,CAAC;AACtF,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,oBAAoB,GAAG,SAAS,GAAG,oBAAoB,GAAG,SAAS,CAAC;AAK9G,MAAM,WAAW,KAAK,CAAC,OAAO,SAAS,MAAM,EAAE,IAAI,SAAS,IAAI,CAAC,OAAO,CAAC;IACxE,QAAQ,CAAC,OAAO,EAAE,SAAS,OAAO,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,GAAG,SAAS,CAAC;IAChE,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IACnC,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;IACpB,QAAQ,CAAC,MAAM,EAAE,MAAM,IAAI,CAAC;IAC5B,QAAQ,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC;IAC3D,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC;CACvC;AACD,wBAAgB,QAAQ,CAAC,OAAO,SAAS,MAAM,EAAE,CAAC,EAAE,CAAC,EAAE,aAAa,EAAE,IAAI,SAAS,IAAI,CAAC,OAAO,CAAC,EAC/F,IAAI,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,EAAE,aAAa,CAAC,EACxC,IAAI,EAAE,IAAI,GACR,KAAK,CAAC,OAAO,EAAE,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../../src/adapters/react.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,WAAW,MAAM,+BAA+B,CAAC;AAElE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAC5D,OAAO,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACpF,eAAO,MAAM,mBAAmB,mDAAqD,CAAC;AACtF,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,oBAAoB,GAAG,oBAAoB,CAAC;AACtF,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,oBAAoB,GAAG,SAAS,GAAG,oBAAoB,GAAG,SAAS,CAAC;AAK9G,MAAM,WAAW,KAAK,CAAC,OAAO,SAAS,MAAM,EAAE,IAAI,SAAS,IAAI,CAAC,OAAO,CAAC;IACxE,QAAQ,CAAC,OAAO,EAAE,SAAS,OAAO,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,GAAG,SAAS,CAAC;IAChE,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IACnC,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;IACpB,QAAQ,CAAC,MAAM,EAAE,MAAM,IAAI,CAAC;IAC5B,QAAQ,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC;IAC3D,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC;CACvC;AACD,wBAAgB,QAAQ,CAAC,OAAO,SAAS,MAAM,EAAE,CAAC,EAAE,CAAC,EAAE,aAAa,EAAE,IAAI,SAAS,IAAI,CAAC,OAAO,CAAC,EAC/F,IAAI,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,EAAE,aAAa,CAAC,EACxC,IAAI,EAAE,IAAI,GACR,KAAK,CAAC,OAAO,EAAE,IAAI,CAAC,CAQtB;AACD,MAAM,WAAW,MAAM,CAAC,CAAC,EAAE,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC,WAAW,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAC/C,QAAQ,CAAC,GAAG,EAAE,MAAM,IAAI,CAAC;IACzB,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;CAC7B;AACD,wBAAgB,SAAS,CAAC,OAAO,SAAS,MAAM,EAAE,CAAC,EAAE,CAAC,EAAE,aAAa,EACpE,IAAI,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,EAAE,aAAa,CAAC,GACtC,MAAM,CAAC,CAAC,EAAE,CAAC,GAAG,aAAa,GAAG,YAAY,GAAG,OAAO,CAAC,CAMvD;AACD,wBAAgB,QAAQ,CAAC,OAAO,SAAS,MAAM,EAAE,CAAC,EAAE,CAAC,EAAE,aAAa,EAAE,IAAI,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,EAAE,aAAa,CAAC,GAAG,OAAO,CAEvH"}
|
package/dist/adapters/react.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useAtomRef, useAtomSet, useAtomValue } from "@effect/atom-react";
|
|
1
|
+
import { useAtomMount, useAtomRef, useAtomSet, useAtomValue } from "@effect/atom-react";
|
|
2
2
|
import { createContext, useCallback, useContext, useMemo } from "react";
|
|
3
3
|
export const DraftSessionContext = createContext(undefined);
|
|
4
4
|
export function usePersistedDraft(config) {
|
|
@@ -6,6 +6,7 @@ export function usePersistedDraft(config) {
|
|
|
6
6
|
return config ? { ...config, session: session ?? config.session } : undefined;
|
|
7
7
|
}
|
|
8
8
|
export function useField(form, name) {
|
|
9
|
+
useAtomMount(form.dirty);
|
|
9
10
|
const value = useAtomRef(form.field(name));
|
|
10
11
|
const error = useAtomValue(form.error(name));
|
|
11
12
|
const onChange = useCallback((next) => form.change(name, next), [form, name]);
|
|
@@ -14,6 +15,7 @@ export function useField(form, name) {
|
|
|
14
15
|
return { choices, error, name, onBlur, onChange, value };
|
|
15
16
|
}
|
|
16
17
|
export function useSubmit(form) {
|
|
18
|
+
useAtomMount(form.dirty);
|
|
17
19
|
const result = useAtomValue(form.submit);
|
|
18
20
|
const set = useAtomSet(form.submit);
|
|
19
21
|
const run = useCallback(() => set(), [set]);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"react.js","sourceRoot":"","sources":["../../src/adapters/react.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"react.js","sourceRoot":"","sources":["../../src/adapters/react.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAExF,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,OAAO,CAAC;AAIxE,MAAM,CAAC,MAAM,mBAAmB,GAAG,aAAa,CAA2B,SAAS,CAAC,CAAC;AAGtF,MAAM,UAAU,iBAAiB,CAAC,MAAwC;IACzE,MAAM,OAAO,GAAG,UAAU,CAAC,mBAAmB,CAAC,CAAC;IAChD,OAAO,MAAM,CAAC,CAAC,CAAC,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,OAAO,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;AAC/E,CAAC;AASD,MAAM,UAAU,QAAQ,CACvB,IAAwC,EACxC,IAAU;IAEV,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACzB,MAAM,KAAK,GAAG,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;IAC3C,MAAM,KAAK,GAAG,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;IAC7C,MAAM,QAAQ,GAAG,WAAW,CAAC,CAAC,IAA4B,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC;IACtG,MAAM,MAAM,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC;IAChE,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC;IAChE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;AAC1D,CAAC;AAMD,MAAM,UAAU,SAAS,CACxB,IAAwC;IAExC,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACzB,MAAM,MAAM,GAAG,YAAY,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACzC,MAAM,GAAG,GAAG,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACpC,MAAM,GAAG,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC5C,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,UAAU,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC;AACpD,CAAC;AACD,MAAM,UAAU,QAAQ,CAA8C,IAAwC;IAC7G,OAAO,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AACjC,CAAC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import * as Atom from "effect/reactivity/Atom";
|
|
2
|
+
import type * as AtomRef from "effect/reactivity/AtomRef";
|
|
3
|
+
interface ListForm<TKey extends string, TItem> {
|
|
4
|
+
readonly blur: (name: TKey) => void;
|
|
5
|
+
readonly change: (name: TKey, value: readonly TItem[]) => void;
|
|
6
|
+
readonly dirty: Atom.Atom<boolean>;
|
|
7
|
+
readonly error: (name: TKey) => Atom.Atom<string | undefined>;
|
|
8
|
+
readonly errorAt: (path: readonly [TKey, ...PropertyKey[]]) => Atom.Atom<string | undefined>;
|
|
9
|
+
readonly field: (name: TKey) => AtomRef.ReadonlyRef<readonly TItem[]>;
|
|
10
|
+
}
|
|
11
|
+
export declare function useList<TKey extends string, TItem>(form: ListForm<NoInfer<TKey>, TItem>, name: TKey): {
|
|
12
|
+
add: (value: TItem) => void;
|
|
13
|
+
error: string | undefined;
|
|
14
|
+
items: {
|
|
15
|
+
error: string | undefined;
|
|
16
|
+
onChange: (next: TItem) => void;
|
|
17
|
+
onRemove: () => void;
|
|
18
|
+
value: TItem;
|
|
19
|
+
}[];
|
|
20
|
+
onBlur: () => void;
|
|
21
|
+
};
|
|
22
|
+
export {};
|
|
23
|
+
//# sourceMappingURL=useList.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useList.d.ts","sourceRoot":"","sources":["../../src/adapters/useList.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,IAAI,MAAM,wBAAwB,CAAC;AAC/C,OAAO,KAAK,KAAK,OAAO,MAAM,2BAA2B,CAAC;AAG1D,UAAU,QAAQ,CAAC,IAAI,SAAS,MAAM,EAAE,KAAK;IAC5C,QAAQ,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,IAAI,CAAC;IACpC,QAAQ,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,KAAK,EAAE,KAAK,IAAI,CAAC;IAC/D,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACnC,QAAQ,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IAC9D,QAAQ,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,SAAS,CAAC,IAAI,EAAE,GAAG,WAAW,EAAE,CAAC,KAAK,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IAC7F,QAAQ,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,WAAW,CAAC,SAAS,KAAK,EAAE,CAAC,CAAC;CACtE;AAED,wBAAgB,OAAO,CAAC,IAAI,SAAS,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,KAAK,CAAC,EAAE,IAAI,EAAE,IAAI;iBAcrF,KAAK;;;;yBAIA,KAAK;;;;;EAMxB"}
|