Customers

This commit is contained in:
David Arranz 2026-04-03 18:15:25 +02:00
parent 836a75dd1e
commit 3ab5216e56
37 changed files with 1013 additions and 801 deletions

View File

@ -61,7 +61,7 @@
"pg-hstore": "^2.3.4", "pg-hstore": "^2.3.4",
"reflect-metadata": "^0.2.2", "reflect-metadata": "^0.2.2",
"response-time": "^2.3.3", "response-time": "^2.3.3",
"sequelize": "^6.37.5", "sequelize": "^6.37.8",
"shallow-equal-object": "^1.1.1", "shallow-equal-object": "^1.1.1",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"uuid": "^11.0.5", "uuid": "^11.0.5",

View File

@ -40,11 +40,11 @@
"axios": "^1.9.0", "axios": "^1.9.0",
"dinero.js": "^1.9.1", "dinero.js": "^1.9.1",
"react-error-boundary": "^6.0.0", "react-error-boundary": "^6.0.0",
"react-hook-form": "^7.56.4", "react-hook-form": "^7.72.1",
"react-i18next": "^15.0.1", "react-i18next": "^15.0.1",
"react-router-dom": "^6.26.0", "react-router-dom": "^6.26.0",
"react-secure-storage": "^1.3.2", "react-secure-storage": "^1.3.2",
"sequelize": "^6.37.5", "sequelize": "^6.37.8",
"tailwindcss": "^4.1.10", "tailwindcss": "^4.1.10",
"tw-animate-css": "^1.2.9", "tw-animate-css": "^1.2.9",
"vite-plugin-html": "^3.2.2" "vite-plugin-html": "^3.2.2"

View File

@ -32,7 +32,7 @@
"@repo/rdx-ui": "workspace:*", "@repo/rdx-ui": "workspace:*",
"@repo/shadcn-ui": "workspace:*", "@repo/shadcn-ui": "workspace:*",
"@tanstack/react-query": "^5.90.6", "@tanstack/react-query": "^5.90.6",
"react-hook-form": "^7.56.2", "react-hook-form": "^7.72.1",
"react-router-dom": "^6.26.0", "react-router-dom": "^6.26.0",
"react-secure-storage": "^1.3.2" "react-secure-storage": "^1.3.2"
} }

View File

@ -47,10 +47,10 @@
"http-status": "^2.1.0", "http-status": "^2.1.0",
"lucide-react": "^0.577.0", "lucide-react": "^0.577.0",
"mime-types": "^3.0.1", "mime-types": "^3.0.1",
"react-hook-form": "^7.58.1", "react-hook-form": "^7.72.1",
"react-i18next": "^15.5.1", "react-i18next": "^15.5.1",
"react-router-dom": "^6.26.0", "react-router-dom": "^6.26.0",
"sequelize": "^6.37.5", "sequelize": "^6.37.8",
"zod": "^4.1.11" "zod": "^4.1.11"
} }
} }

View File

@ -55,11 +55,11 @@
"libphonenumber-js": "^1.12.7", "libphonenumber-js": "^1.12.7",
"lucide-react": "^0.577.0", "lucide-react": "^0.577.0",
"pg-hstore": "^2.3.4", "pg-hstore": "^2.3.4",
"react-hook-form": "^7.58.1", "react-hook-form": "^7.72.1",
"react-i18next": "^15.5.1", "react-i18next": "^15.5.1",
"react-qr-code": "^2.0.18", "react-qr-code": "^2.0.18",
"react-router-dom": "^6.26.0", "react-router-dom": "^6.26.0",
"sequelize": "^6.37.5", "sequelize": "^6.37.8",
"zod": "^4.1.11" "zod": "^4.1.11"
} }
} }

View File

@ -45,10 +45,10 @@
"express": "^4.18.2", "express": "^4.18.2",
"lucide-react": "^0.577.0", "lucide-react": "^0.577.0",
"react-data-table-component": "^7.7.0", "react-data-table-component": "^7.7.0",
"react-hook-form": "^7.58.1", "react-hook-form": "^7.72.1",
"react-i18next": "^16.2.4", "react-i18next": "^16.2.4",
"react-router-dom": "^6.26.0", "react-router-dom": "^6.26.0",
"sequelize": "^6.37.5", "sequelize": "^6.37.8",
"use-debounce": "^10.0.5", "use-debounce": "^10.0.5",
"zod": "^4.1.11" "zod": "^4.1.11"
} }

View File

