Mejorar la ayuda en los diálogos y cambiar checkbox por switch donde proceda.

This commit is contained in:
David Arranz 2026-10-09 18:14:21 +02:00
parent 6f83ceb0b4
commit e472b2ee8c
24 changed files with 249 additions and 216 deletions

View File

@ -15,11 +15,13 @@ paquetes genéricos.
- Card: bloques de información y estado. - Card: bloques de información y estado.
- Select: idioma y selección de sistemas, entornos, cuentas, personas y demás relaciones, con navegación por teclado. - Select: idioma y selección de sistemas, entornos, cuentas, personas y demás relaciones, con navegación por teclado.
- Input y Label: credenciales y datos de usuario. - Input y Label: credenciales y datos de usuario.
- Checkbox: estado activo de usuarios, compañías, personas y departamentos. - Checkbox: filtros de listado, selección múltiple y configuración de columnas.
- SwitchField: campos binarios de formularios, como estado activo, VPN, predeterminado o requerido.
- Table: listados de usuarios, compañías, personas y departamentos. - Table: listados de usuarios, compañías, personas y departamentos.
- Textarea: dirección opcional de las sedes, con varias líneas. - Textarea: dirección opcional de las sedes, con varias líneas.
- Dialog: primitiva de shadcn/ui, con foco y cierre por teclado. - Dialog: primitiva de shadcn/ui, con foco y cierre por teclado.
- RDX UI: FormDialog, FormActions, ContextHelp, SelectWithAction, EntityTable y StatisticCard. - RDX UI: FormDialog, FormActions, ContextHelp, SelectWithAction, SwitchField, EntityTable y
StatisticCard.
## Convención de navegación ## Convención de navegación
@ -86,6 +88,8 @@ Guías oficiales: [monorepos](https://ui.shadcn.com/docs/monorepo) e [instalaci
ContextHelp muestra una explicación breve y permite desplegar ejemplos y recomendaciones mediante teclado. EntityHelp aporta contenido específico en español e inglés para compañías, personas, departamentos, sedes, asignaciones y todas las secciones de cuentas y accesos. Los catálogos explican si la definición es global o exclusiva de la compañía. Los ejemplos son orientativos: nunca se convierten en valores del formulario. ContextHelp muestra una explicación breve y permite desplegar ejemplos y recomendaciones mediante teclado. EntityHelp aporta contenido específico en español e inglés para compañías, personas, departamentos, sedes, asignaciones y todas las secciones de cuentas y accesos. Los catálogos explican si la definición es global o exclusiva de la compañía. Los ejemplos son orientativos: nunca se convierten en valores del formulario.
La ayuda se presenta en una tarjeta con icono circular, título y explicación; debajo, una banda desplegable contiene los ejemplos y recomendaciones. ContextHelp admite `icon?: ReactNode` para sustituir el icono informativo predeterminado, por ejemplo `icon={<UsersIcon />}`. El icono es decorativo y hereda el tamaño y color del contenedor. Se conservan los tokens del tema, los textos aportados por cada módulo y el despliegue nativo por teclado.
El botón Ayuda de Cuentas y accesos y Catálogos informáticos permite consultar la explicación de la sección sin abrir un alta. La misma ayuda aparece dentro de los modales de alta y edición. FormDialog utiliza una variante amplia para personas y formularios con más campos; el número de campos no cambia la navegación a una página propia. El botón Ayuda de Cuentas y accesos y Catálogos informáticos permite consultar la explicación de la sección sin abrir un alta. La misma ayuda aparece dentro de los modales de alta y edición. FormDialog utiliza una variante amplia para personas y formularios con más campos; el número de campos no cambia la navegación a una página propia.
La ayuda de los formularios se coloca en su parte inferior, inmediatamente antes de FormActions. Este pie compartido reúne Guardar y Cancelar dentro del formulario, conservando el envío y el cierre accesible del diálogo. Los diálogos de consulta mantienen únicamente Cerrar. La ayuda de los formularios se coloca en su parte inferior, inmediatamente antes de FormActions. Este pie compartido reúne Guardar y Cancelar dentro del formulario, conservando el envío y el cierre accesible del diálogo. Los diálogos de consulta mantienen únicamente Cerrar.
@ -101,6 +105,18 @@ Su contrato recibe id, etiqueta, opciones, placeholder, valor controlado y callb
No conoce compañías, HTTP, formularios ni modales: esas responsabilidades pertenecen al consumidor. No contiene textos propios que deban traducirse. No conoce compañías, HTTP, formularios ni modales: esas responsabilidades pertenecen al consumidor. No contiene textos propios que deban traducirse.
## SwitchField: activación y desactivación
`SwitchField` vive en `@repo/rdx-ui/components/switch-field`. Compone el Switch de shadcn/ui
con su etiqueta accesible y recibe un valor controlado, `onCheckedChange`, `id`, texto y estado
deshabilitado. No conoce React Hook Form ni ningún DTO: el formulario aporta el valor y actualiza
su fuente de verdad.
Se usa para cambios binarios que expresan una propiedad o un estado de la entidad, incluidos
activo, requerido, contacto, VPN, predeterminado y relación simétrica. No sustituye Checkbox en
filtros ni en elecciones de varias opciones, donde la casilla comunica correctamente inclusión,
exclusión o selección múltiple.
### Formularios que permiten alta rápida ### Formularios que permiten alta rápida
Sistema permite crear un sistema propio de la compañía mediante un modal adicional. Entorno permite crear un entorno para el sistema activo seleccionado; su acción no aparece antes de elegir un sistema. Los campos fijos en edición no ofrecen alta rápida. Uso y otros valores cerrados tampoco ofrecen altas. Sistema permite crear un sistema propio de la compañía mediante un modal adicional. Entorno permite crear un entorno para el sistema activo seleccionado; su acción no aparece antes de elegir un sistema. Los campos fijos en edición no ofrecen alta rápida. Uso y otros valores cerrados tampoco ofrecen altas.

View File

@ -1,5 +1,6 @@
import { ContextHelp } from "@repo/rdx-ui/components/context-help"; import { ContextHelp } from "@repo/rdx-ui/components/context-help";
import { FormActions } from "@repo/rdx-ui/components/form-dialog"; import { FormActions } from "@repo/rdx-ui/components/form-dialog";
import { SwitchField } from "@repo/rdx-ui/components/switch-field";
import { Button } from "@repo/shadcn-ui/components/button"; import { Button } from "@repo/shadcn-ui/components/button";
import { Input } from "@repo/shadcn-ui/components/input"; import { Input } from "@repo/shadcn-ui/components/input";
import { Label } from "@repo/shadcn-ui/components/label"; import { Label } from "@repo/shadcn-ui/components/label";
@ -48,10 +49,12 @@ export function BrandForm(props: BrandFormProps) {
onSelect={selectLogo} onSelect={selectLogo}
/> />
{!quickCreate && ( {!quickCreate && (
<label className="flex items-center gap-2 text-sm"> <SwitchField
<input type="checkbox" {...form.register("active")} /> checked={form.watch("active")}
{t("brands.active")} id={`${id}-active`}
</label> label={t("brands.active")}
onCheckedChange={(active) => form.setValue("active", active, { shouldDirty: true })}
/>
)} )}
</fieldset> </fieldset>
{(mutation.error || form.formState.errors.name || form.formState.errors.active) && ( {(mutation.error || form.formState.errors.name || form.formState.errors.active) && (

View File

@ -1,4 +1,5 @@
import { FormActions } from "@repo/rdx-ui/components/form-dialog"; import { FormActions } from "@repo/rdx-ui/components/form-dialog";
import { SwitchField } from "@repo/rdx-ui/components/switch-field";
import { Button } from "@repo/shadcn-ui/components/button"; import { Button } from "@repo/shadcn-ui/components/button";
import { Input } from "@repo/shadcn-ui/components/input"; import { Input } from "@repo/shadcn-ui/components/input";
import { Label } from "@repo/shadcn-ui/components/label"; import { Label } from "@repo/shadcn-ui/components/label";
@ -33,20 +34,18 @@ export function CatalogRelationForm(props: CatalogRelationFormProps) {
}} }}
/> />
</div> </div>
<label className="flex items-center gap-2 text-sm"> <SwitchField
<input checked={symmetric}
checked={symmetric} disabled={!!item}
disabled={!!item} id={`${id}-symmetric`}
onChange={(event) => { label={t("symmetric")}
form.setValue("symmetric", event.target.checked); onCheckedChange={(isSymmetric) => {
if (event.target.checked) { form.setValue("symmetric", isSymmetric);
form.setValue("inverseName", form.getValues("name")); if (isSymmetric) {
} form.setValue("inverseName", form.getValues("name"));
}} }
type="checkbox" }}
/> />
{t("symmetric")}
</label>
{!symmetric && ( {!symmetric && (
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor={`${id}-inverse`}>{t("inverseName")}</Label> <Label htmlFor={`${id}-inverse`}>{t("inverseName")}</Label>

View File

@ -1,7 +1,7 @@
import { FormActions } from "@repo/rdx-ui/components/form-dialog"; import { FormActions } from "@repo/rdx-ui/components/form-dialog";
import { SelectWithAction } from "@repo/rdx-ui/components/select-with-action"; import { SelectWithAction } from "@repo/rdx-ui/components/select-with-action";
import { SwitchField } from "@repo/rdx-ui/components/switch-field";
import { Button } from "@repo/shadcn-ui/components/button"; import { Button } from "@repo/shadcn-ui/components/button";
import { Checkbox } from "@repo/shadcn-ui/components/checkbox";
import { Input } from "@repo/shadcn-ui/components/input"; import { Input } from "@repo/shadcn-ui/components/input";
import { Label } from "@repo/shadcn-ui/components/label"; import { Label } from "@repo/shadcn-ui/components/label";
import { Textarea } from "@repo/shadcn-ui/components/textarea"; import { Textarea } from "@repo/shadcn-ui/components/textarea";
@ -73,10 +73,19 @@ export function CatalogTemplateForm(props: CatalogTemplateFormProps) {
)} )}
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Checkbox <SwitchField
checked={binding.required} checked={binding.required}
disabled={mutation.isPending} disabled={mutation.isPending}
id={`${id}-required-${binding.fieldId}`} id={`${id}-required-${binding.fieldId}`}
label={
<>
{t("required")}
<span className="sr-only">
{": "}
{field?.values.name ?? ""}
</span>
</>
}
onCheckedChange={(checked) => onCheckedChange={(checked) =>
form.setValue( form.setValue(
"fields", "fields",
@ -89,13 +98,6 @@ export function CatalogTemplateForm(props: CatalogTemplateFormProps) {
) )
} }
/> />
<Label htmlFor={`${id}-required-${binding.fieldId}`}>
{t("required")}
<span className="sr-only">
{": "}
{field?.values.name ?? ""}
</span>
</Label>
<Button <Button
aria-label={t("remove", { name: field?.values.name ?? "" })} aria-label={t("remove", { name: field?.values.name ?? "" })}
disabled={mutation.isPending} disabled={mutation.isPending}

View File

@ -1,5 +1,6 @@
import { FormActions } from "@repo/rdx-ui/components/form-dialog"; import { FormActions } from "@repo/rdx-ui/components/form-dialog";
import { SelectWithAction } from "@repo/rdx-ui/components/select-with-action"; import { SelectWithAction } from "@repo/rdx-ui/components/select-with-action";
import { SwitchField } from "@repo/rdx-ui/components/switch-field";
import { Button } from "@repo/shadcn-ui/components/button"; import { Button } from "@repo/shadcn-ui/components/button";
import { Input } from "@repo/shadcn-ui/components/input"; import { Input } from "@repo/shadcn-ui/components/input";
import { Label } from "@repo/shadcn-ui/components/label"; import { Label } from "@repo/shadcn-ui/components/label";
@ -61,18 +62,19 @@ export function SupportForm(props: SupportFormProps) {
value={values.state} value={values.state}
/> />
</div> </div>
<label className="flex items-center gap-2 text-sm"> <SwitchField
<input type="checkbox" {...form.register("requiresVpn")} /> checked={values.requiresVpn}
{t("support.requiresVpn")} id={`${prefix}-requires-vpn`}
</label> label={t("support.requiresVpn")}
<label className="flex items-center gap-2 text-sm"> onCheckedChange={(requiresVpn) => form.setValue("requiresVpn", requiresVpn)}
<input />
disabled={values.state !== "available"} <SwitchField
type="checkbox" checked={values.preferred}
{...form.register("preferred")} disabled={values.state !== "available"}
/> id={`${prefix}-preferred`}
{t("support.preferred")} label={t("support.preferred")}
</label> onCheckedChange={(preferred) => form.setValue("preferred", preferred)}
/>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor={`${prefix}-notes`}>{t("support.notes")}</Label> <Label htmlFor={`${prefix}-notes`}>{t("support.notes")}</Label>
<textarea <textarea

View File

@ -1,4 +1,5 @@
import { FormActions } from "@repo/rdx-ui/components/form-dialog"; import { FormActions } from "@repo/rdx-ui/components/form-dialog";
import { SwitchField } from "@repo/rdx-ui/components/switch-field";
import { Button } from "@repo/shadcn-ui/components/button"; import { Button } from "@repo/shadcn-ui/components/button";
import { Input } from "@repo/shadcn-ui/components/input"; import { Input } from "@repo/shadcn-ui/components/input";
import { Label } from "@repo/shadcn-ui/components/label"; import { Label } from "@repo/shadcn-ui/components/label";
@ -28,10 +29,12 @@ export function SupportToolForm(props: SupportToolFormProps) {
{...form.register("name")} {...form.register("name")}
/> />
</div> </div>
<label className="flex items-center gap-2 text-sm"> <SwitchField
<input type="checkbox" {...form.register("active")} /> checked={form.watch("active")}
{t("support.active")} id={`${id}-active`}
</label> label={t("support.active")}
onCheckedChange={(active) => form.setValue("active", active, { shouldDirty: true })}
/>
</fieldset> </fieldset>
<SupportFeedback <SupportFeedback
error={mutation.error} error={mutation.error}

View File

@ -1,6 +1,7 @@
import { ContextHelp } from "@repo/rdx-ui/components/context-help"; import { ContextHelp } from "@repo/rdx-ui/components/context-help";
import { FormActions } from "@repo/rdx-ui/components/form-dialog"; import { FormActions } from "@repo/rdx-ui/components/form-dialog";
import { SelectWithAction } from "@repo/rdx-ui/components/select-with-action"; import { SelectWithAction } from "@repo/rdx-ui/components/select-with-action";
import { SwitchField } from "@repo/rdx-ui/components/switch-field";
import { Button } from "@repo/shadcn-ui/components/button"; import { Button } from "@repo/shadcn-ui/components/button";
import { Input } from "@repo/shadcn-ui/components/input"; import { Input } from "@repo/shadcn-ui/components/input";
import { Label } from "@repo/shadcn-ui/components/label"; import { Label } from "@repo/shadcn-ui/components/label";
@ -41,10 +42,12 @@ export function WorkstationForm(props: WorkstationFormProps) {
<Label htmlFor={prefix}>{t("name")}</Label> <Label htmlFor={prefix}>{t("name")}</Label>
<Input id={prefix} maxLength={100} required {...form.register("name")} /> <Input id={prefix} maxLength={100} required {...form.register("name")} />
</div> </div>
<label className="flex items-center gap-2 text-sm"> <SwitchField
<input type="checkbox" {...form.register("active")} /> checked={values.active}
{t("active")} id={`${prefix}-active`}
</label> label={t("active")}
onCheckedChange={(active) => form.setValue("active", active)}
/>
<div className="grid gap-4 sm:grid-cols-2"> <div className="grid gap-4 sm:grid-cols-2">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor={`${prefix}-site`}>{t("site")}</Label> <Label htmlFor={`${prefix}-site`}>{t("site")}</Label>

View File

@ -1,6 +1,6 @@
import type { SelectAction } from "@repo/rdx-ui/components/select-with-action"; import type { SelectAction } from "@repo/rdx-ui/components/select-with-action";
import { SelectWithAction } from "@repo/rdx-ui/components/select-with-action"; import { SelectWithAction } from "@repo/rdx-ui/components/select-with-action";
import { Checkbox } from "@repo/shadcn-ui/components/checkbox"; import { SwitchField } from "@repo/rdx-ui/components/switch-field";
import { Input } from "@repo/shadcn-ui/components/input"; import { Input } from "@repo/shadcn-ui/components/input";
import { Label } from "@repo/shadcn-ui/components/label"; import { Label } from "@repo/shadcn-ui/components/label";
import { Textarea } from "@repo/shadcn-ui/components/textarea"; import { Textarea } from "@repo/shadcn-ui/components/textarea";
@ -38,15 +38,13 @@ export function AccessFieldControl({
const { t } = useTranslation("access"); const { t } = useTranslation("access");
if (field.type === "boolean") { if (field.type === "boolean") {
return ( return (
<div className="flex items-center gap-2"> <SwitchField
<Checkbox checked={Boolean(value)}
checked={Boolean(value)} disabled={disabled}
disabled={disabled} id={id}
id={id} label={t(field.key)}
onCheckedChange={(checked) => form.setValue(field.key, checked === true)} onCheckedChange={(checked) => form.setValue(field.key, checked)}
/> />
<Label htmlFor={id}>{t(field.key)}</Label>
</div>
); );
} }
if (field.type === "permissions") { if (field.type === "permissions") {

View File

@ -1,7 +1,7 @@
import { FormActions } from "@repo/rdx-ui/components/form-dialog"; import { FormActions } from "@repo/rdx-ui/components/form-dialog";
import { SelectWithAction } from "@repo/rdx-ui/components/select-with-action"; import { SelectWithAction } from "@repo/rdx-ui/components/select-with-action";
import { SwitchField } from "@repo/rdx-ui/components/switch-field";
import { Button } from "@repo/shadcn-ui/components/button"; import { Button } from "@repo/shadcn-ui/components/button";
import { Checkbox } from "@repo/shadcn-ui/components/checkbox";
import { Input } from "@repo/shadcn-ui/components/input"; import { Input } from "@repo/shadcn-ui/components/input";
import { Label } from "@repo/shadcn-ui/components/label"; import { Label } from "@repo/shadcn-ui/components/label";
import { useId } from "react"; import { useId } from "react";
@ -70,17 +70,13 @@ export function AssignmentForm(props: AssignmentFormProps) {
/> />
</div> </div>
</div> </div>
<div className="flex items-center gap-2"> <SwitchField
<Checkbox checked={!form.watch("endDate") && form.watch("isPrimary")}
checked={!form.watch("endDate") && form.watch("isPrimary")} disabled={!!form.watch("endDate")}
disabled={!!form.watch("endDate")} id={`${id}-primary`}
id={`${id}-primary`} label={t("primaryDepartment")}
onCheckedChange={(checked) => onCheckedChange={(checked) => form.setValue("isPrimary", checked, { shouldDirty: true })}
form.setValue("isPrimary", checked === true, { shouldDirty: true }) />
}
/>
<Label htmlFor={`${id}-primary`}>{t("primaryDepartment")}</Label>
</div>
<p className="text-xs text-muted-foreground">{t("assignmentHelp")}</p> <p className="text-xs text-muted-foreground">{t("assignmentHelp")}</p>
<CompanyFeedback <CompanyFeedback
error={mutation.error} error={mutation.error}

View File

@ -1,6 +1,6 @@
import { FormActions } from "@repo/rdx-ui/components/form-dialog"; import { FormActions } from "@repo/rdx-ui/components/form-dialog";
import { SwitchField } from "@repo/rdx-ui/components/switch-field";
import { Button } from "@repo/shadcn-ui/components/button"; import { Button } from "@repo/shadcn-ui/components/button";
import { Checkbox } from "@repo/shadcn-ui/components/checkbox";
import { Input } from "@repo/shadcn-ui/components/input"; import { Input } from "@repo/shadcn-ui/components/input";
import { Label } from "@repo/shadcn-ui/components/label"; import { Label } from "@repo/shadcn-ui/components/label";
import { useId } from "react"; import { useId } from "react";
@ -28,16 +28,12 @@ export function CompanyForm(props: CompanyFormProps) {
<Label htmlFor={`${id}-name`}>{t("name")}</Label> <Label htmlFor={`${id}-name`}>{t("name")}</Label>
<Input id={`${id}-name`} maxLength={100} required {...form.register("name")} /> <Input id={`${id}-name`} maxLength={100} required {...form.register("name")} />
</div> </div>
<div className="flex items-center gap-2"> <SwitchField
<Checkbox checked={form.watch("active")}
checked={form.watch("active")} id={`${id}-active`}
id={`${id}-active`} label={t("activeCompany")}
onCheckedChange={(checked) => onCheckedChange={(checked) => form.setValue("active", checked, { shouldDirty: true })}
form.setValue("active", checked === true, { shouldDirty: true }) />
}
/>
<Label htmlFor={`${id}-active`}>{t("activeCompany")}</Label>
</div>
<CompanyLogoField <CompanyLogoField
companyId={savedCompanyId} companyId={savedCompanyId}
disabled={mutation.isPending} disabled={mutation.isPending}

View File

@ -1,6 +1,6 @@
import { FormActions } from "@repo/rdx-ui/components/form-dialog"; import { FormActions } from "@repo/rdx-ui/components/form-dialog";
import { SwitchField } from "@repo/rdx-ui/components/switch-field";
import { Button } from "@repo/shadcn-ui/components/button"; import { Button } from "@repo/shadcn-ui/components/button";
import { Checkbox } from "@repo/shadcn-ui/components/checkbox";
import { Input } from "@repo/shadcn-ui/components/input"; import { Input } from "@repo/shadcn-ui/components/input";
import { Label } from "@repo/shadcn-ui/components/label"; import { Label } from "@repo/shadcn-ui/components/label";
import { useId } from "react"; import { useId } from "react";
@ -22,17 +22,13 @@ export function DepartmentForm(props: DepartmentFormProps) {
<Label htmlFor={id}>{t("name")}</Label> <Label htmlFor={id}>{t("name")}</Label>
<Input id={id} maxLength={100} required {...form.register("name")} /> <Input id={id} maxLength={100} required {...form.register("name")} />
</div> </div>
<div className="flex items-center gap-2"> <SwitchField
<Checkbox checked={form.watch("active")}
checked={form.watch("active")} disabled={requireActive}
disabled={requireActive} id={`${id}-active`}
id={`${id}-active`} label={t("activeDepartment")}
onCheckedChange={(checked) => onCheckedChange={(checked) => form.setValue("active", checked, { shouldDirty: true })}
form.setValue("active", checked === true, { shouldDirty: true }) />
}
/>
<Label htmlFor={`${id}-active`}>{t("activeDepartment")}</Label>
</div>
{department && ( {department && (
<p className="text-sm text-muted-foreground">{t("departmentDeactivationHelp")}</p> <p className="text-sm text-muted-foreground">{t("departmentDeactivationHelp")}</p>
)} )}

View File

@ -1,6 +1,6 @@
import { FormActions } from "@repo/rdx-ui/components/form-dialog"; import { FormActions } from "@repo/rdx-ui/components/form-dialog";
import { SwitchField } from "@repo/rdx-ui/components/switch-field";
import { Button } from "@repo/shadcn-ui/components/button"; import { Button } from "@repo/shadcn-ui/components/button";
import { Checkbox } from "@repo/shadcn-ui/components/checkbox";
import { Input } from "@repo/shadcn-ui/components/input"; import { Input } from "@repo/shadcn-ui/components/input";
import { Label } from "@repo/shadcn-ui/components/label"; import { Label } from "@repo/shadcn-ui/components/label";
import { Textarea } from "@repo/shadcn-ui/components/textarea"; import { Textarea } from "@repo/shadcn-ui/components/textarea";
@ -35,16 +35,12 @@ export function InterventionTemplateForm(props: InterventionTemplateFormProps) {
{...form.register("description")} {...form.register("description")}
/> />
</div> </div>
<div className="flex items-center gap-2"> <SwitchField
<Checkbox checked={form.watch("active")}
checked={form.watch("active")} id={`${prefix}-active`}
id={`${prefix}-active`} label={t("active")}
onCheckedChange={(value) => onCheckedChange={(active) => form.setValue("active", active, { shouldDirty: true })}
form.setValue("active", value === true, { shouldDirty: true }) />
}
/>
<Label htmlFor={`${prefix}-active`}>{t("active")}</Label>
</div>
<h3 className="font-medium">{t("interventionTemplates.steps")}</h3> <h3 className="font-medium">{t("interventionTemplates.steps")}</h3>
{steps.fields.map((step, index) => ( {steps.fields.map((step, index) => (
<TemplateStepFields <TemplateStepFields

View File

@ -1,5 +1,5 @@
import { SwitchField } from "@repo/rdx-ui/components/switch-field";
import { Button } from "@repo/shadcn-ui/components/button"; import { Button } from "@repo/shadcn-ui/components/button";
import { Checkbox } from "@repo/shadcn-ui/components/checkbox";
import { Input } from "@repo/shadcn-ui/components/input"; import { Input } from "@repo/shadcn-ui/components/input";
import { Label } from "@repo/shadcn-ui/components/label"; import { Label } from "@repo/shadcn-ui/components/label";
import { Textarea } from "@repo/shadcn-ui/components/textarea"; import { Textarea } from "@repo/shadcn-ui/components/textarea";
@ -73,16 +73,14 @@ export function TemplateStepFields({
{...form.register(`steps.${index}.instructions`)} {...form.register(`steps.${index}.instructions`)}
/> />
</div> </div>
<div className="flex gap-2 items-center"> <SwitchField
<Checkbox checked={form.watch(`steps.${index}.required`)}
checked={form.watch(`steps.${index}.required`)} id={`${id}-required`}
id={`${id}-required`} label={t("interventionTemplates.required")}
onCheckedChange={(value) => onCheckedChange={(required) =>
form.setValue(`steps.${index}.required`, value === true, { shouldDirty: true }) form.setValue(`steps.${index}.required`, required, { shouldDirty: true })
} }
/> />
<Label htmlFor={`${id}-required`}>{t("interventionTemplates.required")}</Label>
</div>
</section> </section>
); );
} }

View File

@ -1,7 +1,7 @@
import { UniqueID } from "@repo/rdx-ddd"; import { UniqueID } from "@repo/rdx-ddd";
import { SelectWithAction } from "@repo/rdx-ui/components/select-with-action"; import { SelectWithAction } from "@repo/rdx-ui/components/select-with-action";
import { SwitchField } from "@repo/rdx-ui/components/switch-field";
import { Button } from "@repo/shadcn-ui/components/button"; import { Button } from "@repo/shadcn-ui/components/button";
import { Checkbox } from "@repo/shadcn-ui/components/checkbox";
import { Input } from "@repo/shadcn-ui/components/input"; import { Input } from "@repo/shadcn-ui/components/input";
import { Label } from "@repo/shadcn-ui/components/label"; import { Label } from "@repo/shadcn-ui/components/label";
import { Textarea } from "@repo/shadcn-ui/components/textarea"; import { Textarea } from "@repo/shadcn-ui/components/textarea";
@ -87,16 +87,12 @@ export function InterventionSteps({
{...form.register(`steps.${index}.instructions`)} {...form.register(`steps.${index}.instructions`)}
/> />
</details> </details>
<div className="flex gap-2 items-center"> <SwitchField
<Checkbox checked={form.watch(`steps.${index}.required`)}
checked={form.watch(`steps.${index}.required`)} id={`${id}-required`}
id={`${id}-required`} label={t("interventionTemplates.required")}
onCheckedChange={(value) => onCheckedChange={(required) => form.setValue(`steps.${index}.required`, required)}
form.setValue(`steps.${index}.required`, value === true) />
}
/>
<Label htmlFor={`${id}-required`}>{t("interventionTemplates.required")}</Label>
</div>
<SelectWithAction <SelectWithAction
id={`${id}-state`} id={`${id}-state`}
label={t("interventions.state")} label={t("interventions.state")}

View File

@ -1,6 +1,6 @@
import { FormActions } from "@repo/rdx-ui/components/form-dialog"; import { FormActions } from "@repo/rdx-ui/components/form-dialog";
import { SwitchField } from "@repo/rdx-ui/components/switch-field";
import { Button } from "@repo/shadcn-ui/components/button"; import { Button } from "@repo/shadcn-ui/components/button";
import { Checkbox } from "@repo/shadcn-ui/components/checkbox";
import { Input } from "@repo/shadcn-ui/components/input"; import { Input } from "@repo/shadcn-ui/components/input";
import { Label } from "@repo/shadcn-ui/components/label"; import { Label } from "@repo/shadcn-ui/components/label";
import { useId } from "react"; import { useId } from "react";
@ -36,27 +36,21 @@ export function PersonForm(props: PersonFormProps) {
<Input id={`${id}-phone`} maxLength={50} type="tel" {...form.register("phone")} /> <Input id={`${id}-phone`} maxLength={50} type="tel" {...form.register("phone")} />
</div> </div>
</div> </div>
<div className="flex items-center gap-2"> <SwitchField
<Checkbox checked={form.watch("active")}
checked={form.watch("active")} disabled={requireActive}
disabled={requireActive} id={`${id}-active`}
id={`${id}-active`} label={t("activePerson")}
onCheckedChange={(checked) => onCheckedChange={(active) => form.setValue("active", active, { shouldDirty: true })}
form.setValue("active", checked === true, { shouldDirty: true }) />
} <SwitchField
/> checked={form.watch("isContact")}
<Label htmlFor={`${id}-active`}>{t("activePerson")}</Label> id={`${id}-contact`}
</div> label={t("isContact")}
<div className="flex items-center gap-2"> onCheckedChange={(isContact) =>
<Checkbox form.setValue("isContact", isContact, { shouldDirty: true })
checked={form.watch("isContact")} }
id={`${id}-contact`} />
onCheckedChange={(checked) =>
form.setValue("isContact", checked === true, { shouldDirty: true })
}
/>
<Label htmlFor={`${id}-contact`}>{t("isContact")}</Label>
</div>
{person?.active && !form.watch("active") && ( {person?.active && !form.watch("active") && (
<p className="text-sm text-muted-foreground">{t("deactivationHelp")}</p> <p className="text-sm text-muted-foreground">{t("deactivationHelp")}</p>
)} )}

View File

@ -1,6 +1,7 @@
import { ContextHelp } from "@repo/rdx-ui/components/context-help"; import { ContextHelp } from "@repo/rdx-ui/components/context-help";
import { FormActions } from "@repo/rdx-ui/components/form-dialog"; import { FormActions } from "@repo/rdx-ui/components/form-dialog";
import { SelectWithAction } from "@repo/rdx-ui/components/select-with-action"; import { SelectWithAction } from "@repo/rdx-ui/components/select-with-action";
import { SwitchField } from "@repo/rdx-ui/components/switch-field";
import { Button } from "@repo/shadcn-ui/components/button"; import { Button } from "@repo/shadcn-ui/components/button";
import { Label } from "@repo/shadcn-ui/components/label"; import { Label } from "@repo/shadcn-ui/components/label";
import { Textarea } from "@repo/shadcn-ui/components/textarea"; import { Textarea } from "@repo/shadcn-ui/components/textarea";
@ -125,14 +126,13 @@ export function PrinterConfigurationForm(props: PrinterConfigurationFormProps) {
/> />
</div> </div>
</div> </div>
<label className="flex items-center gap-2 text-sm"> <SwitchField
<input checked={values.isDefault}
type="checkbox" disabled={!["pending", "configured"].includes(values.state)}
{...form.register("isDefault")} id={`${prefix}-default`}
disabled={!["pending", "configured"].includes(values.state)} label={t("printers.default")}
/> onCheckedChange={(isDefault) => form.setValue("isDefault", isDefault)}
{t("printers.default")} />
</label>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor={`${prefix}-notes`}>{t("mail.notes")}</Label> <Label htmlFor={`${prefix}-notes`}>{t("mail.notes")}</Label>
<Textarea <Textarea

View File

@ -1,7 +1,7 @@
import { FormActions } from "@repo/rdx-ui/components/form-dialog"; import { FormActions } from "@repo/rdx-ui/components/form-dialog";
import { SelectWithAction } from "@repo/rdx-ui/components/select-with-action"; import { SelectWithAction } from "@repo/rdx-ui/components/select-with-action";
import { SwitchField } from "@repo/rdx-ui/components/switch-field";
import { Button } from "@repo/shadcn-ui/components/button"; import { Button } from "@repo/shadcn-ui/components/button";
import { Checkbox } from "@repo/shadcn-ui/components/checkbox";
import { Input } from "@repo/shadcn-ui/components/input"; import { Input } from "@repo/shadcn-ui/components/input";
import { Label } from "@repo/shadcn-ui/components/label"; import { Label } from "@repo/shadcn-ui/components/label";
import { Textarea } from "@repo/shadcn-ui/components/textarea"; import { Textarea } from "@repo/shadcn-ui/components/textarea";
@ -54,14 +54,12 @@ export function ProcessTemplateForm(props: ProcessTemplateFormProps) {
/> />
<p className="text-sm text-muted-foreground">{t("processes.instructionsHelp")}</p> <p className="text-sm text-muted-foreground">{t("processes.instructionsHelp")}</p>
</div> </div>
<div className="flex items-center gap-2"> <SwitchField
<Checkbox checked={form.watch("active")}
checked={form.watch("active")} id={`${prefix}-active`}
id={`${prefix}-active`} label={t("active")}
onCheckedChange={(v) => form.setValue("active", v === true)} onCheckedChange={(active) => form.setValue("active", active)}
/> />
<Label htmlFor={`${prefix}-active`}>{t("active")}</Label>
</div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor={`${prefix}-notes`}>{t("mail.notes")}</Label> <Label htmlFor={`${prefix}-notes`}>{t("mail.notes")}</Label>
<Textarea id={`${prefix}-notes`} maxLength={4000} rows={3} {...form.register("notes")} /> <Textarea id={`${prefix}-notes`} maxLength={4000} rows={3} {...form.register("notes")} />

View File

@ -1,8 +1,8 @@
import { ContextHelp } from "@repo/rdx-ui/components/context-help"; import { ContextHelp } from "@repo/rdx-ui/components/context-help";
import { FormActions } from "@repo/rdx-ui/components/form-dialog"; import { FormActions } from "@repo/rdx-ui/components/form-dialog";
import { SelectWithAction } from "@repo/rdx-ui/components/select-with-action"; import { SelectWithAction } from "@repo/rdx-ui/components/select-with-action";
import { SwitchField } from "@repo/rdx-ui/components/switch-field";
import { Button } from "@repo/shadcn-ui/components/button"; import { Button } from "@repo/shadcn-ui/components/button";
import { Checkbox } from "@repo/shadcn-ui/components/checkbox";
import { Input } from "@repo/shadcn-ui/components/input"; import { Input } from "@repo/shadcn-ui/components/input";
import { Label } from "@repo/shadcn-ui/components/label"; import { Label } from "@repo/shadcn-ui/components/label";
import { Textarea } from "@repo/shadcn-ui/components/textarea"; import { Textarea } from "@repo/shadcn-ui/components/textarea";
@ -104,14 +104,12 @@ export function RemoteConnectionForm(props: RemoteConnectionFormProps) {
/> />
</div> </div>
</div> </div>
<div className="flex items-center gap-2"> <SwitchField
<Checkbox checked={v.requiresVpn}
checked={v.requiresVpn} id={`${prefix}-vpn`}
id={`${prefix}-vpn`} label={t("remote.requiresVpn")}
onCheckedChange={(checked) => form.setValue("requiresVpn", checked === true)} onCheckedChange={(requiresVpn) => form.setValue("requiresVpn", requiresVpn)}
/> />
<Label htmlFor={`${prefix}-vpn`}>{t("remote.requiresVpn")}</Label>
</div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor={`${prefix}-notes`}>{t("mail.notes")}</Label> <Label htmlFor={`${prefix}-notes`}>{t("mail.notes")}</Label>
<Textarea <Textarea

View File

@ -1,8 +1,8 @@
import { ContextHelp } from "@repo/rdx-ui/components/context-help"; import { ContextHelp } from "@repo/rdx-ui/components/context-help";
import { FormActions } from "@repo/rdx-ui/components/form-dialog"; import { FormActions } from "@repo/rdx-ui/components/form-dialog";
import { SelectWithAction } from "@repo/rdx-ui/components/select-with-action"; import { SelectWithAction } from "@repo/rdx-ui/components/select-with-action";
import { SwitchField } from "@repo/rdx-ui/components/switch-field";
import { Button } from "@repo/shadcn-ui/components/button"; import { Button } from "@repo/shadcn-ui/components/button";
import { Checkbox } from "@repo/shadcn-ui/components/checkbox";
import { Input } from "@repo/shadcn-ui/components/input"; import { Input } from "@repo/shadcn-ui/components/input";
import { Label } from "@repo/shadcn-ui/components/label"; import { Label } from "@repo/shadcn-ui/components/label";
import { Textarea } from "@repo/shadcn-ui/components/textarea"; import { Textarea } from "@repo/shadcn-ui/components/textarea";
@ -44,14 +44,12 @@ export function TeleworkAuthorizationForm(props: TeleworkAuthorizationFormProps)
value={values.personId} value={values.personId}
/> />
</div> </div>
<div className="flex items-center gap-2"> <SwitchField
<Checkbox checked={values.active}
checked={values.active} id={`${prefix}-active`}
id={`${prefix}-active`} label={t("telework.active")}
onCheckedChange={(checked) => form.setValue("active", checked === true)} onCheckedChange={(active) => form.setValue("active", active)}
/> />
<Label htmlFor={`${prefix}-active`}>{t("telework.active")}</Label>
</div>
<div className="grid gap-4 sm:grid-cols-2"> <div className="grid gap-4 sm:grid-cols-2">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor={`${prefix}-start`}>{t("telework.startsOn")}</Label> <Label htmlFor={`${prefix}-start`}>{t("telework.startsOn")}</Label>

View File

@ -1,6 +1,7 @@
import { ContextHelp } from "@repo/rdx-ui/components/context-help"; import { ContextHelp } from "@repo/rdx-ui/components/context-help";
import { FormActions } from "@repo/rdx-ui/components/form-dialog"; import { FormActions } from "@repo/rdx-ui/components/form-dialog";
import { SelectWithAction } from "@repo/rdx-ui/components/select-with-action"; import { SelectWithAction } from "@repo/rdx-ui/components/select-with-action";
import { SwitchField } from "@repo/rdx-ui/components/switch-field";
import { Button } from "@repo/shadcn-ui/components/button"; import { Button } from "@repo/shadcn-ui/components/button";
import { Input } from "@repo/shadcn-ui/components/input"; import { Input } from "@repo/shadcn-ui/components/input";
import { Label } from "@repo/shadcn-ui/components/label"; import { Label } from "@repo/shadcn-ui/components/label";
@ -86,10 +87,12 @@ export function WorkProfileForm(props: WorkProfileFormProps) {
</Button> </Button>
</div> </div>
)} )}
<label className="flex items-center gap-2 text-sm"> <SwitchField
<input type="checkbox" {...form.register("active")} /> checked={values.active}
{t("profiles.active")} id={`${prefix}-active`}
</label> label={t("profiles.active")}
onCheckedChange={(active) => form.setValue("active", active)}
/>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor={`${prefix}-notes`}>{t("profiles.notes")}</Label> <Label htmlFor={`${prefix}-notes`}>{t("profiles.notes")}</Label>
<Textarea <Textarea

View File

@ -1,6 +1,6 @@
import { FormActions } from "@repo/rdx-ui/components/form-dialog"; import { FormActions } from "@repo/rdx-ui/components/form-dialog";
import { SwitchField } from "@repo/rdx-ui/components/switch-field";
import { Button } from "@repo/shadcn-ui/components/button"; import { Button } from "@repo/shadcn-ui/components/button";
import { Checkbox } from "@repo/shadcn-ui/components/checkbox";
import { Input } from "@repo/shadcn-ui/components/input"; import { Input } from "@repo/shadcn-ui/components/input";
import { Label } from "@repo/shadcn-ui/components/label"; import { Label } from "@repo/shadcn-ui/components/label";
import { useId } from "react"; import { useId } from "react";
@ -55,17 +55,13 @@ export function UserForm(props: UserFormProps) {
</div> </div>
)} )}
{user && ( {user && (
<div className="flex items-center gap-2"> <SwitchField
<Checkbox checked={form.watch("active")}
checked={form.watch("active")} disabled={user.isInitialAdmin}
disabled={user.isInitialAdmin} id={`${id}-active`}
id={`${id}-active`} label={t("active")}
onCheckedChange={(checked) => onCheckedChange={(active) => form.setValue("active", active, { shouldDirty: true })}
form.setValue("active", checked === true, { shouldDirty: true }) />
}
/>
<Label htmlFor={`${id}-active`}>{t("active")}</Label>
</div>
)} )}
{user?.isInitialAdmin && ( {user?.isInitialAdmin && (
<p className="text-xs text-muted-foreground">{t("adminProtected")}</p> <p className="text-xs text-muted-foreground">{t("adminProtected")}</p>

View File

@ -1,7 +1,7 @@
import { FormActions } from "@repo/rdx-ui/components/form-dialog"; import { FormActions } from "@repo/rdx-ui/components/form-dialog";
import { SelectWithAction } from "@repo/rdx-ui/components/select-with-action"; import { SelectWithAction } from "@repo/rdx-ui/components/select-with-action";
import { SwitchField } from "@repo/rdx-ui/components/switch-field";
import { Button } from "@repo/shadcn-ui/components/button"; import { Button } from "@repo/shadcn-ui/components/button";
import { Checkbox } from "@repo/shadcn-ui/components/checkbox";
import { Input } from "@repo/shadcn-ui/components/input"; import { Input } from "@repo/shadcn-ui/components/input";
import { Label } from "@repo/shadcn-ui/components/label"; import { Label } from "@repo/shadcn-ui/components/label";
import { Textarea } from "@repo/shadcn-ui/components/textarea"; import { Textarea } from "@repo/shadcn-ui/components/textarea";
@ -95,10 +95,19 @@ export function CatalogTemplateForm(props: CatalogTemplateFormProps) {
)} )}
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Checkbox <SwitchField
checked={binding.required} checked={binding.required}
disabled={mutation.isPending} disabled={mutation.isPending}
id={`${id}-required-${binding.fieldId}`} id={`${id}-required-${binding.fieldId}`}
label={
<>
{t("required")}
<span className="sr-only">
{": "}
{field?.values.name ?? ""}
</span>
</>
}
onCheckedChange={(checked) => onCheckedChange={(checked) =>
form.setValue( form.setValue(
"fields", "fields",
@ -111,13 +120,6 @@ export function CatalogTemplateForm(props: CatalogTemplateFormProps) {
) )
} }
/> />
<Label htmlFor={`${id}-required-${binding.fieldId}`}>
{t("required")}
<span className="sr-only">
{": "}
{field?.values.name ?? ""}
</span>
</Label>
<Button <Button
aria-label={t("remove", { name: field?.values.name ?? "" })} aria-label={t("remove", { name: field?.values.name ?? "" })}
disabled={mutation.isPending} disabled={mutation.isPending}

View File

@ -1,3 +1,6 @@
import { ChevronDownIcon, InfoIcon } from "lucide-react";
import type { ReactNode } from "react";
export function ContextHelp({ export function ContextHelp({
title, title,
description, description,
@ -5,6 +8,7 @@ export function ContextHelp({
guidance, guidance,
examplesLabel, examplesLabel,
scopeNote, scopeNote,
icon = <InfoIcon />,
}: { }: {
title: string; title: string;
description: string; description: string;
@ -12,17 +16,35 @@ export function ContextHelp({
guidance: string; guidance: string;
examplesLabel: string; examplesLabel: string;
scopeNote?: string | undefined; scopeNote?: string | undefined;
icon?: ReactNode;
}) { }) {
return ( return (
<aside aria-label={title} className="space-y-2 rounded-lg border bg-muted/40 p-4 text-sm"> <aside
<p className="font-medium">{title}</p> aria-label={title}
<p className="text-muted-foreground">{description}</p> className="space-y-4 rounded-xl border bg-muted/30 p-4 text-sm sm:p-5"
{scopeNote && <p>{scopeNote}</p>} >
<details className="group"> <div className="flex items-start gap-3">
<summary className="cursor-pointer rounded-sm font-medium text-primary focus-visible:outline-2 focus-visible:outline-ring"> <span
aria-hidden="true"
className="flex size-10 shrink-0 items-center justify-center rounded-full bg-primary/10 text-primary [&_svg]:size-5"
>
{icon}
</span>
<div className="min-w-0 space-y-1 pt-1">
<p className="text-base font-semibold text-foreground">{title}</p>
<p className="leading-relaxed text-muted-foreground">{description}</p>
{scopeNote && <p className="leading-relaxed text-muted-foreground">{scopeNote}</p>}
</div>
</div>
<details className="group overflow-hidden rounded-lg border border-primary/10">
<summary className="flex cursor-pointer list-none items-center gap-3 rounded-lg bg-primary/5 px-4 py-3 font-semibold text-primary focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-ring [&::-webkit-details-marker]:hidden">
<ChevronDownIcon
aria-hidden="true"
className="size-4 shrink-0 -rotate-90 transition-transform group-open:rotate-0 motion-reduce:transition-none"
/>
{examplesLabel} {examplesLabel}
</summary> </summary>
<div className="mt-2 space-y-2 text-muted-foreground"> <div className="space-y-2 px-4 py-3 leading-relaxed text-muted-foreground">
<p>{examples}</p> <p>{examples}</p>
<p>{guidance}</p> <p>{guidance}</p>
</div> </div>

View File

@ -0,0 +1,18 @@
import { Label } from "@repo/shadcn-ui/components/label";
import { Switch } from "@repo/shadcn-ui/components/switch";
import type { ComponentProps, ReactNode } from "react";
type SwitchFieldProps = Readonly<
ComponentProps<typeof Switch> & {
label: ReactNode;
}
>;
export function SwitchField({ id, label, ...switchProps }: SwitchFieldProps) {
return (
<div className="flex items-center gap-2">
<Switch id={id} {...switchProps} />
<Label htmlFor={id}>{label}</Label>
</div>
);
}