@@ -41,7 +54,8 @@ export function ProfileSearchPage(): JSXElement {
class="inline-grid w-96 gap-2"
onSubmit={(e) => {
e.preventDefault();
- goToPage();
+ e.stopPropagation();
+ void form.handleSubmit();
}}
>
@@ -50,31 +64,38 @@ export function ProfileSearchPage(): JSXElement {
- setValid(result.success)}
- // fine unless we read a reactive state after the await
- // eslint-disable-next-line solid/reactivity
- isValid={async (name: string) => {
- try {
- const result = await queryClient.fetchQuery(
- getUserProfile(name),
- );
- setSelectedProfileName(name);
- return result !== null || "Unknown user";
- } catch (e) {
- return "Unknown user or error fetching.";
- }
+ {
+ try {
+ const result = await queryClient.fetchQuery(
+ getUserProfile(field.value),
+ );
+ return result !== null ? undefined : "Unknown user";
+ } catch (e) {
+ return "Unknown user";
+ }
+ },
}}
+ children={(field) => (
+
+ )}
/>
-
diff --git a/frontend/src/ts/components/ui/ValidatedInput.tsx b/frontend/src/ts/components/ui/ValidatedInput.tsx
deleted file mode 100644
index db8a720c7b47..000000000000
--- a/frontend/src/ts/components/ui/ValidatedInput.tsx
+++ /dev/null
@@ -1,93 +0,0 @@
-import {
- splitProps,
- createEffect,
- JSXElement,
- on,
- onCleanup,
- onMount,
-} from "solid-js";
-
-import {
- ValidatedHtmlInputElement,
- ValidationOptions,
-} from "../../elements/input-validation";
-import { useRefWithUtils } from "../../hooks/useRefWithUtils";
-
-export function ValidatedInput
(
- props: ValidationOptions & {
- value?: string;
- placeholder?: string;
- class?: string;
- type?: string;
- autocomplete?: string;
- name?: string;
- onInput?: (value: T) => void;
- onFocus?: () => void;
- disabled?: boolean;
- revalidateOn?: () => unknown;
- },
-): JSXElement {
- // Refs are assigned by SolidJS via the ref attribute
- const [inputRef, inputEl] = useRefWithUtils();
-
- let validatedInput: ValidatedHtmlInputElement | undefined;
-
- createEffect(() => {
- validatedInput?.setValue(props.value ?? null);
- });
-
- onMount(() => {
- const element = inputEl();
- if (element === undefined) return;
-
- const [_, others] = splitProps(props, [
- "value",
- "class",
- "placeholder",
- "type",
- "autocomplete",
- "name",
- "onFocus",
- "revalidateOn",
- ]);
- validatedInput = new ValidatedHtmlInputElement(
- element,
- others as ValidationOptions,
- );
- validatedInput.setValue(props.value ?? null);
- });
-
- createEffect(
- on(
- () => props.revalidateOn?.(),
- () => {
- if (validatedInput && inputEl()?.getValue() !== "") {
- validatedInput.triggerValidation();
- }
- },
- { defer: true },
- ),
- );
-
- onCleanup(() => {
- validatedInput?.destroy();
- });
-
- return (
-
- props.onInput?.(e.target.value as T)}
- onFocus={() => props.onFocus?.()}
- />
-
- );
-}
diff --git a/frontend/src/ts/components/ui/form/Checkbox.tsx b/frontend/src/ts/components/ui/form/Checkbox.tsx
new file mode 100644
index 000000000000..c7c89a1f329a
--- /dev/null
+++ b/frontend/src/ts/components/ui/form/Checkbox.tsx
@@ -0,0 +1,54 @@
+import { AnyFieldApi } from "@tanstack/solid-form";
+import { Accessor, JSXElement } from "solid-js";
+
+import { cn } from "../../../utils/cn";
+import { Fa } from "../../common/Fa";
+
+export function Checkbox(props: {
+ field: Accessor;
+ label?: string;
+ class?: string;
+ disabled?: boolean;
+}): JSXElement {
+ const checked = () => props.field().state.value as boolean;
+
+ return (
+
+
+
+ );
+}
diff --git a/frontend/src/ts/components/ui/form/FieldIndicator.tsx b/frontend/src/ts/components/ui/form/FieldIndicator.tsx
new file mode 100644
index 000000000000..0e34dac1677e
--- /dev/null
+++ b/frontend/src/ts/components/ui/form/FieldIndicator.tsx
@@ -0,0 +1,60 @@
+import type { AnyFieldApi } from "@tanstack/solid-form";
+
+import { Match, Switch } from "solid-js";
+
+import { Balloon } from "../../common/Balloon";
+import { Fa } from "../../common/Fa";
+import { LoadingCircle } from "../../common/LoadingCircle";
+
+export type FieldIndicatorProps = {
+ field: AnyFieldApi;
+};
+
+export function FieldIndicator(props: FieldIndicatorProps) {
+ //@ts-expect-error custom meta attributes
+ const hasWarning = () => props.field.getMeta().hasWarning as boolean;
+ //@ts-expect-error custom meta attributes
+ const getWarnings = () => props.field.getMeta().warnings as string[];
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/ts/components/ui/form/InputField.tsx b/frontend/src/ts/components/ui/form/InputField.tsx
new file mode 100644
index 000000000000..e2f5d03563c2
--- /dev/null
+++ b/frontend/src/ts/components/ui/form/InputField.tsx
@@ -0,0 +1,45 @@
+import { AnyFieldApi } from "@tanstack/solid-form";
+import { Accessor, JSXElement, Show } from "solid-js";
+
+import { cn } from "../../../utils/cn";
+import { FieldIndicator } from "./FieldIndicator";
+
+export function InputField(props: {
+ field: Accessor;
+ placeholder?: string;
+ showIndicator?: true;
+ autocomplete?: string;
+ type?: string;
+ disabled?: boolean;
+ class?: string;
+ onFocus?: () => void;
+}): JSXElement {
+ return (
+
+ props.field().handleBlur()}
+ onInput={(e) => props.field().handleChange(e.target.value)}
+ disabled={props.disabled}
+ onFocus={() => props.onFocus?.()}
+ />
+
+
+
+
+ );
+}
diff --git a/frontend/src/ts/components/ui/form/SubmitButton.tsx b/frontend/src/ts/components/ui/form/SubmitButton.tsx
new file mode 100644
index 000000000000..bca2f98f2817
--- /dev/null
+++ b/frontend/src/ts/components/ui/form/SubmitButton.tsx
@@ -0,0 +1,53 @@
+import { JSXElement, splitProps } from "solid-js";
+
+import { Button, ButtonProps } from "../../common/Button";
+
+type FormStateSlice = {
+ canSubmit: boolean;
+ isSubmitting: boolean;
+ isValid: boolean;
+ isDirty: boolean;
+};
+
+type SubscribableForm = {
+ Subscribe: (props: {
+ selector: (state: {
+ canSubmit: boolean;
+ isSubmitting: boolean;
+ isValid: boolean;
+ isDirty: boolean;
+ }) => FormStateSlice;
+ children: (state: () => FormStateSlice) => JSXElement;
+ }) => JSXElement;
+};
+
+export function SubmitButton(
+ props: {
+ form: SubscribableForm;
+ } & Omit,
+): JSXElement {
+ const [local, others] = splitProps(props, ["disabled"]);
+ return (
+ ({
+ canSubmit: state.canSubmit,
+ isSubmitting: state.isSubmitting,
+ isValid: state.isValid,
+ isDirty: state.isDirty,
+ })}
+ children={(state) => (
+
+ )}
+ />
+ );
+}
diff --git a/frontend/src/ts/components/ui/form/utils.ts b/frontend/src/ts/components/ui/form/utils.ts
new file mode 100644
index 000000000000..5f7cb828f2ae
--- /dev/null
+++ b/frontend/src/ts/components/ui/form/utils.ts
@@ -0,0 +1,55 @@
+import { AnyFieldApi } from "@tanstack/solid-form";
+import { ZodSchema } from "zod";
+
+export type ValidationResult = { type: "error" | "warning"; message: string };
+export function fromSchema(
+ schema: ZodSchema,
+): (args: { value: T }) => undefined | string[] {
+ return ({ value }) => {
+ const result = schema.safeParse(value);
+ return result.success
+ ? undefined
+ : result.error.issues.map((it) => it.message);
+ };
+}
+
+export function handleResult(
+ field: AnyFieldApi,
+ results: ValidationResult[] | undefined,
+): undefined | string[] {
+ if (results === undefined || results.length === 0) return undefined;
+
+ const byType = Object.groupBy(results, (it) => it.type);
+
+ if ((byType.warning?.length ?? 0) > 0) {
+ field.setMeta((meta) => ({
+ ...meta,
+ hasWarning: true,
+ warnings: byType.warning?.map((it) => it.message),
+ }));
+ }
+ if ((byType.error?.length ?? 0) > 0) {
+ return byType.error?.map((it) => it.message);
+ }
+ return undefined;
+}
+
+type ValidatorCallback = (args: { value: T }) => string | undefined;
+
+export function allFieldsMandatory(): ValidatorCallback {
+ return ({ value }: { value: T }): string | undefined => {
+ const hasInvalid = Object.values(value).some(
+ (v) => v === undefined || v === "",
+ );
+
+ return hasInvalid ? "all fields are mandatory" : undefined;
+ };
+}
+
+export function fieldMandatory(message?: string): ValidatorCallback {
+ return ({ value }) => {
+ return value !== undefined && value !== ""
+ ? undefined
+ : (message ?? "mandatory");
+ };
+}
diff --git a/frontend/src/ts/controllers/page-controller.ts b/frontend/src/ts/controllers/page-controller.ts
index b9311ab84ee1..31428fbc0985 100644
--- a/frontend/src/ts/controllers/page-controller.ts
+++ b/frontend/src/ts/controllers/page-controller.ts
@@ -8,7 +8,6 @@ import {
import * as Settings from "../pages/settings";
import * as Account from "../pages/account";
import * as PageTest from "../pages/test";
-import { resetForm } from "../stores/login";
import * as PageLoading from "../pages/loading";
import * as Friends from "../pages/friends";
import * as Page404 from "../pages/404";
@@ -45,11 +44,7 @@ const pages = {
settings: Settings.page,
about: solidPage("about"),
account: Account.page,
- login: solidPage("login", {
- beforeShow: async () => {
- resetForm();
- },
- }),
+ login: solidPage("login"),
profile: solidPage("profile", {
beforeShow: async (options) => {
setSelectedProfileName(options.params?.["uidOrName"]);
diff --git a/frontend/src/ts/modals/google-sign-up.ts b/frontend/src/ts/modals/google-sign-up.ts
index 6b04e0197d29..95b2558ee7e2 100644
--- a/frontend/src/ts/modals/google-sign-up.ts
+++ b/frontend/src/ts/modals/google-sign-up.ts
@@ -21,7 +21,6 @@ import { resetIgnoreAuthCallback } from "../firebase";
import { ValidatedHtmlInputElement } from "../elements/input-validation";
import { UserNameSchema } from "@monkeytype/schemas/users";
import { remoteValidation } from "../utils/remote-validation";
-import { enableLoginPageInputs, hideLoginPageLoader } from "../stores/login";
let signedInUser: UserCredential | undefined = undefined;
@@ -62,8 +61,6 @@ async function hide(): Promise {
showNoticeNotification("Sign up process cancelled", {
durationMs: 5000,
});
- hideLoginPageLoader();
- enableLoginPageInputs();
if (getAdditionalUserInfo(signedInUser)?.isNewUser) {
await Ape.users.delete();
await signedInUser?.user.delete().catch(() => {
@@ -110,8 +107,6 @@ async function apply(): Promise {
await updateProfile(signedInUser.user, { displayName: name });
await sendEmailVerification(signedInUser.user);
showSuccessNotification("Account created");
- enableLoginPageInputs();
- hideLoginPageLoader();
await AccountController.loadUser(signedInUser.user);
signedInUser = undefined;
@@ -121,8 +116,6 @@ async function apply(): Promise {
} catch (e) {
console.log(e);
showErrorNotification("Failed to sign in with Google", { error: e });
- hideLoginPageLoader();
- enableLoginPageInputs();
if (signedInUser && getAdditionalUserInfo(signedInUser)?.isNewUser) {
await Ape.users.delete();
await signedInUser?.user.delete().catch(() => {
diff --git a/frontend/src/ts/stores/login.ts b/frontend/src/ts/stores/login.ts
index 6732b7e8ba3d..1073132fc9f0 100644
--- a/frontend/src/ts/stores/login.ts
+++ b/frontend/src/ts/stores/login.ts
@@ -7,16 +7,3 @@ export const [
enableLoginPageInputs: (set) => set(true),
disableLoginPageInputs: (set) => set(false),
});
-
-export const [
- getLoginPageLoader,
- { showLoginPageLoader, hideLoginPageLoader },
-] = createSignalWithSetters(false)({
- showLoginPageLoader: (set) => set(true),
- hideLoginPageLoader: (set) => set(false),
-});
-
-export function resetForm(): void {
- hideLoginPageLoader();
- enableLoginPageInputs();
-}
diff --git a/frontend/src/ts/utils/remote-validation.ts b/frontend/src/ts/utils/remote-validation.ts
index 3aac4366e4b1..4e91b20dcec9 100644
--- a/frontend/src/ts/utils/remote-validation.ts
+++ b/frontend/src/ts/utils/remote-validation.ts
@@ -30,3 +30,39 @@ export function remoteValidation(
return handler;
};
}
+
+export function remoteValidationForm(
+ call: (
+ val: V,
+ ) => Promise<{ status: number; body: { data?: T; message: string } }>,
+ options?: {
+ check?: (data: T) => IsValidResponse;
+ on4xx?: IsValidResonseOrFunction;
+ on5xx?: IsValidResonseOrFunction;
+ },
+): (val: { value: V }) => Promise {
+ return async (val: { value: V }) => {
+ let validationResult;
+
+ const result = await call(val.value);
+ if (result.status <= 299) {
+ validationResult = options?.check?.(result.body.data as T) ?? undefined;
+ } else {
+ let handler: IsValidResonseOrFunction | undefined;
+ if (result.status <= 499) {
+ handler = options?.on4xx ?? ((message) => message);
+ } else {
+ handler =
+ options?.on5xx ?? "Server unavailable. Please try again later.";
+ }
+
+ if (typeof handler === "function") {
+ validationResult = handler(result.body.message);
+ } else {
+ validationResult = handler;
+ }
+ }
+
+ return validationResult === true ? undefined : validationResult;
+ };
+}
diff --git a/frontend/src/ts/utils/typo-list.ts b/frontend/src/ts/utils/typo-list.ts
index 897e45cc60f8..01d0578e254b 100644
--- a/frontend/src/ts/utils/typo-list.ts
+++ b/frontend/src/ts/utils/typo-list.ts
@@ -132,6 +132,7 @@ export default [
"icloud.ocm",
"icloud.om",
"icloud.cm",
+ "example.edu",
];
// source(from line 1 - 48): Generated using ChatGPT
// source(from line 48 - 127): https://github.com/gruns/typo
diff --git a/frontend/storybook/stories/Checkbox.stories.tsx b/frontend/storybook/stories/Checkbox.stories.tsx
new file mode 100644
index 000000000000..8b3603700ab4
--- /dev/null
+++ b/frontend/storybook/stories/Checkbox.stories.tsx
@@ -0,0 +1,64 @@
+import preview from "#.storybook/preview";
+import { AnyFieldApi } from "@tanstack/solid-form";
+import { Accessor, Component, createSignal } from "solid-js";
+
+import { Checkbox } from "../../src/ts/components/ui/form/Checkbox";
+
+function createFieldMock(options: { name?: string; value?: boolean }) {
+ const [value, setValue] = createSignal(options.value ?? false);
+ return {
+ name: options.name ?? "test",
+ get state() {
+ return { value: value() };
+ },
+ handleChange(v: boolean) {
+ setValue(v);
+ },
+ handleBlur() {},
+ } as unknown as AnyFieldApi;
+}
+
+const meta = preview.meta({
+ title: "UI/Form/Checkbox",
+ component: Checkbox as Component<{
+ field: Accessor;
+ label?: string;
+ disabled?: boolean;
+ }>,
+ parameters: {
+ layout: "centered",
+ },
+ tags: ["autodocs"],
+ argTypes: {
+ label: { control: "text" },
+ disabled: { control: "boolean" },
+ },
+});
+
+export const Default = meta.story({
+ render: () => {
+ const unchecked = createFieldMock({});
+ const checked = createFieldMock({ value: true });
+ const checked2xl = createFieldMock({ value: true });
+ const disabledUnchecked = createFieldMock({});
+ const disabledChecked = createFieldMock({ value: true });
+ const withLabel = createFieldMock({ value: true });
+
+ return (
+
+
Unchecked
+
unchecked} />
+ Checked
+ checked} />
+ Checked 2xl
+ checked2xl} />
+ Disabled
+ disabledUnchecked} disabled={true} />
+ Disabled Checked
+ disabledChecked} disabled={true} />
+ With Label
+ withLabel} label="checkbox" />
+
+ );
+ },
+});
diff --git a/frontend/storybook/stories/FieldIndicator.stories.tsx b/frontend/storybook/stories/FieldIndicator.stories.tsx
new file mode 100644
index 000000000000..a85cc2ce8f12
--- /dev/null
+++ b/frontend/storybook/stories/FieldIndicator.stories.tsx
@@ -0,0 +1,90 @@
+import preview from "#.storybook/preview";
+import { AnyFieldApi } from "@tanstack/solid-form";
+import { Component } from "solid-js";
+
+import { FieldIndicator } from "../../src/ts/components/ui/form/FieldIndicator";
+
+type MetaState = {
+ isTouched?: boolean;
+ isValid?: boolean;
+ isValidating?: boolean;
+ errors?: string[];
+ hasWarning?: boolean;
+ warnings?: string[];
+};
+
+function createFieldMock(meta: MetaState) {
+ const stateMeta = {
+ isTouched: true,
+ isValid: true,
+ isValidating: false,
+ errors: [],
+ ...meta,
+ };
+
+ return {
+ get state() {
+ return {
+ meta: stateMeta,
+ };
+ },
+
+ getMeta() {
+ return {
+ hasWarning: meta.hasWarning ?? false,
+ warnings: meta.warnings ?? [],
+ };
+ },
+ } as unknown as AnyFieldApi;
+}
+
+const meta = preview.meta({
+ title: "UI/Form/FieldIndicator",
+ component: FieldIndicator as Component<{
+ field?: AnyFieldApi;
+ }>,
+ parameters: {
+ layout: "centered",
+ },
+ tags: ["autodocs"],
+ argTypes: {},
+});
+
+export const Validating = meta.story({
+ args: {
+ field: createFieldMock({
+ isValidating: true,
+ }),
+ },
+});
+
+export const Warning = meta.story({
+ args: {
+ field: createFieldMock({
+ isValid: true,
+ hasWarning: true,
+ warnings: ["are you sure?", "are you really sure?"],
+ }),
+ },
+});
+
+export const Valid = meta.story({
+ args: {
+ field: createFieldMock({
+ isValid: true,
+ }),
+ },
+});
+
+export const Error = meta.story({
+ args: {
+ field: createFieldMock({
+ isValid: false,
+ errors: [
+ "Failed validation",
+ "Extra error",
+ "very very very very very very very very very long error",
+ ],
+ }),
+ },
+});
diff --git a/frontend/storybook/stories/Form.stories.tsx b/frontend/storybook/stories/Form.stories.tsx
new file mode 100644
index 000000000000..54296e4f806e
--- /dev/null
+++ b/frontend/storybook/stories/Form.stories.tsx
@@ -0,0 +1,108 @@
+import preview from "#.storybook/preview";
+import { createForm } from "@tanstack/solid-form";
+import { createSignal } from "solid-js";
+import { z } from "zod";
+
+import { Checkbox } from "../../src/ts/components/ui/form/Checkbox";
+import { InputField } from "../../src/ts/components/ui/form/InputField";
+import { SubmitButton } from "../../src/ts/components/ui/form/SubmitButton";
+import {
+ fieldMandatory,
+ fromSchema,
+} from "../../src/ts/components/ui/form/utils";
+import { showNoticeNotification } from "../../src/ts/stores/notifications";
+import { sleep } from "../../src/ts/utils/misc";
+
+const meta = preview.meta({
+ title: "UI/Form",
+ parameters: {
+ layout: "centered",
+ },
+ tags: ["autodocs"],
+ argTypes: {},
+});
+
+export const withValidation = meta.story({
+ render: () => {
+ const form = createForm(() => ({
+ defaultValues: {
+ username: "",
+ password: "",
+ rememberMe: true,
+ },
+ onSubmit: async () => {
+ setEditable(false);
+ await sleep(1000);
+ setEditable(true);
+ },
+ onSubmitInvalid: () => {
+ showNoticeNotification("Please fill in all fields");
+ },
+ }));
+
+ const [isEditable, setEditable] = createSignal(true);
+
+ return (
+ {
+ await sleep(500);
+ return value === "kevin" ? undefined : "you must be kevin";
+ },
+ }}
+ children={(field) => (
+
+ )}
+ />
+ (
+
+ )}
+ />
+ (
+
+ )}
+ />
+
+
+
+ );
+ },
+});
diff --git a/frontend/storybook/stories/InputField.stories.tsx b/frontend/storybook/stories/InputField.stories.tsx
new file mode 100644
index 000000000000..2118782f5aa6
--- /dev/null
+++ b/frontend/storybook/stories/InputField.stories.tsx
@@ -0,0 +1,109 @@
+import preview from "#.storybook/preview";
+import { AnyFieldApi } from "@tanstack/solid-form";
+import { Component, Accessor } from "solid-js";
+
+import { InputField } from "../../src/ts/components/ui/form/InputField";
+
+type MetaState = {
+ isTouched?: boolean;
+ isValid?: boolean;
+ isValidating?: boolean;
+ errors?: string[];
+ hasWarning?: boolean;
+ warnings?: string[];
+};
+
+function createFieldMock(options: {
+ name?: string;
+ value?: string;
+ meta?: MetaState;
+}) {
+ const stateMeta = {
+ isTouched: true,
+ isValid: true,
+ isValidating: false,
+ errors: [],
+ ...(options.meta ?? {}),
+ };
+ return {
+ name: options.name ?? "test",
+ get state() {
+ return {
+ value: options.value ?? "",
+ meta: stateMeta,
+ };
+ },
+ getMeta() {
+ return {
+ hasWarning: options.meta?.hasWarning ?? false,
+ warnings: options.meta?.warnings ?? [],
+ };
+ },
+ } as unknown as AnyFieldApi;
+}
+
+const meta = preview.meta({
+ title: "UI/Form/InputField",
+ component: InputField as Component<{
+ field: Accessor;
+ placeholder?: string;
+ showIndicator?: true;
+ autocomplete?: string;
+ type?: string;
+ disabled?: boolean;
+ onFocus?: () => void;
+ }>,
+ parameters: {
+ layout: "centered",
+ },
+ tags: ["autodocs"],
+ argTypes: {
+ placeholder: { control: "text" },
+ showIndicator: { control: "boolean" },
+ autocomplete: { control: "text" },
+ type: { control: "text" },
+ disabled: { control: "boolean" },
+ },
+});
+
+export const Default = meta.story({
+ args: {
+ field: () => createFieldMock({}),
+ },
+});
+
+export const withIndicator = meta.story({
+ args: {
+ showIndicator: true,
+ field: () => createFieldMock({}),
+ },
+});
+
+export const withPlaceholder = meta.story({
+ args: {
+ placeholder: "placeholder",
+ field: () => createFieldMock({}),
+ },
+});
+
+export const withAutocomplete = meta.story({
+ args: {
+ autocomplete: "autocomplete",
+ field: () => createFieldMock({}),
+ },
+});
+
+export const withTypePassword = meta.story({
+ args: {
+ type: "password",
+ placeholder: "password",
+ field: () => createFieldMock({ value: "test" }),
+ },
+});
+
+export const disabled = meta.story({
+ args: {
+ disabled: true,
+ field: () => createFieldMock({ value: "test", meta: { isValid: true } }),
+ },
+});
diff --git a/frontend/storybook/stories/ValidatedInput.stories.tsx b/frontend/storybook/stories/ValidatedInput.stories.tsx
deleted file mode 100644
index ab59122aeee4..000000000000
--- a/frontend/storybook/stories/ValidatedInput.stories.tsx
+++ /dev/null
@@ -1,91 +0,0 @@
-import preview from "#.storybook/preview";
-import { Component } from "solid-js";
-import { z } from "zod";
-
-import { ValidatedInput } from "../../src/ts/components/ui/ValidatedInput";
-
-const meta = preview.meta({
- title: "UI/ValidatedInput",
- component: ValidatedInput as Component<{
- value?: string;
- placeholder?: string;
- class?: string;
- }>,
- parameters: {
- layout: "centered",
- },
- tags: ["autodocs"],
- argTypes: {
- value: { control: "text" },
- placeholder: { control: "text" },
- class: { control: "text" },
- },
-});
-
-export const Default = meta.story({
- args: {
- placeholder: "Type something...",
- },
-});
-
-export const WithValue = meta.story({
- args: {
- placeholder: "Enter a value",
- value: "hello",
- },
-});
-
-export const WithSchemaValidation = meta.story({
- render: () => (
-
- ),
-});
-
-export const AllVariants = meta.story({
- render: () => (
-
-
-
-
-
- With schema (3-20 chars)
-
-
-
-
- ),
-});
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index dc39825ddab2..22a2422de4c0 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -262,8 +262,8 @@ importers:
specifier: 1.2.0
version: 1.2.0
'@leonabcd123/modern-caps-lock':
- specifier: 2.2.2
- version: 2.2.2
+ specifier: 3.0.4
+ version: 3.0.4
'@monkeytype/contracts':
specifier: workspace:*
version: link:../packages/contracts
@@ -300,6 +300,9 @@ importers:
'@tanstack/solid-db':
specifier: 0.2.10
version: 0.2.10(solid-js@1.9.10)(typescript@6.0.0-beta)
+ '@tanstack/solid-form':
+ specifier: 1.28.4
+ version: 1.28.4(solid-js@1.9.10)
'@tanstack/solid-query':
specifier: 5.90.23
version: 5.90.23(solid-js@1.9.10)
@@ -561,7 +564,7 @@ importers:
version: 10.2.16(storybook@10.2.16(@testing-library/dom@10.4.1)(prettier@3.7.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))
'@storybook/addon-vitest':
specifier: ^10.2.14
- version: 10.2.16(@vitest/browser-playwright@4.0.18(playwright@1.58.2)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.70.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.1))(vitest@4.0.18))(@vitest/browser@4.0.18(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.70.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.1))(vitest@4.0.18(@types/node@24.9.1)(@vitest/browser-playwright@4.0.18)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.31.1)(sass@1.70.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.1)))(@vitest/runner@4.0.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@10.2.16(@testing-library/dom@10.4.1)(prettier@3.7.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vitest@4.0.18(@types/node@24.9.1)(@vitest/browser-playwright@4.0.18)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.31.1)(sass@1.70.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.1))
+ version: 10.2.16(@vitest/browser-playwright@4.0.18)(@vitest/browser@4.0.18(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.70.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.1))(vitest@4.0.18))(@vitest/runner@4.0.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@10.2.16(@testing-library/dom@10.4.1)(prettier@3.7.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vitest@4.0.18)
'@storybook/builder-vite':
specifier: ^10.2.14
version: 10.2.16(esbuild@0.27.3)(rollup@4.52.5)(storybook@10.2.16(@testing-library/dom@10.4.1)(prettier@3.7.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.70.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.1))
@@ -570,13 +573,13 @@ importers:
version: 4.2.1(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.70.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.1))
'@vitest/browser':
specifier: ^4.0.18
- version: 4.0.18(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.70.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.1))(vitest@4.0.18(@types/node@24.9.1)(@vitest/browser-playwright@4.0.18)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.31.1)(sass@1.70.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.1))
+ version: 4.0.18(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.70.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.1))(vitest@4.0.18)
'@vitest/browser-playwright':
specifier: ^4.0.18
version: 4.0.18(playwright@1.58.2)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.70.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.1))(vitest@4.0.18)
'@vitest/coverage-v8':
specifier: ^4.0.18
- version: 4.0.18(@vitest/browser@4.0.18(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.70.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.1))(vitest@4.0.18(@types/node@24.9.1)(@vitest/browser-playwright@4.0.18)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.31.1)(sass@1.70.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.1)))(vitest@4.0.18(@types/node@24.9.1)(@vitest/browser-playwright@4.0.18)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.31.1)(sass@1.70.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.1))
+ version: 4.0.18(@vitest/browser@4.0.18(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.70.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.1))(vitest@4.0.18))(vitest@4.0.18)
playwright:
specifier: ^1.58.2
version: 1.58.2
@@ -2333,8 +2336,8 @@ packages:
'@kwsites/promise-deferred@1.1.1':
resolution: {integrity: sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==}
- '@leonabcd123/modern-caps-lock@2.2.2':
- resolution: {integrity: sha512-0e8onO4ovdeVj9d/1Ddl1q7nq/p+PNsLscp1yOXQLOiyANUCLq41/H8qh5RDZAOs6j9yNzyxlP5h4o8u2HCwFA==}
+ '@leonabcd123/modern-caps-lock@3.0.4':
+ resolution: {integrity: sha512-GUUlmHBu3r6H9KaE9sCxqr0iTSLMev7vdzoovrotyaTSS+AtOrJNdptlWf2DnYAHG8U4EJ++mjyUJdNDjaYI9Q==}
'@mapbox/node-pre-gyp@1.0.11':
resolution: {integrity: sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==}
@@ -3390,6 +3393,10 @@ packages:
peerDependencies:
typescript: '>=4.7'
+ '@tanstack/devtools-event-client@0.4.1':
+ resolution: {integrity: sha512-GRxmPw4OHZ2oZeIEUkEwt/NDvuEqzEYRAjzUVMs+I0pd4C7k1ySOiuJK2CqF+K/yEAR3YZNkW3ExrpDarh9Vwg==}
+ engines: {node: '>=18'}
+
'@tanstack/eslint-plugin-query@5.91.4':
resolution: {integrity: sha512-8a+GAeR7oxJ5laNyYBQ6miPK09Hi18o5Oie/jx8zioXODv/AUFLZQecKabPdpQSLmuDXEBPKFh+W5DKbWlahjQ==}
peerDependencies:
@@ -3399,6 +3406,13 @@ packages:
typescript:
optional: true
+ '@tanstack/form-core@1.28.4':
+ resolution: {integrity: sha512-2eox5ePrJ6kvA1DXD5QHk/GeGr3VFZ0uYR63UgQOe7bUg6h1JfXaIMqTjZK9sdGyE4oRNqFpoW54H0pZM7nObQ==}
+
+ '@tanstack/pacer-lite@0.1.1':
+ resolution: {integrity: sha512-y/xtNPNt/YeyoVxE/JCx+T7yjEzpezmbb+toK8DDD1P4m7Kzs5YR956+7OKexG3f8aXgC3rLZl7b1V+yNUSy5w==}
+ engines: {node: '>=18'}
+
'@tanstack/pacer-lite@0.2.1':
resolution: {integrity: sha512-3PouiFjR4B6x1c969/Pl4ZIJleof1M0n6fNX8NRiC9Sqv1g06CVDlEaXUR4212ycGFyfq4q+t8Gi37Xy+z34iQ==}
engines: {node: '>=18'}
@@ -3420,6 +3434,11 @@ packages:
peerDependencies:
solid-js: '>=1.9.0'
+ '@tanstack/solid-form@1.28.4':
+ resolution: {integrity: sha512-ZhA/oQQJzfY0Un5XFiHbfCwuWyShATHIajdx6xjcm34g0xpwoioGyxAPm7pXLpnUAzln0liCkKX+L77rKj3/6A==}
+ peerDependencies:
+ solid-js: '>=1.9.9'
+
'@tanstack/solid-query-devtools@5.91.3':
resolution: {integrity: sha512-xzVwIIxQPbiublZP3RkGp8KVjt8zenv5y1YTSRarP32mLUHJgfdofvjsDvMEhhL/lomz90qa0jCIGsSxoSTyYQ==}
peerDependencies:
@@ -3431,12 +3450,20 @@ packages:
peerDependencies:
solid-js: ^1.6.0
+ '@tanstack/solid-store@0.9.2':
+ resolution: {integrity: sha512-W6D6WnIL0Fch2zcLd7qCFf3eLD6AqwZiPoQnoRFKhe8DFXdxM4GRvfLpsAIQQHsun97EcttbwbfVq14SZfT7jg==}
+ peerDependencies:
+ solid-js: ^1.6.0
+
'@tanstack/solid-table@8.21.3':
resolution: {integrity: sha512-PmhfSLBxVKiFs01LtYOYrCRhCyTUjxmb4KlxRQiqcALtip8+DOJeeezQM4RSX/GUS0SMVHyH/dNboCpcO++k2A==}
engines: {node: '>=12'}
peerDependencies:
solid-js: '>=1.3'
+ '@tanstack/store@0.9.2':
+ resolution: {integrity: sha512-K013lUJEFJK2ofFQ/hZKJUmCnpcV00ebLyOyFOWQvyQHUOZp/iYO84BM6aOGiV81JzwbX0APTVmW8YI7yiG5oA==}
+
'@tanstack/table-core@8.21.3':
resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==}
engines: {node: '>=12'}
@@ -12074,7 +12101,7 @@ snapshots:
'@kwsites/promise-deferred@1.1.1': {}
- '@leonabcd123/modern-caps-lock@2.2.2': {}
+ '@leonabcd123/modern-caps-lock@3.0.4': {}
'@mapbox/node-pre-gyp@1.0.11(encoding@0.1.13)':
dependencies:
@@ -12890,13 +12917,13 @@ snapshots:
dependencies:
storybook: 10.2.16(@testing-library/dom@10.4.1)(prettier@3.7.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
- '@storybook/addon-vitest@10.2.16(@vitest/browser-playwright@4.0.18(playwright@1.58.2)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.70.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.1))(vitest@4.0.18))(@vitest/browser@4.0.18(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.70.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.1))(vitest@4.0.18(@types/node@24.9.1)(@vitest/browser-playwright@4.0.18)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.31.1)(sass@1.70.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.1)))(@vitest/runner@4.0.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@10.2.16(@testing-library/dom@10.4.1)(prettier@3.7.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vitest@4.0.18(@types/node@24.9.1)(@vitest/browser-playwright@4.0.18)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.31.1)(sass@1.70.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.1))':
+ '@storybook/addon-vitest@10.2.16(@vitest/browser-playwright@4.0.18)(@vitest/browser@4.0.18(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.70.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.1))(vitest@4.0.18))(@vitest/runner@4.0.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@10.2.16(@testing-library/dom@10.4.1)(prettier@3.7.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vitest@4.0.18)':
dependencies:
'@storybook/global': 5.0.0
'@storybook/icons': 2.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
storybook: 10.2.16(@testing-library/dom@10.4.1)(prettier@3.7.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
optionalDependencies:
- '@vitest/browser': 4.0.18(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.70.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.1))(vitest@4.0.18(@types/node@24.9.1)(@vitest/browser-playwright@4.0.18)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.31.1)(sass@1.70.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.1))
+ '@vitest/browser': 4.0.18(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.70.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.1))(vitest@4.0.18)
'@vitest/browser-playwright': 4.0.18(playwright@1.58.2)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.70.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.1))(vitest@4.0.18)
'@vitest/runner': 4.0.18
vitest: 4.0.18(@types/node@24.9.1)(@vitest/browser-playwright@4.0.18)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.31.1)(sass@1.70.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.1)
@@ -13025,6 +13052,8 @@ snapshots:
'@tanstack/pacer-lite': 0.2.1
typescript: 6.0.0-beta
+ '@tanstack/devtools-event-client@0.4.1': {}
+
'@tanstack/eslint-plugin-query@5.91.4(eslint@9.39.1(jiti@2.6.1))(typescript@6.0.0-beta)':
dependencies:
'@typescript-eslint/utils': 8.52.0(eslint@9.39.1(jiti@2.6.1))(typescript@6.0.0-beta)
@@ -13034,6 +13063,14 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ '@tanstack/form-core@1.28.4':
+ dependencies:
+ '@tanstack/devtools-event-client': 0.4.1
+ '@tanstack/pacer-lite': 0.1.1
+ '@tanstack/store': 0.9.2
+
+ '@tanstack/pacer-lite@0.1.1': {}
+
'@tanstack/pacer-lite@0.2.1': {}
'@tanstack/query-core@5.90.20': {}
@@ -13055,6 +13092,12 @@ snapshots:
transitivePeerDependencies:
- typescript
+ '@tanstack/solid-form@1.28.4(solid-js@1.9.10)':
+ dependencies:
+ '@tanstack/form-core': 1.28.4
+ '@tanstack/solid-store': 0.9.2(solid-js@1.9.10)
+ solid-js: 1.9.10
+
'@tanstack/solid-query-devtools@5.91.3(@tanstack/solid-query@5.90.23(solid-js@1.9.10))(solid-js@1.9.10)':
dependencies:
'@tanstack/query-devtools': 5.93.0
@@ -13066,11 +13109,18 @@ snapshots:
'@tanstack/query-core': 5.90.20
solid-js: 1.9.10
+ '@tanstack/solid-store@0.9.2(solid-js@1.9.10)':
+ dependencies:
+ '@tanstack/store': 0.9.2
+ solid-js: 1.9.10
+
'@tanstack/solid-table@8.21.3(solid-js@1.9.10)':
dependencies:
'@tanstack/table-core': 8.21.3
solid-js: 1.9.10
+ '@tanstack/store@0.9.2': {}
+
'@tanstack/table-core@8.21.3': {}
'@testing-library/dom@10.4.1':
@@ -13478,7 +13528,7 @@ snapshots:
'@vitest/browser-playwright@4.0.18(playwright@1.58.2)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.70.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.1))(vitest@4.0.18)':
dependencies:
- '@vitest/browser': 4.0.18(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.70.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.1))(vitest@4.0.18(@types/node@24.9.1)(@vitest/browser-playwright@4.0.18)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.31.1)(sass@1.70.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.1))
+ '@vitest/browser': 4.0.18(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.70.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.1))(vitest@4.0.18)
'@vitest/mocker': 4.0.18(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.70.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.1))
playwright: 1.58.2
tinyrainbow: 3.0.3
@@ -13489,7 +13539,7 @@ snapshots:
- utf-8-validate
- vite
- '@vitest/browser@4.0.18(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.70.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.1))(vitest@4.0.18(@types/node@24.9.1)(@vitest/browser-playwright@4.0.18)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.31.1)(sass@1.70.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.1))':
+ '@vitest/browser@4.0.18(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.70.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.1))(vitest@4.0.18)':
dependencies:
'@vitest/mocker': 4.0.18(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.70.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.1))
'@vitest/utils': 4.0.18
@@ -13540,7 +13590,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@vitest/coverage-v8@4.0.18(@vitest/browser@4.0.18(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.70.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.1))(vitest@4.0.18(@types/node@24.9.1)(@vitest/browser-playwright@4.0.18)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.31.1)(sass@1.70.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.1)))(vitest@4.0.18(@types/node@24.9.1)(@vitest/browser-playwright@4.0.18)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.31.1)(sass@1.70.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.1))':
+ '@vitest/coverage-v8@4.0.18(@vitest/browser@4.0.18(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.70.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.1))(vitest@4.0.18))(vitest@4.0.18)':
dependencies:
'@bcoe/v8-coverage': 1.0.2
'@vitest/utils': 4.0.18
@@ -13554,7 +13604,7 @@ snapshots:
tinyrainbow: 3.0.3
vitest: 4.0.18(@types/node@24.9.1)(@vitest/browser-playwright@4.0.18)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.31.1)(sass@1.70.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.1)
optionalDependencies:
- '@vitest/browser': 4.0.18(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.70.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.1))(vitest@4.0.18(@types/node@24.9.1)(@vitest/browser-playwright@4.0.18)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.31.1)(sass@1.70.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.1))
+ '@vitest/browser': 4.0.18(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.70.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.1))(vitest@4.0.18)
'@vitest/expect@3.2.4':
dependencies: