foldkit 0.24.0 → 0.26.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/README.md +70 -55
- package/dist/fieldValidation/index.d.ts +39 -30
- package/dist/fieldValidation/index.d.ts.map +1 -1
- package/dist/fieldValidation/index.js +25 -30
- package/dist/fieldValidation/public.d.ts +2 -2
- package/dist/fieldValidation/public.d.ts.map +1 -1
- package/dist/fieldValidation/public.js +1 -1
- package/dist/html/index.d.ts +44 -9
- package/dist/html/index.d.ts.map +1 -1
- package/dist/html/index.js +15 -3
- package/dist/html/lazy.d.ts +12 -0
- package/dist/html/lazy.d.ts.map +1 -0
- package/dist/html/lazy.js +35 -0
- package/dist/html/public.d.ts +2 -1
- package/dist/html/public.d.ts.map +1 -1
- package/dist/html/public.js +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/managedResource/index.d.ts +38 -0
- package/dist/managedResource/index.d.ts.map +1 -0
- package/dist/managedResource/index.js +20 -0
- package/dist/managedResource/public.d.ts +5 -0
- package/dist/managedResource/public.d.ts.map +1 -0
- package/dist/managedResource/public.js +2 -0
- package/dist/runtime/managedResource.d.ts +114 -0
- package/dist/runtime/managedResource.d.ts.map +1 -0
- package/dist/runtime/managedResource.js +92 -0
- package/dist/runtime/public.d.ts +2 -2
- package/dist/runtime/public.d.ts.map +1 -1
- package/dist/runtime/public.js +1 -1
- package/dist/runtime/runtime.d.ts +79 -90
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +95 -19
- package/dist/runtime/subscription.d.ts +25 -0
- package/dist/runtime/subscription.d.ts.map +1 -0
- package/dist/runtime/subscription.js +7 -0
- package/dist/struct/index.d.ts +2 -0
- package/dist/struct/index.d.ts.map +1 -1
- package/dist/struct/index.js +4 -0
- package/dist/struct/public.d.ts +1 -1
- package/dist/struct/public.d.ts.map +1 -1
- package/dist/struct/public.js +1 -1
- package/dist/subscription/public.d.ts +3 -0
- package/dist/subscription/public.d.ts.map +1 -0
- package/dist/subscription/public.js +1 -0
- package/dist/ui/anchor.d.ts +19 -0
- package/dist/ui/anchor.d.ts.map +1 -0
- package/dist/ui/{menu/anchor.js → anchor.js} +3 -2
- package/dist/ui/disclosure/index.d.ts.map +1 -1
- package/dist/ui/disclosure/index.js +3 -2
- package/dist/ui/index.d.ts +2 -0
- package/dist/ui/index.d.ts.map +1 -1
- package/dist/ui/index.js +2 -0
- package/dist/ui/listbox/multi.d.ts +172 -0
- package/dist/ui/listbox/multi.d.ts.map +1 -0
- package/dist/ui/listbox/multi.js +25 -0
- package/dist/ui/listbox/multiPublic.d.ts +3 -0
- package/dist/ui/listbox/multiPublic.d.ts.map +1 -0
- package/dist/ui/listbox/multiPublic.js +1 -0
- package/dist/ui/listbox/public.d.ts +7 -3
- package/dist/ui/listbox/public.d.ts.map +1 -1
- package/dist/ui/listbox/public.js +4 -1
- package/dist/ui/listbox/{index.d.ts → shared.d.ts} +78 -27
- package/dist/ui/listbox/shared.d.ts.map +1 -0
- package/dist/ui/listbox/{index.js → shared.js} +208 -199
- package/dist/ui/listbox/single.d.ts +172 -0
- package/dist/ui/listbox/single.d.ts.map +1 -0
- package/dist/ui/listbox/single.js +29 -0
- package/dist/ui/menu/index.d.ts +1 -4
- package/dist/ui/menu/index.d.ts.map +1 -1
- package/dist/ui/menu/index.js +2 -3
- package/dist/ui/menu/public.d.ts +3 -2
- package/dist/ui/menu/public.d.ts.map +1 -1
- package/dist/ui/menu/public.js +2 -1
- package/dist/ui/popover/index.d.ts +75 -0
- package/dist/ui/popover/index.d.ts.map +1 -0
- package/dist/ui/popover/index.js +237 -0
- package/dist/ui/popover/public.d.ts +5 -0
- package/dist/ui/popover/public.d.ts.map +1 -0
- package/dist/ui/popover/public.js +2 -0
- package/dist/ui/switch/index.d.ts +47 -0
- package/dist/ui/switch/index.d.ts.map +1 -0
- package/dist/ui/switch/index.js +66 -0
- package/dist/ui/switch/public.d.ts +3 -0
- package/dist/ui/switch/public.d.ts.map +1 -0
- package/dist/ui/switch/public.js +1 -0
- package/dist/ui/transition.d.ts +5 -0
- package/dist/ui/transition.d.ts.map +1 -0
- package/dist/ui/transition.js +3 -0
- package/package.json +17 -1
- package/dist/ui/listbox/index.d.ts.map +0 -1
- package/dist/ui/menu/anchor.d.ts +0 -18
- package/dist/ui/menu/anchor.d.ts.map +0 -1
package/README.md
CHANGED
|
@@ -1,41 +1,52 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<picture>
|
|
3
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/devinjameson/foldkit/main/packages/website/public/logo-dark.svg">
|
|
4
|
+
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/devinjameson/foldkit/main/packages/website/public/logo.svg">
|
|
5
|
+
<img src="https://raw.githubusercontent.com/devinjameson/foldkit/main/packages/website/public/logo.svg" alt="Foldkit" width="350">
|
|
6
|
+
</picture>
|
|
7
|
+
</p>
|
|
2
8
|
|
|
3
|
-
>
|
|
9
|
+
<p align="center">
|
|
10
|
+
<a href="https://www.npmjs.com/package/foldkit"><img src="https://img.shields.io/npm/v/foldkit" alt="npm version"></a>
|
|
11
|
+
</p>
|
|
4
12
|
|
|
5
|
-
|
|
13
|
+
<h3 align="center">Beautifully boring frontend applications.</h3>
|
|
6
14
|
|
|
7
|
-
>
|
|
15
|
+
<p align="center">
|
|
16
|
+
<a href="https://foldkit.dev"><strong>Documentation</strong></a> · <a href="https://github.com/devinjameson/foldkit#examples"><strong>Examples</strong></a> · <a href="https://foldkit.dev/getting-started"><strong>Getting Started</strong></a>
|
|
17
|
+
</p>
|
|
8
18
|
|
|
9
19
|
---
|
|
10
20
|
|
|
11
|
-
|
|
21
|
+
Foldkit is an [Elm Architecture](https://guide.elm-lang.org/architecture/) framework for TypeScript, powered by [Effect](https://effect.website/). One Model, one update function, one way to do things. No hooks, no local state, no hidden mutations.
|
|
12
22
|
|
|
13
|
-
|
|
23
|
+
> [!WARNING]
|
|
24
|
+
> Foldkit is pre-1.0. APIs may change between minor versions.
|
|
14
25
|
|
|
15
|
-
|
|
16
|
-
- **Controlled side effects** — Side effects are described as `Command<Message>` values and executed by the runtime, not performed directly in update functions.
|
|
17
|
-
- **Explicit state transitions** — Every state change is modeled as a specific message type and is captured in the update function.
|
|
26
|
+
## Who It's For
|
|
18
27
|
|
|
19
|
-
|
|
28
|
+
Foldkit is for developers who want their architecture to prevent bugs, not just catch them. If you want a single pattern that scales from a counter to a multiplayer game without complexity creep, this is it.
|
|
29
|
+
|
|
30
|
+
It's not incremental. There's no React interop, no escape hatch from Effect, no way to "just use hooks for this one part." You're all in or you're not.
|
|
31
|
+
|
|
32
|
+
## Built on Effect
|
|
20
33
|
|
|
21
|
-
|
|
34
|
+
Every Foldkit application is an [Effect](https://effect.website/) program. Your Model is a [Schema](https://effect.website/docs/schema/introduction/). Side effects are values you return, not callbacks you fire — the runtime handles when and how. If you already know Effect, Foldkit feels natural. If you're new to Effect, Foldkit is a great way to immerse yourself in it.
|
|
22
35
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
36
|
+
## Get Started
|
|
37
|
+
|
|
38
|
+
`create-foldkit-app` is the recommended way to start a new project. It scaffolds a complete setup with Tailwind, TypeScript, ESLint, Prettier, and the Vite plugin for state-preserving HMR — and lets you choose from a set of examples as your starting point.
|
|
26
39
|
|
|
27
40
|
```bash
|
|
28
41
|
npx create-foldkit-app@latest --wizard
|
|
29
42
|
```
|
|
30
43
|
|
|
31
|
-
|
|
44
|
+
## Counter
|
|
32
45
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
See the full example at [examples/counter/src/main.ts](https://github.com/devinjameson/foldkit/blob/main/examples/counter/src/main.ts)
|
|
46
|
+
This is a complete Foldkit program. State lives in a single Model. Events become Messages. A pure function handles every transition.
|
|
36
47
|
|
|
37
48
|
```ts
|
|
38
|
-
import { Match as M, Schema } from 'effect'
|
|
49
|
+
import { Match as M, Schema as S } from 'effect'
|
|
39
50
|
import { Runtime } from 'foldkit'
|
|
40
51
|
import { Command } from 'foldkit/command'
|
|
41
52
|
import { Html, html } from 'foldkit/html'
|
|
@@ -43,7 +54,7 @@ import { m } from 'foldkit/message'
|
|
|
43
54
|
|
|
44
55
|
// MODEL
|
|
45
56
|
|
|
46
|
-
const Model =
|
|
57
|
+
const Model = S.Struct({ count: S.Number })
|
|
47
58
|
type Model = typeof Model.Type
|
|
48
59
|
|
|
49
60
|
// MESSAGE
|
|
@@ -52,33 +63,33 @@ const ClickedDecrement = m('ClickedDecrement')
|
|
|
52
63
|
const ClickedIncrement = m('ClickedIncrement')
|
|
53
64
|
const ClickedReset = m('ClickedReset')
|
|
54
65
|
|
|
55
|
-
const Message =
|
|
66
|
+
const Message = S.Union(ClickedDecrement, ClickedIncrement, ClickedReset)
|
|
56
67
|
export type Message = typeof Message.Type
|
|
57
68
|
|
|
58
69
|
// UPDATE
|
|
59
70
|
|
|
60
71
|
const update = (
|
|
61
|
-
|
|
72
|
+
model: Model,
|
|
62
73
|
message: Message,
|
|
63
74
|
): [Model, ReadonlyArray<Command<Message>>] =>
|
|
64
75
|
M.value(message).pipe(
|
|
65
76
|
M.withReturnType<[Model, ReadonlyArray<Command<Message>>]>(),
|
|
66
77
|
M.tagsExhaustive({
|
|
67
|
-
ClickedDecrement: () => [count - 1, []],
|
|
68
|
-
ClickedIncrement: () => [count + 1, []],
|
|
69
|
-
ClickedReset: () => [0, []],
|
|
78
|
+
ClickedDecrement: () => [{ count: model.count - 1 }, []],
|
|
79
|
+
ClickedIncrement: () => [{ count: model.count + 1 }, []],
|
|
80
|
+
ClickedReset: () => [{ count: 0 }, []],
|
|
70
81
|
}),
|
|
71
82
|
)
|
|
72
83
|
|
|
73
84
|
// INIT
|
|
74
85
|
|
|
75
|
-
const init: Runtime.ElementInit<Model, Message> = () => [0, []]
|
|
86
|
+
const init: Runtime.ElementInit<Model, Message> = () => [{ count: 0 }, []]
|
|
76
87
|
|
|
77
88
|
// VIEW
|
|
78
89
|
|
|
79
90
|
const { div, button, Class, OnClick } = html<Message>()
|
|
80
91
|
|
|
81
|
-
const view = (
|
|
92
|
+
const view = (model: Model): Html =>
|
|
82
93
|
div(
|
|
83
94
|
[
|
|
84
95
|
Class(
|
|
@@ -86,7 +97,10 @@ const view = (count: Model): Html =>
|
|
|
86
97
|
),
|
|
87
98
|
],
|
|
88
99
|
[
|
|
89
|
-
div(
|
|
100
|
+
div(
|
|
101
|
+
[Class('text-6xl font-bold text-gray-800')],
|
|
102
|
+
[model.count.toString()],
|
|
103
|
+
),
|
|
90
104
|
div(
|
|
91
105
|
[Class('flex flex-wrap justify-center gap-4')],
|
|
92
106
|
[
|
|
@@ -115,41 +129,42 @@ const element = Runtime.makeElement({
|
|
|
115
129
|
Runtime.run(element)
|
|
116
130
|
```
|
|
117
131
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
## Development
|
|
132
|
+
Source: [examples/counter/src/main.ts](https://github.com/devinjameson/foldkit/blob/main/examples/counter/src/main.ts)
|
|
121
133
|
|
|
122
|
-
|
|
134
|
+
## What Ships With Foldkit
|
|
123
135
|
|
|
124
|
-
|
|
125
|
-
git clone https://github.com/devinjameson/foldkit.git
|
|
126
|
-
cd foldkit
|
|
127
|
-
pnpm install
|
|
136
|
+
Foldkit is a complete system, not a collection of libraries you stitch together.
|
|
128
137
|
|
|
129
|
-
|
|
130
|
-
|
|
138
|
+
- **Commands** — Side effects are described as Effects that return Messages and are executed by the runtime. Use any Effect combinator you want — retry, timeout, race, parallel. You write the Effect, the runtime runs it.
|
|
139
|
+
- **Routing** — Type-safe bidirectional routing. URLs parse into typed routes and routes build back into URLs. No string matching, no mismatches between parsing and building.
|
|
140
|
+
- **Subscriptions** — Declare which streams your app needs as a function of the Model. The runtime diffs and switches them when the Model changes.
|
|
141
|
+
- **Managed Resources** — Model-driven lifecycle for long-lived browser resources like WebSockets, AudioContext, and RTCPeerConnection. Acquire on state change, release on cleanup.
|
|
142
|
+
- **UI Components** — Dialog, menu, tabs, listbox, disclosure — fully accessible primitives that are easy to style and customize.
|
|
143
|
+
- **Field Validation** — Per-field validation state modeled as a discriminated union. Define rules as data, apply them in update, and the Model tracks the result.
|
|
144
|
+
- **Virtual DOM** — Declarative Views powered by [Snabbdom](https://github.com/snabbdom/snabbdom). Fast, keyed diffing. Views are plain functions of your Model.
|
|
145
|
+
- **HMR** — Vite plugin with state-preserving hot module replacement. Change your view, keep your state.
|
|
131
146
|
|
|
132
|
-
|
|
133
|
-
pnpm dev:example:counter
|
|
134
|
-
```
|
|
147
|
+
## Correctness You (And Your LLM) Can See
|
|
135
148
|
|
|
136
|
-
|
|
149
|
+
Every state change flows through one update function. Every side effect is declared explicitly — in Commands, Subscription streams, and Managed Resource lifecycles. You don't have to hold a mental model of what runs when — you can point at it.
|
|
137
150
|
|
|
138
|
-
|
|
151
|
+
This is what makes Foldkit unusually AI-friendly. The same property that makes the code easy for humans to reason about makes it easy for LLMs to generate and review. The architecture makes correctness visible, whether the reader is a person or an LLM.
|
|
139
152
|
|
|
140
|
-
|
|
153
|
+
## Examples
|
|
141
154
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
- [
|
|
147
|
-
- [
|
|
148
|
-
- [
|
|
149
|
-
- [
|
|
150
|
-
- [ ]
|
|
151
|
-
|
|
152
|
-
|
|
155
|
+
- **[Counter](https://github.com/devinjameson/foldkit/blob/main/examples/counter/src/main.ts)** — Increment/decrement with reset
|
|
156
|
+
- **[Stopwatch](https://github.com/devinjameson/foldkit/blob/main/examples/stopwatch/src/main.ts)** — Timer with start/stop/reset
|
|
157
|
+
- **[Weather](https://github.com/devinjameson/foldkit/blob/main/examples/weather/src/main.ts)** — HTTP requests with async state handling
|
|
158
|
+
- **[Todo](https://github.com/devinjameson/foldkit/blob/main/examples/todo/src/main.ts)** — CRUD operations with localStorage persistence
|
|
159
|
+
- **[Form](https://github.com/devinjameson/foldkit/blob/main/examples/form/src/main.ts)** — Form validation with async email checking
|
|
160
|
+
- **[Routing](https://github.com/devinjameson/foldkit/blob/main/examples/routing/src/main.ts)** — URL routing with parser combinators
|
|
161
|
+
- **[Shopping Cart](https://github.com/devinjameson/foldkit/blob/main/examples/shopping-cart/src/main.ts)** — Nested models and complex state
|
|
162
|
+
- **[Snake](https://github.com/devinjameson/foldkit/blob/main/examples/snake/src/main.ts)** — Classic game built with Subscriptions
|
|
163
|
+
- **[WebSocket Chat](https://github.com/devinjameson/foldkit/blob/main/examples/websocket-chat/src/main.ts)** — Managed Resources with WebSocket integration
|
|
164
|
+
- **[Auth](https://github.com/devinjameson/foldkit/blob/main/examples/auth/src/main.ts)** — Authentication flow with model-as-union
|
|
165
|
+
- **[Query Sync](https://github.com/devinjameson/foldkit/blob/main/examples/query-sync/src/main.ts)** — URL query parameter sync with filtering and sorting
|
|
166
|
+
- **[Error View](https://github.com/devinjameson/foldkit/blob/main/examples/error-view/src/main.ts)** — Custom error fallback UI
|
|
167
|
+
- **[Typing Game](https://github.com/devinjameson/foldkit/tree/main/packages/typing-game)** — Multiplayer typing game with Effect RPC backend
|
|
153
168
|
|
|
154
169
|
## License
|
|
155
170
|
|
|
@@ -12,7 +12,7 @@ export declare const makeField: <A, I>(value: S.Schema<A, I>) => {
|
|
|
12
12
|
}>;
|
|
13
13
|
Invalid: import("../schema").CallableTaggedStruct<"Invalid", {
|
|
14
14
|
value: S.Schema<A, I, never>;
|
|
15
|
-
|
|
15
|
+
errors: S.NonEmptyArray<typeof S.String>;
|
|
16
16
|
}>;
|
|
17
17
|
Union: S.Union<[import("../schema").CallableTaggedStruct<"NotValidated", {
|
|
18
18
|
value: S.Schema<A, I, never>;
|
|
@@ -22,53 +22,62 @@ export declare const makeField: <A, I>(value: S.Schema<A, I>) => {
|
|
|
22
22
|
value: S.Schema<A, I, never>;
|
|
23
23
|
}>, import("../schema").CallableTaggedStruct<"Invalid", {
|
|
24
24
|
value: S.Schema<A, I, never>;
|
|
25
|
-
|
|
25
|
+
errors: S.NonEmptyArray<typeof S.String>;
|
|
26
26
|
}>]>;
|
|
27
|
+
validate: (fieldValidations: ReadonlyArray<Validation<A>>) => (fieldValue: A) => {
|
|
28
|
+
readonly _tag: "Valid";
|
|
29
|
+
readonly value: A;
|
|
30
|
+
} | {
|
|
31
|
+
readonly _tag: "Invalid";
|
|
32
|
+
readonly value: A;
|
|
33
|
+
readonly errors: readonly [string, ...string[]];
|
|
34
|
+
};
|
|
35
|
+
validateAll: (fieldValidations: ReadonlyArray<Validation<A>>) => (fieldValue: A) => {
|
|
36
|
+
readonly _tag: "Valid";
|
|
37
|
+
readonly value: A;
|
|
38
|
+
} | {
|
|
39
|
+
readonly _tag: "Invalid";
|
|
40
|
+
readonly value: A;
|
|
41
|
+
readonly errors: readonly [string, ...string[]];
|
|
42
|
+
};
|
|
27
43
|
};
|
|
44
|
+
/** An error message for a validation rule — either a static string or a function that receives the invalid value. */
|
|
45
|
+
export type ValidationMessage<T> = string | ((value: T) => string);
|
|
28
46
|
/** A tuple of a predicate and error message used for field validation. */
|
|
29
|
-
export type Validation<T> = [Predicate.Predicate<T>,
|
|
47
|
+
export type Validation<T> = [Predicate.Predicate<T>, ValidationMessage<T>];
|
|
48
|
+
export declare const resolveMessage: <T>(message: ValidationMessage<T>, value: T) => string;
|
|
30
49
|
/** Creates a `Validation` that checks if a string is non-empty. */
|
|
31
|
-
export declare const required: (message?: string) => Validation<string>;
|
|
50
|
+
export declare const required: (message?: ValidationMessage<string>) => Validation<string>;
|
|
32
51
|
/** Creates a `Validation` that checks if a string meets a minimum length. */
|
|
33
|
-
export declare const minLength: (min: number, message?: string) => Validation<string>;
|
|
52
|
+
export declare const minLength: (min: number, message?: ValidationMessage<string>) => Validation<string>;
|
|
34
53
|
/** Creates a `Validation` that checks if a string does not exceed a maximum length. */
|
|
35
|
-
export declare const maxLength: (max: number, message?: string) => Validation<string>;
|
|
54
|
+
export declare const maxLength: (max: number, message?: ValidationMessage<string>) => Validation<string>;
|
|
36
55
|
/** Creates a `Validation` that checks if a string matches a regular expression. */
|
|
37
|
-
export declare const pattern: (regex: RegExp, message?: string) => Validation<string>;
|
|
56
|
+
export declare const pattern: (regex: RegExp, message?: ValidationMessage<string>) => Validation<string>;
|
|
38
57
|
/** Creates a `Validation` that checks if a string is a valid email format. */
|
|
39
|
-
export declare const email: (message?: string) => Validation<string>;
|
|
58
|
+
export declare const email: (message?: ValidationMessage<string>) => Validation<string>;
|
|
40
59
|
/** Creates a `Validation` that checks if a string is a valid URL format. */
|
|
41
|
-
export declare const url: (message?: string) => Validation<string>;
|
|
60
|
+
export declare const url: (message?: ValidationMessage<string>) => Validation<string>;
|
|
42
61
|
/** Creates a `Validation` that checks if a string begins with a specified prefix. */
|
|
43
|
-
export declare const startsWith: (prefix: string, message?: string) => Validation<string>;
|
|
62
|
+
export declare const startsWith: (prefix: string, message?: ValidationMessage<string>) => Validation<string>;
|
|
44
63
|
/** Creates a `Validation` that checks if a string ends with a specified suffix. */
|
|
45
|
-
export declare const endsWith: (suffix: string, message?: string) => Validation<string>;
|
|
64
|
+
export declare const endsWith: (suffix: string, message?: ValidationMessage<string>) => Validation<string>;
|
|
46
65
|
/** Creates a `Validation` that checks if a string contains a specified substring. */
|
|
47
|
-
export declare const includes: (substring: string, message?: string) => Validation<string>;
|
|
66
|
+
export declare const includes: (substring: string, message?: ValidationMessage<string>) => Validation<string>;
|
|
48
67
|
/** Creates a `Validation` that checks if a string exactly matches an expected value. */
|
|
49
|
-
export declare const equals: (expected: string, message?: string) => Validation<string>;
|
|
68
|
+
export declare const equals: (expected: string, message?: ValidationMessage<string>) => Validation<string>;
|
|
50
69
|
/** Creates a `Validation` that checks if a number is greater than or equal to a minimum value. */
|
|
51
|
-
export declare const min: (num: number, message?:
|
|
70
|
+
export declare const min: (num: number, message?: ValidationMessage<number>) => Validation<number>;
|
|
52
71
|
/** Creates a `Validation` that checks if a number is less than or equal to a maximum value. */
|
|
53
|
-
export declare const max: (num: number, message?:
|
|
72
|
+
export declare const max: (num: number, message?: ValidationMessage<number>) => Validation<number>;
|
|
54
73
|
/** Creates a `Validation` that checks if a number falls within a specified inclusive range. */
|
|
55
|
-
export declare const between: (min: number, max: number, message?:
|
|
74
|
+
export declare const between: (min: number, max: number, message?: ValidationMessage<number>) => Validation<number>;
|
|
56
75
|
/** Creates a `Validation` that checks if a number is greater than zero. */
|
|
57
|
-
export declare const positive: (message?:
|
|
76
|
+
export declare const positive: (message?: ValidationMessage<number>) => Validation<number>;
|
|
58
77
|
/** Creates a `Validation` that checks if a number is zero or greater. */
|
|
59
|
-
export declare const nonNegative: (message?:
|
|
78
|
+
export declare const nonNegative: (message?: ValidationMessage<number>) => Validation<number>;
|
|
60
79
|
/** Creates a `Validation` that checks if a number is a whole number (integer). */
|
|
61
|
-
export declare const integer: (message?:
|
|
80
|
+
export declare const integer: (message?: ValidationMessage<number>) => Validation<number>;
|
|
62
81
|
/** Creates a `Validation` that checks if a string is one of a specified set of allowed values. */
|
|
63
|
-
export declare const oneOf: (values: ReadonlyArray<string>, message?: string) => Validation<string>;
|
|
64
|
-
/** Runs validations against a value, returning the first failure as `Invalid` or `Valid` if all pass. */
|
|
65
|
-
export declare const validateField: <T>(fieldValidations: ReadonlyArray<Validation<T>>) => (value: T) => {
|
|
66
|
-
_tag: "Invalid";
|
|
67
|
-
value: T;
|
|
68
|
-
error: string;
|
|
69
|
-
} | {
|
|
70
|
-
_tag: "Valid";
|
|
71
|
-
value: T;
|
|
72
|
-
error?: never;
|
|
73
|
-
};
|
|
82
|
+
export declare const oneOf: (values: ReadonlyArray<string>, message?: ValidationMessage<string>) => Validation<string>;
|
|
74
83
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/fieldValidation/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,SAAS,EACT,MAAM,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/fieldValidation/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,SAAS,EACT,MAAM,IAAI,CAAC,EAIZ,MAAM,QAAQ,CAAA;AAIf,0HAA0H;AAC1H,eAAO,MAAM,SAAS,GAAI,CAAC,EAAE,CAAC,EAAE,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC;;;;;;;;;;;;;;;;;;;;;;;;iCAO9B,aAAa,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,MAAM,YAAY,CAAC;;;;;;;;oCAe/C,aAAa,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,MAAM,YAAY,CAAC;;;;;;;;CAuBrE,CAAA;AAED,qHAAqH;AACrH,MAAM,MAAM,iBAAiB,CAAC,CAAC,IAAI,MAAM,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC,KAAK,MAAM,CAAC,CAAA;AAElE,0EAA0E;AAC1E,MAAM,MAAM,UAAU,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,iBAAiB,CAAC,CAAC,CAAC,CAAC,CAAA;AAE1E,eAAO,MAAM,cAAc,GAAI,CAAC,EAC9B,SAAS,iBAAiB,CAAC,CAAC,CAAC,EAC7B,OAAO,CAAC,KACP,MAAkE,CAAA;AAIrE,mEAAmE;AACnE,eAAO,MAAM,QAAQ,GACnB,UAAS,iBAAiB,CAAC,MAAM,CAAc,KAC9C,UAAU,CAAC,MAAM,CAAiC,CAAA;AAErD,6EAA6E;AAC7E,eAAO,MAAM,SAAS,GACpB,KAAK,MAAM,EACX,UAAU,iBAAiB,CAAC,MAAM,CAAC,KAClC,UAAU,CAAC,MAAM,CAGnB,CAAA;AAED,uFAAuF;AACvF,eAAO,MAAM,SAAS,GACpB,KAAK,MAAM,EACX,UAAU,iBAAiB,CAAC,MAAM,CAAC,KAClC,UAAU,CAAC,MAAM,CAGnB,CAAA;AAED,mFAAmF;AACnF,eAAO,MAAM,OAAO,GAClB,OAAO,MAAM,EACb,UAAS,iBAAiB,CAAC,MAAM,CAAoB,KACpD,UAAU,CAAC,MAAM,CAAwD,CAAA;AAI5E,8EAA8E;AAC9E,eAAO,MAAM,KAAK,GAChB,UAAS,iBAAiB,CAAC,MAAM,CAA2B,KAC3D,UAAU,CAAC,MAAM,CAAkC,CAAA;AAItD,4EAA4E;AAC5E,eAAO,MAAM,GAAG,GACd,UAAS,iBAAiB,CAAC,MAAM,CAAiB,KACjD,UAAU,CAAC,MAAM,CAAgC,CAAA;AAEpD,qFAAqF;AACrF,eAAO,MAAM,UAAU,GACrB,QAAQ,MAAM,EACd,UAAU,iBAAiB,CAAC,MAAM,CAAC,KAClC,UAAU,CAAC,MAAM,CAGnB,CAAA;AAED,mFAAmF;AACnF,eAAO,MAAM,QAAQ,GACnB,QAAQ,MAAM,EACd,UAAU,iBAAiB,CAAC,MAAM,CAAC,KAClC,UAAU,CAAC,MAAM,CAGnB,CAAA;AAED,qFAAqF;AACrF,eAAO,MAAM,QAAQ,GACnB,WAAW,MAAM,EACjB,UAAU,iBAAiB,CAAC,MAAM,CAAC,KAClC,UAAU,CAAC,MAAM,CAGnB,CAAA;AAED,wFAAwF;AACxF,eAAO,MAAM,MAAM,GACjB,UAAU,MAAM,EAChB,UAAU,iBAAiB,CAAC,MAAM,CAAC,KAClC,UAAU,CAAC,MAAM,CAGnB,CAAA;AAID,kGAAkG;AAClG,eAAO,MAAM,GAAG,GACd,KAAK,MAAM,EACX,UAAU,iBAAiB,CAAC,MAAM,CAAC,KAClC,UAAU,CAAC,MAAM,CAGnB,CAAA;AAED,+FAA+F;AAC/F,eAAO,MAAM,GAAG,GACd,KAAK,MAAM,EACX,UAAU,iBAAiB,CAAC,MAAM,CAAC,KAClC,UAAU,CAAC,MAAM,CAGnB,CAAA;AAED,+FAA+F;AAC/F,eAAO,MAAM,OAAO,GAClB,KAAK,MAAM,EACX,KAAK,MAAM,EACX,UAAU,iBAAiB,CAAC,MAAM,CAAC,KAClC,UAAU,CAAC,MAAM,CAMnB,CAAA;AAED,2EAA2E;AAC3E,eAAO,MAAM,QAAQ,GACnB,UAAS,iBAAiB,CAAC,MAAM,CAAsB,KACtD,UAAU,CAAC,MAAM,CAAsC,CAAA;AAE1D,yEAAyE;AACzE,eAAO,MAAM,WAAW,GACtB,UAAS,iBAAiB,CAAC,MAAM,CAA0B,KAC1D,UAAU,CAAC,MAAM,CAA+C,CAAA;AAEnE,kFAAkF;AAClF,eAAO,MAAM,OAAO,GAClB,UAAS,iBAAiB,CAAC,MAAM,CAA4B,KAC5D,UAAU,CAAC,MAAM,CAAgD,CAAA;AAIpE,kGAAkG;AAClG,eAAO,MAAM,KAAK,GAChB,QAAQ,aAAa,CAAC,MAAM,CAAC,EAC7B,UAAU,iBAAiB,CAAC,MAAM,CAAC,KAClC,UAAU,CAAC,MAAM,CAMnB,CAAA"}
|
|
@@ -1,25 +1,38 @@
|
|
|
1
|
-
import { Array, Number as Number_, Option, Schema as S, String, flow, } from 'effect';
|
|
1
|
+
import { Array, Number as Number_, Option, Predicate, Schema as S, String, flow, pipe, } from 'effect';
|
|
2
2
|
import { ts } from '../schema';
|
|
3
3
|
/** Creates a tagged union of field states (`NotValidated`, `Validating`, `Valid`, `Invalid`) for a given value schema. */
|
|
4
4
|
export const makeField = (value) => {
|
|
5
5
|
const NotValidated = ts('NotValidated', { value });
|
|
6
6
|
const Validating = ts('Validating', { value });
|
|
7
7
|
const Valid = ts('Valid', { value });
|
|
8
|
-
const Invalid = ts('Invalid', { value,
|
|
8
|
+
const Invalid = ts('Invalid', { value, errors: S.NonEmptyArray(S.String) });
|
|
9
|
+
const validate = (fieldValidations) => (fieldValue) => pipe(fieldValidations, Array.findFirst(([predicate]) => !predicate(fieldValue)), Option.match({
|
|
10
|
+
onNone: () => Valid({ value: fieldValue }),
|
|
11
|
+
onSome: ([, message]) => Invalid({
|
|
12
|
+
value: fieldValue,
|
|
13
|
+
errors: [resolveMessage(message, fieldValue)],
|
|
14
|
+
}),
|
|
15
|
+
}));
|
|
16
|
+
const validateAll = (fieldValidations) => (fieldValue) => pipe(fieldValidations, Array.filterMap(([predicate, message]) => predicate(fieldValue)
|
|
17
|
+
? Option.none()
|
|
18
|
+
: Option.some(resolveMessage(message, fieldValue))), Array.match({
|
|
19
|
+
onEmpty: () => Valid({ value: fieldValue }),
|
|
20
|
+
onNonEmpty: errors => Invalid({ value: fieldValue, errors }),
|
|
21
|
+
}));
|
|
9
22
|
return {
|
|
10
23
|
NotValidated,
|
|
11
24
|
Validating,
|
|
12
25
|
Valid,
|
|
13
26
|
Invalid,
|
|
14
27
|
Union: S.Union(NotValidated, Validating, Valid, Invalid),
|
|
28
|
+
validate,
|
|
29
|
+
validateAll,
|
|
15
30
|
};
|
|
16
31
|
};
|
|
32
|
+
export const resolveMessage = (message, value) => (typeof message === 'string' ? message : message(value));
|
|
17
33
|
// STRING VALIDATORS
|
|
18
34
|
/** Creates a `Validation` that checks if a string is non-empty. */
|
|
19
|
-
export const required = (message = 'Required') => [
|
|
20
|
-
String.isNonEmpty,
|
|
21
|
-
message,
|
|
22
|
-
];
|
|
35
|
+
export const required = (message = 'Required') => [String.isNonEmpty, message];
|
|
23
36
|
/** Creates a `Validation` that checks if a string meets a minimum length. */
|
|
24
37
|
export const minLength = (min, message) => [
|
|
25
38
|
flow(String.length, Number_.greaterThanOrEqualTo(min)),
|
|
@@ -71,14 +84,11 @@ export const max = (num, message) => [
|
|
|
71
84
|
];
|
|
72
85
|
/** Creates a `Validation` that checks if a number falls within a specified inclusive range. */
|
|
73
86
|
export const between = (min, max, message) => [
|
|
74
|
-
|
|
87
|
+
Predicate.and(Number_.greaterThanOrEqualTo(min), Number_.lessThanOrEqualTo(max)),
|
|
75
88
|
message ?? `Must be between ${min} and ${max}`,
|
|
76
89
|
];
|
|
77
90
|
/** Creates a `Validation` that checks if a number is greater than zero. */
|
|
78
|
-
export const positive = (message = 'Must be positive') => [
|
|
79
|
-
Number_.greaterThan(0),
|
|
80
|
-
message,
|
|
81
|
-
];
|
|
91
|
+
export const positive = (message = 'Must be positive') => [Number_.greaterThan(0), message];
|
|
82
92
|
/** Creates a `Validation` that checks if a number is zero or greater. */
|
|
83
93
|
export const nonNegative = (message = 'Must be non-negative') => [Number_.greaterThanOrEqualTo(0), message];
|
|
84
94
|
/** Creates a `Validation` that checks if a number is a whole number (integer). */
|
|
@@ -87,23 +97,8 @@ export const integer = (message = 'Must be a whole number') => [value => Number.
|
|
|
87
97
|
/** Creates a `Validation` that checks if a string is one of a specified set of allowed values. */
|
|
88
98
|
export const oneOf = (values, message) => {
|
|
89
99
|
const joinedValues = Array.join(values, ', ');
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/** Runs validations against a value, returning the first failure as `Invalid` or `Valid` if all pass. */
|
|
95
|
-
export const validateField = (fieldValidations) => (value) => {
|
|
96
|
-
for (const [predicate, message] of fieldValidations) {
|
|
97
|
-
if (!predicate(value)) {
|
|
98
|
-
return {
|
|
99
|
-
_tag: 'Invalid',
|
|
100
|
-
value,
|
|
101
|
-
error: message,
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
return {
|
|
106
|
-
_tag: 'Valid',
|
|
107
|
-
value,
|
|
108
|
-
};
|
|
100
|
+
return [
|
|
101
|
+
value => Array.contains(values, value),
|
|
102
|
+
message ?? `Must be one of: ${joinedValues}`,
|
|
103
|
+
];
|
|
109
104
|
};
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { makeField, required, minLength, maxLength, pattern, email, url, startsWith, endsWith, includes, equals, min, max, between, positive, nonNegative, integer, oneOf,
|
|
2
|
-
export type { Validation } from './index';
|
|
1
|
+
export { makeField, required, minLength, maxLength, pattern, email, url, startsWith, endsWith, includes, equals, min, max, between, positive, nonNegative, integer, oneOf, } from './index';
|
|
2
|
+
export type { Validation, ValidationMessage } from './index';
|
|
3
3
|
//# sourceMappingURL=public.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"public.d.ts","sourceRoot":"","sources":["../../src/fieldValidation/public.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,SAAS,EACT,QAAQ,EACR,SAAS,EACT,SAAS,EACT,OAAO,EACP,KAAK,EACL,GAAG,EACH,UAAU,EACV,QAAQ,EACR,QAAQ,EACR,MAAM,EACN,GAAG,EACH,GAAG,EACH,OAAO,EACP,QAAQ,EACR,WAAW,EACX,OAAO,EACP,KAAK,
|
|
1
|
+
{"version":3,"file":"public.d.ts","sourceRoot":"","sources":["../../src/fieldValidation/public.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,SAAS,EACT,QAAQ,EACR,SAAS,EACT,SAAS,EACT,OAAO,EACP,KAAK,EACL,GAAG,EACH,UAAU,EACV,QAAQ,EACR,QAAQ,EACR,MAAM,EACN,GAAG,EACH,GAAG,EACH,OAAO,EACP,QAAQ,EACR,WAAW,EACX,OAAO,EACP,KAAK,GACN,MAAM,SAAS,CAAA;AAEhB,YAAY,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { makeField, required, minLength, maxLength, pattern, email, url, startsWith, endsWith, includes, equals, min, max, between, positive, nonNegative, integer, oneOf,
|
|
1
|
+
export { makeField, required, minLength, maxLength, pattern, email, url, startsWith, endsWith, includes, equals, min, max, between, positive, nonNegative, integer, oneOf, } from './index';
|
package/dist/html/index.d.ts
CHANGED
|
@@ -13,7 +13,8 @@ export type Html = Effect.Effect<VNode | null, never, Dispatch>;
|
|
|
13
13
|
type Child = Html | string;
|
|
14
14
|
/** Union of all valid HTML, SVG, and MathML tag names. */
|
|
15
15
|
export type TagName = 'a' | 'abbr' | 'address' | 'area' | 'article' | 'aside' | 'audio' | 'b' | 'base' | 'bdi' | 'bdo' | 'blockquote' | 'body' | 'br' | 'button' | 'canvas' | 'caption' | 'cite' | 'code' | 'col' | 'colgroup' | 'data' | 'datalist' | 'dd' | 'del' | 'details' | 'dfn' | 'dialog' | 'div' | 'dl' | 'dt' | 'em' | 'embed' | 'fieldset' | 'figcaption' | 'figure' | 'footer' | 'form' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'head' | 'header' | 'hgroup' | 'hr' | 'html' | 'i' | 'iframe' | 'img' | 'input' | 'ins' | 'kbd' | 'label' | 'legend' | 'li' | 'link' | 'main' | 'map' | 'mark' | 'menu' | 'meta' | 'meter' | 'nav' | 'noscript' | 'object' | 'ol' | 'optgroup' | 'option' | 'output' | 'p' | 'picture' | 'portal' | 'pre' | 'progress' | 'q' | 'rp' | 'rt' | 'ruby' | 's' | 'samp' | 'script' | 'search' | 'section' | 'select' | 'slot' | 'small' | 'source' | 'span' | 'strong' | 'style' | 'sub' | 'summary' | 'sup' | 'table' | 'tbody' | 'td' | 'template' | 'textarea' | 'tfoot' | 'th' | 'thead' | 'time' | 'title' | 'tr' | 'track' | 'u' | 'ul' | 'var' | 'video' | 'wbr' | 'animate' | 'animateMotion' | 'animateTransform' | 'circle' | 'clipPath' | 'defs' | 'desc' | 'ellipse' | 'feBlend' | 'feColorMatrix' | 'feComponentTransfer' | 'feComposite' | 'feConvolveMatrix' | 'feDiffuseLighting' | 'feDisplacementMap' | 'feDistantLight' | 'feDropShadow' | 'feFlood' | 'feFuncA' | 'feFuncB' | 'feFuncG' | 'feFuncR' | 'feGaussianBlur' | 'feImage' | 'feMerge' | 'feMergeNode' | 'feMorphology' | 'feOffset' | 'fePointLight' | 'feSpecularLighting' | 'feSpotLight' | 'feTile' | 'feTurbulence' | 'filter' | 'foreignObject' | 'g' | 'image' | 'line' | 'linearGradient' | 'marker' | 'mask' | 'metadata' | 'mpath' | 'path' | 'pattern' | 'polygon' | 'polyline' | 'radialGradient' | 'rect' | 'set' | 'stop' | 'svg' | 'switch' | 'symbol' | 'text' | 'textPath' | 'tspan' | 'use' | 'view' | 'annotation' | 'annotation-xml' | 'math' | 'maction' | 'menclose' | 'merror' | 'mfenced' | 'mfrac' | 'mglyph' | 'mi' | 'mlabeledtr' | 'mlongdiv' | 'mmultiscripts' | 'mn' | 'mo' | 'mover' | 'mpadded' | 'mphantom' | 'mprescripts' | 'mroot' | 'mrow' | 'ms' | 'mscarries' | 'mscarry' | 'msgroup' | 'msline' | 'mspace' | 'msqrt' | 'msrow' | 'mstack' | 'mstyle' | 'msub' | 'msubsup' | 'msup' | 'mtable' | 'mtd' | 'mtext' | 'mtr' | 'munder' | 'munderover' | 'semantics';
|
|
16
|
-
|
|
16
|
+
/** Union of all HTML, SVG, and MathML attributes a virtual DOM element can carry. */
|
|
17
|
+
export type Attribute<Message> = Data.TaggedEnum<{
|
|
17
18
|
Key: {
|
|
18
19
|
readonly value: string;
|
|
19
20
|
};
|
|
@@ -302,6 +303,24 @@ type Attribute<Message> = Data.TaggedEnum<{
|
|
|
302
303
|
AriaActiveDescendant: {
|
|
303
304
|
readonly value: string;
|
|
304
305
|
};
|
|
306
|
+
AriaSort: {
|
|
307
|
+
readonly value: string;
|
|
308
|
+
};
|
|
309
|
+
AriaMultiSelectable: {
|
|
310
|
+
readonly value: boolean;
|
|
311
|
+
};
|
|
312
|
+
AriaModal: {
|
|
313
|
+
readonly value: boolean;
|
|
314
|
+
};
|
|
315
|
+
AriaBusy: {
|
|
316
|
+
readonly value: boolean;
|
|
317
|
+
};
|
|
318
|
+
AriaErrorMessage: {
|
|
319
|
+
readonly value: string;
|
|
320
|
+
};
|
|
321
|
+
AriaRoleDescription: {
|
|
322
|
+
readonly value: string;
|
|
323
|
+
};
|
|
305
324
|
Attribute: {
|
|
306
325
|
readonly key: string;
|
|
307
326
|
readonly value: string;
|
|
@@ -401,14 +420,6 @@ type Attribute<Message> = Data.TaggedEnum<{
|
|
|
401
420
|
readonly f: (element: Element) => void;
|
|
402
421
|
};
|
|
403
422
|
}>;
|
|
404
|
-
declare const Attribute: <A>(args: {
|
|
405
|
-
readonly key: string;
|
|
406
|
-
readonly value: string;
|
|
407
|
-
}) => {
|
|
408
|
-
readonly _tag: "Attribute";
|
|
409
|
-
readonly key: string;
|
|
410
|
-
readonly value: string;
|
|
411
|
-
};
|
|
412
423
|
type AttributeWithoutKey<Message> = Exclude<Attribute<Message>, {
|
|
413
424
|
_tag: 'Key';
|
|
414
425
|
}>;
|
|
@@ -856,6 +867,30 @@ export declare const html: <Message>() => {
|
|
|
856
867
|
readonly _tag: "AriaActiveDescendant";
|
|
857
868
|
readonly value: string;
|
|
858
869
|
};
|
|
870
|
+
AriaSort: (value: string) => {
|
|
871
|
+
readonly _tag: "AriaSort";
|
|
872
|
+
readonly value: string;
|
|
873
|
+
};
|
|
874
|
+
AriaMultiSelectable: (value: boolean) => {
|
|
875
|
+
readonly _tag: "AriaMultiSelectable";
|
|
876
|
+
readonly value: boolean;
|
|
877
|
+
};
|
|
878
|
+
AriaModal: (value: boolean) => {
|
|
879
|
+
readonly _tag: "AriaModal";
|
|
880
|
+
readonly value: boolean;
|
|
881
|
+
};
|
|
882
|
+
AriaBusy: (value: boolean) => {
|
|
883
|
+
readonly _tag: "AriaBusy";
|
|
884
|
+
readonly value: boolean;
|
|
885
|
+
};
|
|
886
|
+
AriaErrorMessage: (value: string) => {
|
|
887
|
+
readonly _tag: "AriaErrorMessage";
|
|
888
|
+
readonly value: string;
|
|
889
|
+
};
|
|
890
|
+
AriaRoleDescription: (value: string) => {
|
|
891
|
+
readonly _tag: "AriaRoleDescription";
|
|
892
|
+
readonly value: string;
|
|
893
|
+
};
|
|
859
894
|
Attribute: (key: string, value: string) => {
|
|
860
895
|
readonly _tag: "Attribute";
|
|
861
896
|
readonly key: string;
|