foldkit 0.104.0 → 0.105.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 CHANGED
@@ -13,7 +13,7 @@
13
13
  <h3 align="center">The frontend framework for correctness.</h3>
14
14
 
15
15
  <p align="center">
16
- <a href="https://foldkit.dev"><strong>Documentation</strong></a> · <a href="https://foldkit.dev/manifesto"><strong>Manifesto</strong></a> · <a href="https://foldkit.dev/example-apps"><strong>Examples</strong></a> · <a href="https://foldkit.dev/getting-started"><strong>Getting Started</strong></a>
16
+ <a href="https://foldkit.dev"><strong>Documentation</strong></a> · <a href="https://foldkit.dev/get-started/manifesto"><strong>Manifesto</strong></a> · <a href="https://foldkit.dev/example-apps"><strong>Examples</strong></a> · <a href="https://foldkit.dev/get-started/getting-started"><strong>Getting Started</strong></a>
17
17
  </p>
18
18
 
19
19
  ---
@@ -35,7 +35,7 @@ Every Foldkit application is an [Effect](https://effect.website/) program. Your
35
35
 
36
36
  ## Coming from React?
37
37
 
38
- [Coming from React](https://foldkit.dev/coming-from-react) is a guided walk through the differences. [Foldkit vs React: Side by Side](https://foldkit.dev/foldkit-vs-react-side-by-side) implements the same pixel-art editor in both frameworks so you can read them line by line.
38
+ [Coming from React](https://foldkit.dev/react/coming-from-react) is a guided walk through the differences. [Foldkit vs React: Side by Side](https://foldkit.dev/react/foldkit-vs-react-side-by-side) implements the same pixel-art editor in both frameworks so you can read them line by line.
39
39
 
40
40
  ## Get Started
41
41
 
@@ -0,0 +1,125 @@
1
+ import { Array, Option, Schema as S } from 'effect';
2
+ import { type Rule, type RuleMessage } from './rule.js';
3
+ /** The `NotValidated` state: user hasn't interacted yet. */
4
+ export type NotValidated<A> = Readonly<{
5
+ _tag: 'NotValidated';
6
+ value: A;
7
+ }>;
8
+ /** The `Validating` state: async validation is in flight. */
9
+ export type Validating<A> = Readonly<{
10
+ _tag: 'Validating';
11
+ value: A;
12
+ }>;
13
+ /** The `Valid` state: every rule passed. */
14
+ export type Valid<A> = Readonly<{
15
+ _tag: 'Valid';
16
+ value: A;
17
+ }>;
18
+ /** The `Invalid` state: one or more rules failed. Carries a non-empty `errors` array. */
19
+ export type Invalid<A> = Readonly<{
20
+ _tag: 'Invalid';
21
+ value: A;
22
+ errors: Array.NonEmptyReadonlyArray<string>;
23
+ }>;
24
+ /** The four-state union that represents a field's value in the Model. */
25
+ export type Field<A> = NotValidated<A> | Validating<A> | Valid<A> | Invalid<A>;
26
+ /** Constructs a `NotValidated` state. */
27
+ export declare const NotValidated: <A>(field: Readonly<{
28
+ value: A;
29
+ }>) => NotValidated<A>;
30
+ /** Constructs a `Validating` state. */
31
+ export declare const Validating: <A>(field: Readonly<{
32
+ value: A;
33
+ }>) => Validating<A>;
34
+ /** Constructs a `Valid` state. */
35
+ export declare const Valid: <A>(field: Readonly<{
36
+ value: A;
37
+ }>) => Valid<A>;
38
+ /** Constructs an `Invalid` state. */
39
+ export declare const Invalid: <A>(field: Readonly<{
40
+ value: A;
41
+ errors: Array.NonEmptyReadonlyArray<string>;
42
+ }>) => Invalid<A>;
43
+ /** Builds the four-state `Field` Schema for a value of the given Schema. Put the
44
+ * result in your Model. The value Schema should match what the control
45
+ * actually holds as the user edits, not the type you parse it into:
46
+ * `Field(S.String)` for text inputs, `Field(S.Array(S.String))` for a
47
+ * multi-select. A scalar like a checkbox's boolean usually stays plain
48
+ * `S.Boolean` in the Model; wrap it in `Field` only when it needs the
49
+ * validation lifecycle. Validation rules stay separate, in a `makeRules`
50
+ * bundle. */
51
+ export declare const Field: <A, I>(valueSchema: S.Codec<A, I>) => S.Union<readonly [S.TaggedStruct<"NotValidated", {
52
+ readonly value: S.Codec<A, I, never, never>;
53
+ }>, S.TaggedStruct<"Validating", {
54
+ readonly value: S.Codec<A, I, never, never>;
55
+ }>, S.TaggedStruct<"Valid", {
56
+ readonly value: S.Codec<A, I, never, never>;
57
+ }>, S.TaggedStruct<"Invalid", {
58
+ readonly value: S.Codec<A, I, never, never>;
59
+ readonly errors: S.NonEmptyArray<S.String>;
60
+ }>]>;
61
+ /** A field's validation rules: the required message (if any), the list of rules,
62
+ * and an empty predicate. Produced by `makeRules`; consumed by the module's
63
+ * operations (`validate`, `validateAll`, `isValid`, `isRequired`). The fields
64
+ * are accessible but treating them as stable is discouraged. Prefer the
65
+ * operations so internal shape changes don't break callers. */
66
+ export type Rules<A> = Readonly<{
67
+ requiredMessage: Option.Option<RuleMessage<A>>;
68
+ rules: ReadonlyArray<Rule<A>>;
69
+ isEmpty: (value: A) => boolean;
70
+ }>;
71
+ /** Options accepted by `makeRules`. */
72
+ export type MakeRulesOptions<A> = Readonly<{
73
+ /** When present, the field is required: empty values become `Invalid`
74
+ * with the given message, and `isValid` requires `Valid`. Absent
75
+ * means the field is optional: empty values stay `NotValidated`, and
76
+ * `isValid` accepts `Valid` or `NotValidated`. */
77
+ required?: RuleMessage<A>;
78
+ rules?: ReadonlyArray<Rule<A>>;
79
+ /** Predicate for what counts as "empty" for this field. Defaults to empty
80
+ * string and empty array; every other value is treated as present. Pass
81
+ * `(value) => value.trim() === ''` to treat whitespace-only input as empty. */
82
+ isEmpty?: (value: A) => boolean;
83
+ }>;
84
+ /** Creates a `Rules` bundle from options. The value type defaults to `string`;
85
+ * for other field values, annotate it: `makeRules<ReadonlyArray<Tag>>({ ... })`. */
86
+ export declare const makeRules: <A = string>(options?: MakeRulesOptions<A>) => Rules<A>;
87
+ /** Validates a new value and returns the next field state.
88
+ *
89
+ * For required fields, an empty value produces `Invalid` with the
90
+ * required message. For optional fields, an empty value produces
91
+ * `NotValidated`. Non-empty values run through the field's rules;
92
+ * the first failure becomes `Invalid`, otherwise the result is `Valid`. */
93
+ export declare const validate: <A>(rules: Rules<A>) => (value: A) => Field<A>;
94
+ /** Like `validate` but collects every failing rule into the
95
+ * `Invalid` state's errors array instead of stopping at the first. */
96
+ export declare const validateAll: <A>(rules: Rules<A>) => (value: A) => Field<A>;
97
+ /** Returns true when the field's current state is acceptable given its
98
+ * rules. For required fields, only `Valid` returns `true`. For optional
99
+ * fields, `Valid` or `NotValidated` both return `true`. `Invalid` and
100
+ * `Validating` always return `false`.
101
+ *
102
+ * The name is distinct from the `Valid` tag on purpose: `isValid`
103
+ * answers "is this state an acceptable result?", which for an optional
104
+ * field is broader than `_tag === 'Valid'`. For pattern-matching on the
105
+ * state itself, check the `_tag` directly. */
106
+ export declare const isValid: <A>(rules: Rules<A>) => (state: Field<A>) => boolean;
107
+ /** Returns true when the rules mark the field as required. Useful for
108
+ * rendering affordances like a `*` next to required field labels. */
109
+ export declare const isRequired: <A>(rules: Rules<A>) => boolean;
110
+ /** Returns true when the state's tag is `Invalid`. Tag-only predicate;
111
+ * unlike `!isValid(rules)(state)`, this does not treat `NotValidated`
112
+ * or `Validating` as errors. Use for "has the user seen a rule failure
113
+ * on this field?" affordances like red borders or per-step error
114
+ * indicators. */
115
+ export declare const isInvalid: (state: Field<unknown>) => boolean;
116
+ /** Returns true when every field is acceptable per its rules, by `isValid`.
117
+ * Each pair is a field's `[state, rules]`. The pairs in one call share a
118
+ * value type, so a call gates fields of a single type; for a form that mixes
119
+ * types, call `allValid` once per type and combine the results with `&&`.
120
+ * Use for form-level submit gates. */
121
+ export declare const allValid: <A>(pairs: ReadonlyArray<readonly [Field<A>, Rules<A>]>) => boolean;
122
+ /** Returns true when any state in the input has tag `Invalid`. Use for
123
+ * "this step/section has errors" affordances, independent of rules. */
124
+ export declare const anyInvalid: (states: ReadonlyArray<Field<unknown>>) => boolean;
125
+ //# sourceMappingURL=fieldValidation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fieldValidation.d.ts","sourceRoot":"","sources":["../../src/fieldValidation/fieldValidation.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,127 @@
1
+ import { Array, Option, Result, Schema as S, String, pipe } from 'effect';
2
+ import { resolveMessage } from './rule.js';
3
+ /** Constructs a `NotValidated` state. */
4
+ export const NotValidated = (field) => ({
5
+ _tag: 'NotValidated',
6
+ value: field.value,
7
+ });
8
+ /** Constructs a `Validating` state. */
9
+ export const Validating = (field) => ({
10
+ _tag: 'Validating',
11
+ value: field.value,
12
+ });
13
+ /** Constructs a `Valid` state. */
14
+ export const Valid = (field) => ({
15
+ _tag: 'Valid',
16
+ value: field.value,
17
+ });
18
+ /** Constructs an `Invalid` state. */
19
+ export const Invalid = (field) => ({
20
+ _tag: 'Invalid',
21
+ value: field.value,
22
+ errors: field.errors,
23
+ });
24
+ /** Builds the four-state `Field` Schema for a value of the given Schema. Put the
25
+ * result in your Model. The value Schema should match what the control
26
+ * actually holds as the user edits, not the type you parse it into:
27
+ * `Field(S.String)` for text inputs, `Field(S.Array(S.String))` for a
28
+ * multi-select. A scalar like a checkbox's boolean usually stays plain
29
+ * `S.Boolean` in the Model; wrap it in `Field` only when it needs the
30
+ * validation lifecycle. Validation rules stay separate, in a `makeRules`
31
+ * bundle. */
32
+ export const Field = (valueSchema) => S.Union([
33
+ S.TaggedStruct('NotValidated', { value: valueSchema }),
34
+ S.TaggedStruct('Validating', { value: valueSchema }),
35
+ S.TaggedStruct('Valid', { value: valueSchema }),
36
+ S.TaggedStruct('Invalid', {
37
+ value: valueSchema,
38
+ errors: S.NonEmptyArray(S.String),
39
+ }),
40
+ ]);
41
+ const isEmptyValue = (value) => {
42
+ if (typeof value === 'string') {
43
+ return String.isEmpty(value);
44
+ }
45
+ if (Array.isArray(value)) {
46
+ return Array.isReadonlyArrayEmpty(value);
47
+ }
48
+ return false;
49
+ };
50
+ /** Creates a `Rules` bundle from options. The value type defaults to `string`;
51
+ * for other field values, annotate it: `makeRules<ReadonlyArray<Tag>>({ ... })`. */
52
+ export const makeRules = (options = {}) => ({
53
+ requiredMessage: Option.fromNullishOr(options.required),
54
+ rules: options.rules ?? [],
55
+ isEmpty: options.isEmpty ?? isEmptyValue,
56
+ });
57
+ // OPERATIONS
58
+ /** Validates a new value and returns the next field state.
59
+ *
60
+ * For required fields, an empty value produces `Invalid` with the
61
+ * required message. For optional fields, an empty value produces
62
+ * `NotValidated`. Non-empty values run through the field's rules;
63
+ * the first failure becomes `Invalid`, otherwise the result is `Valid`. */
64
+ export const validate = (rules) => (value) => {
65
+ if (rules.isEmpty(value)) {
66
+ return Option.match(rules.requiredMessage, {
67
+ onNone: () => NotValidated({ value }),
68
+ onSome: message => Invalid({ value, errors: [resolveMessage(message, value)] }),
69
+ });
70
+ }
71
+ return pipe(rules.rules, Array.findFirst(([predicate]) => !predicate(value)), Option.match({
72
+ onNone: () => Valid({ value }),
73
+ onSome: ([, message]) => Invalid({ value, errors: [resolveMessage(message, value)] }),
74
+ }));
75
+ };
76
+ /** Like `validate` but collects every failing rule into the
77
+ * `Invalid` state's errors array instead of stopping at the first. */
78
+ export const validateAll = (rules) => (value) => {
79
+ if (rules.isEmpty(value)) {
80
+ return Option.match(rules.requiredMessage, {
81
+ onNone: () => NotValidated({ value }),
82
+ onSome: message => Invalid({ value, errors: [resolveMessage(message, value)] }),
83
+ });
84
+ }
85
+ return pipe(rules.rules, Array.filterMap(([predicate, message]) => !predicate(value)
86
+ ? Result.succeed(resolveMessage(message, value))
87
+ : Result.failVoid), Array.match({
88
+ onEmpty: () => Valid({ value }),
89
+ onNonEmpty: errors => Invalid({ value, errors }),
90
+ }));
91
+ };
92
+ /** Returns true when the field's current state is acceptable given its
93
+ * rules. For required fields, only `Valid` returns `true`. For optional
94
+ * fields, `Valid` or `NotValidated` both return `true`. `Invalid` and
95
+ * `Validating` always return `false`.
96
+ *
97
+ * The name is distinct from the `Valid` tag on purpose: `isValid`
98
+ * answers "is this state an acceptable result?", which for an optional
99
+ * field is broader than `_tag === 'Valid'`. For pattern-matching on the
100
+ * state itself, check the `_tag` directly. */
101
+ export const isValid = (rules) => (state) => {
102
+ if (state._tag === 'Invalid' || state._tag === 'Validating') {
103
+ return false;
104
+ }
105
+ if (Option.isSome(rules.requiredMessage)) {
106
+ return state._tag === 'Valid';
107
+ }
108
+ return true;
109
+ };
110
+ /** Returns true when the rules mark the field as required. Useful for
111
+ * rendering affordances like a `*` next to required field labels. */
112
+ export const isRequired = (rules) => Option.isSome(rules.requiredMessage);
113
+ /** Returns true when the state's tag is `Invalid`. Tag-only predicate;
114
+ * unlike `!isValid(rules)(state)`, this does not treat `NotValidated`
115
+ * or `Validating` as errors. Use for "has the user seen a rule failure
116
+ * on this field?" affordances like red borders or per-step error
117
+ * indicators. */
118
+ export const isInvalid = (state) => state._tag === 'Invalid';
119
+ /** Returns true when every field is acceptable per its rules, by `isValid`.
120
+ * Each pair is a field's `[state, rules]`. The pairs in one call share a
121
+ * value type, so a call gates fields of a single type; for a form that mixes
122
+ * types, call `allValid` once per type and combine the results with `&&`.
123
+ * Use for form-level submit gates. */
124
+ export const allValid = (pairs) => Array.every(pairs, ([state, rules]) => isValid(rules)(state));
125
+ /** Returns true when any state in the input has tag `Invalid`. Use for
126
+ * "this step/section has errors" affordances, independent of rules. */
127
+ export const anyInvalid = (states) => Array.some(states, isInvalid);
@@ -1,121 +1,3 @@
1
- import { Option, Predicate, Schema as S } from 'effect';
2
- /** An error message for a rule: either a static string, or a function that receives the invalid value. */
3
- export type RuleMessage = string | ((value: string) => string);
4
- /** A tuple of a predicate and error message used for field validation. */
5
- export type Rule = readonly [Predicate.Predicate<string>, RuleMessage];
6
- export declare const resolveMessage: (message: RuleMessage, value: string) => string;
7
- /** The `NotValidated` state: user hasn't interacted yet. */
8
- export declare const NotValidated: import("../schema/index.js").CallableTaggedStruct<"NotValidated", {
9
- value: S.String;
10
- }>;
11
- /** The `Validating` state: async validation is in flight. */
12
- export declare const Validating: import("../schema/index.js").CallableTaggedStruct<"Validating", {
13
- value: S.String;
14
- }>;
15
- /** The `Valid` state: every rule passed. */
16
- export declare const Valid: import("../schema/index.js").CallableTaggedStruct<"Valid", {
17
- value: S.String;
18
- }>;
19
- /** The `Invalid` state: one or more rules failed. Carries a non-empty `errors` array. */
20
- export declare const Invalid: import("../schema/index.js").CallableTaggedStruct<"Invalid", {
21
- value: S.String;
22
- errors: S.NonEmptyArray<S.String>;
23
- }>;
24
- /** The four-state union that represents a field's value in the Model. */
25
- export declare const Field: S.Union<readonly [import("../schema/index.js").CallableTaggedStruct<"NotValidated", {
26
- value: S.String;
27
- }>, import("../schema/index.js").CallableTaggedStruct<"Validating", {
28
- value: S.String;
29
- }>, import("../schema/index.js").CallableTaggedStruct<"Valid", {
30
- value: S.String;
31
- }>, import("../schema/index.js").CallableTaggedStruct<"Invalid", {
32
- value: S.String;
33
- errors: S.NonEmptyArray<S.String>;
34
- }>]>;
35
- export type Field = typeof Field.Type;
36
- /** A field's validation rules: the required message (if any), the list of rules,
37
- * and an empty predicate. Produced by `makeRules`; consumed by the module's
38
- * operations (`validate`, `validateAll`, `isValid`, `isRequired`, `allValid`).
39
- * The fields are accessible but treating them as stable is discouraged.
40
- * Prefer the operations so internal shape changes don't break callers. */
41
- export type Rules = Readonly<{
42
- requiredMessage: Option.Option<RuleMessage>;
43
- rules: ReadonlyArray<Rule>;
44
- isEmpty: (value: string) => boolean;
45
- }>;
46
- /** Options accepted by `makeRules`. */
47
- export type MakeRulesOptions = Readonly<{
48
- /** When present, the field is required: empty values become `Invalid`
49
- * with the given message, and `isValid` requires `Valid`. Absent
50
- * means the field is optional: empty values stay `NotValidated`, and
51
- * `isValid` accepts `Valid` or `NotValidated`. */
52
- required?: RuleMessage;
53
- rules?: ReadonlyArray<Rule>;
54
- /** Predicate for what counts as "empty" for this field. Defaults to
55
- * `String.isEmpty` (the empty string only). Pass `(v) => v.trim() === ''`
56
- * to treat whitespace-only input as empty. */
57
- isEmpty?: (value: string) => boolean;
58
- }>;
59
- export declare const makeRules: (options?: MakeRulesOptions) => Rules;
60
- /** Validates a new value and returns the next field state.
61
- *
62
- * For required fields, an empty value produces `Invalid` with the
63
- * required message. For optional fields, an empty value produces
64
- * `NotValidated`. Non-empty values run through the field's rules;
65
- * the first failure becomes `Invalid`, otherwise the result is `Valid`. */
66
- export declare const validate: (rules: Rules) => (value: string) => Field;
67
- /** Like `validate` but collects every failing rule into the
68
- * `Invalid` state's errors array instead of stopping at the first. */
69
- export declare const validateAll: (rules: Rules) => (value: string) => Field;
70
- /** Returns true when the field's current state is acceptable given its
71
- * rules. For required fields, only `Valid` returns `true`. For optional
72
- * fields, `Valid` or `NotValidated` both return `true`. `Invalid` and
73
- * `Validating` always return `false`.
74
- *
75
- * The name is distinct from the `Valid` tag on purpose: `isValid`
76
- * answers "is this state an acceptable result?", which for an optional
77
- * field is broader than `_tag === 'Valid'`. For pattern-matching on the
78
- * state itself, check the `_tag` directly. */
79
- export declare const isValid: (rules: Rules) => (state: Field) => boolean;
80
- /** Returns true when the rules mark the field as required. Useful for
81
- * rendering affordances like a `*` next to required field labels. */
82
- export declare const isRequired: (rules: Rules) => boolean;
83
- /** Returns true when the state's tag is `Invalid`. Tag-only predicate;
84
- * unlike `!isValid(rules)(state)`, this does not treat `NotValidated`
85
- * or `Validating` as errors. Use for "has the user seen a rule failure
86
- * on this field?" affordances like red borders or per-step error
87
- * indicators. */
88
- export declare const isInvalid: (state: Field) => boolean;
89
- /** Returns true when every `(state, rules)` pair in the input is
90
- * acceptable per `isValid`. Use for form-level submit gates. */
91
- export declare const allValid: (pairs: ReadonlyArray<readonly [Field, Rules]>) => boolean;
92
- /** Returns true when any state in the input has tag `Invalid`. Use for
93
- * "this step/section has errors" affordances, independent of rules. */
94
- export declare const anyInvalid: (states: ReadonlyArray<Field>) => boolean;
95
- /** Creates a `Rule` that checks if a string meets a minimum length. */
96
- export declare const minLength: (min: number, message?: RuleMessage) => Rule;
97
- /** Creates a `Rule` that checks if a string does not exceed a maximum length. */
98
- export declare const maxLength: (max: number, message?: RuleMessage) => Rule;
99
- /** Creates a `Rule` that checks if a string matches a regular expression. */
100
- export declare const pattern: (regex: RegExp, message?: RuleMessage) => Rule;
101
- /** Creates a `Rule` that checks if a string is a valid email format. */
102
- export declare const email: (message?: RuleMessage) => Rule;
103
- /** Creates a `Rule` that checks if a string is a valid URL format.
104
- *
105
- * By default the URL must include an `http://` or `https://` protocol.
106
- * Pass `{ requireProtocol: false }` to accept bare domains. */
107
- export declare const url: (options?: Readonly<{
108
- message?: RuleMessage;
109
- requireProtocol?: boolean;
110
- }>) => Rule;
111
- /** Creates a `Rule` that checks if a string begins with a specified prefix. */
112
- export declare const startsWith: (prefix: string, message?: RuleMessage) => Rule;
113
- /** Creates a `Rule` that checks if a string ends with a specified suffix. */
114
- export declare const endsWith: (suffix: string, message?: RuleMessage) => Rule;
115
- /** Creates a `Rule` that checks if a string contains a specified substring. */
116
- export declare const includes: (substring: string, message?: RuleMessage) => Rule;
117
- /** Creates a `Rule` that checks if a string exactly matches an expected value. */
118
- export declare const equals: (expected: string, message?: RuleMessage) => Rule;
119
- /** Creates a `Rule` that checks if a string is one of a specified set of allowed values. */
120
- export declare const oneOf: (values: ReadonlyArray<string>, message?: RuleMessage) => Rule;
1
+ export * from './fieldValidation.js';
2
+ export * as Rule from './rule.js';
121
3
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/fieldValidation/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/fieldValidation/index.ts"],"names":[],"mappings":"AAAA,cAAc,sBAAsB,CAAA;AAEpC,OAAO,KAAK,IAAI,MAAM,WAAW,CAAA"}
@@ -1,144 +1,2 @@
1
- import { Array, Number as Number_, Option, Result, Schema as S, String, flow, pipe, } from 'effect';
2
- import { ts } from '../schema/index.js';
3
- export const resolveMessage = (message, value) => typeof message === 'string' ? message : message(value);
4
- // STATE
5
- /** The `NotValidated` state: user hasn't interacted yet. */
6
- export const NotValidated = ts('NotValidated', { value: S.String });
7
- /** The `Validating` state: async validation is in flight. */
8
- export const Validating = ts('Validating', { value: S.String });
9
- /** The `Valid` state: every rule passed. */
10
- export const Valid = ts('Valid', { value: S.String });
11
- /** The `Invalid` state: one or more rules failed. Carries a non-empty `errors` array. */
12
- export const Invalid = ts('Invalid', {
13
- value: S.String,
14
- errors: S.NonEmptyArray(S.String),
15
- });
16
- /** The four-state union that represents a field's value in the Model. */
17
- export const Field = S.Union([NotValidated, Validating, Valid, Invalid]);
18
- export const makeRules = (options = {}) => ({
19
- requiredMessage: Option.fromNullishOr(options.required),
20
- rules: options.rules ?? [],
21
- isEmpty: options.isEmpty ?? String.isEmpty,
22
- });
23
- // OPERATIONS
24
- /** Validates a new value and returns the next field state.
25
- *
26
- * For required fields, an empty value produces `Invalid` with the
27
- * required message. For optional fields, an empty value produces
28
- * `NotValidated`. Non-empty values run through the field's rules;
29
- * the first failure becomes `Invalid`, otherwise the result is `Valid`. */
30
- export const validate = (rules) => (value) => {
31
- if (rules.isEmpty(value)) {
32
- return Option.match(rules.requiredMessage, {
33
- onNone: () => NotValidated({ value }),
34
- onSome: message => Invalid({ value, errors: [resolveMessage(message, value)] }),
35
- });
36
- }
37
- return pipe(rules.rules, Array.findFirst(([predicate]) => !predicate(value)), Option.match({
38
- onNone: () => Valid({ value }),
39
- onSome: ([, message]) => Invalid({ value, errors: [resolveMessage(message, value)] }),
40
- }));
41
- };
42
- /** Like `validate` but collects every failing rule into the
43
- * `Invalid` state's errors array instead of stopping at the first. */
44
- export const validateAll = (rules) => (value) => {
45
- if (rules.isEmpty(value)) {
46
- return Option.match(rules.requiredMessage, {
47
- onNone: () => NotValidated({ value }),
48
- onSome: message => Invalid({ value, errors: [resolveMessage(message, value)] }),
49
- });
50
- }
51
- return pipe(rules.rules, Array.filterMap(([predicate, message]) => !predicate(value)
52
- ? Result.succeed(resolveMessage(message, value))
53
- : Result.failVoid), Array.match({
54
- onEmpty: () => Valid({ value }),
55
- onNonEmpty: errors => Invalid({ value, errors }),
56
- }));
57
- };
58
- /** Returns true when the field's current state is acceptable given its
59
- * rules. For required fields, only `Valid` returns `true`. For optional
60
- * fields, `Valid` or `NotValidated` both return `true`. `Invalid` and
61
- * `Validating` always return `false`.
62
- *
63
- * The name is distinct from the `Valid` tag on purpose: `isValid`
64
- * answers "is this state an acceptable result?", which for an optional
65
- * field is broader than `_tag === 'Valid'`. For pattern-matching on the
66
- * state itself, check the `_tag` directly. */
67
- export const isValid = (rules) => (state) => {
68
- if (state._tag === 'Invalid' || state._tag === 'Validating') {
69
- return false;
70
- }
71
- if (Option.isSome(rules.requiredMessage)) {
72
- return state._tag === 'Valid';
73
- }
74
- return true;
75
- };
76
- /** Returns true when the rules mark the field as required. Useful for
77
- * rendering affordances like a `*` next to required field labels. */
78
- export const isRequired = (rules) => Option.isSome(rules.requiredMessage);
79
- /** Returns true when the state's tag is `Invalid`. Tag-only predicate;
80
- * unlike `!isValid(rules)(state)`, this does not treat `NotValidated`
81
- * or `Validating` as errors. Use for "has the user seen a rule failure
82
- * on this field?" affordances like red borders or per-step error
83
- * indicators. */
84
- export const isInvalid = (state) => state._tag === 'Invalid';
85
- /** Returns true when every `(state, rules)` pair in the input is
86
- * acceptable per `isValid`. Use for form-level submit gates. */
87
- export const allValid = (pairs) => Array.every(pairs, ([state, rules]) => isValid(rules)(state));
88
- /** Returns true when any state in the input has tag `Invalid`. Use for
89
- * "this step/section has errors" affordances, independent of rules. */
90
- export const anyInvalid = (states) => Array.some(states, isInvalid);
91
- // STRING RULES
92
- /** Creates a `Rule` that checks if a string meets a minimum length. */
93
- export const minLength = (min, message) => [
94
- flow(String.length, Number_.isGreaterThanOrEqualTo(min)),
95
- message ?? `Must be at least ${min} characters`,
96
- ];
97
- /** Creates a `Rule` that checks if a string does not exceed a maximum length. */
98
- export const maxLength = (max, message) => [
99
- flow(String.length, Number_.isLessThanOrEqualTo(max)),
100
- message ?? `Must be at most ${max} characters`,
101
- ];
102
- /** Creates a `Rule` that checks if a string matches a regular expression. */
103
- export const pattern = (regex, message = 'Invalid format') => [flow(String.match(regex), Option.isSome), message];
104
- const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
105
- /** Creates a `Rule` that checks if a string is a valid email format. */
106
- export const email = (message = 'Invalid email address') => pattern(EMAIL_REGEX, message);
107
- const STRICT_URL_REGEX = /^https?:\/\/.+/;
108
- const PERMISSIVE_URL_REGEX = /^(https?:\/\/)?\S+\.\S+$/;
109
- /** Creates a `Rule` that checks if a string is a valid URL format.
110
- *
111
- * By default the URL must include an `http://` or `https://` protocol.
112
- * Pass `{ requireProtocol: false }` to accept bare domains. */
113
- export const url = (options = {}) => {
114
- const { message = 'Invalid URL', requireProtocol = true } = options;
115
- return pattern(requireProtocol ? STRICT_URL_REGEX : PERMISSIVE_URL_REGEX, message);
116
- };
117
- /** Creates a `Rule` that checks if a string begins with a specified prefix. */
118
- export const startsWith = (prefix, message) => [
119
- flow(String.startsWith(prefix)),
120
- message ?? `Must start with ${prefix}`,
121
- ];
122
- /** Creates a `Rule` that checks if a string ends with a specified suffix. */
123
- export const endsWith = (suffix, message) => [
124
- flow(String.endsWith(suffix)),
125
- message ?? `Must end with ${suffix}`,
126
- ];
127
- /** Creates a `Rule` that checks if a string contains a specified substring. */
128
- export const includes = (substring, message) => [
129
- flow(String.includes(substring)),
130
- message ?? `Must contain ${substring}`,
131
- ];
132
- /** Creates a `Rule` that checks if a string exactly matches an expected value. */
133
- export const equals = (expected, message) => [
134
- value => value === expected,
135
- message ?? `Must match ${expected}`,
136
- ];
137
- /** Creates a `Rule` that checks if a string is one of a specified set of allowed values. */
138
- export const oneOf = (values, message) => {
139
- const joinedValues = Array.join(values, ', ');
140
- return [
141
- value => Array.contains(values, value),
142
- message ?? `Must be one of: ${joinedValues}`,
143
- ];
144
- };
1
+ export * from './fieldValidation.js';
2
+ export * as Rule from './rule.js';
@@ -1,3 +1,4 @@
1
- export { makeRules, validate, validateAll, isValid, isInvalid, isRequired, allValid, anyInvalid, resolveMessage, NotValidated, Validating, Valid, Invalid, Field, minLength, maxLength, pattern, email, url, startsWith, endsWith, includes, equals, oneOf, } from './index.js';
2
- export type { Rules, MakeRulesOptions, Rule, RuleMessage } from './index.js';
1
+ export { makeRules, validate, validateAll, isValid, isInvalid, isRequired, allValid, anyInvalid, NotValidated, Validating, Valid, Invalid, Field, } from './index.js';
2
+ export type { Rules, MakeRulesOptions } from './index.js';
3
+ export * as Rule from './rule.js';
3
4
  //# 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,WAAW,EACX,OAAO,EACP,SAAS,EACT,UAAU,EACV,QAAQ,EACR,UAAU,EACV,cAAc,EACd,YAAY,EACZ,UAAU,EACV,KAAK,EACL,OAAO,EACP,KAAK,EACL,SAAS,EACT,SAAS,EACT,OAAO,EACP,KAAK,EACL,GAAG,EACH,UAAU,EACV,QAAQ,EACR,QAAQ,EACR,MAAM,EACN,KAAK,GACN,MAAM,YAAY,CAAA;AAEnB,YAAY,EAAE,KAAK,EAAE,gBAAgB,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA"}
1
+ {"version":3,"file":"public.d.ts","sourceRoot":"","sources":["../../src/fieldValidation/public.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,SAAS,EACT,QAAQ,EACR,WAAW,EACX,OAAO,EACP,SAAS,EACT,UAAU,EACV,QAAQ,EACR,UAAU,EACV,YAAY,EACZ,UAAU,EACV,KAAK,EACL,OAAO,EACP,KAAK,GACN,MAAM,YAAY,CAAA;AAEnB,YAAY,EAAE,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA;AAEzD,OAAO,KAAK,IAAI,MAAM,WAAW,CAAA"}
@@ -1 +1,2 @@
1
- export { makeRules, validate, validateAll, isValid, isInvalid, isRequired, allValid, anyInvalid, resolveMessage, NotValidated, Validating, Valid, Invalid, Field, minLength, maxLength, pattern, email, url, startsWith, endsWith, includes, equals, oneOf, } from './index.js';
1
+ export { makeRules, validate, validateAll, isValid, isInvalid, isRequired, allValid, anyInvalid, NotValidated, Validating, Valid, Invalid, Field, } from './index.js';
2
+ export * as Rule from './rule.js';
@@ -0,0 +1,46 @@
1
+ import { Predicate, Schema as S } from 'effect';
2
+ /** An error message for a rule: either a static string, or a function that receives the invalid value. */
3
+ export type RuleMessage<A> = string | ((value: A) => string);
4
+ /** A tuple of a predicate and error message used for field validation. */
5
+ export type Rule<A> = readonly [Predicate.Predicate<A>, RuleMessage<A>];
6
+ /** Resolves a `RuleMessage` to a concrete string, applying it to the value when the message is a function. */
7
+ export declare const resolveMessage: <A>(message: RuleMessage<A>, value: A) => string;
8
+ /** Creates a `Rule` that checks if a string meets a minimum length. */
9
+ export declare const minLength: (min: number, message?: RuleMessage<string>) => Rule<string>;
10
+ /** Creates a `Rule` that checks if a string does not exceed a maximum length. */
11
+ export declare const maxLength: (max: number, message?: RuleMessage<string>) => Rule<string>;
12
+ /** Creates a `Rule` that checks if a string matches a regular expression. */
13
+ export declare const pattern: (regex: RegExp, message?: RuleMessage<string>) => Rule<string>;
14
+ /** Creates a `Rule` that checks if a string is a valid email format. */
15
+ export declare const email: (message?: RuleMessage<string>) => Rule<string>;
16
+ /** Creates a `Rule` that checks if a string is a valid URL format.
17
+ *
18
+ * By default the URL must include an `http://` or `https://` protocol.
19
+ * Pass `{ requireProtocol: false }` to accept bare domains. */
20
+ export declare const url: (options?: Readonly<{
21
+ message?: RuleMessage<string>;
22
+ requireProtocol?: boolean;
23
+ }>) => Rule<string>;
24
+ /** Creates a `Rule` that checks if a string begins with a specified prefix. */
25
+ export declare const startsWith: (prefix: string, message?: RuleMessage<string>) => Rule<string>;
26
+ /** Creates a `Rule` that checks if a string ends with a specified suffix. */
27
+ export declare const endsWith: (suffix: string, message?: RuleMessage<string>) => Rule<string>;
28
+ /** Creates a `Rule` that checks if a string contains a specified substring. */
29
+ export declare const includes: (substring: string, message?: RuleMessage<string>) => Rule<string>;
30
+ /** Creates a `Rule` that checks if a string exactly matches an expected value. */
31
+ export declare const equals: (expected: string, message?: RuleMessage<string>) => Rule<string>;
32
+ /** Creates a `Rule` that checks if a string is one of a specified set of allowed values. */
33
+ export declare const oneOf: (values: ReadonlyArray<string>, message?: RuleMessage<string>) => Rule<string>;
34
+ /** Creates a `Rule` that checks an array holds at least `min` items. */
35
+ export declare const minItems: (min: number, message?: RuleMessage<ReadonlyArray<unknown>>) => Rule<ReadonlyArray<unknown>>;
36
+ /** Creates a `Rule` that checks an array holds at most `max` items. */
37
+ export declare const maxItems: (max: number, message?: RuleMessage<ReadonlyArray<unknown>>) => Rule<ReadonlyArray<unknown>>;
38
+ /** Creates a `Rule` that passes when the value decodes through `schema`.
39
+ *
40
+ * Use it to reuse a Schema you already maintain (a domain codec, a refined or
41
+ * branded type you decode to on submit) as a field rule, so the rule stays in
42
+ * sync with the schema instead of duplicating its logic. It does nothing a
43
+ * custom rule can't, so prefer the dedicated rules for plain checks. Decoding
44
+ * is synchronous: the schema must decode without running an effect. */
45
+ export declare const fromSchema: <A, I>(schema: S.Codec<A, I>, message: RuleMessage<I>) => Rule<I>;
46
+ //# sourceMappingURL=rule.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rule.d.ts","sourceRoot":"","sources":["../../src/fieldValidation/rule.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,77 @@
1
+ import { Array, Number as Number_, Option, Schema as S, String, flow, } from 'effect';
2
+ /** Resolves a `RuleMessage` to a concrete string, applying it to the value when the message is a function. */
3
+ export const resolveMessage = (message, value) => typeof message === 'string' ? message : message(value);
4
+ // STRING RULES
5
+ /** Creates a `Rule` that checks if a string meets a minimum length. */
6
+ export const minLength = (min, message) => [
7
+ flow(String.length, Number_.isGreaterThanOrEqualTo(min)),
8
+ message ?? `Must be at least ${min} characters`,
9
+ ];
10
+ /** Creates a `Rule` that checks if a string does not exceed a maximum length. */
11
+ export const maxLength = (max, message) => [
12
+ flow(String.length, Number_.isLessThanOrEqualTo(max)),
13
+ message ?? `Must be at most ${max} characters`,
14
+ ];
15
+ /** Creates a `Rule` that checks if a string matches a regular expression. */
16
+ export const pattern = (regex, message = 'Invalid format') => [flow(String.match(regex), Option.isSome), message];
17
+ const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
18
+ /** Creates a `Rule` that checks if a string is a valid email format. */
19
+ export const email = (message = 'Invalid email address') => pattern(EMAIL_REGEX, message);
20
+ const STRICT_URL_REGEX = /^https?:\/\/.+/;
21
+ const PERMISSIVE_URL_REGEX = /^(https?:\/\/)?\S+\.\S+$/;
22
+ /** Creates a `Rule` that checks if a string is a valid URL format.
23
+ *
24
+ * By default the URL must include an `http://` or `https://` protocol.
25
+ * Pass `{ requireProtocol: false }` to accept bare domains. */
26
+ export const url = (options = {}) => {
27
+ const { message = 'Invalid URL', requireProtocol = true } = options;
28
+ return pattern(requireProtocol ? STRICT_URL_REGEX : PERMISSIVE_URL_REGEX, message);
29
+ };
30
+ /** Creates a `Rule` that checks if a string begins with a specified prefix. */
31
+ export const startsWith = (prefix, message) => [
32
+ String.startsWith(prefix),
33
+ message ?? `Must start with ${prefix}`,
34
+ ];
35
+ /** Creates a `Rule` that checks if a string ends with a specified suffix. */
36
+ export const endsWith = (suffix, message) => [
37
+ String.endsWith(suffix),
38
+ message ?? `Must end with ${suffix}`,
39
+ ];
40
+ /** Creates a `Rule` that checks if a string contains a specified substring. */
41
+ export const includes = (substring, message) => [
42
+ String.includes(substring),
43
+ message ?? `Must contain ${substring}`,
44
+ ];
45
+ /** Creates a `Rule` that checks if a string exactly matches an expected value. */
46
+ export const equals = (expected, message) => [
47
+ value => value === expected,
48
+ message ?? `Must match ${expected}`,
49
+ ];
50
+ /** Creates a `Rule` that checks if a string is one of a specified set of allowed values. */
51
+ export const oneOf = (values, message) => {
52
+ const joinedValues = Array.join(values, ', ');
53
+ return [
54
+ value => Array.contains(values, value),
55
+ message ?? `Must be one of: ${joinedValues}`,
56
+ ];
57
+ };
58
+ // ARRAY RULES
59
+ /** Creates a `Rule` that checks an array holds at least `min` items. */
60
+ export const minItems = (min, message) => [
61
+ flow(Array.length, Number_.isGreaterThanOrEqualTo(min)),
62
+ message ?? `Must select at least ${min}`,
63
+ ];
64
+ /** Creates a `Rule` that checks an array holds at most `max` items. */
65
+ export const maxItems = (max, message) => [
66
+ flow(Array.length, Number_.isLessThanOrEqualTo(max)),
67
+ message ?? `Must select at most ${max}`,
68
+ ];
69
+ // SCHEMA RULES
70
+ /** Creates a `Rule` that passes when the value decodes through `schema`.
71
+ *
72
+ * Use it to reuse a Schema you already maintain (a domain codec, a refined or
73
+ * branded type you decode to on submit) as a field rule, so the rule stays in
74
+ * sync with the schema instead of duplicating its logic. It does nothing a
75
+ * custom rule can't, so prefer the dedicated rules for plain checks. Decoding
76
+ * is synchronous: the schema must decode without running an effect. */
77
+ export const fromSchema = (schema, message) => [flow(S.decodeOption(schema), Option.isSome), message];
@@ -1,8 +1,8 @@
1
- import { Effect } from 'effect';
1
+ import { Effect, Option } from 'effect';
2
2
  import type { File } from './file.js';
3
3
  /**
4
4
  * Opens the native file picker allowing a single file to be selected. Resolves
5
- * with an array containing the selected file, or an empty array if the user
5
+ * with `Option.some(file)` if the user picked one, or `Option.none()` if they
6
6
  * cancelled. Mirrors Elm's `File.Select.file`.
7
7
  *
8
8
  * The `accept` argument is a list of MIME types or file extensions that
@@ -12,12 +12,17 @@ import type { File } from './file.js';
12
12
  * ```typescript
13
13
  * SelectResume(
14
14
  * File.select(['application/pdf']).pipe(
15
- * Effect.map(files => SelectedResume({ files })),
15
+ * Effect.map(
16
+ * Option.match({
17
+ * onNone: () => CancelledSelectResume(),
18
+ * onSome: file => SelectedResume({ file }),
19
+ * }),
20
+ * ),
16
21
  * ),
17
22
  * )
18
23
  * ```
19
24
  */
20
- export declare const select: (accept: ReadonlyArray<string>) => Effect.Effect<ReadonlyArray<File>>;
25
+ export declare const select: (accept: ReadonlyArray<string>) => Effect.Effect<Option.Option<File>>;
21
26
  /**
22
27
  * Opens the native file picker allowing multiple files to be selected at
23
28
  * once. Resolves with the array of selected files, or an empty array if the
@@ -1 +1 @@
1
- {"version":3,"file":"select.d.ts","sourceRoot":"","sources":["../../src/file/select.ts"],"names":[],"mappings":"AAAA,OAAO,EAAS,MAAM,EAAE,MAAM,QAAQ,CAAA;AAEtC,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AA2CrC;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,MAAM,GACjB,QAAQ,aAAa,CAAC,MAAM,CAAC,KAC5B,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC,CAA4C,CAAA;AAEhF;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,cAAc,GACzB,QAAQ,aAAa,CAAC,MAAM,CAAC,KAC5B,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC,CAA2C,CAAA"}
1
+ {"version":3,"file":"select.d.ts","sourceRoot":"","sources":["../../src/file/select.ts"],"names":[],"mappings":"AAAA,OAAO,EAAS,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAE9C,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AA2CrC;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,MAAM,GACjB,QAAQ,aAAa,CAAC,MAAM,CAAC,KAC5B,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CACkC,CAAA;AAEtE;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,cAAc,GACzB,QAAQ,aAAa,CAAC,MAAM,CAAC,KAC5B,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC,CAA2C,CAAA"}
@@ -27,7 +27,7 @@ const openPicker = ({ accept, multiple, }) => Effect.callback((resume, signal) =
27
27
  });
28
28
  /**
29
29
  * Opens the native file picker allowing a single file to be selected. Resolves
30
- * with an array containing the selected file, or an empty array if the user
30
+ * with `Option.some(file)` if the user picked one, or `Option.none()` if they
31
31
  * cancelled. Mirrors Elm's `File.Select.file`.
32
32
  *
33
33
  * The `accept` argument is a list of MIME types or file extensions that
@@ -37,12 +37,17 @@ const openPicker = ({ accept, multiple, }) => Effect.callback((resume, signal) =
37
37
  * ```typescript
38
38
  * SelectResume(
39
39
  * File.select(['application/pdf']).pipe(
40
- * Effect.map(files => SelectedResume({ files })),
40
+ * Effect.map(
41
+ * Option.match({
42
+ * onNone: () => CancelledSelectResume(),
43
+ * onSome: file => SelectedResume({ file }),
44
+ * }),
45
+ * ),
41
46
  * ),
42
47
  * )
43
48
  * ```
44
49
  */
45
- export const select = (accept) => openPicker({ accept, multiple: false });
50
+ export const select = (accept) => openPicker({ accept, multiple: false }).pipe(Effect.map(Array.head));
46
51
  /**
47
52
  * Opens the native file picker allowing multiple files to be selected at
48
53
  * once. Resolves with the array of selected files, or an empty array if the
@@ -9,7 +9,7 @@ export declare const Model: S.Struct<{
9
9
  export type Model = typeof Model.Type;
10
10
  export declare const ClickedChooseResume: import("../../schema/index.js").CallableTaggedStruct<"ClickedChooseResume", {}>;
11
11
  export declare const SelectedResume: import("../../schema/index.js").CallableTaggedStruct<"SelectedResume", {
12
- files: S.$Array<S.Schema<File>>;
12
+ file: S.Schema<File>;
13
13
  }>;
14
14
  export declare const CancelledSelectResume: import("../../schema/index.js").CallableTaggedStruct<"CancelledSelectResume", {}>;
15
15
  export declare const SucceededReadPreview: import("../../schema/index.js").CallableTaggedStruct<"SucceededReadPreview", {
@@ -18,7 +18,7 @@ export declare const SucceededReadPreview: import("../../schema/index.js").Calla
18
18
  export declare const FailedReadPreview: import("../../schema/index.js").CallableTaggedStruct<"FailedReadPreview", {}>;
19
19
  export declare const ClickedRemoveResume: import("../../schema/index.js").CallableTaggedStruct<"ClickedRemoveResume", {}>;
20
20
  export declare const Message: S.Union<readonly [import("../../schema/index.js").CallableTaggedStruct<"ClickedChooseResume", {}>, import("../../schema/index.js").CallableTaggedStruct<"SelectedResume", {
21
- files: S.$Array<S.Schema<File>>;
21
+ file: S.Schema<File>;
22
22
  }>, import("../../schema/index.js").CallableTaggedStruct<"CancelledSelectResume", {}>, import("../../schema/index.js").CallableTaggedStruct<"SucceededReadPreview", {
23
23
  dataUrl: S.String;
24
24
  }>, import("../../schema/index.js").CallableTaggedStruct<"FailedReadPreview", {}>, import("../../schema/index.js").CallableTaggedStruct<"ClickedRemoveResume", {}>]>;
@@ -27,7 +27,7 @@ export declare const SelectResume: Command.CommandDefinitionNoArgs<"SelectResume
27
27
  readonly _tag: "CancelledSelectResume";
28
28
  } | {
29
29
  readonly _tag: "SelectedResume";
30
- readonly files: readonly File[];
30
+ readonly file: File;
31
31
  }, never, never>>;
32
32
  export declare const ReadResumePreview: Command.CommandDefinitionWithArgs<"ReadResumePreview", {
33
33
  file: S.Schema<File>;
@@ -1 +1 @@
1
- {"version":3,"file":"resumeUpload.d.ts","sourceRoot":"","sources":["../../../src/test/apps/resumeUpload.ts"],"names":[],"mappings":"AAAA,OAAO,EAAS,MAAM,EAAsB,MAAM,IAAI,CAAC,EAAE,MAAM,QAAQ,CAAA;AAEvE,OAAO,KAAK,OAAO,MAAM,wBAAwB,CAAA;AAEjD,OAAO,EAAE,KAAK,IAAI,EAAQ,MAAM,qBAAqB,CAAA;AAMrD,eAAO,MAAM,KAAK;;;;EAIhB,CAAA;AAEF,MAAM,MAAM,KAAK,GAAG,OAAO,KAAK,CAAC,IAAI,CAAA;AAIrC,eAAO,MAAM,mBAAmB,iFAA2B,CAAA;AAC3D,eAAO,MAAM,cAAc;;EAEzB,CAAA;AACF,eAAO,MAAM,qBAAqB,mFAA6B,CAAA;AAC/D,eAAO,MAAM,oBAAoB;;EAE/B,CAAA;AACF,eAAO,MAAM,iBAAiB,+EAAyB,CAAA;AACvD,eAAO,MAAM,mBAAmB,iFAA2B,CAAA;AAE3D,eAAO,MAAM,OAAO;;;;oKAOlB,CAAA;AACF,MAAM,MAAM,OAAO,GAAG,OAAO,OAAO,CAAC,IAAI,CAAA;AAIzC,eAAO,MAAM,YAAY;;;;;iBAaxB,CAAA;AAED,eAAO,MAAM,iBAAiB;;;;;;;iBAU7B,CAAA;AAID,eAAO,MAAM,YAAY,EAAE,KAI1B,CAAA;AAID,KAAK,YAAY,GAAG,SAAS,CAAC,KAAK,EAAE,aAAa,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAA;AAE7E,eAAO,MAAM,MAAM,GAAI,OAAO,KAAK,EAAE,SAAS,OAAO,KAAG,YAmCrD,CAAA;AAwBH,eAAO,MAAM,IAAI,GAAI,OAAO,KAAK,KAAG,IAqBnC,CAAA"}
1
+ {"version":3,"file":"resumeUpload.d.ts","sourceRoot":"","sources":["../../../src/test/apps/resumeUpload.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAsB,MAAM,IAAI,CAAC,EAAE,MAAM,QAAQ,CAAA;AAEhE,OAAO,KAAK,OAAO,MAAM,wBAAwB,CAAA;AAEjD,OAAO,EAAE,KAAK,IAAI,EAAQ,MAAM,qBAAqB,CAAA;AAMrD,eAAO,MAAM,KAAK;;;;EAIhB,CAAA;AAEF,MAAM,MAAM,KAAK,GAAG,OAAO,KAAK,CAAC,IAAI,CAAA;AAIrC,eAAO,MAAM,mBAAmB,iFAA2B,CAAA;AAC3D,eAAO,MAAM,cAAc;;EAEzB,CAAA;AACF,eAAO,MAAM,qBAAqB,mFAA6B,CAAA;AAC/D,eAAO,MAAM,oBAAoB;;EAE/B,CAAA;AACF,eAAO,MAAM,iBAAiB,+EAAyB,CAAA;AACvD,eAAO,MAAM,mBAAmB,iFAA2B,CAAA;AAE3D,eAAO,MAAM,OAAO;;;;oKAOlB,CAAA;AACF,MAAM,MAAM,OAAO,GAAG,OAAO,OAAO,CAAC,IAAI,CAAA;AAIzC,eAAO,MAAM,YAAY;;;;;iBAaxB,CAAA;AAED,eAAO,MAAM,iBAAiB;;;;;;;iBAU7B,CAAA;AAID,eAAO,MAAM,YAAY,EAAE,KAI1B,CAAA;AAID,KAAK,YAAY,GAAG,SAAS,CAAC,KAAK,EAAE,aAAa,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAA;AAE7E,eAAO,MAAM,MAAM,GAAI,OAAO,KAAK,EAAE,SAAS,OAAO,KAAG,YA+BrD,CAAA;AAwBH,eAAO,MAAM,IAAI,GAAI,OAAO,KAAK,KAAG,IAqBnC,CAAA"}
@@ -1,4 +1,4 @@
1
- import { Array, Effect, Match as M, Option, Schema as S } from 'effect';
1
+ import { Effect, Match as M, Option, Schema as S } from 'effect';
2
2
  import * as Command from '../../command/index.js';
3
3
  import * as File from '../../file/index.js';
4
4
  import { html } from '../../html/index.js';
@@ -13,7 +13,7 @@ export const Model = S.Struct({
13
13
  // MESSAGE
14
14
  export const ClickedChooseResume = m('ClickedChooseResume');
15
15
  export const SelectedResume = m('SelectedResume', {
16
- files: S.Array(File.File),
16
+ file: File.File,
17
17
  });
18
18
  export const CancelledSelectResume = m('CancelledSelectResume');
19
19
  export const SucceededReadPreview = m('SucceededReadPreview', {
@@ -30,9 +30,9 @@ export const Message = S.Union([
30
30
  ClickedRemoveResume,
31
31
  ]);
32
32
  // COMMAND
33
- export const SelectResume = Command.define('SelectResume', SelectedResume, CancelledSelectResume)(File.select(['application/pdf']).pipe(Effect.map(Array.match({
34
- onEmpty: () => CancelledSelectResume(),
35
- onNonEmpty: files => SelectedResume({ files }),
33
+ export const SelectResume = Command.define('SelectResume', SelectedResume, CancelledSelectResume)(File.select(['application/pdf']).pipe(Effect.map(Option.match({
34
+ onNone: () => CancelledSelectResume(),
35
+ onSome: file => SelectedResume({ file }),
36
36
  }))));
37
37
  export const ReadResumePreview = Command.define('ReadResumePreview', { file: File.File }, SucceededReadPreview, FailedReadPreview)(({ file }) => File.readAsDataUrl(file).pipe(Effect.map(dataUrl => SucceededReadPreview({ dataUrl })), Effect.catch(() => Effect.succeed(FailedReadPreview()))));
38
38
  // INIT
@@ -43,17 +43,14 @@ export const initialModel = {
43
43
  };
44
44
  export const update = (model, message) => M.value(message).pipe(M.withReturnType(), M.tagsExhaustive({
45
45
  ClickedChooseResume: () => [model, [SelectResume()]],
46
- SelectedResume: ({ files }) => Option.match(Array.head(files), {
47
- onNone: () => [model, []],
48
- onSome: firstFile => [
49
- evo(model, {
50
- maybeResume: () => Option.some(firstFile),
51
- maybePreviewDataUrl: () => Option.none(),
52
- readStatus: () => 'Reading',
53
- }),
54
- [ReadResumePreview({ file: firstFile })],
55
- ],
56
- }),
46
+ SelectedResume: ({ file }) => [
47
+ evo(model, {
48
+ maybeResume: () => Option.some(file),
49
+ maybePreviewDataUrl: () => Option.none(),
50
+ readStatus: () => 'Reading',
51
+ }),
52
+ [ReadResumePreview({ file })],
53
+ ],
57
54
  CancelledSelectResume: () => [model, []],
58
55
  SucceededReadPreview: ({ dataUrl }) => [
59
56
  evo(model, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "foldkit",
3
- "version": "0.104.0",
3
+ "version": "0.105.0",
4
4
  "description": "A TypeScript frontend framework, built on Effect and architected like Elm",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -200,6 +200,7 @@
200
200
  "import": "./dist/devTools/public.js"
201
201
  }
202
202
  },
203
+ "sideEffects": false,
203
204
  "files": [
204
205
  "dist"
205
206
  ],