0.2.5 (#43)
All checks were successful
Publish / publish (push) Successful in 52s
Lint / lint (push) Successful in 14s

Co-authored-by: Julien Valverdé <julien.valverde@mailo.com>
Co-authored-by: Renovate Bot <renovate-bot@valverde.cloud>
Reviewed-on: #43
This commit was merged in pull request #43.
This commit is contained in:
2026-03-31 21:01:12 +02:00
parent 67b01d4621
commit ff13e941e3
20 changed files with 1018 additions and 756 deletions

View File

@@ -13,30 +13,30 @@
"clean:modules": "rm -rf node_modules"
},
"devDependencies": {
"@tanstack/react-router": "^1.154.12",
"@tanstack/react-router-devtools": "^1.154.12",
"@tanstack/router-plugin": "^1.154.12",
"@types/react": "^19.2.9",
"@tanstack/react-router": "^1.168.3",
"@tanstack/react-router-devtools": "^1.166.11",
"@tanstack/router-plugin": "^1.167.4",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.2",
"globals": "^17.0.0",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"type-fest": "^5.4.1",
"vite": "^7.3.1"
"@vitejs/plugin-react": "^6.0.1",
"globals": "^17.4.0",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"type-fest": "^5.5.0",
"vite": "^8.0.2"
},
"dependencies": {
"@effect/platform": "^0.94.2",
"@effect/platform-browser": "^0.74.0",
"@radix-ui/themes": "^3.2.1",
"@effect/platform": "^0.96.0",
"@effect/platform-browser": "^0.76.0",
"@radix-ui/themes": "^3.3.0",
"@typed/id": "^0.17.2",
"effect": "^3.19.15",
"effect": "^3.21.0",
"effect-fc": "workspace:*",
"react-icons": "^5.5.0"
"react-icons": "^5.6.0"
},
"overrides": {
"@types/react": "^19.2.9",
"effect": "^3.19.15",
"react": "^19.2.3"
"@types/react": "^19.2.14",
"effect": "^3.21.0",
"react": "^19.2.4"
}
}

View File