@ -58,7 +58,7 @@ export type CustomerPatchProps = Partial<
// Customer // Customer
export interface ICustomer { export interface ICustomer {
// comportamiento // comportamiento
update(partialCustomer: CustomerPatchProps): Result<Customer, Error>; update(partialCustomer: CustomerPatchProps): Result<void, Error>;
// propiedades (getters) // propiedades (getters)
readonly isIndividual: boolean; readonly isIndividual: boolean;
@ -148,7 +148,7 @@ export class Customer extends AggregateRoot<CustomerInternalProps> implements IC
return new Customer(props, id); return new Customer(props, id);
} }
public update(partialCustomer: CustomerPatchProps): Result<Customer, Error> { public update(partialCustomer: CustomerPatchProps): Result<void, Error> {
const { address: partialAddress, ...rest } = partialCustomer; const { address: partialAddress, ...rest } = partialCustomer;
Object.assign(this.props, rest); Object.assign(this.props, rest);

View File

@ -11,7 +11,7 @@ import type { FindOptions, InferAttributes, OrderItem, Sequelize, Transaction }
import type { CustomerSummary, ICustomerRepository } from "../../../../application"; import type { CustomerSummary, ICustomerRepository } from "../../../../application";
import type { Customer } from "../../../../domain"; import type { Customer } from "../../../../domain";
import type { SequelizeCustomerDomainMapper, SequelizeCustomerSummaryMapper } from "../../mappers"; import type { SequelizeCustomerDomainMapper, SequelizeCustomerSummaryMapper } from "../mappers";
import { CustomerModel } from "../models/sequelize-customer.model"; import { CustomerModel } from "../models/sequelize-customer.model";
export class CustomerRepository export class CustomerRepository

View File

@ -0,0 +1,28 @@
export const COUNTRY_OPTIONS = [
{ value: "es", label: "España" },
{ value: "fr", label: "Francia" },
{ value: "de", label: "Alemania" },
{ value: "it", label: "Italia" },
{ value: "pt", label: "Portugal" },
{ value: "us", label: "Estados Unidos" },
{ value: "mx", label: "México" },
{ value: "ar", label: "Argentina" },
] as const;
export const LANGUAGE_OPTIONS = [
{ value: "es", label: "Español" },
{ value: "en", label: "Inglés" },
{ value: "fr", label: "Francés" },
{ value: "de", label: "Alemán" },
{ value: "it", label: "Italiano" },
{ value: "pt", label: "Portugués" },
] as const;
export const CURRENCY_OPTIONS = [
{ value: "EUR", label: "Euro" },
{ value: "USD", label: "Dólar estadounidense" },
{ value: "GBP", label: "Libra esterlina" },
{ value: "ARS", label: "Peso argentino" },
{ value: "MXN", label: "Peso mexicano" },
{ value: "JPY", label: "Yen japonés" },
] as const;

View File

@ -0,0 +1 @@
export * from "./customer.constants";

View File

@ -1,4 +1,5 @@
export * from "./api"; export * from "./api";
export * from "./constants";
export * from "./entities"; export * from "./entities";
export * from "./hooks"; export * from "./hooks";
export * from "./ui"; export * from "./ui";

View File

@ -9,8 +9,8 @@ import {
import { useFormContext } from "react-hook-form"; import { useFormContext } from "react-hook-form";
import { useTranslation } from "../../../i18n"; import { useTranslation } from "../../../i18n";
import { CURRENCY_OPTIONS, LANGUAGE_OPTIONS } from "../../constants"; import { CURRENCY_OPTIONS, LANGUAGE_OPTIONS } from "../../../shared";
import type { CustomerFormData } from "../../schemas"; import type { CustomerFormData } from "../../types";
interface CustomerAdditionalConfigFieldsProps { interface CustomerAdditionalConfigFieldsProps {
className?: string; className?: string;

View File

@ -9,8 +9,8 @@ import {
import { useFormContext } from "react-hook-form"; import { useFormContext } from "react-hook-form";
import { useTranslation } from "../../../i18n"; import { useTranslation } from "../../../i18n";
import { COUNTRY_OPTIONS } from "../../constants"; import { COUNTRY_OPTIONS } from "../../../shared";
import type { CustomerFormData } from "../../schemas"; import type { CustomerFormData } from "../../types";
interface CustomerAddressFieldsProps { interface CustomerAddressFieldsProps {
className?: string; className?: string;

View File

@ -1,30 +1,23 @@
import { TextAreaField, TextField } from "@repo/rdx-ui/components"; import { RadioGroupField, TextAreaField, TextField } from "@repo/rdx-ui/components";
import { import {
Field, Field,
FieldDescription, FieldDescription,
FieldError,
FieldGroup, FieldGroup,
FieldLabel, FieldLabel,
FieldLegend, FieldLegend,
FieldSet, FieldSet,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
RadioGroup,
RadioGroupItem,
} from "@repo/shadcn-ui/components"; } from "@repo/shadcn-ui/components";
import { useEffect } from "react"; import { useEffect } from "react";
import { Controller, useFormContext } from "react-hook-form"; import { Controller, useFormContext } from "react-hook-form";
import { useTranslation } from "../../../i18n"; import { useTranslation } from "../../../i18n";
import type { CustomerFormData } from "../../schemas"; import type { CustomerFormData } from "../../types";
import { CustomerTaxesMultiSelect } from "./customer-taxes-multi-select"; import { CustomerTaxesMultiSelect } from "./customer-taxes-multi-select";
interface CustomerBasicInfoFieldsProps { interface CustomerBasicInfoFieldsProps extends React.ComponentProps<typeof FieldSet> {
focusRef?: React.RefObject<HTMLInputElement>; focusRef?: React.RefObject<HTMLInputElement | null>;
className?: string;
} }
export const CustomerBasicInfoFields = ({ export const CustomerBasicInfoFields = ({
@ -33,82 +26,55 @@ export const CustomerBasicInfoFields = ({
...props ...props
}: CustomerBasicInfoFieldsProps) => { }: CustomerBasicInfoFieldsProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { control } = useFormContext<CustomerFormData>(); const { control, setFocus } = useFormContext<CustomerFormData>();
// Enfoca el primer campo recibido // Enfoca el primer campo recibido
useEffect(() => { useEffect(() => {
focusRef?.current?.focus?.(); setFocus("name");
}, [focusRef]); }, [setFocus]);
return ( return (
<FieldSet className={className} {...props}> <FieldSet className={className} {...props}>
<FieldLegend>{t("form_groups.basic_info.title")}</FieldLegend> <FieldLegend>{t("form_groups.basic_info.title")}</FieldLegend>
<FieldDescription>{t("form_groups.basic_info.description")}</FieldDescription> <FieldDescription>{t("form_groups.basic_info.description")}</FieldDescription>
<FieldGroup className="grid grid-cols-1 gap-x-6 lg:grid-cols-4"> <FieldGroup className="grid grid-cols-1 gap-x-6 lg:grid-cols-4">
<Field className="lg:col-span-2" ref={focusRef}>
<TextField <TextField
control={control} className="lg:col-span-2"
description={t("form_fields.name.description")} description={t("form_fields.name.description")}
label={t("form_fields.name.label")} label={t("form_fields.name.label")}
name="name" name="name"
placeholder={t("form_fields.name.placeholder")} placeholder={t("form_fields.name.placeholder")}
required required
/> />
</Field>
<Field className="lg:col-span-1 lg:col-start-1"> <RadioGroupField
<FormField className="lg:col-span-1 lg:col-start-1"
control={control} description={t("form_fields.customer_type.description")}
items={[
{
value: "true",
label: t("form_fields.customer_type.company"),
},
{
value: "false",
label: t("form_fields.customer_type.individual"),
},
]}
label={t("form_fields.customer_type.label")}
name="is_company" name="is_company"
render={({ field }) => (
<FormItem className="space-y-3">
<FormLabel>{t("form_fields.customer_type.label")}</FormLabel>
<FormControl>
<RadioGroup
className="flex items-center gap-6"
defaultValue={field.value ? "true" : "false"}
onValueChange={(value: string) => {
field.onChange(value === "false" ? "false" : "true");
}}
>
<FormItem className="flex items-center space-x-2">
<FormControl>
<RadioGroupItem id="rgi-company" value="true" />
</FormControl>
<FormLabel className="cursor-pointer" htmlFor="rgi-company">
{t("form_fields.customer_type.company")}
</FormLabel>
</FormItem>
<FormItem className="flex items-center space-x-2">
<FormControl>
<RadioGroupItem id="rgi-individual" value="false" />
</FormControl>
<FormLabel className="cursor-pointer" htmlFor="rgi-individual">
{t("form_fields.customer_type.individual")}
</FormLabel>
</FormItem>
</RadioGroup>
</FormControl>
<FormMessage />
</FormItem>
)}
/> />
</Field>
<Field className="lg:col-span-1 " ref={focusRef}>
<TextField <TextField
control={control} className="lg:col-span-1"
description={t("form_fields.tin.description")} description={t("form_fields.tin.description")}
label={t("form_fields.tin.label")} label={t("form_fields.tin.label")}
name="tin" name="tin"
placeholder={t("form_fields.tin.placeholder")} placeholder={t("form_fields.tin.placeholder")}
required required
/> />
</Field>
<TextField <TextField
className="lg:col-span-full" className="lg:col-span-full"
control={control}
description={t("form_fields.trade_name.description")} description={t("form_fields.trade_name.description")}
label={t("form_fields.trade_name.label")} label={t("form_fields.trade_name.label")}
name="trade_name" name="trade_name"
@ -117,7 +83,6 @@ export const CustomerBasicInfoFields = ({
<TextField <TextField
className="lg:col-span-2 lg:col-start-1" className="lg:col-span-2 lg:col-start-1"
control={control}
description={t("form_fields.reference.description")} description={t("form_fields.reference.description")}
label={t("form_fields.reference.label")} label={t("form_fields.reference.label")}
name="reference" name="reference"
@ -129,10 +94,11 @@ export const CustomerBasicInfoFields = ({
control={control} control={control}
name="default_taxes" name="default_taxes"
render={({ field, fieldState }) => ( render={({ field, fieldState }) => (
<Field className={"gap-1"} data-invalid={fieldState.invalid}> <Field className="gap-1" data-invalid={fieldState.invalid}>
<FieldLabel htmlFor={"default_taxes"}> <FieldLabel htmlFor="default_taxes">
{t("form_fields.default_taxes.label")} {t("form_fields.default_taxes.label")}
</FieldLabel> </FieldLabel>
<CustomerTaxesMultiSelect <CustomerTaxesMultiSelect
description={t("form_fields.default_taxes.description")} description={t("form_fields.default_taxes.description")}
label={t("form_fields.default_taxes.label")} label={t("form_fields.default_taxes.label")}
@ -141,13 +107,14 @@ export const CustomerBasicInfoFields = ({
required required
value={field.value} value={field.value}
/> />
<FieldDescription>{t("form_fields.default_taxes.description")}</FieldDescription>
<FieldError errors={[fieldState.error]} />
</Field> </Field>
)} )}
/> />
</Field> </Field>
<TextAreaField <TextAreaField
className="lg:col-span-full" className="lg:col-span-full"
control={control}
description={t("form_fields.legal_record.description")} description={t("form_fields.legal_record.description")}
label={t("form_fields.legal_record.label")} label={t("form_fields.legal_record.label")}
name="legal_record" name="legal_record"

View File

@ -34,10 +34,10 @@ export const CustomerContactFields = ({ className, ...props }: CustomerContactFi
className="lg:col-span-2" className="lg:col-span-2"
control={control} control={control}
description={t("form_fields.email_primary.description")} description={t("form_fields.email_primary.description")}
icon={ label={t("form_fields.email_primary.label")}
leftIcon={
<AtSignIcon className="h-[18px] w-[18px] text-muted-foreground" strokeWidth={1.5} /> <AtSignIcon className="h-[18px] w-[18px] text-muted-foreground" strokeWidth={1.5} />
} }
label={t("form_fields.email_primary.label")}
name="email_primary" name="email_primary"
placeholder={t("form_fields.email_primary.placeholder")} placeholder={t("form_fields.email_primary.placeholder")}
required required
@ -48,13 +48,13 @@ export const CustomerContactFields = ({ className, ...props }: CustomerContactFi
className="lg:col-span-2" className="lg:col-span-2"
control={control} control={control}
description={t("form_fields.mobile_primary.description")} description={t("form_fields.mobile_primary.description")}
icon={ label={t("form_fields.mobile_primary.label")}
leftIcon={
<SmartphoneIcon <SmartphoneIcon
className="h-[18px] w-[18px] text-muted-foreground" className="h-[18px] w-[18px] text-muted-foreground"
strokeWidth={1.5} strokeWidth={1.5}
/> />
} }
label={t("form_fields.mobile_primary.label")}
name="mobile_primary" name="mobile_primary"
placeholder={t("form_fields.mobile_primary.placeholder")} placeholder={t("form_fields.mobile_primary.placeholder")}
typePreset="phone" typePreset="phone"
@ -64,10 +64,10 @@ export const CustomerContactFields = ({ className, ...props }: CustomerContactFi
className="lg:col-span-2" className="lg:col-span-2"
control={control} control={control}
description={t("form_fields.phone_primary.description")} description={t("form_fields.phone_primary.description")}
icon={ label={t("form_fields.phone_primary.label")}
leftIcon={
<PhoneIcon className="h-[18px] w-[18px] text-muted-foreground" strokeWidth={1.5} /> <PhoneIcon className="h-[18px] w-[18px] text-muted-foreground" strokeWidth={1.5} />
} }
label={t("form_fields.phone_primary.label")}
name="phone_primary" name="phone_primary"
placeholder={t("form_fields.phone_primary.placeholder")} placeholder={t("form_fields.phone_primary.placeholder")}
typePreset="phone" typePreset="phone"
@ -79,10 +79,10 @@ export const CustomerContactFields = ({ className, ...props }: CustomerContactFi
className="lg:col-span-2 lg:col-start-1" className="lg:col-span-2 lg:col-start-1"
control={control} control={control}
description={t("form_fields.email_secondary.description")} description={t("form_fields.email_secondary.description")}
icon={ label={t("form_fields.email_secondary.label")}
leftIcon={
<AtSignIcon className="h-[18px] w-[18px] text-muted-foreground" strokeWidth={1.5} /> <AtSignIcon className="h-[18px] w-[18px] text-muted-foreground" strokeWidth={1.5} />
} }
label={t("form_fields.email_secondary.label")}
name="email_secondary" name="email_secondary"
placeholder={t("form_fields.email_secondary.placeholder")} placeholder={t("form_fields.email_secondary.placeholder")}
typePreset="email" typePreset="email"
@ -92,13 +92,13 @@ export const CustomerContactFields = ({ className, ...props }: CustomerContactFi
className="lg:col-span-2" className="lg:col-span-2"
control={control} control={control}
description={t("form_fields.mobile_secondary.description")} description={t("form_fields.mobile_secondary.description")}
icon={ label={t("form_fields.mobile_secondary.label")}
leftIcon={
<SmartphoneIcon <SmartphoneIcon
className="h-[18px] w-[18px] text-muted-foreground" className="h-[18px] w-[18px] text-muted-foreground"
strokeWidth={1.5} strokeWidth={1.5}
/> />
} }
label={t("form_fields.mobile_secondary.label")}
name="mobile_secondary" name="mobile_secondary"
placeholder={t("form_fields.mobile_secondary.placeholder")} placeholder={t("form_fields.mobile_secondary.placeholder")}
typePreset="phone" typePreset="phone"
@ -107,10 +107,10 @@ export const CustomerContactFields = ({ className, ...props }: CustomerContactFi
className="lg:col-span-2" className="lg:col-span-2"
control={control} control={control}
description={t("form_fields.phone_secondary.description")} description={t("form_fields.phone_secondary.description")}
icon={ label={t("form_fields.phone_secondary.label")}
leftIcon={
<PhoneIcon className="h-[18px] w-[18px] text-muted-foreground" strokeWidth={1.5} /> <PhoneIcon className="h-[18px] w-[18px] text-muted-foreground" strokeWidth={1.5} />
} }
label={t("form_fields.phone_secondary.label")}
name="phone_secondary" name="phone_secondary"
placeholder={t("form_fields.phone_secondary.placeholder")} placeholder={t("form_fields.phone_secondary.placeholder")}
typePreset="phone" typePreset="phone"
@ -134,13 +134,13 @@ export const CustomerContactFields = ({ className, ...props }: CustomerContactFi
className="lg:col-span-2" className="lg:col-span-2"
control={control} control={control}
description={t("form_fields.website.description")} description={t("form_fields.website.description")}
icon={ label={t("form_fields.website.label")}
leftIcon={
<GlobeIcon <GlobeIcon
className="h-[18px] w-[18px] text-muted-foreground" className="h-[18px] w-[18px] text-muted-foreground"
strokeWidth={1.5} strokeWidth={1.5}
/> />
} }
label={t("form_fields.website.label")}
name="website" name="website"
placeholder={t("form_fields.website.placeholder")} placeholder={t("form_fields.website.placeholder")}
typePreset="text" typePreset="text"

View File

@ -17,7 +17,7 @@ export const CustomerEditForm = ({ formId, onSubmit, className, focusRef }: Cust
return ( return (
<form id={formId} noValidate onSubmit={onSubmit}> <form id={formId} noValidate onSubmit={onSubmit}>
<FormDebug enabled /> <FormDebug enabled />
<section className={cn("space-y-12 p-6", className)}> <section className={cn("space-y-12 p-6 bg-red-800", className)}>
<CustomerBasicInfoFields focusRef={focusRef} /> <CustomerBasicInfoFields focusRef={focusRef} />
<CustomerAddressFields /> <CustomerAddressFields />
<CustomerContactFields /> <CustomerContactFields />

View File

@ -28,7 +28,7 @@
"@repo/rdx-logger": "workspace:*", "@repo/rdx-logger": "workspace:*",
"@repo/rdx-utils": "workspace:*", "@repo/rdx-utils": "workspace:*",
"express": "^4.18.2", "express": "^4.18.2",
"sequelize": "^6.37.5", "sequelize": "^6.37.8",
"zod": "^4.1.11" "zod": "^4.1.11"
} }
} }

View File

@ -27,7 +27,7 @@
"@repo/rdx-logger": "workspace:*", "@repo/rdx-logger": "workspace:*",
"@repo/rdx-utils": "workspace:*", "@repo/rdx-utils": "workspace:*",
"express": "^4.18.2", "express": "^4.18.2",
"sequelize": "^6.37.5", "sequelize": "^6.37.8",
"zod": "^4.1.11" "zod": "^4.1.11"
} }
} }

View File

@ -27,7 +27,7 @@
"@repo/rdx-logger": "workspace:*", "@repo/rdx-logger": "workspace:*",
"@repo/rdx-utils": "workspace:*", "@repo/rdx-utils": "workspace:*",
"express": "^4.18.2", "express": "^4.18.2",
"sequelize": "^6.37.5", "sequelize": "^6.37.8",
"zod": "^4.1.11" "zod": "^4.1.11"
} }
} }

View File

@ -37,5 +37,5 @@
"engines": { "engines": {
"node": ">=24" "node": ">=24"
}, },
"packageManager": "pnpm@10.29.3" "packageManager": "pnpm@10.33.0"
} }

View File

@ -18,6 +18,6 @@
"typescript": "^5.9.3" "typescript": "^5.9.3"
}, },
"dependencies": { "dependencies": {
"sequelize": "^6.37.5" "sequelize": "^6.37.8"
} }
} }

View File

@ -1,106 +0,0 @@
// DatePickerField.tsx
import {
Button,
Calendar,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
Popover,
PopoverContent,
PopoverTrigger,
} from "@repo/shadcn-ui/components";
import { CalendarIcon, LockIcon } from "lucide-react";
import { cn } from "@repo/shadcn-ui/lib/utils";
import { format } from "date-fns";
import { Control, FieldPath, FieldValues } from "react-hook-form";
import { useTranslation } from "../../locales/i18n.ts";
type DatePickerFieldProps<TFormValues extends FieldValues> = {
control: Control<TFormValues>;
name: FieldPath<TFormValues>;
label?: string;
placeholder?: string;
description?: string;
disabled?: boolean;
required?: boolean;
readOnly?: boolean;
className?: string;
formatDateFn?: (iso: string) => string;
};
export function DatePickerField<TFormValues extends FieldValues>({
control,
name,
label,
placeholder,
description,
disabled = false,
required = false,
readOnly = false,
className,
formatDateFn = (iso) => format(new Date(iso), "dd/MM/yyyy"),
}: DatePickerFieldProps<TFormValues>) {
const { t } = useTranslation();
const isDisabled = disabled || readOnly;
return (
<FormField
control={control}
name={name}
render={({ field }) => (
<FormItem className={cn("space-y-0", className)}>
{label && (
<div className='flex justify-between items-center'>
<FormLabel className='m-0'>{label}</FormLabel>
{required && <span className='text-xs text-destructive'>{t("common.required")}</span>}
</div>
)}
<Popover>
<PopoverTrigger asChild>
<FormControl>
<Button
variant='outline'
disabled={isDisabled}
className={cn(
"w-full justify-start text-left font-normal",
!field.value && "text-muted-foreground",
disabled && "bg-muted text-muted-foreground cursor-not-allowed",
readOnly && !disabled && "bg-muted text-foreground cursor-default"
)}
>
{readOnly ? (
<LockIcon className='mr-2 h-4 w-4 opacity-70' />
) : (
<CalendarIcon className='mr-2 h-4 w-4' />
)}
{field.value ? formatDateFn(field.value) : placeholder}
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className='w-auto p-0'>
<Calendar
mode='single'
selected={field.value ? new Date(field.value) : undefined}
onSelect={(date) => {
if (!readOnly) {
field.onChange(date?.toISOString());
}
}}
initialFocus
/>
</PopoverContent>
</Popover>
<p className={cn("text-xs text-muted-foreground", !description && "invisible")}>
{description || "\u00A0"}
</p>
<FormMessage />
</FormItem>
)}
/>
);
}

View File

@ -1,86 +0,0 @@
import {
Field,
FieldDescription,
FieldError,
FieldLabel,
Textarea,
} from "@repo/shadcn-ui/components";
import { cn } from "@repo/shadcn-ui/lib/utils";
import {
type Control,
Controller,
type FieldPath,
type FieldValues,
useFormState,
} from "react-hook-form";
import type { CommonInputProps } from "./types.js";
type TextAreaFieldProps<TFormValues extends FieldValues> = CommonInputProps & {
control: Control<TFormValues>;
name: FieldPath<TFormValues>;
label?: string;
required?: boolean;
readOnly?: boolean;
description?: string;
orientation?: "vertical" | "horizontal" | "responsive";
inputClassName?: string;
};
export function TextAreaField<TFormValues extends FieldValues>({
control,
name,
label,
description,
required = false,
readOnly = false,
orientation = "vertical",
className,
inputClassName,
...inputRest
}: TextAreaFieldProps<TFormValues>) {
const { isSubmitting } = useFormState({ control, name });
const disabled = isSubmitting || inputRest.disabled;
return (
<Controller
control={control}
name={name}
render={({ field, fieldState }) => {
return (
<Field
className={cn("gap-1", className)}
data-invalid={fieldState.invalid}
orientation={orientation}
>
{label && <FieldLabel htmlFor={name}>{label}</FieldLabel>}
<Textarea
aria-invalid={fieldState.invalid}
id={name}
onBlur={field.onBlur}
onChange={field.onChange}
ref={field.ref}
value={field.value ?? ""}
{...inputRest}
aria-disabled={disabled}
className={cn(
"font-medium bg-muted/50 hover:bg-inherit hover:border-ring hover:ring-ring/50 hover:ring-[2px]",
inputClassName
)}
disabled={disabled}
/>
<FieldDescription>{description || "\u00A0"}</FieldDescription>
<FieldError errors={[fieldState.error]} />
</Field>
);
}}
/>
);
}

View File

@ -1,86 +0,0 @@
import { Field, FieldDescription, FieldError, FieldLabel, Input } from "@repo/shadcn-ui/components";
import { cn } from "@repo/shadcn-ui/lib/utils";
import {
type Control,
Controller,
type FieldPath,
type FieldValues,
useFormState,
} from "react-hook-form";
import type { CommonInputProps } from "./types.js";
type TextFieldProps<TFormValues extends FieldValues> = CommonInputProps & {
control: Control<TFormValues>;
name: FieldPath<TFormValues>;
label?: string;
description?: string;
orientation?: "vertical" | "horizontal" | "responsive";
inputClassName?: string;
};
export function TextField<TFormValues extends FieldValues>({
control,
name,
label,
description,
required = false,
readOnly = false,
orientation = "vertical",
className,
inputClassName,
...inputRest
}: TextFieldProps<TFormValues>) {
const { isSubmitting } = useFormState({ control, name });
const disabled = isSubmitting || inputRest.disabled;
function handleKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
if (e.key === "Enter") {
const form = (e.currentTarget as HTMLInputElement).form;
if (form) form.requestSubmit();
}
}
return (
<Controller
control={control}
name={name}
render={({ field, fieldState }) => {
return (
<Field
className={cn("gap-1", className)}
data-invalid={fieldState.invalid}
orientation={orientation}
>
{label && <FieldLabel htmlFor={name}>{label}</FieldLabel>}
<Input
aria-invalid={fieldState.invalid}
id={name}
onBlur={field.onBlur}
onChange={field.onChange}
onKeyDown={handleKeyDown}
ref={field.ref}
value={field.value ?? ""}
{...inputRest}
aria-disabled={disabled}
className={cn(
"font-medium bg-muted/50 hover:bg-inherit hover:border-ring hover:ring-ring/50 hover:ring-[2px]",
inputClassName
)}
disabled={disabled}
/>
<FieldDescription>{description || "\u00A0"}</FieldDescription>
<FieldError errors={[fieldState.error]} />
</Field>
);
}}
/>
);
}

View File

@ -0,0 +1,149 @@
import {
Button,
Calendar,
Field,
FieldDescription,
FieldError,
FormControl,
FormField,
Popover,
PopoverContent,
PopoverTrigger,
} from "@repo/shadcn-ui/components";
import { cn } from "@repo/shadcn-ui/lib/utils";
import { format, isValid, parseISO } from "date-fns";
import { CalendarIcon, LockIcon } from "lucide-react";
import React from "react";
import { type FieldPath, type FieldValues, useFormContext } from "react-hook-form";
import { FormFieldLabel } from "./form-field-label.tsx";
type DatePickerFieldProps<TFormValues extends FieldValues> = {
name: FieldPath<TFormValues>;
label?: string;
description?: string;
disabled?: boolean;
required?: boolean;
readOnly?: boolean;
placeholder?: string;
orientation?: "vertical" | "horizontal" | "responsive";
className?: string;
inputClassName?: string;
formatDateFn?: (value: string) => string;
};
const parseFieldDate = (value?: string): Date | undefined => {
if (!value) return undefined;
const parsed = parseISO(value);
if (!isValid(parsed)) return undefined;
return parsed;
};
const toDateOnlyString = (date: Date): string => {
return format(date, "yyyy-MM-dd");
};
export function DatePickerField<TFormValues extends FieldValues>({
name,
label,
placeholder,
description,
disabled = false,
required = false,
readOnly = false,
orientation = "vertical",
className,
inputClassName,
formatDateFn = (value) => {
const parsed = parseFieldDate(value);
return parsed ? format(parsed, "dd/MM/yyyy") : value;
},
}: DatePickerFieldProps<TFormValues>) {
const triggerId = React.useId();
const { control, formState } = useFormContext<TFormValues>();
const isDisabled = Boolean(disabled || readOnly || formState.isSubmitting);
return (
<FormField
control={control}
name={name}
render={({ field, fieldState }) => {
const selectedDate = parseFieldDate(field.value);
const displayValue = field.value ? formatDateFn(field.value) : null;
return (
<Field
className={cn("gap-1", className)}
data-invalid={fieldState.invalid}
orientation={orientation}
>
{label ? (
<FormFieldLabel htmlFor={triggerId} required={required}>
{label}
</FormFieldLabel>
) : null}
<Popover>
<PopoverTrigger asChild>
<FormControl>
<Button
aria-invalid={fieldState.invalid}
aria-required={required || undefined}
className={cn(
"h-9 w-full justify-start bg-muted/50 text-left font-medium",
"hover:border-ring/60",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
!displayValue && "text-muted-foreground",
inputClassName
)}
disabled={isDisabled}
id={triggerId}
type="button"
variant="outline"
>
{readOnly ? (
<LockIcon aria-hidden="true" className="mr-2 h-4 w-4 opacity-70" />
) : (
<CalendarIcon aria-hidden="true" className="mr-2 h-4 w-4" />
)}
<span className="truncate">{displayValue ?? placeholder ?? "Select date"}</span>
</Button>
</FormControl>
</PopoverTrigger>
{isDisabled ? null : (
<PopoverContent align="start" className="w-auto p-0">
<Calendar
initialFocus
mode="single"
onSelect={(date) => {
field.onChange(date ? toDateOnlyString(date) : "");
field.onBlur();
}}
selected={selectedDate}
/>
</PopoverContent>
)}
</Popover>
{description ? (
<FieldDescription>{description}</FieldDescription>
) : (
<div aria-hidden="true" className="min-h-5" />
)}
<FieldError errors={[fieldState.error]} />
</Field>
);
}}
/>
);
}

View File

@ -0,0 +1,31 @@
import { FieldLabel } from "@repo/shadcn-ui/components";
import { cn } from "@repo/shadcn-ui/lib/utils";
interface FormFieldLabelProps extends React.ComponentProps<typeof FieldLabel> {
required?: boolean;
optional?: boolean;
}
export const FormFieldLabel = ({
children,
required = false,
optional = false,
className,
...props
}: FormFieldLabelProps) => {
return (
<FieldLabel className={cn(className)} {...props}>
{children}
{required ? (
<span aria-hidden="true" className="ml-1 text-destructive">
*
</span>
) : null}
{!required && optional ? (
<span className="ml-1 text-muted-foreground text-xs">(optional)</span>
) : null}
</FieldLabel>
);
};

View File

@ -1,7 +1,8 @@
export * from "./DatePickerField.tsx"; export * from "./date-picker-field.tsx";
export * from "./date-picker-input-field/index.ts"; export * from "./date-picker-input-field/index.ts";
export * from "./form-field-label.tsx";
export * from "./multi-select-field.tsx"; export * from "./multi-select-field.tsx";
export * from "./SelectField.tsx"; export * from "./radio-group-field.tsx";
export * from "./TextAreaField.tsx"; export * from "./select-field.tsx";
export * from "./TextField.tsx"; export * from "./text-area-field.tsx";
export type * from "./types.d.ts"; export * from "./text-field.tsx";

View File

@ -8,11 +8,10 @@ import {
CommandItem, CommandItem,
CommandList, CommandList,
CommandSeparator, CommandSeparator,
Field,
FieldDescription,
FieldError,
FormControl, FormControl,
FormDescription,
FormItem,
FormLabel,
FormMessage,
Popover, Popover,
PopoverContent, PopoverContent,
PopoverTrigger, PopoverTrigger,
@ -20,178 +19,92 @@ import {
} from "@repo/shadcn-ui/components"; } from "@repo/shadcn-ui/components";
import { cn } from "@repo/shadcn-ui/lib/utils"; import { cn } from "@repo/shadcn-ui/lib/utils";
import { type VariantProps, cva } from "class-variance-authority"; import { type VariantProps, cva } from "class-variance-authority";
import { import { CheckIcon, ChevronDownIcon } from "lucide-react";
CheckCircle2Icon,
CheckIcon,
ChevronDownIcon,
Loader2Icon,
WandSparklesIcon,
XCircleIcon,
} from "lucide-react";
import * as React from "react"; import * as React from "react";
import { import { type FieldPath, type FieldValues, useController, useFormContext } from "react-hook-form";
type Control,
type FieldPath,
type FieldValues,
useController,
useFormContext,
useFormState,
} from "react-hook-form";
import { useTranslation } from "../../locales/i18n.ts"; import { useTranslation } from "../../locales/i18n.ts";
/* -------------------- Variants -------------------- */ import { FormFieldLabel } from "./form-field-label.tsx";
const multiSelectFieldVariants = cva( const multiSelectFieldVariants = cva("m-1", {
"m-1 transition ease-in-out delay-150 hover:-translate-y-1 hover:scale-110 duration-300",
{
variants: { variants: {
variant: { variant: {
default: default: "border-foreground/10 bg-primary text-primary-foreground",
"border-foreground/10 text-foreground bg-primary hover:bg-primary/80 text-primary-foreground", secondary: "border-foreground/10 bg-secondary text-secondary-foreground",
secondary: destructive: "border-transparent bg-destructive text-destructive-foreground",
"border-foreground/10 bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
inverted: "inverted", inverted: "inverted",
}, },
}, },
defaultVariants: { defaultVariants: {
variant: "default", variant: "default",
}, },
} });
);
/* -------------------- Tipos -------------------- */ export interface MultiSelectFieldOptionType {
export type MultiSelectFieldOptionType = {
label: string; label: string;
value: string; value: string;
group?: string; group?: string;
icon?: React.ComponentType<{ className?: string }>; icon?: React.ComponentType<{ className?: string }>;
}; disabled?: boolean;
/**
* Props base (visuales y de comportamiento)
*/
interface MultiSelectFieldBaseProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof multiSelectFieldVariants> {
options: MultiSelectFieldOptionType[];
onValueChange?: (values: string[]) => void; // ahora opcional (RHF-first)
onValidateOption?: (value: string) => boolean;
defaultValue?: string[];
placeholder?: string;
animation?: number;
maxCount?: number;
modalPopover?: boolean;
asChild?: boolean;
className?: string;
selectAllVisible?: boolean;
} }
/** type MultiSelectFieldProps<T extends FieldValues> = VariantProps<
* Props adicionales para paridad con TextField (RHF + UX) typeof multiSelectFieldVariants
*/ > & {
interface MultiSelectFieldTextFieldLikeProps<T extends FieldValues> {
name: FieldPath<T>; name: FieldPath<T>;
control?: Control<T>; // opcional; si no, se usa useFormContext() options: MultiSelectFieldOptionType[];
label?: string; label?: string;
description?: string; description?: string;
required?: boolean; required?: boolean;
readOnly?: boolean; readOnly?: boolean;
disabled?: boolean;
disabledWhileSubmitting?: boolean; placeholder?: string;
showSuccessWhenValid?: boolean; maxCount?: number;
showValidatingSpinner?: boolean; modalPopover?: boolean;
} selectAllVisible?: boolean;
/** className?: string;
* Props completas inputClassName?: string;
*/ badgeClassName?: string;
export type MultiSelectFieldProps<T extends FieldValues> = MultiSelectFieldBaseProps &
MultiSelectFieldTextFieldLikeProps<T>;
/* -------------------- Componente -------------------- */ onValueChange?: (values: string[]) => void;
};
export const MultiSelectFieldInner = React.forwardRef( export const MultiSelectField = <T extends FieldValues>({
<T extends FieldValues>(
{
// RHF-like
name, name,
control: controlProp, options,
label, label,
description, description,
required, required = false,
readOnly, readOnly = false,
disabled = false,
disabledWhileSubmitting = true,
showSuccessWhenValid = true,
showValidatingSpinner = true,
// tu API actual
options,
onValueChange,
onValidateOption,
variant,
defaultValue = [],
placeholder, placeholder,
animation = 0,
maxCount = 3, maxCount = 3,
modalPopover = false, modalPopover = false,
asChild = false,
className,
selectAllVisible = false, selectAllVisible = false,
className,
// button props, etc. inputClassName,
...buttonProps badgeClassName,
}: MultiSelectFieldProps<T>, variant,
ref: React.Ref<HTMLButtonElement> onValueChange,
) => { }: MultiSelectFieldProps<T>) => {
const formCtx = useFormContext<T>();
const control = controlProp ?? formCtx?.control;
if (!control) {
throw new Error(
"MultiSelectField requiere 'control' o estar dentro de <FormProvider> (useFormContext)."
);
}
const { t } = useTranslation(); const { t } = useTranslation();
const triggerId = React.useId();
const { control, formState } = useFormContext<T>();
// RHF: estado del campo
const { field, fieldState } = useController({ const { field, fieldState } = useController({
control, control,
name, name,
defaultValue: undefined,
}); });
const { isSubmitting, isValidating } = useFormState({ control, name });
// Inicializa el valor con defaultValue si RHF no trae nada:
React.useEffect(() => {
if (!Array.isArray(field.value) || field.value.length === 0) {
if (defaultValue.length > 0) field.onChange(defaultValue);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const selectedValues: string[] = Array.isArray(field.value) ? field.value : [];
const disabled = (disabledWhileSubmitting && isSubmitting) || buttonProps.disabled || readOnly;
const invalid = fieldState.invalid && (fieldState.isDirty || fieldState.isTouched);
const valid =
!fieldState.invalid &&
(fieldState.isDirty || fieldState.isTouched) &&
selectedValues.length > 0;
// A11y ids
const describedById = description ? `${name}-desc` : undefined;
const errorId = fieldState.error ? `${name}-err` : undefined;
const [isPopoverOpen, setIsPopoverOpen] = React.useState(false); const [isPopoverOpen, setIsPopoverOpen] = React.useState(false);
const [isAnimating, setIsAnimating] = React.useState(false);
// Agrupar opciones const selectedValues: string[] = Array.isArray(field.value) ? field.value : [];
const isDisabled = Boolean(disabled || readOnly || formState.isSubmitting);
const grouped = options.reduce<Record<string, MultiSelectFieldOptionType[]>>((acc, item) => { const grouped = options.reduce<Record<string, MultiSelectFieldOptionType[]>>((acc, item) => {
const key = item.group || ""; const key = item.group || "";
if (!acc[key]) acc[key] = []; if (!acc[key]) acc[key] = [];
@ -204,14 +117,11 @@ export const MultiSelectFieldInner = React.forwardRef(
onValueChange?.(values); onValueChange?.(values);
}; };
const toggleOption = (option: string) => { const toggleOption = (optionValue: string) => {
if (onValidateOption && !onValidateOption(option)) { const next = selectedValues.includes(optionValue)
console.warn(`Option "${option}" is not valid.`); ? selectedValues.filter((value) => value !== optionValue)
return; : [...selectedValues, optionValue];
}
const next = selectedValues.includes(option)
? selectedValues.filter((v) => v !== option)
: [...selectedValues, option];
emitChange(next); emitChange(next);
}; };
@ -221,162 +131,124 @@ export const MultiSelectFieldInner = React.forwardRef(
const toggleAll = () => { const toggleAll = () => {
if (selectedValues.length === options.length) { if (selectedValues.length === options.length) {
handleClear(); handleClear();
} else { return;
emitChange(options.map((o) => o.value));
} }
};
const handleInputKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => { emitChange(options.filter((option) => !option.disabled).map((option) => option.value));
if (event.key === "Enter") setIsPopoverOpen(true);
else if (event.key === "Backspace" && !event.currentTarget.value) {
const next = selectedValues.slice(0, -1);
emitChange(next);
}
}; };
const buttonAriaDescribedBy = cn(describedById, errorId);
return ( return (
<FormItem className='space-y-0'> <Field
{label && ( className={cn("gap-1", className)}
<div className='mb-1 flex items-center gap-2'> data-invalid={fieldState.invalid}
<FormLabel className='m-0'>{label}</FormLabel> orientation="vertical"
{required && <span className='text-xs text-destructive'></span>} >
{fieldState.isDirty && ( {label ? (
<span className='text-[10px] text-muted-foreground'>(modificado)</span> <FormFieldLabel htmlFor={triggerId} required={required}>
)} {label}
</div> </FormFieldLabel>
)} ) : null}
<FormControl> <FormControl>
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen} modal={modalPopover}> <Popover modal={modalPopover} onOpenChange={setIsPopoverOpen} open={isPopoverOpen}>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button <Button
ref={ref} aria-expanded={isPopoverOpen}
type='button' aria-invalid={fieldState.invalid}
{...buttonProps} aria-required={required || undefined}
disabled={disabled}
onClick={() => setIsPopoverOpen((p) => !p)}
aria-invalid={invalid || undefined}
aria-describedby={buttonAriaDescribedBy}
aria-errormessage={errorId}
aria-busy={(showValidatingSpinner && isValidating) || undefined}
className={cn( className={cn(
"flex w-full min-h-10 h-auto items-center justify-between rounded-md border bg-inherit hover:bg-inherit [&_svg]:pointer-events-auto p-1", "min-h-10 h-auto w-full justify-between bg-muted/50 p-1 text-left font-medium",
invalid && "border-destructive", "hover:border-ring/60",
valid && showSuccessWhenValid && "border-green-500", "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
className inputClassName
)} )}
disabled={isDisabled}
id={triggerId}
type="button"
variant="outline"
> >
{/* Contenido del trigger */}
{selectedValues.length > 0 ? ( {selectedValues.length > 0 ? (
<div className='flex w-full items-center justify-between'> <div className="flex w-full items-center justify-between gap-2">
<div className='flex flex-wrap items-center'> <div className="flex flex-wrap items-center">
{selectedValues.slice(0, maxCount).map((value) => { {selectedValues.slice(0, maxCount).map((value) => {
const option = options.find((o) => o.value === value); const option = options.find((item) => item.value === value);
const Icon = option?.icon; const Icon = option?.icon;
return ( return (
<Badge <Badge
className={cn(multiSelectFieldVariants({ variant }), badgeClassName)}
key={value} key={value}
className={cn(
isAnimating ? "animate-bounce" : "",
multiSelectFieldVariants({ variant })
)}
style={{ animationDuration: `${animation}s` }}
> >
{Icon && <Icon className='mr-2 h-4 w-4' />} {Icon ? <Icon className="mr-2 h-4 w-4" /> : null}
{option?.label} {option?.label}
</Badge> </Badge>
); );
})} })}
{selectedValues.length > maxCount && (
<Badge {selectedValues.length > maxCount ? (
className={cn( <Badge className={cn(multiSelectFieldVariants({ variant }), badgeClassName)}>
"bg-primary text-foreground border-foreground/1 hover:bg-primary", +{selectedValues.length - maxCount}
isAnimating ? "animate-bounce" : "",
multiSelectFieldVariants({ variant })
)}
style={{ animationDuration: `${animation}s` }}
>
{`+ ${selectedValues.length - maxCount} more`}
<XCircleIcon
className='ml-2 h-4 w-4 cursor-pointer'
onClick={(e) => {
e.stopPropagation();
clearExtraOptions();
}}
/>
</Badge> </Badge>
)} ) : null}
</div> </div>
<div className='flex items-center gap-2'> <ChevronDownIcon aria-hidden="true" className="h-4 w-4 text-muted-foreground" />
{showValidatingSpinner && isValidating && (
<Loader2Icon className='h-4 w-4 animate-spin' />
)}
{showSuccessWhenValid && valid && !isValidating && !invalid && (
<CheckCircle2Icon className='h-4 w-4 text-green-600' />
)}
<Separator orientation='vertical' className='flex h-full min-h-6' />
<ChevronDownIcon className='mx-2 h-4 cursor-pointer text-muted-foreground' />
</div>
</div> </div>
) : ( ) : (
<div className='mx-auto flex w-full items-center justify-between'> <div className="flex w-full items-center justify-between gap-2">
<span className='mx-3 text-sm text-muted-foreground'> <span className="truncate text-sm text-muted-foreground">
{placeholder || t("components.multi_select.select_options")} {placeholder || t("components.multi_select.select_options")}
</span> </span>
<div className='flex items-center gap-2'> <ChevronDownIcon aria-hidden="true" className="h-4 w-4 text-muted-foreground" />
{showValidatingSpinner && isValidating && (
<Loader2Icon className='h-4 w-4 animate-spin' />
)}
{showSuccessWhenValid && valid && !isValidating && !invalid && (
<CheckCircle2Icon className='h-4 w-4 text-green-600' />
)}
<ChevronDownIcon className='mx-2 h-4 cursor-pointer text-muted-foreground' />
</div>
</div> </div>
)} )}
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent {isDisabled ? null : (
className='w-auto p-0' <PopoverContent align="start" className="w-auto p-0">
align='start'
onEscapeKeyDown={() => setIsPopoverOpen(false)}
>
<Command> <Command>
<CommandInput placeholder={t("common.search")} onKeyDown={handleInputKeyDown} /> <CommandInput placeholder={t("common.search")} />
<CommandList> <CommandList>
<CommandEmpty>{t("components.multi_select.no_results")}</CommandEmpty> <CommandEmpty>{t("components.multi_select.no_results")}</CommandEmpty>
{selectAllVisible && ( {selectAllVisible ? (
<CommandItem key='all' onSelect={toggleAll} className='cursor-pointer'> <CommandItem className="cursor-pointer" onSelect={toggleAll}>
<div <div
className={cn( className={cn(
"mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary", "mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary",
selectedValues.length === options.length selectedValues.length ===
options.filter((option) => !option.disabled).length
? "bg-primary text-primary-foreground" ? "bg-primary text-primary-foreground"
: "opacity-50 [&_svg]:invisible" : "opacity-50 [&_svg]:invisible"
)} )}
> >
<CheckIcon className='h-4 w-4' /> <CheckIcon className="h-4 w-4" />
</div> </div>
<span>(Select All)</span> <span>{t("common.select_all")}</span>
</CommandItem> </CommandItem>
)} ) : null}
{Object.keys(grouped).map((group) => ( {Object.entries(grouped).map(([group, groupOptions]) => (
<CommandGroup key={`group-${group || "ungrouped"}`} heading={group}> <CommandGroup
{grouped[group].map((option) => { heading={group || undefined}
key={`group-${group || "ungrouped"}`}
>
{groupOptions.map((option) => {
const isSelected = selectedValues.includes(option.value); const isSelected = selectedValues.includes(option.value);
const Icon = option.icon; const Icon = option.icon;
const optionDisabled = Boolean(option.disabled);
return ( return (
<CommandItem <CommandItem
className="cursor-pointer"
disabled={optionDisabled}
key={option.value} key={option.value}
onSelect={() => toggleOption(option.value)} onSelect={() => {
className='cursor-pointer' if (optionDisabled) return;
toggleOption(option.value);
}}
> >
<div <div
className={cn( className={cn(
@ -391,7 +263,9 @@ export const MultiSelectFieldInner = React.forwardRef(
)} )}
/> />
</div> </div>
{Icon && <Icon className='mr-2 h-4 w-4 text-muted-foreground' />}
{Icon ? <Icon className="mr-2 h-4 w-4 text-muted-foreground" /> : null}
<span>{option.label}</span> <span>{option.label}</span>
</CommandItem> </CommandItem>
); );
@ -402,21 +276,40 @@ export const MultiSelectFieldInner = React.forwardRef(
<CommandSeparator /> <CommandSeparator />
<CommandGroup> <CommandGroup>
<div className='flex items-center justify-between'> <div className="flex items-center justify-between">
{selectedValues.length > 0 && ( {selectedValues.length > 0 ? (
<> <>
<CommandItem <CommandItem
className="flex-1 cursor-pointer justify-center"
onSelect={handleClear} onSelect={handleClear}
className='flex-1 cursor-pointer justify-center'
> >
{t("components.multi_select.clear_selection")} {t("components.multi_select.clear_selection")}
</CommandItem> </CommandItem>
<Separator orientation='vertical' className='flex h-full min-h-6' />
</> {selectedValues.length > maxCount ? (
)} <>
<Separator className="flex h-full min-h-6" orientation="vertical" />
<CommandItem <CommandItem
onSelect={() => setIsPopoverOpen(false)} className="flex-1 cursor-pointer justify-center"
className='flex-1 cursor-pointer justify-center' onSelect={clearExtraOptions}
>
{t("components.multi_select.keep_first", {
count: maxCount,
})}
</CommandItem>
</>
) : null}
<Separator className="flex h-full min-h-6" orientation="vertical" />
</>
) : null}
<CommandItem
className="flex-1 cursor-pointer justify-center"
onSelect={() => {
field.onBlur();
setIsPopoverOpen(false);
}}
> >
{t("components.multi_select.close")} {t("components.multi_select.close")}
</CommandItem> </CommandItem>
@ -425,36 +318,17 @@ export const MultiSelectFieldInner = React.forwardRef(
</CommandList> </CommandList>
</Command> </Command>
</PopoverContent> </PopoverContent>
{animation! > 0 && selectedValues.length > 0 && (
<WandSparklesIcon
className={cn(
"my-2 h-3 w-3 cursor-pointer bg-background text-foreground",
isAnimating ? "" : "text-muted-foreground"
)}
onClick={() => setIsAnimating((s) => !s)}
/>
)} )}
</Popover> </Popover>
</FormControl> </FormControl>
<div className='mt-1 flex items-start justify-between'> {description ? (
<FormDescription <FieldDescription>{description}</FieldDescription>
id={describedById} ) : (
className={cn("text-xs truncate", !description && "invisible")} <div aria-hidden="true" className="min-h-5" />
> )}
{description || "\u00A0"}
</FormDescription>
</div>
<FormMessage id={errorId} /> <FieldError errors={[fieldState.error]} />
</FormItem> </Field>
); );
} };
);
MultiSelectFieldInner.displayName = "MultiSelectField";
export const MultiSelectField = MultiSelectFieldInner as <T extends FieldValues>(
p: MultiSelectFieldProps<T> & { ref?: React.Ref<HTMLButtonElement> }
) => React.JSX.Element;

View File

@ -0,0 +1,139 @@
import {
Field,
FieldDescription,
FieldError,
FormControl,
RadioGroup,
RadioGroupItem,
} from "@repo/shadcn-ui/components";
import { cn } from "@repo/shadcn-ui/lib/utils";
import * as React from "react";
import { Controller, type FieldPath, type FieldValues, useFormContext } from "react-hook-form";
import { FormFieldLabel } from "./form-field-label.tsx";
export interface RadioGroupFieldItem {
value: string;
label: string;
description?: string;
disabled?: boolean;
}
type RadioGroupFieldProps<TFormValues extends FieldValues> = {
name: FieldPath<TFormValues>;
items: RadioGroupFieldItem[];
label?: string;
description?: string;
disabled?: boolean;
required?: boolean;
readOnly?: boolean;
orientation?: "vertical" | "horizontal" | "responsive";
className?: string;
inputClassName?: string;
itemClassName?: string;
};
export const RadioGroupField = <TFormValues extends FieldValues>({
name,
items,
label,
description,
disabled = false,
required = false,
readOnly = false,
orientation = "vertical",
className,
inputClassName,
itemClassName,
}: RadioGroupFieldProps<TFormValues>) => {
const baseId = React.useId();
const { control, formState } = useFormContext<TFormValues>();
const isDisabled = Boolean(disabled || readOnly || formState.isSubmitting);
return (
<Controller
control={control}
name={name}
render={({ field, fieldState }) => (
<Field
className={cn("gap-1", className)}
data-invalid={fieldState.invalid}
orientation={orientation}
>
{label ? (
<FormFieldLabel
htmlFor={`${baseId}-${items[0]?.value ?? "option"}`}
required={required}
>
{label}
</FormFieldLabel>
) : null}
<FormControl>
<RadioGroup
className={cn(
"gap-3",
orientation === "horizontal" && "flex flex-row flex-wrap items-center gap-6",
orientation === "responsive" &&
"flex flex-col gap-3 sm:flex-row sm:flex-wrap sm:items-center sm:gap-6",
inputClassName
)}
disabled={isDisabled}
name={field.name}
onValueChange={field.onChange}
value={field.value ?? ""}
>
{items.map((item, index) => {
const itemId = `${baseId}-${index}-${item.value}`;
const itemDisabled = Boolean(isDisabled || item.disabled);
return (
<div className={cn("flex items-start gap-2", itemClassName)} key={item.value}>
<RadioGroupItem
aria-invalid={fieldState.invalid}
aria-required={required || undefined}
className="mt-0.5"
disabled={itemDisabled}
id={itemId}
onBlur={field.onBlur}
value={item.value}
/>
<div className="grid gap-0.5">
<label
className={cn(
"cursor-pointer text-sm font-medium leading-none",
itemDisabled && "cursor-not-allowed opacity-70"
)}
htmlFor={itemId}
>
{item.label}
</label>
{item.description ? (
<p className="text-muted-foreground text-sm">{item.description}</p>
) : null}
</div>
</div>
);
})}
</RadioGroup>
</FormControl>
{description ? (
<FieldDescription>{description}</FieldDescription>
) : (
<div aria-hidden="true" className="min-h-5" />
)}
<FieldError errors={[fieldState.error]} />
</Field>
)}
/>
);
};

View File

@ -2,7 +2,6 @@ import {
Field, Field,
FieldDescription, FieldDescription,
FieldError, FieldError,
FieldLabel,
FormControl, FormControl,
Select, Select,
SelectContent, SelectContent,
@ -11,28 +10,28 @@ import {
SelectValue, SelectValue,
} from "@repo/shadcn-ui/components"; } from "@repo/shadcn-ui/components";
import { cn } from "@repo/shadcn-ui/lib/utils"; import { cn } from "@repo/shadcn-ui/lib/utils";
import { import React from "react";
type Control, import { Controller, type FieldPath, type FieldValues, useFormContext } from "react-hook-form";
Controller,
type FieldPath,
type FieldValues,
useController,
useFormState,
} from "react-hook-form";
import { useTranslation } from "../../locales/i18n.ts"; import { FormFieldLabel } from "./form-field-label.tsx";
interface SelectFieldItem {
value: string;
label: string;
}
type SelectFieldProps<TFormValues extends FieldValues> = { type SelectFieldProps<TFormValues extends FieldValues> = {
control: Control<TFormValues>;
name: FieldPath<TFormValues>; name: FieldPath<TFormValues>;
label?: string; label?: string;
description?: string; description?: string;
disabled?: boolean; disabled?: boolean;
required?: boolean; required?: boolean;
readOnly?: boolean; readOnly?: boolean;
placeholder?: string; placeholder?: string;
items: Array<{ value: string; label: string }>; items: SelectFieldItem[];
orientation?: "vertical" | "horizontal" | "responsive"; orientation?: "vertical" | "horizontal" | "responsive";
@ -41,27 +40,26 @@ type SelectFieldProps<TFormValues extends FieldValues> = {
}; };
export function SelectField<TFormValues extends FieldValues>({ export function SelectField<TFormValues extends FieldValues>({
control,
name, name,
items,
label, label,
placeholder,
description, description,
disabled = false, disabled = false,
required = false, required = false,
readOnly = false, readOnly = false,
placeholder,
items,
orientation = "vertical", orientation = "vertical",
className, className,
inputClassName, inputClassName,
}: SelectFieldProps<TFormValues>) { }: SelectFieldProps<TFormValues>) {
const { t } = useTranslation(); const triggerId = React.useId();
const { control, formState } = useFormContext<TFormValues>();
const { isSubmitting } = useFormState({ control, name }); const isDisabled = Boolean(disabled || readOnly || formState.isSubmitting);
const { field, fieldState } = useController({ control, name });
const isDisabled = disabled || readOnly;
return ( return (
<Controller <Controller
@ -74,16 +72,28 @@ export function SelectField<TFormValues extends FieldValues>({
data-invalid={fieldState.invalid} data-invalid={fieldState.invalid}
orientation={orientation} orientation={orientation}
> >
{label && <FieldLabel htmlFor={name}>{label}</FieldLabel>} {label ? (
<FormFieldLabel htmlFor={triggerId} required={required}>
{label}
</FormFieldLabel>
) : null}
<Select defaultValue={field.value} disabled={isDisabled} onValueChange={field.onChange}> <Select
disabled={isDisabled}
onValueChange={field.onChange}
value={field.value ?? undefined}
>
<FormControl> <FormControl>
<SelectTrigger <SelectTrigger
aria-invalid={fieldState.invalid}
aria-required={required}
className={cn( className={cn(
"w-full h-8", "bg-muted/50 font-medium",
"font-medium bg-muted/50 hover:bg-inherit hover:border-ring hover:ring-ring/50 hover:ring-[2px]", "hover:border-ring/60",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
inputClassName inputClassName
)} )}
id={triggerId}
> >
<SelectValue <SelectValue
className={"placeholder:font-normal placeholder:italic"} className={"placeholder:font-normal placeholder:italic"}
@ -100,7 +110,11 @@ export function SelectField<TFormValues extends FieldValues>({
</SelectContent> </SelectContent>
</Select> </Select>
<FieldDescription>{description || "\u00A0"}</FieldDescription> {description ? (
<FieldDescription>{description}</FieldDescription>
) : (
<div aria-hidden="true" className="min-h-5" />
)}
<FieldError errors={[fieldState.error]} /> <FieldError errors={[fieldState.error]} />
</Field> </Field>
); );

View File

@ -0,0 +1,89 @@
import { Field, FieldDescription, FieldError, Textarea } from "@repo/shadcn-ui/components";
import { cn } from "@repo/shadcn-ui/lib/utils";
import * as React from "react";
import { type FieldPath, type FieldValues, useFormContext } from "react-hook-form";
import { FormFieldLabel } from "./form-field-label.tsx";
import type { NativeTextareaProps } from "./types.ts";
type TextAreaFieldProps<TFormValues extends FieldValues> = NativeTextareaProps & {
name: FieldPath<TFormValues>;
label?: string;
description?: string;
orientation?: "vertical" | "horizontal" | "responsive";
inputClassName?: string;
};
export function TextAreaField<TFormValues extends FieldValues>({
name,
label,
description,
required = false,
readOnly = false,
orientation = "vertical",
className,
inputClassName,
...inputRest
}: TextAreaFieldProps<TFormValues>) {
const { register, formState, getFieldState } = useFormContext<TFormValues>();
const inputId = React.useId();
const disabled = formState.isSubmitting || inputRest.disabled;
// Obtener error del campo (tipado seguro)
const fieldError = getFieldState(name, formState).error;
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
inputRest.onKeyDown?.(e);
if (e.defaultPrevented) return;
// Cmd/Ctrl + Enter para submit
if ((e.metaKey || e.ctrlKey) && e.key === "Enter" && !e.nativeEvent.isComposing) {
e.preventDefault();
e.currentTarget.form?.requestSubmit();
}
};
return (
<Field className={cn("gap-1", className)} data-invalid={!!fieldError} orientation={orientation}>
{label ? (
<FormFieldLabel htmlFor={inputId} required={required}>
{label}
</FormFieldLabel>
) : null}
<Textarea
{...inputRest}
{...register(name)}
aria-invalid={!!fieldError}
className={cn(
"bg-muted/50 font-medium",
"hover:border-ring/60",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
inputClassName
)}
disabled={disabled}
id={inputId}
onKeyDown={handleKeyDown}
readOnly={readOnly}
required={required}
/>
{description ? (
<FieldDescription>{description}</FieldDescription>
) : (
<div aria-hidden="true" className="min-h-5" />
)}
<FieldError errors={[fieldError]} />
</Field>
);
}

View File

@ -0,0 +1,82 @@
export type TextFieldTypePreset =
| "text"
| "email"
| "phone"
| "search"
| "url"
| "password"
| "number";
export interface ResolvedInputPreset {
type: React.HTMLInputTypeAttribute;
inputMode?: React.HTMLAttributes<HTMLInputElement>["inputMode"];
autoComplete?: string;
spellCheck?: boolean;
enterKeyHint?: React.HTMLAttributes<HTMLInputElement>["enterKeyHint"];
}
export const getInputPresetProps = (preset: TextFieldTypePreset = "text"): ResolvedInputPreset => {
switch (preset) {
case "email":
return {
type: "email",
inputMode: "email",
autoComplete: "email",
spellCheck: false,
enterKeyHint: "next",
};
case "phone":
return {
type: "tel",
inputMode: "tel",
autoComplete: "tel",
spellCheck: false,
enterKeyHint: "next",
};
case "search":
return {
type: "search",
inputMode: "search",
autoComplete: "off",
spellCheck: false,
enterKeyHint: "search",
};
case "url":
return {
type: "url",
inputMode: "url",
autoComplete: "url",
spellCheck: false,
enterKeyHint: "next",
};
case "password":
return {
type: "password",
autoComplete: "current-password",
spellCheck: false,
enterKeyHint: "done",
};
case "number":
return {
type: "text",
inputMode: "numeric",
autoComplete: "off",
spellCheck: false,
enterKeyHint: "next",
};
case "text":
default:
return {
type: "text",
autoComplete: "off",
spellCheck: false,
enterKeyHint: "next",
};
}
};

View File

@ -0,0 +1,109 @@
import {
Field,
FieldDescription,
FieldError,
InputGroup,
InputGroupAddon,
InputGroupInput,
} from "@repo/shadcn-ui/components";
import { cn } from "@repo/shadcn-ui/lib/utils";
import * as React from "react";
import { type FieldPath, type FieldValues, useFormContext } from "react-hook-form";
import { FormFieldLabel } from "./form-field-label.tsx";
import { type TextFieldTypePreset, getInputPresetProps } from "./text-field-presets.tsx";
import type { NativeInputProps } from "./types.ts";
type TextFieldProps<TFormValues extends FieldValues> = NativeInputProps & {
name: FieldPath<TFormValues>;
label?: string;
description?: string;
orientation?: "vertical" | "horizontal" | "responsive";
inputClassName?: string;
leftIcon?: React.ReactNode;
rightIcon?: React.ReactNode;
typePreset?: TextFieldTypePreset;
};
export const TextField = <TFormValues extends FieldValues>({
name,
label,
description,
required = false,
readOnly = false,
orientation = "vertical",
className,
inputClassName,
leftIcon,
rightIcon,
typePreset,
...inputRest
}: TextFieldProps<TFormValues>) => {
const { register, formState, getFieldState } = useFormContext<TFormValues>();
const inputId = React.useId();
const disabled = formState.isSubmitting || inputRest.disabled;
const presetProps = getInputPresetProps(typePreset);
// Obtener error del campo (tipado seguro)
const fieldError = getFieldState(name, formState).error;
return (
<Field className={cn("gap-1", className)} data-invalid={!!fieldError} orientation={orientation}>
{label ? (
<FormFieldLabel htmlFor={inputId} required={required}>
{label}
</FormFieldLabel>
) : null}
<InputGroup
className={cn(
"bg-muted/50 font-medium",
"hover:border-ring/60",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
inputClassName
)}
>
{leftIcon && (
<InputGroupAddon aria-hidden="true" className={cn("bg-muted/50 font-medium")}>
{leftIcon}
</InputGroupAddon>
)}
<InputGroupInput
{...presetProps}
{...inputRest}
{...register(name)}
aria-invalid={!!fieldError}
disabled={disabled}
id={inputId}
readOnly={readOnly}
required={required}
/>
{rightIcon && <InputGroupAddon aria-hidden="true">{rightIcon}</InputGroupAddon>}
</InputGroup>
{description ? (
<FieldDescription>{description}</FieldDescription>
) : (
<div aria-hidden="true" className="min-h-5" />
)}
<FieldError errors={[fieldError]} />
</Field>
);
};

View File

@ -1,4 +0,0 @@
export type CommonInputProps = Omit<
React.InputHTMLAttributes<HTMLInputElement | HTMLTextAreaElement>,
"name" | "value" | "onChange" | "onBlur" | "ref" | "type"
>;

View File

@ -0,0 +1,9 @@
export type NativeInputProps = Omit<
React.InputHTMLAttributes<HTMLInputElement>,
"name" | "value" | "defaultValue" | "onChange" | "type"
>;
export type NativeTextareaProps = Omit<
React.TextareaHTMLAttributes<HTMLTextAreaElement>,
"name" | "value" | "defaultValue" | "onChange"
>;

View File

@ -56,7 +56,7 @@
"pnpm": "^10.10.0", "pnpm": "^10.10.0",
"radix-ui": "latest", "radix-ui": "latest",
"react-day-picker": "9.14.0", "react-day-picker": "9.14.0",
"react-hook-form": "^7.65.0", "react-hook-form": "^7.72.1",
"react-resizable-panels": "^4.8.0", "react-resizable-panels": "^4.8.0",
"recharts": "2.15.4", "recharts": "2.15.4",
"sonner": "^2.0.7", "sonner": "^2.0.7",

View File

@ -250,8 +250,8 @@ importers:
specifier: ^6.0.0 specifier: ^6.0.0
version: 6.0.0(react@19.2.0) version: 6.0.0(react@19.2.0)
react-hook-form: react-hook-form:
specifier: ^7.56.4 specifier: ^7.72.1
version: 7.66.0(react@19.2.0) version: 7.72.1(react@19.2.0)
react-i18next: react-i18next:
specifier: ^15.0.1 specifier: ^15.0.1
version: 15.7.4(i18next@25.6.0(typescript@5.8.3))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.8.3) version: 15.7.4(i18next@25.6.0(typescript@5.8.3))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.8.3)
@ -326,8 +326,8 @@ importers:
specifier: ^5.90.6 specifier: ^5.90.6
version: 5.90.6(react@19.2.0) version: 5.90.6(react@19.2.0)
react-hook-form: react-hook-form:
specifier: ^7.56.2 specifier: ^7.72.1
version: 7.66.0(react@19.2.0) version: 7.72.1(react@19.2.0)
react-router-dom: react-router-dom:
specifier: ^6.26.0 specifier: ^6.26.0
version: 6.30.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0) version: 6.30.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
@ -361,7 +361,7 @@ importers:
dependencies: dependencies:
'@hookform/resolvers': '@hookform/resolvers':
specifier: ^5.0.1 specifier: ^5.0.1
version: 5.2.2(react-hook-form@7.66.0(react@19.2.0)) version: 5.2.2(react-hook-form@7.72.1(react@19.2.0))
'@repo/i18next': '@repo/i18next':
specifier: workspace:* specifier: workspace:*
version: link:../../packages/i18n version: link:../../packages/i18n
@ -405,8 +405,8 @@ importers:
specifier: ^3.0.1 specifier: ^3.0.1
version: 3.0.1 version: 3.0.1
react-hook-form: react-hook-form:
specifier: ^7.58.1 specifier: ^7.72.1
version: 7.66.0(react@19.2.0) version: 7.72.1(react@19.2.0)
react-i18next: react-i18next:
specifier: ^15.5.1 specifier: ^15.5.1
version: 15.7.4(i18next@25.6.0(typescript@5.9.3))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3) version: 15.7.4(i18next@25.6.0(typescript@5.9.3))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
@ -467,7 +467,7 @@ importers:
version: link:../customers version: link:../customers
'@hookform/resolvers': '@hookform/resolvers':
specifier: ^5.0.1 specifier: ^5.0.1
version: 5.2.2(react-hook-form@7.66.0(react@19.2.0)) version: 5.2.2(react-hook-form@7.72.1(react@19.2.0))
'@repo/i18next': '@repo/i18next':
specifier: workspace:* specifier: workspace:*
version: link:../../packages/i18n version: link:../../packages/i18n
@ -517,8 +517,8 @@ importers:
specifier: ^2.3.4 specifier: ^2.3.4
version: 2.3.4 version: 2.3.4
react-hook-form: react-hook-form:
specifier: ^7.58.1 specifier: ^7.72.1
version: 7.66.0(react@19.2.0) version: 7.72.1(react@19.2.0)
react-i18next: react-i18next:
specifier: ^15.5.1 specifier: ^15.5.1
version: 15.7.4(i18next@25.6.0(typescript@5.9.3))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3) version: 15.7.4(i18next@25.6.0(typescript@5.9.3))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
@ -573,7 +573,7 @@ importers:
version: link:../core version: link:../core
'@hookform/resolvers': '@hookform/resolvers':
specifier: ^5.0.1 specifier: ^5.0.1
version: 5.2.2(react-hook-form@7.66.0(react@19.2.0)) version: 5.2.2(react-hook-form@7.72.1(react@19.2.0))
'@repo/i18next': '@repo/i18next':
specifier: workspace:* specifier: workspace:*
version: link:../../packages/i18n version: link:../../packages/i18n
@ -611,8 +611,8 @@ importers:
specifier: ^7.7.0 specifier: ^7.7.0
version: 7.7.0(react@19.2.0)(styled-components@6.1.19(react-dom@19.2.0(react@19.2.0))(react@19.2.0)) version: 7.7.0(react@19.2.0)(styled-components@6.1.19(react-dom@19.2.0(react@19.2.0))(react@19.2.0))
react-hook-form: react-hook-form:
specifier: ^7.58.1 specifier: ^7.72.1
version: 7.66.0(react@19.2.0) version: 7.72.1(react@19.2.0)
react-i18next: react-i18next:
specifier: ^16.2.4 specifier: ^16.2.4
version: 16.2.4(i18next@25.6.0(typescript@5.9.3))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3) version: 16.2.4(i18next@25.6.0(typescript@5.9.3))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
@ -1003,7 +1003,7 @@ importers:
version: 5.2.7 version: 5.2.7
'@hookform/resolvers': '@hookform/resolvers':
specifier: ^5.2.2 specifier: ^5.2.2
version: 5.2.2(react-hook-form@7.66.0(react@19.2.0)) version: 5.2.2(react-hook-form@7.72.1(react@19.2.0))
add: add:
specifier: ^2.0.6 specifier: ^2.0.6
version: 2.0.6 version: 2.0.6
@ -1044,8 +1044,8 @@ importers:
specifier: 9.14.0 specifier: 9.14.0
version: 9.14.0(react@19.2.0) version: 9.14.0(react@19.2.0)
react-hook-form: react-hook-form:
specifier: ^7.65.0 specifier: ^7.72.1
version: 7.66.0(react@19.2.0) version: 7.72.1(react@19.2.0)
react-resizable-panels: react-resizable-panels:
specifier: ^4.8.0 specifier: ^4.8.0
version: 4.8.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) version: 4.8.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
@ -5991,8 +5991,8 @@ packages:
peerDependencies: peerDependencies:
react: '>=16.13.1' react: '>=16.13.1'
react-hook-form@7.66.0: react-hook-form@7.72.1:
resolution: {integrity: sha512-xXBqsWGKrY46ZqaHDo+ZUYiMUgi8suYu5kdrS20EG8KiL7VRQitEbNjm+UcrDYrNi1YLyfpmAeGjCZYXLT9YBw==} resolution: {integrity: sha512-RhwBoy2ygeVZje+C+bwJ8g0NjTdBmDlJvAUHTxRjTmSUKPYsKfMphkS2sgEMotsY03bP358yEYlnUeZy//D9Ig==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
peerDependencies: peerDependencies:
react: ^16.8.0 || ^17 || ^18 || ^19 react: ^16.8.0 || ^17 || ^18 || ^19
@ -7754,10 +7754,10 @@ snapshots:
- '@types/react' - '@types/react'
- supports-color - supports-color
'@hookform/resolvers@5.2.2(react-hook-form@7.66.0(react@19.2.0))': '@hookform/resolvers@5.2.2(react-hook-form@7.72.1(react@19.2.0))':
dependencies: dependencies:
'@standard-schema/utils': 0.3.0 '@standard-schema/utils': 0.3.0
react-hook-form: 7.66.0(react@19.2.0) react-hook-form: 7.72.1(react@19.2.0)
'@humanfs/core@0.19.1': {} '@humanfs/core@0.19.1': {}
@ -12249,7 +12249,7 @@ snapshots:
'@babel/runtime': 7.28.4 '@babel/runtime': 7.28.4
react: 19.2.0 react: 19.2.0
react-hook-form@7.66.0(react@19.2.0): react-hook-form@7.72.1(react@19.2.0):
dependencies: dependencies:
react: 19.2.0 react: 19.2.0