Facturas de cliente

This commit is contained in:
David Arranz 2025-10-14 19:57:02 +02:00
parent cb9c8e5061
commit acada25dfd
27 changed files with 335 additions and 883 deletions

View File

@ -71,9 +71,9 @@
"title": "Invoice details",
"description": ""
},
"basic_into": {
"basic_info": {
"title": "Invoice information",
"description": ""
"description": "Basic invoice information"
},
"totals": {
"title": "Invoice totals",

View File

@ -72,9 +72,9 @@
"title": "Detalles de la factura",
"description": ""
},
"basic_into": {
"basic_info": {
"title": "Información de la factura",
"description": ""
"description": "Información básica de la factura"
},
"totals": {
"title": "Totales de la factura",

View File

@ -25,30 +25,21 @@ export const CustomerInvoiceEditForm = ({
return (
<form noValidate id={formId} onSubmit={form.handleSubmit(onSubmit, onError)}>
<section className={cn("space-y-6", className)}>
<div className='w-full grid grid-cols-4'>
<div className="col-span-3">
<div className="w-full border p-6 bg-background">
<InvoiceBasicInfoFields className="flex flex-col" />
</div>
<div className='col-span-1'>
<InvoiceRecipient className="flex flex-col" />
</div>
</div>
<div className="mx-auto grid w-full grid-cols-1 grid-flow-col gap-6 lg:grid-cols-4 items-stretch">
<div className="lg:col-start-1 lg:col-span-3 h-full">
<InvoiceItems className="h-full flex flex-col" />
</div>
<div className='w-full grid grid-cols-1 lg:grid-cols-4 gap-6'>
<div className="h-full ">
<InvoiceItems className="col-start-1 lg:col-span-3 border p-6 bg-background -p-6" />
<InvoiceRecipient className='lg:col-span-1 border p-6 bg-background' />
</div>
<div className="w-full border p-6 bg-background">
<InvoiceTotals />
</div>
</div>
</section>
</form>
);

View File

@ -1,13 +1,8 @@
import {
DatePickerInputField,
Description,
Field,
FieldGroup,
Fieldset,
Legend,
TextField
} from "@repo/rdx-ui/components";
import { FileTextIcon } from "lucide-react";
import { FieldDescription, FieldGroup, FieldLegend, FieldSet } from '@repo/shadcn-ui/components';
import { ComponentProps } from 'react';
import { useFormContext } from "react-hook-form";
import { useTranslation } from "../../i18n";
@ -17,28 +12,16 @@ export const InvoiceBasicInfoFields = (props: ComponentProps<"fieldset">) => {
const { t } = useTranslation();
const { control } = useFormContext<InvoiceFormData>();
return (
<Fieldset {...props} className='border n'>
<Legend>
<FileTextIcon className='size-4 stroke-2' />{t("form_groups.basic_into.title")}
</Legend>
<FieldSet {...props}>
<FieldLegend className='hidden text-foreground' variant='label'>
{t("form_groups.basic_info.title")}
</FieldLegend>
<FieldDescription className='hidden'>{t("form_groups.basic_info.description")}</FieldDescription>
<Description>{t("form_groups.basic_into.description")}</Description>
<FieldGroup className='grid grid-cols-1'>
<Field>
<TextField
className='hidden'
control={control}
name='invoice_number'
readOnly
label={t("form_fields.invoice_number.label")}
placeholder={t("form_fields.invoice_number.placeholder")}
description={t("form_fields.invoice_number.description")}
/>
</Field>
<Field>
<FieldGroup className='flex flex-row flex-wrap gap-6 xl:flex-nowrap'>
<DatePickerInputField
className='min-w-44 flex-1 sm:max-w-44'
control={control}
name='invoice_date'
numberOfMonths={2}
@ -47,10 +30,9 @@ export const InvoiceBasicInfoFields = (props: ComponentProps<"fieldset">) => {
placeholder={t("form_fields.invoice_date.placeholder")}
description={t("form_fields.invoice_date.description")}
/>
</Field>
<Field>
<DatePickerInputField
className='min-w-44 flex-1 sm:max-w-44'
control={control}
numberOfMonths={2}
name='operation_date'
@ -58,24 +40,18 @@ export const InvoiceBasicInfoFields = (props: ComponentProps<"fieldset">) => {
placeholder={t("form_fields.operation_date.placeholder")}
description={t("form_fields.operation_date.description")}
/>
</Field>
<Field >
<TextField
typePreset='text'
className='min-w-16 flex-1 sm:max-w-16'
control={control}
name='series'
label={t("form_fields.series.label")}
placeholder={t("form_fields.series.placeholder")}
description={t("form_fields.series.description")}
/>
</Field>
<Field>
<TextField
typePreset='text'
className='min-w-32 flex-1 sm:max-w-44'
maxLength={256}
control={control}
name='reference'
@ -83,11 +59,9 @@ export const InvoiceBasicInfoFields = (props: ComponentProps<"fieldset">) => {
placeholder={t("form_fields.reference.placeholder")}
description={t("form_fields.reference.description")}
/>
</Field>
<Field>
<TextField
typePreset='text'
className='min-w-32 flex-1 xs:max-w-full'
maxLength={256}
control={control}
name='description'
@ -95,8 +69,7 @@ export const InvoiceBasicInfoFields = (props: ComponentProps<"fieldset">) => {
placeholder={t("form_fields.description.placeholder")}
description={t("form_fields.description.description")}
/>
</Field>
</FieldGroup>
</Fieldset>
</FieldSet>
);
};

View File

@ -1,25 +1,25 @@
import { Rows4Icon } from "lucide-react";
import { Description, FieldGroup, Fieldset, Legend } from '@repo/rdx-ui/components';
import { FieldDescription, FieldGroup, FieldLegend, FieldSet } from '@repo/shadcn-ui/components';
import { ComponentProps } from 'react';
import { useTranslation } from '../../i18n';
import { ItemsEditor } from "./items";
export const InvoiceItems = ({ className, ...props }: ComponentProps<"fieldset">) => {
export const InvoiceItems = (props: ComponentProps<"fieldset">) => {
const { t } = useTranslation();
return (
<Fieldset {...props}>
<Legend>
<Rows4Icon className='size-6 text-muted-foreground' />{t('form_groups.items.title')}
</Legend>
<FieldSet {...props}>
<FieldLegend className='hidden text-foreground' variant='label'>
{t('form_groups.items.title')}
</FieldLegend>
<FieldDescription className='hidden'>{t("form_groups.items.description")}</FieldDescription>
<Description>{t("form_groups.items.description")}</Description>
<FieldGroup className='grid grid-cols-1'>
<ItemsEditor />
</FieldGroup>
</Fieldset>
</FieldSet>
);
};

View File

@ -1,4 +1,5 @@
import { Description, FieldGroup, Fieldset, Legend, TextAreaField } from "@repo/rdx-ui/components";
import { TextAreaField } from "@repo/rdx-ui/components";
import { FieldDescription, FieldGroup, FieldLegend, FieldSet } from '@repo/shadcn-ui/components';
import { StickyNoteIcon } from "lucide-react";
import { ComponentProps } from 'react';
import { useFormContext } from "react-hook-form";
@ -10,12 +11,12 @@ export const InvoiceNotes = (props: ComponentProps<"fieldset">) => {
const { control } = useFormContext<InvoiceFormData>();
return (
<Fieldset {...props}>
<Legend>
<StickyNoteIcon className='size-6 text-muted-foreground' />{t("form_groups.basic_into.title")}
</Legend>
<FieldSet {...props}>
<FieldLegend>
<StickyNoteIcon className='size-6 text-muted-foreground' />{t("form_groups.basic_info.title")}
</FieldLegend>
<Description>{t("form_groups.basic_into.description")}</Description>
<FieldDescription>{t("form_groups.basic_info.description")}</FieldDescription>
<FieldGroup className='grid grid-cols-1 gap-x-6 h-full min-h-0'>
<TextAreaField
maxLength={1024}
@ -27,6 +28,6 @@ export const InvoiceNotes = (props: ComponentProps<"fieldset">) => {
description={t("form_fields.notes.description")}
/>
</FieldGroup>
</Fieldset>
</FieldSet>
);
};

View File

@ -1,6 +1,5 @@
import { formatCurrency } from '@erp/core';
import { Description, FieldGroup, Fieldset, Legend } from "@repo/rdx-ui/components";
import { Badge } from "@repo/shadcn-ui/components";
import { Badge, FieldDescription, FieldGroup, FieldLegend, FieldSet } from '@repo/shadcn-ui/components';
import { ReceiptIcon } from "lucide-react";
import { ComponentProps } from 'react';
import { useFormContext, useWatch } from "react-hook-form";
@ -23,12 +22,13 @@ export const InvoiceTaxSummary = (props: ComponentProps<"fieldset">) => {
const displayTaxes = taxes || [];
return (
<Fieldset {...props}>
<Legend className='flex items-center gap-2 text-foreground'>
<FieldGroup>
<FieldSet {...props}>
<FieldLegend className='flex items-center gap-2 text-foreground'>
<ReceiptIcon className='size-5' /> {t("form_groups.tax_resume.title")}
</Legend>
</FieldLegend>
<Description>{t("form_groups.tax_resume.description")}</Description>
<FieldDescription>{t("form_groups.tax_resume.description")}</FieldDescription>
<FieldGroup className='grid grid-cols-1'>
<div className='space-y-3'>
{displayTaxes.map((tax, index) => (
@ -62,6 +62,7 @@ export const InvoiceTaxSummary = (props: ComponentProps<"fieldset">) => {
)}
</div>
</FieldGroup>
</Fieldset>
</FieldSet>
</FieldGroup>
);
};

View File

@ -1,6 +1,5 @@
import { formatCurrency } from "@erp/core";
import { Description, FieldGroup, Fieldset, Legend } from "@repo/rdx-ui/components";
import { Separator } from "@repo/shadcn-ui/components";
import { FieldDescription, FieldGroup, FieldLegend, FieldSet, Separator } from '@repo/shadcn-ui/components';
import { ReceiptIcon } from "lucide-react";
import { ComponentProps } from "react";
import { useFormContext, useWatch } from "react-hook-form";
@ -22,12 +21,12 @@ export const InvoiceTotals = (props: ComponentProps<"fieldset">) => {
return (
<Fieldset {...props}>
<Legend>
<FieldSet {...props}>
<FieldLegend>
<ReceiptIcon className='size-6 text-muted-foreground' />{t("form_groups.totals.title")}
</Legend>
</FieldLegend>
<Description>{t("form_groups.totals.description")}</Description>
<FieldDescription>{t("form_groups.totals.description")}</FieldDescription>
<FieldGroup className='grid grid-cols-1'>
{/* Sección: Subtotal y Descuentos */}
<div className="space-y-1.5">
@ -145,6 +144,6 @@ export const InvoiceTotals = (props: ComponentProps<"fieldset">) => {
</div>
</div>
</FieldGroup>
</Fieldset >
</FieldSet >
);
};

View File

@ -1,7 +1,6 @@
import { Description, FieldGroup, Fieldset, Legend } from "@repo/rdx-ui/components";
import { FieldDescription, FieldGroup, FieldLegend, FieldSet } from '@repo/shadcn-ui/components';
import { useFormContext } from "react-hook-form";
import { UserIcon } from "lucide-react";
import { ComponentProps } from 'react';
import { useTranslation } from "../../../i18n";
import { RecipientModalSelectorField } from "./recipient-modal-selector-field";
@ -14,18 +13,18 @@ export const InvoiceRecipient = (props: ComponentProps<"fieldset">) => {
const recipient = getValues('recipient');
return (
<Fieldset {...props}>
<Legend className='flex items-center gap-2 text-foreground'>
<UserIcon className='size-5' /> {t("form_groups.customer.title")}
</Legend>
<Description>{t("form_groups.customer.description")}</Description>
<FieldGroup>
<FieldSet {...props}>
<FieldLegend className='hidden text-foreground' variant='label'>
{t('form_groups.recipient.title')}
</FieldLegend>
<FieldDescription className='hidden'>{t("form_groups.recipient.description")}</FieldDescription>
<FieldGroup className='grid grid-cols-1'>
<RecipientModalSelectorField
control={control}
name='customer_id'
initialRecipient={recipient}
/>
</FieldGroup>
</Fieldset>
</FieldSet>
);
};

View File

@ -28,7 +28,7 @@ export function PageHeader({ icon, title, description, status, rightSlot, classN
{icon && <div className='shrink-0'>{icon}</div>}
<div>
<div className='flex items-center gap-3'>
<h1 className='text-xl font-semibold text-foreground'>{title}</h1>
<h1 className='text-lg font-semibold text-foreground'>{title}</h1>
{status && <CustomerInvoiceStatusBadge status={status} />}
</div>
{description && <p className='text-sm text-muted-foreground'>{description}</p>}

View File

@ -3,7 +3,7 @@ import {
UnsavedChangesProvider,
useHookForm
} from "@erp/core/hooks";
import { AppContent, AppHeader } from "@repo/rdx-ui/components";
import { AppBreadcrumb, AppContent, AppHeader } from "@repo/rdx-ui/components";
import { showErrorToast, showSuccessToast } from "@repo/rdx-ui/helpers";
import { FilePenIcon } from "lucide-react";
import { useMemo } from 'react';
@ -86,9 +86,10 @@ export const InvoiceUpdateComp = ({
return (
<UnsavedChangesProvider isDirty={form.formState.isDirty}>
<AppHeader>
<AppBreadcrumb />
<PageHeader
title={`${t("pages.edit.title")} ${invoiceData.invoice_number}`}
icon={<FilePenIcon className='size-12 text-primary' aria-hidden />}
icon={<FilePenIcon className='size-6 text-primary' aria-hidden />}
rightSlot={
<FormCommitButtonGroup
isLoading={isPending}

View File

@ -1,4 +1,4 @@
import { Description, Field, FieldGroup, Fieldset, Legend } from "@repo/rdx-ui/components";
import { Field, FieldDescription, FieldGroup, FieldLegend, FieldSet } from '@repo/shadcn-ui/components';
import { SelectField } from "@repo/rdx-ui/components";
import { useFormContext } from "react-hook-form";
@ -11,9 +11,9 @@ export const CustomerAdditionalConfigFields = () => {
const { control } = useFormContext<CustomerFormData>();
return (
<Fieldset>
<Legend>{t("form_groups.preferences.title")}</Legend>
<Description>{t("form_groups.preferences.description")}</Description>
<FieldSet>
<FieldLegend>{t("form_groups.preferences.title")}</FieldLegend>
<FieldDescription>{t("form_groups.preferences.description")}</FieldDescription>
<FieldGroup className='grid grid-cols-1 gap-x-6 lg:grid-cols-4'>
<Field className='lg:col-span-2'>
<SelectField
@ -39,6 +39,6 @@ export const CustomerAdditionalConfigFields = () => {
/>
</Field>
</FieldGroup>
</Fieldset>
</FieldSet>
);
};

View File

@ -1,12 +1,8 @@
import {
Description,
Field,
FieldGroup,
Fieldset,
Legend,
SelectField,
TextField,
TextField
} from "@repo/rdx-ui/components";
import { Field, FieldDescription, FieldGroup, FieldLegend, FieldSet } from '@repo/shadcn-ui/components';
import { useFormContext } from "react-hook-form";
import { COUNTRY_OPTIONS } from "../../constants";
import { useTranslation } from "../../i18n";
@ -17,9 +13,9 @@ export const CustomerAddressFields = () => {
const { control } = useFormContext<CustomerFormData>();
return (
<Fieldset>
<Legend>{t("form_groups.address.title")}</Legend>
<Description>{t("form_groups.address.description")}</Description>
<FieldSet>
<FieldLegend>{t("form_groups.address.title")}</FieldLegend>
<FieldDescription>{t("form_groups.address.description")}</FieldDescription>
<FieldGroup className='grid grid-cols-1 gap-x-6 lg:grid-cols-4'>
<TextField
className='lg:col-span-2'
@ -75,6 +71,6 @@ export const CustomerAddressFields = () => {
/>
</Field>
</FieldGroup>
</Fieldset>
</FieldSet>
);
};

View File

@ -1,21 +1,16 @@
import {
Description,
Field,
FieldGroup,
Fieldset,
Legend,
TextAreaField,
TextField,
TextField
} from "@repo/rdx-ui/components";
import {
FormControl,
Field, FieldDescription, FieldGroup, FieldLegend, FieldSet, FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
RadioGroup,
RadioGroupItem,
} from "@repo/shadcn-ui/components";
RadioGroupItem
} from '@repo/shadcn-ui/components';
import { Controller, useFormContext, useWatch } from "react-hook-form";
import { CustomerInvoiceTaxesMultiSelect } from '../../../../../customer-invoices/src/web/components';
import { useTranslation } from "../../i18n";
@ -32,9 +27,9 @@ export const CustomerBasicInfoFields = () => {
});
return (
<Fieldset>
<Legend>{t("form_groups.basic_info.title")}</Legend>
<Description>{t("form_groups.basic_info.description")}</Description>
<FieldSet>
<FieldLegend>{t("form_groups.basic_info.title")}</FieldLegend>
<FieldDescription>{t("form_groups.basic_info.description")}</FieldDescription>
<FieldGroup className='grid grid-cols-1 gap-x-6 lg:grid-cols-4'>
<Field className='lg:col-span-2'>
<TextField
@ -131,6 +126,6 @@ export const CustomerBasicInfoFields = () => {
description={t("form_fields.legal_record.description")}
/>
</FieldGroup>
</Fieldset>
</FieldSet>
);
};

View File

@ -1,17 +1,11 @@
import {
Description,
Field,
FieldGroup,
Fieldset,
Legend,
TextField,
TextField
} from "@repo/rdx-ui/components";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
Separator,
} from "@repo/shadcn-ui/components";
CollapsibleTrigger, Field, FieldDescription, FieldGroup, FieldLegend, FieldSet, Separator
} from '@repo/shadcn-ui/components';
import { AtSignIcon, ChevronDown, GlobeIcon, PhoneIcon, SmartphoneIcon } from "lucide-react";
import { useState } from "react";
@ -24,9 +18,9 @@ export const CustomerContactFields = () => {
const { control } = useFormContext();
return (
<Fieldset>
<Legend>{t("form_groups.contact_info.title")}</Legend>
<Description>{t("form_groups.contact_info.description")}</Description>
<FieldSet>
<FieldLegend>{t("form_groups.contact_info.title")}</FieldLegend>
<FieldDescription>{t("form_groups.contact_info.description")}</FieldDescription>
<FieldGroup className='grid grid-cols-1 gap-x-6 lg:grid-cols-4'>
<TextField
className='lg:col-span-2'
@ -153,6 +147,6 @@ export const CustomerContactFields = () => {
</CollapsibleContent>
</Collapsible>
</FieldGroup>
</Fieldset>
</FieldSet>
);
};

View File

@ -1,116 +1,77 @@
// DatePickerField.tsx
import {
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
Field,
FieldDescription,
FieldError,
FieldLabel,
Textarea,
} from "@repo/shadcn-ui/components";
import { cn } from "@repo/shadcn-ui/lib/utils";
import { Control, FieldPath, FieldValues, useController } from "react-hook-form";
import { useTranslation } from "../../locales/i18n.ts";
import { cn } from '@repo/shadcn-ui/lib/utils';
import { Control, Controller, FieldPath, FieldValues, useFormState } from "react-hook-form";
import { CommonInputProps } from "./types.js";
type TextAreaFieldProps<TFormValues extends FieldValues> = CommonInputProps & {
control: Control<TFormValues>;
name: FieldPath<TFormValues>;
label?: string;
placeholder?: string;
description?: string;
disabled?: boolean;
required?: boolean;
readOnly?: boolean;
className?: string;
/** Contador de caracteres (si usas maxLength) */
showCounter?: boolean;
maxLength?: number;
rows?: number;
label?: string;
description?: string;
orientation?: "vertical" | "horizontal" | "responsive",
inputClassName?: string;
};
export function TextAreaField<TFormValues extends FieldValues>({
control,
name,
label,
placeholder,
description,
disabled = false,
required = false,
readOnly = false,
orientation = 'vertical',
className,
showCounter = false,
maxLength,
rows = 3
inputClassName,
...inputRest
}: TextAreaFieldProps<TFormValues>) {
const { t } = useTranslation();
const isDisabled = disabled || readOnly;
const { field, fieldState } = useController({ control, name });
const describedById = description ? `${name}-desc` : undefined;
const errorId = fieldState.error ? `${name}-err` : undefined;
const valueLength = (field.value?.length ?? 0) as number;
const { isSubmitting, isValidating } = useFormState({ control, name });
const disabled = isSubmitting || inputRest.disabled;
return (
<FormField
<Controller
control={control}
name={name}
render={({ field }) => (
<FormItem className={cn("space-y-0 flex flex-col ", className)}>
{label && (
<div className='mb-1 flex justify-between gap-2'>
<div className='flex items-center gap-2'>
<FormLabel
htmlFor={name}
className={cn("m-0", disabled ? "text-muted-foreground" : "")}
>
{label}
</FormLabel>
{required && (
<span className='text-xs text-destructive'>{t("common.required")}</span>
)}
</div>
{/* Punto “unsaved” */}
{fieldState.isDirty && (
<span className='text-[10px] text-muted-foreground'>{t("common.modified")}</span>
)}
</div>
)}
<FormControl>
render={({ field, fieldState }) => {
return (
<Field data-invalid={fieldState.invalid} orientation={orientation} className={className}>
{label && <FieldLabel className='text-xs text-muted-foreground text-nowrap' htmlFor={name}>{label}</FieldLabel>}
<Textarea
disabled={isDisabled}
placeholder={placeholder}
className={"placeholder:font-normal placeholder:italic bg-background flex flex-1 min-h-0 h-full"}
maxLength={maxLength}
spellCheck={true}
rows={rows}
{...field}
ref={field.ref}
id={name}
value={field.value ?? ""}
onChange={field.onChange}
onBlur={field.onBlur}
aria-invalid={fieldState.invalid}
aria-busy={isValidating}
{...inputRest}
disabled={disabled}
aria-disabled={disabled}
className={cn(inputClassName)}
/>
</FormControl>
<div className='mt-1 flex items-start justify-between'>
<FormDescription
id={describedById}
className={cn("text-xs truncate", !description && "invisible")}
>
{description || "\u00A0"}
</FormDescription>
{showCounter && typeof maxLength === "number" && (
<p className='text-xs text-muted-foreground'>
{valueLength} / {maxLength}
</p>
)}
</div>
<FormMessage id={errorId} />
</FormItem>
)}
{false && <FieldDescription className='text-xs'>{description || "\u00A0"}</FieldDescription>}
<FieldError errors={[fieldState.error]} />
</Field>
);
}}
/>
);
}

View File

@ -1,471 +1,83 @@
import {
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
Input,
Field,
FieldDescription,
FieldError,
FieldLabel,
Input
} from "@repo/shadcn-ui/components";
import { cn } from "@repo/shadcn-ui/lib/utils";
import { CheckIcon, Loader2Icon, XIcon } from "lucide-react";
import { Control, FieldPath, FieldValues, useController, useFormState } from "react-hook-form";
import { useTranslation } from "../../locales/i18n.ts";
import { Control, Controller, FieldPath, FieldValues, useFormState } from "react-hook-form";
import { CommonInputProps } from "./types.js";
/**
*
* // Email con normalización y contador
* <TextField
* name="email"
* label="Email"
* description="Usa tu email de trabajo"
* typePreset="email"
* placeholder="tú@empresa.com"
* icon={<Mail className="h-4 w-4" />}
* iconPosition="left"
* maxLength={120}
* showCounter
* clearable
* />
*
* // Teléfono con normalización (mantiene + y dígitos), prefix clicable
* <TextField
* name="mobile"
* label="Móvil"
* description="Incluye prefijo internacional"
* typePreset="phone"
* placeholder="+34 600 000 000"
* prefix={<span className="text-xs">+34</span>}
* onPrefixClick={() => {
* // Alternar prefijo, o abrir selector de país...
* }}
* clearable
* />
*
* // Número decimal con normalización (',' → '.'), suffix clicable para unidad
* <TextField
* name="price"
* label="Precio"
* description="Con IVA"
* typePreset="number"
* placeholder="0.00"
* suffix={<span className="text-xs">EUR</span>}
* onSuffixClick={() => {
* // Cambiar moneda, etc.
* }}
* inputMode="decimal" // si quieres sobreescribir
* icon={<Hash className="h-4 w-4" />}
* iconPosition="left"
* />
*
* // Password (sin normalizaciones)
* <TextField
* name="password"
* label="Contraseña"
* typePreset="password"
* placeholder="••••••••"
* autoComplete="new-password"
* />
*
*/
/** Presets de comportamiento */
type TextFieldTypePreset = "text" | "email" | "phone" | "number" | "password";
type Normalizer = (value: string) => string;
type TextFieldProps<TFormValues extends FieldValues> = CommonInputProps & {
control: Control<TFormValues>;
name: FieldPath<TFormValues>;
label?: string;
placeholder?: string;
description?: string;
required?: boolean;
readOnly?: boolean;
className?: string;
orientation?: "vertical" | "horizontal" | "responsive",
inputClassName?: string;
typePreset?: TextFieldTypePreset;
icon?: React.ReactNode; // Icono con tamaño: <MailIcon className="h-[18px] w-[18px]" />
iconPosition?: "left" | "right"; // 'left' por defecto
/** Addons laterales (pueden ser clicables, a diferencia de `icon`) */
prefix?: React.ReactNode;
suffix?: React.ReactNode;
onPrefixClick?: () => void;
onSuffixClick?: () => void;
/** UX extra */
clearable?: boolean;
submitOnEnter?: boolean;
disabledWhileSubmitting?: boolean;
showSuccessWhenValid?: boolean;
showValidatingSpinner?: boolean;
/** Transformaciones */
transformOnBlur?: (value: string) => string;
normalizeOnChange?: (value: string) => string;
/** Contador de caracteres (si usas maxLength) */
showCounter?: boolean;
/** Forzar type/inputMode/autocomplete si no quieres los del preset */
forceType?: React.HTMLInputTypeAttribute;
forceInputMode?: React.HTMLAttributes<HTMLInputElement>["inputMode"];
forceAutoComplete?: string;
};
/* ---------- Helpers de presets ---------- */
function presetInputType(p: TextFieldTypePreset): React.HTMLInputTypeAttribute {
switch (p) {
case "password":
return "password";
case "number":
return "text"; // usamos text + normalización para control fino (decimales, signos)
case "email":
return "email";
case "phone":
return "tel";
case "text":
return "text";
default:
return "text";
}
}
function presetInputMode(
p: TextFieldTypePreset
): React.HTMLAttributes<HTMLInputElement>["inputMode"] {
switch (p) {
case "phone":
return "tel";
case "number":
return "decimal";
case "email":
return "email";
default:
return undefined;
}
}
function presetAutoComplete(p: TextFieldTypePreset): string | undefined {
switch (p) {
case "email":
return "email";
case "phone":
return "tel";
case "password":
return "current-password";
default:
return undefined;
}
}
function presetTransformOnBlur(p: TextFieldTypePreset): ((v: string) => string) | undefined {
switch (p) {
case "email":
return (v) => v.trim().toLowerCase();
case "text":
return undefined;
case "password":
return undefined;
case "phone":
return undefined;
case "number":
return undefined;
default:
return undefined;
}
}
/** Normalizador “suave” para números: permite signo inicial y un solo separador decimal '.' */
function normalizeNumber(value: string): string {
// Sustituye comas por punto y elimina caracteres inválidos
let v = value.replace(/,/g, ".");
// Mantén solo dígitos, un punto y signo inicial
v = v
.replace(/[^\d.+-]/g, "")
.replace(/(?!^)-/g, "") // solo un signo al inicio
.replace(/(\..*)\./g, "$1"); // solo un punto
return v;
}
/** Normalizador para teléfonos: mantiene dígitos y '+' inicial */
function normalizePhone(value: string): string {
let v = value.replace(/[^\d+]/g, "");
v = v.replace(/(?!^)\+/g, ""); // '+' solo al inicio
return v;
}
function presetNormalizeOnChange(p: TextFieldTypePreset): Normalizer | undefined {
switch (p) {
case "phone":
return normalizePhone;
case "number":
return normalizeNumber;
default:
return undefined;
}
}
/* ---------- Componente ---------- */
export function TextField<TFormValues extends FieldValues>({
control,
name,
label,
description,
required,
readOnly,
required = false,
readOnly = false,
orientation = 'vertical',
className,
inputClassName,
typePreset = "text",
icon,
iconPosition = "left",
prefix,
suffix,
onPrefixClick,
onSuffixClick,
clearable = false,
submitOnEnter = false,
disabledWhileSubmitting = true,
showSuccessWhenValid = false,
showValidatingSpinner = true,
transformOnBlur,
normalizeOnChange,
showCounter = false,
forceType,
forceInputMode,
forceAutoComplete,
maxLength,
...rest
...inputRest
}: TextFieldProps<TFormValues>) {
const { t } = useTranslation();
const { isSubmitting, isValidating } = useFormState({ control, name });
const { field, fieldState } = useController({ control, name });
// Presets → defaults (permiten override por props explícitas)
const effectiveType = forceType ?? presetInputType(typePreset);
const effectiveInputMode = forceInputMode ?? presetInputMode(typePreset);
const effectiveAutoComplete = forceAutoComplete ?? presetAutoComplete(typePreset);
const effectiveTransformOnBlur = transformOnBlur ?? presetTransformOnBlur(typePreset);
const effectiveNormalizeOnChange = normalizeOnChange ?? presetNormalizeOnChange(typePreset);
const hasIcon = Boolean(icon);
const isLeftIcon = iconPosition === "left";
const hasPrefix = prefix != null;
const hasSuffix = suffix != null;
// padding a partir de adornos
const inputPadding = cn(
hasIcon && isLeftIcon && "pl-10",
hasIcon && !isLeftIcon && "pr-10",
hasPrefix && "pl-10",
hasSuffix && "pr-10"
);
const invalid = fieldState.invalid && (fieldState.isTouched || fieldState.isDirty);
const valid =
!fieldState.invalid &&
(fieldState.isTouched || fieldState.isDirty) &&
field.value != null &&
String(field.value).length > 0;
const disabled = (disabledWhileSubmitting && isSubmitting) || rest.disabled || readOnly;
const describedById = description ? `${name}-desc` : undefined;
const errorId = fieldState.error ? `${name}-err` : undefined;
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
const raw = e.target.value;
const next = effectiveNormalizeOnChange ? effectiveNormalizeOnChange(raw) : raw;
field.onChange(next);
}
function handleBlur(e: React.FocusEvent<HTMLInputElement>) {
if (effectiveTransformOnBlur) {
const next = effectiveTransformOnBlur(e.target.value ?? "");
if (next !== e.target.value) {
field.onChange(next);
}
}
field.onBlur();
}
const disabled = isSubmitting || inputRest.disabled;
function handleKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
if (submitOnEnter && e.key === "Enter") {
if (e.key === "Enter") {
const form = (e.currentTarget as HTMLInputElement).form;
if (form) form.requestSubmit();
}
}
function handleClear() {
field.onChange("");
}
const valueLength = (field.value?.length ?? 0) as number;
return (
<FormField
<Controller
control={control}
name={name}
render={({ field }) => (
<FormItem className={cn("space-y-0", className)}>
{label && (
<div className='mb-1 flex justify-between gap-2'>
<div className='flex items-center gap-2'>
<FormLabel
htmlFor={name}
className={cn("m-0", disabled ? "text-muted-foreground" : "")}
>
{label}
</FormLabel>
{required && (
<span className='text-xs text-destructive'>{t("common.required")}</span>
)}
</div>
{/* Punto “unsaved” */}
{fieldState.isDirty && (
<span className='text-[10px] text-muted-foreground'>{t("common.modified")}</span>
)}
</div>
)}
<FormControl>
<div className={cn("relative")}>
{/* Prefix clicable (si tiene onClick) */}
{hasPrefix && (
<button
type={onPrefixClick ? "button" : undefined}
onClick={onPrefixClick}
tabIndex={onPrefixClick ? 0 : -1}
className={cn(
"absolute left-2 top-1/2 -translate-y-1/2 text-muted-foreground",
!onPrefixClick && "pointer-events-none"
)}
aria-label='prefix'
>
{prefix}
</button>
)}
{/* Icono decorativo */}
{hasIcon && (
<span
aria-hidden='true'
className={cn(
"pointer-events-none absolute top-1/2 -translate-y-1/2",
isLeftIcon ? "left-3" : "right-3"
)}
>
{icon}
</span>
)}
render={({ field, fieldState }) => {
return (
<Field data-invalid={fieldState.invalid} orientation={orientation} className={className}>
{label && <FieldLabel className='text-xs text-muted-foreground text-nowrap' htmlFor={name}>{label}</FieldLabel>}
<Input
id={name}
type={effectiveType}
inputMode={effectiveInputMode}
autoComplete={effectiveAutoComplete}
placeholder={rest.placeholder}
value={field.value ?? ""}
onChange={handleChange}
onBlur={handleBlur}
onKeyDown={handleKeyDown}
ref={field.ref}
id={name}
value={field.value ?? ""}
onChange={field.onChange}
onBlur={field.onBlur}
onKeyDown={handleKeyDown}
aria-invalid={fieldState.invalid}
aria-busy={isValidating}
{...inputRest}
disabled={disabled}
readOnly={readOnly}
aria-invalid={invalid || undefined}
aria-describedby={cn(describedById, errorId)}
aria-errormessage={errorId}
aria-busy={(showValidatingSpinner && isValidating) || undefined}
maxLength={maxLength}
{...rest}
className={cn(
"placeholder:font-normal placeholder:italic bg-background",
inputPadding,
invalid && "border-destructive focus-visible:ring-destructive",
valid && showSuccessWhenValid && "border-green-500 focus-visible:ring-green-500",
// Si hay suffix interactivo y spinner/check, reserva padding derecho
(hasSuffix || showValidatingSpinner || valid) && "pr-10",
inputClassName
)}
aria-disabled={disabled}
className={cn(inputClassName)}
/>
{/* Suffix clicable */}
{hasSuffix && (
<button
type={onSuffixClick ? "button" : undefined}
onClick={onSuffixClick}
tabIndex={onSuffixClick ? 0 : -1}
className={cn(
"absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground",
!onSuffixClick && "pointer-events-none"
)}
aria-label='suffix'
>
{suffix}
</button>
)}
{/* Spinner de validación */}
{showValidatingSpinner && isValidating && (
<span className='absolute right-2 top-1/2 -translate-y-1/2'>
<Loader2Icon className='h-4 w-4 animate-spin' />
</span>
)}
{/* Check de válido */}
{showSuccessWhenValid && valid && !isValidating && !invalid && (
<span className='absolute right-2 top-1/2 -translate-y-1/2 text-green-600'>
<CheckIcon className='h-4 w-4' />
</span>
)}
{/* Botón clear */}
{clearable && !disabled && (field.value ?? "") !== "" && (
<button
type='button'
aria-label='Borrar'
onClick={handleClear}
className='absolute right-2 top-1/2 -translate-y-1/2 rounded p-1 hover:bg-muted'
>
<XIcon className='h-4 w-4' />
</button>
)}
</div>
</FormControl>
<div className='mt-1 flex items-start justify-between'>
<FormDescription
id={describedById}
className={cn("text-xs truncate", !description && "invisible")}
>
{description || "\u00A0"}
</FormDescription>
{showCounter && typeof maxLength === "number" && (
<p className='text-xs text-muted-foreground'>
{valueLength} / {maxLength}
</p>
)}
</div>
<FormMessage id={errorId} />
</FormItem>
)}
{false && <FieldDescription className='text-xs'>{description || "\u00A0"}</FieldDescription>}
<FieldError errors={[fieldState.error]} />
</Field>
);
}}
/>
);
}

View File

@ -8,51 +8,59 @@ import {
CardFooter,
CardHeader,
CardTitle,
Field,
FieldDescription,
FieldError,
FieldLabel,
FormControl,
FormDescription,
FormItem,
FormLabel,
FormMessage,
Input,
Popover,
PopoverContent,
PopoverTrigger,
PopoverTrigger
} from "@repo/shadcn-ui/components";
import { CalendarIcon, LockIcon, XIcon } from "lucide-react";
import { cn } from "@repo/shadcn-ui/lib/utils";
import { format, isValid, parse } from "date-fns";
import { useEffect, useState } from "react";
import { FieldValues } from "react-hook-form";
import { ControllerFieldState, ControllerRenderProps, FieldValues, Path, UseFormStateReturn } from "react-hook-form";
import { useTranslation } from "../../../locales/i18n.ts";
import { ControllerFieldState, ControllerRenderProps, UseFormStateReturn } from "react-hook-form";
export type SUICalendarProps = Omit<React.ComponentProps<
typeof Calendar>, "select" | "onSelect">
type DatePickerInputCompProps<TFormValues extends FieldValues> = SUICalendarProps & {
field: ControllerRenderProps<TFormValues>;
type DatePickerInputCompProps<TFormValues extends FieldValues = FieldValues> = SUICalendarProps & {
field: ControllerRenderProps<TFormValues, Path<TFormValues>>;
fieldState: ControllerFieldState;
formState: UseFormStateReturn<TFormValues>;
htmlFor: string,
displayDateFormat: string; // e.g. "dd/MM/yyyy"
parseDateFormat: string; // e.g. "yyyy/MM/dd"
label: string;
label?: string;
placeholder?: string;
description?: string;
disabled?: boolean;
required?: boolean;
readOnly?: boolean;
invalid?: boolean;
orientation?: "vertical" | "horizontal" | "responsive",
className?: string;
};
export function DatePickerInputComp<TFormValues extends FieldValues>({
export function DatePickerInputComp<TFormValues>({
field,
fieldState,
formState,
htmlFor,
parseDateFormat,
displayDateFormat,
@ -62,6 +70,10 @@ export function DatePickerInputComp<TFormValues extends FieldValues>({
disabled = false,
required = false,
readOnly = false,
invalid = false,
orientation = "vertical",
className,
...calendarProps
}: DatePickerInputCompProps<TFormValues>) {
@ -69,9 +81,6 @@ export function DatePickerInputComp<TFormValues extends FieldValues>({
const isDisabled = disabled;
const isReadOnly = readOnly && !disabled;
const describedById = description ? `${field.name}-desc` : undefined;
const errorId = fieldState.error ? `${field.name}-err` : undefined;
const [open, setOpen] = useState(false); // Popover
const [displayValue, setDisplayValue] = useState<string>("");
@ -120,30 +129,14 @@ export function DatePickerInputComp<TFormValues extends FieldValues>({
};
return (
<FormItem className={cn("space-y-0", className)}>
{label && (
<div className='mb-1 flex justify-between gap-2'>
<div className='flex items-center gap-2'>
<FormLabel
htmlFor={field.name}
className={cn("m-0", disabled ? "text-muted-foreground" : "")}
>
{label}
</FormLabel>
{required && <span className='text-xs text-destructive'>{t("common.required")}</span>}
</div>
{/* Punto “unsaved” */}
{fieldState.isDirty && (
<span className='text-[10px] text-muted-foreground'>{t("common.modified")}</span>
)}
</div>
)}
<Field data-invalid={invalid} orientation={orientation} className={className}>
{label && <FieldLabel className='text-xs text-muted-foreground text-nowrap' htmlFor={htmlFor}>{label}</FieldLabel>}
<Popover modal={true} open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<FormControl>
<div className='relative'>
<input
<Input
type='text'
value={displayValue}
onChange={(e) => handleDisplayValueChange(e.target.value)}
@ -250,16 +243,9 @@ export function DatePickerInputComp<TFormValues extends FieldValues>({
</p>
)}
<div className='mt-1 flex items-start justify-between'>
<FormDescription
id={describedById}
className={cn("text-xs truncate", !description && "invisible")}
>
{description || "\u00A0"}
</FormDescription>
</div>
{false && <FieldDescription className='text-xs'>{description || "\u00A0"}</FieldDescription>}
<FormMessage id={errorId} />
</FormItem>
<FieldError errors={[fieldState.error]} />
</Field>
);
}

View File

@ -1,31 +1,33 @@
import { FormField } from "@repo/shadcn-ui/components";
import { Control, FieldPath, FieldValues } from "react-hook-form";
import { Control, Controller, FieldPath, FieldValues } from "react-hook-form";
import { DatePickerInputComp, SUICalendarProps } from "./date-picker-input-comp.tsx";
type DatePickerInputFieldProps<TFormValues extends FieldValues> = SUICalendarProps & {
control: Control<TFormValues>;
name: FieldPath<TFormValues>;
label: string;
label?: string;
placeholder?: string;
description?: string;
disabled?: boolean;
required?: boolean;
readOnly?: boolean;
className?: string;
displayDateFormat?: string; // e.g. "dd/MM/yyyy"
parseDateFormat?: string; // e.g. "yyyy-MM-dd"
orientation?: "vertical" | "horizontal" | "responsive",
};
export function DatePickerInputField<TFormValues extends FieldValues>({
control,
name,
displayDateFormat = "dd-MM-yyyy",
displayDateFormat = "dd-MM-y1qyyy",
parseDateFormat = "yyyy-MM-dd",
...props
}: DatePickerInputFieldProps<TFormValues>) {
return (
<FormField
<Controller
control={control}
name={name}
render={({ field, fieldState, formState }) => (
@ -33,6 +35,9 @@ export function DatePickerInputField<TFormValues extends FieldValues>({
field={field}
fieldState={fieldState}
formState={formState}
htmlFor={name}
displayDateFormat={displayDateFormat}
parseDateFormat={parseDateFormat}
{...props}

View File

@ -1,57 +0,0 @@
import { cn } from "@repo/shadcn-ui/lib/utils";
import * as React from "react";
export const Fieldset = ({ className, children, ...props }: React.ComponentProps<"fieldset">) => (
<fieldset
data-slot='fieldset'
className={cn(
"bg-card rounded-xl p-6",
className
)}
{...props}
>
{children}
</fieldset>
);
export const FieldGroup = ({ className, children, ...props }: React.ComponentProps<"div">) => (
<div data-slot='control' className={cn("space-y-6", className)} {...props}>
{children}
</div>
);
export const Field = ({ className, children, ...props }: React.ComponentProps<"div">) => (
<div
data-slot='field'
className={cn(
"bg-transparent",
className
)}
{...props}
>
{children}
</div>
);
export const Legend = ({ className, children, ...props }: React.ComponentProps<"div">) => (
<div
data-slot='legend'
className={cn(
"text-sm flex items-center gap-2 text-muted-foreground font-medium",
className
)}
{...props}
>
{children}
</div>
);
export const Description = ({ className, children, ...props }: React.ComponentProps<"p">) => (
<p
data-slot='text'
className={cn("text-base text-muted-foreground", className)}
{...props}
>
{children}
</p>
);

View File

@ -1,6 +1,6 @@
export * from "./date-picker-input-field/index.ts";
export * from "./DatePickerField.tsx";
export * from "./fieldset.tsx";
export * from "./multi-select-field.tsx";
export * from "./SelectField.tsx";
export * from "./TextAreaField.tsx";

View File

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

View File

@ -21,16 +21,16 @@ export function TeamSwitcher({
teams,
}: {
teams: {
name: string;
logo: React.ElementType;
plan: string;
}[];
name: string
logo: React.ElementType
plan: string
}[]
}) {
const { isMobile } = useSidebar();
const [activeTeam, setActiveTeam] = React.useState(teams[0]);
const { isMobile } = useSidebar()
const [activeTeam, setActiveTeam] = React.useState(teams[0])
if (!activeTeam) {
return null;
return null
}
return (
@ -39,49 +39,51 @@ export function TeamSwitcher({
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuButton
size='lg'
className='data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground'
size="lg"
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
>
<div className='bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-8 items-center justify-center rounded-lg'>
<activeTeam.logo className='size-4' />
<div className="bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-8 items-center justify-center rounded-lg">
<activeTeam.logo className="size-4" />
</div>
<div className='grid flex-1 text-left text-sm leading-tight'>
<span className='truncate font-medium'>{activeTeam.name}</span>
<span className='truncate text-xs'>{activeTeam.plan}</span>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-medium">{activeTeam.name}</span>
<span className="truncate text-xs">{activeTeam.plan}</span>
</div>
<ChevronsUpDown className='ml-auto' />
<ChevronsUpDown className="ml-auto" />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent
className='w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg'
align='start'
className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
align="start"
side={isMobile ? "bottom" : "right"}
sideOffset={4}
>
<DropdownMenuLabel className='text-muted-foreground text-xs'>Teams</DropdownMenuLabel>
<DropdownMenuLabel className="text-muted-foreground text-xs">
Teams
</DropdownMenuLabel>
{teams.map((team, index) => (
<DropdownMenuItem
key={team.name}
onClick={() => setActiveTeam(team)}
className='gap-2 p-2'
className="gap-2 p-2"
>
<div className='flex size-6 items-center justify-center rounded-md border'>
<team.logo className='size-3.5 shrink-0' />
<div className="flex size-6 items-center justify-center rounded-md border">
<team.logo className="size-3.5 shrink-0" />
</div>
{team.name}
<DropdownMenuShortcut>{index + 1}</DropdownMenuShortcut>
</DropdownMenuItem>
))}
<DropdownMenuSeparator />
<DropdownMenuItem className='gap-2 p-2'>
<div className='flex size-6 items-center justify-center rounded-md border bg-transparent'>
<Plus className='size-4' />
<DropdownMenuItem className="gap-2 p-2">
<div className="flex size-6 items-center justify-center rounded-md border bg-transparent">
<Plus className="size-4" />
</div>
<div className='text-muted-foreground font-medium'>Add team</div>
<div className="text-muted-foreground font-medium">Add team</div>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
);
)
}

View File

@ -22,6 +22,12 @@
"no_results": "No se han encontrado resultados.",
"select_options": "Seleccionar opciones",
"select_all": "Seleccionar todo"
},
"date_picker_input_field": {
"invalid_date": "Fecha inválida",
"clear_date": "Limpiar fecha",
"today": "Hoy",
"close": "Cerrar"
}
}
}

View File

@ -1,6 +1,6 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { type VariantProps, cva } from "class-variance-authority"
import * as React from "react"
import { cn } from "@repo/shadcn-ui/lib/utils"
@ -35,7 +35,6 @@ const buttonVariants = cva(
},
}
)
function Button({
className,
variant,
@ -47,7 +46,6 @@ function Button({
asChild?: boolean
}) {
const Comp = asChild ? Slot : "button"
return (
<Comp
data-slot="button"
@ -56,5 +54,4 @@ function Button({
/>
)
}
export { Button, buttonVariants }

View File

@ -1,9 +1,9 @@
import { type VariantProps, cva } from "class-variance-authority"
import { useMemo } from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@repo/shadcn-ui/lib/utils"
import { Label } from "@repo/shadcn-ui/components/label"
import { Separator } from "@repo/shadcn-ui/components/separator"
import { cn } from "@repo/shadcn-ui/lib/utils"
function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) {
return (
@ -198,7 +198,7 @@ function FieldError({
return null
}
if (errors?.length == 1) {
if (errors?.length === 1) {
return errors[0]?.message
}
@ -206,7 +206,7 @@ function FieldError({
<ul className="ml-4 flex list-disc flex-col gap-1">
{errors.map(
(error, index) =>
error?.message && <li key={index}>{error.message}</li>
error?.message && <li key={`error-${index}`}>{error.message}</li>
)}
</ul>
)
@ -229,14 +229,10 @@ function FieldError({
}
export {
Field,
FieldLabel,
FieldDescription,
Field, FieldContent, FieldDescription,
FieldError,
FieldGroup,
FieldLegend,
FieldGroup, FieldLabel, FieldLegend,
FieldSeparator,
FieldSet,
FieldContent,
FieldTitle,
FieldSet, FieldTitle
}

View File

@ -81,12 +81,6 @@
--sidebar-border: oklch(0.9173 0.0067 286.2663);
--sidebar-ring: oklch(0.623 0.214 259.815);
--radius: 0.40rem;
--shadow-x: 0px;
--shadow-y: 1px;
--shadow-blur: 3px;
--shadow-spread: 0px;
--shadow-opacity: 0.1;
--shadow-color: oklch(0 0 0);
--shadow-2xs: 1px 1px 6px 0px hsl(0 0% 0% / 0.05);
--shadow-xs: 1px 1px 6px 0px hsl(0 0% 0% / 0.05);
--shadow-sm: 1px 1px 6px 0px hsl(0 0% 0% / 0.1), 1px 1px 2px -1px hsl(0 0% 0% / 0.1);