@@ -1,24 +1,22 @@
import { Callout, Flex, Spinner, TextField } from "@radix-ui/themes"
import { Array, Option } from "effect"
import { Array, Option, Struct } from "effect"
import { Component, Form, Subscribable } from "effect-fc"
export declare namespace TextFieldFormInputView {
export interface Props
extends TextField.RootProps, Form.useInput.Options {
readonly field: Form.FormField<any, string>
export interface Props extends Omit<TextField.RootProps, "form">, Form.useInput.Options {
readonly form: Form.Form<readonly PropertyKey[], any, string>
}
}
export class TextFieldFormInputView extends Component.make("TextFieldFormInputView")(function*(
props: TextFieldFormInputView.Props
) {
const input = yield* Form.useInput(props.field, props)
const input = yield* Form.useInput(props.form, props)
const [issues, isValidating, isSubmitting] = yield* Subscribable.useSubscribables([
props.field.issues,
props.field.isValidating,
props.field.isSubmitting,
props.form.issues,
props.form.isValidating,
props.form.isSubmitting,
])
return (
@@ -27,7 +25,7 @@ export class TextFieldFormInputView extends Component.make("TextFieldFormInputVi
value={input.value}
onChange={e => input.setValue(e.target.value)}
disabled={isSubmitting}
{...props}
{...Struct.omit(props, "form")}
>
{isValidating &&
<TextField.Slot side="right">

View File

@@ -4,21 +4,19 @@ import { Component, Form, Subscribable } from "effect-fc"
export declare namespace TextFieldOptionalFormInputView {
export interface Props
extends Omit<TextField.RootProps, "defaultValue">, Form.useOptionalInput.Options<string> {
readonly field: Form.FormField<any, Option.Option<string>>
export interface Props extends Omit<TextField.RootProps, "form" | "defaultValue">, Form.useOptionalInput.Options<string> {
readonly form: Form.Form<readonly PropertyKey[], any, Option.Option<string>>
}
}
export class TextFieldOptionalFormInputView extends Component.make("TextFieldOptionalFormInputView")(function*(
props: TextFieldOptionalFormInputView.Props
) {
const input = yield* Form.useOptionalInput(props.field, props)
const input = yield* Form.useOptionalInput(props.form, props)
const [issues, isValidating, isSubmitting] = yield* Subscribable.useSubscribables([
props.field.issues,
props.field.isValidating,
props.field.isSubmitting,
props.form.issues,
props.form.isValidating,
props.form.isSubmitting,
])
return (
@@ -27,7 +25,7 @@ export class TextFieldOptionalFormInputView extends Component.make("TextFieldOpt
value={input.value}
onChange={e => input.setValue(e.target.value)}
disabled={!input.enabled || isSubmitting}
{...Struct.omit(props, "defaultValue")}
{...Struct.omit(props, "form", "defaultValue")}
>
<TextField.Slot side="left">
<Switch

View File

@@ -1,11 +1,11 @@
import { Button, Container, Flex, Text } from "@radix-ui/themes"
import { createFileRoute } from "@tanstack/react-router"
import { Console, Effect, Match, Option, ParseResult, Schema } from "effect"
import { Component, Form, Subscribable } from "effect-fc"
import { TextFieldFormInputView } from "@/lib/form/TextFieldFormInputView"
import { TextFieldOptionalFormInputView } from "@/lib/form/TextFieldOptionalFormInputView"
import { DateTimeUtcFromZonedInput } from "@/lib/schema"
import { runtime } from "@/runtime"
import { Button, Container, Flex, Text } from "@radix-ui/themes"
import { createFileRoute } from "@tanstack/react-router"
import { Console, Effect, Match, Option, ParseResult, Schema } from "effect"
import { Component, Form, Subscribable } from "effect-fc"
const email = Schema.pattern<typeof Schema.String>(
@@ -40,34 +40,42 @@ const RegisterFormSubmitSchema = Schema.Struct({
})
class RegisterFormService extends Effect.Service<RegisterFormService>()("RegisterFormService", {
scoped: Form.service({
schema: RegisterFormSchema.pipe(
Schema.compose(
Schema.transformOrFail(
Schema.typeSchema(RegisterFormSchema),
Schema.typeSchema(RegisterFormSchema),
{
decode: v => Effect.andThen(Effect.sleep("500 millis"), ParseResult.succeed(v)),
encode: ParseResult.succeed,
},
scoped: Effect.gen(function*() {
const form = yield* Form.service({
schema: RegisterFormSchema.pipe(
Schema.compose(
Schema.transformOrFail(
Schema.typeSchema(RegisterFormSchema),
Schema.typeSchema(RegisterFormSchema),
{
decode: v => Effect.andThen(Effect.sleep("500 millis"), ParseResult.succeed(v)),
encode: ParseResult.succeed,
},
),
),
),
),
initialEncodedValue: { email: "", password: "", birth: Option.none() },
f: Effect.fnUntraced(function*([value]) {
yield* Effect.sleep("500 millis")
return yield* Schema.decode(RegisterFormSubmitSchema)(value)
}),
debounce: "500 millis",
initialEncodedValue: { email: "", password: "", birth: Option.none() },
f: Effect.fnUntraced(function*([value]) {
yield* Effect.sleep("500 millis")
return yield* Schema.decode(RegisterFormSubmitSchema)(value)
}),
})
return {
form,
emailField: Form.focusObjectField(form, "email"),
passwordField: Form.focusObjectField(form, "password"),
birthField: Form.focusObjectField(form, "birth"),
} as const
})
}) {}
class RegisterFormView extends Component.make("RegisterFormView")(function*() {
const form = yield* RegisterFormService
const [canSubmit, submitResult] = yield* Subscribable.useSubscribables([
form.canSubmit,
form.mutation.result,
form.form.canSubmit,
form.form.mutation.result,
])
const runPromise = yield* Component.useRunPromise()
@@ -84,20 +92,22 @@ class RegisterFormView extends Component.make("RegisterFormView")(function*() {
<Container width="300">
<form onSubmit={e => {
e.preventDefault()
void runPromise(form.submit)
void runPromise(form.form.submit)
}}>
<Flex direction="column" gap="2">
<TextFieldFormInput
field={yield* form.field(["email"])}
form={form.emailField}
debounce="250 millis"
/>
<TextFieldFormInput
field={yield* form.field(["password"])}
form={form.passwordField}
debounce="250 millis"
/>
<TextFieldOptionalFormInput
type="datetime-local"
field={yield* form.field(["birth"])}
form={form.birthField}
defaultValue=""
/>

View File

@@ -1,13 +1,13 @@
import { Box, Button, Flex, IconButton } from "@radix-ui/themes"
import { GetRandomValues, makeUuid4 } from "@typed/id"
import { Chunk, type DateTime, Effect, Match, Option, Ref, Schema, Stream } from "effect"
import { Component, Form, Subscribable } from "effect-fc"
import { FaArrowDown, FaArrowUp } from "react-icons/fa"
import { FaDeleteLeft } from "react-icons/fa6"
import * as Domain from "@/domain"
import { TextFieldFormInputView } from "@/lib/form/TextFieldFormInputView"
import { TextFieldOptionalFormInputView } from "@/lib/form/TextFieldOptionalFormInputView"
import { DateTimeUtcFromZonedInput } from "@/lib/schema"
import { Box, Button, Flex, IconButton } from "@radix-ui/themes"
import { GetRandomValues, makeUuid4 } from "@typed/id"
import { Chunk, type DateTime, Effect, Match, Option, Ref, Schema, Stream } from "effect"
import { Component, Form, Lens, Subscribable } from "effect-fc"
import { FaArrowDown, FaArrowUp } from "react-icons/fa"
import { FaDeleteLeft } from "react-icons/fa6"
import { TodosState } from "./TodosState"
@@ -59,20 +59,19 @@ export class TodoView extends Component.make("TodoView")(function*(props: TodoPr
Match.tag("new", () => Ref.update(state.ref, Chunk.prepend(todo)).pipe(
Effect.andThen(makeTodo),
Effect.andThen(Schema.encode(TodoFormSchema)),
Effect.andThen(v => Ref.set(form.encodedValue, v)),
Effect.andThen(v => Lens.set(form.encodedValue, v)),
)),
Match.tag("edit", ({ id }) => Ref.set(state.getElementRef(id), todo)),
Match.exhaustive,
),
autosubmit: props._tag === "edit",
debounce: "250 millis",
})
return [
indexRef,
form,
yield* form.field(["content"]),
yield* form.field(["completedAt"]),
Form.focusObjectField(form, "content"),
Form.focusObjectField(form, "completedAt"),
] as const
}), [props._tag, props._tag === "edit" ? props.id : undefined])
@@ -92,11 +91,14 @@ export class TodoView extends Component.make("TodoView")(function*(props: TodoPr
<Flex direction="row" align="center" gap="2">
<Box flexGrow="1">
<Flex direction="column" align="stretch" gap="2">
<TextFieldFormInput field={contentField} />
<TextFieldFormInput
form={contentField}
debounce="250 millis"
/>
<Flex direction="row" justify="center" align="center" gap="2">
<TextFieldOptionalFormInput
field={completedAtField}
form={completedAtField}
type="datetime-local"
defaultValue=""
/>