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.
- 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.
- 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.
- Textarea: dirección opcional de las sedes, con varias líneas.
- 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
@ -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.
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.
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.
## 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
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 { 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 { Input } from "@repo/shadcn-ui/components/input";
import { Label } from "@repo/shadcn-ui/components/label";
@ -48,10 +49,12 @@ export function BrandForm(props: BrandFormProps) {
onSelect={selectLogo}
/>
{!quickCreate && (
<label className="flex items-center gap-2 text-sm">
<input type="checkbox" {...form.register("active")} />
{t("brands.active")}
</label>
<SwitchField
checked={form.watch("active")}
id={`${id}-active`}
label={t("brands.active")}
onCheckedChange={(active) => form.setValue("active", active, { shouldDirty: true })}
/>
)}
</fieldset>
{(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 { SwitchField } from "@repo/rdx-ui/components/switch-field";
import { Button } from "@repo/shadcn-ui/components/button";
import { Input } from "@repo/shadcn-ui/components/input";
import { Label } from "@repo/shadcn-ui/components/label";
@ -33,20 +34,18 @@ export function CatalogRelationForm(props: CatalogRelationFormProps) {
}}
/>
</div>
<label className="flex items-center gap-2 text-sm">
<input
checked={symmetric}
disabled={!!item}
onChange={(event) => {
form.setValue("symmetric", event.target.checked);
if (event.target.checked) {
form.setValue("inverseName", form.getValues("name"));
}
}}
type="checkbox"
/>
{t("symmetric")}
</label>
<SwitchField
checked={symmetric}
disabled={!!item}
id={`${id}-symmetric`}
label={t("symmetric")}
onCheckedChange={(isSymmetric) => {
form.setValue("symmetric", isSymmetric);
if (isSymmetric) {
form.setValue("inverseName", form.getValues("name"));
}
}}
/>
{!symmetric && (
<div className="space-y-2">
<Label htmlFor={`${id}-inverse`}>{t("inverseName")}</Label>

View File

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

View File

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

View File

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

View File

@ -1,6 +1,7 @@
import { ContextHelp } from "@repo/rdx-ui/components/context-help";
import { FormActions } from "@repo/rdx-ui/components/form-dialog";
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 { Input } from "@repo/shadcn-ui/components/input";
import { Label } from "@repo/shadcn-ui/components/label";
@ -41,10 +42,12 @@ export function WorkstationForm(props: WorkstationFormProps) {
<Label htmlFor={prefix}>{t("name")}</Label>
<Input id={prefix} maxLength={100} required {...form.register("name")} />
</div>
<label className="flex items-center gap-2 text-sm">
<input type="checkbox" {...form.register("active")} />
{t("active")}
</label>
<SwitchField
checked={values.active}
id={`${prefix}-active`}
label={t("active")}
onCheckedChange={(active) => form.setValue("active", active)}
/>
<div className="grid gap-4 sm:grid-cols-2">
<div className="space-y-2">
<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 { 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 { Label } from "@repo/shadcn-ui/components/label";
import { Textarea } from "@repo/shadcn-ui/components/textarea";
@ -38,15 +38,13 @@ export function AccessFieldControl({
const { t } = useTranslation("access");
if (field.type === "boolean") {
return (
<div className="flex items-center gap-2">
<Checkbox
checked={Boolean(value)}
disabled={disabled}
id={id}
onCheckedChange={(checked) => form.setValue(field.key, checked === true)}
/>
<Label htmlFor={id}>{t(field.key)}</Label>
</div>
<SwitchField
checked={Boolean(value)}
disabled={disabled}
id={id}
label={t(field.key)}
onCheckedChange={(checked) => form.setValue(field.key, checked)}
/>
);
}
if (field.type === "permissions") {

View File

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

View File

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

View File

@ -1,6 +1,6 @@
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 { Checkbox } from "@repo/shadcn-ui/components/checkbox";
import { Input } from "@repo/shadcn-ui/components/input";
import { Label } from "@repo/shadcn-ui/components/label";
import { useId } from "react";
@ -22,17 +22,13 @@ export function DepartmentForm(props: DepartmentFormProps) {
<Label htmlFor={id}>{t("name")}</Label>
<Input id={id} maxLength={100} required {...form.register("name")} />
</div>
<div className="flex items-center gap-2">
<Checkbox
checked={form.watch("active")}
disabled={requireActive}
id={`${id}-active`}
onCheckedChange={(checked) =>
form.setValue("active", checked === true, { shouldDirty: true })
}
/>
<Label htmlFor={`${id}-active`}>{t("activeDepartment")}</Label>
</div>
<SwitchField
checked={form.watch("active")}
disabled={requireActive}
id={`${id}-active`}
label={t("activeDepartment")}
onCheckedChange={(checked) => form.setValue("active", checked, { shouldDirty: true })}
/>
{department && (
<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 { SwitchField } from "@repo/rdx-ui/components/switch-field";
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 { Label } from "@repo/shadcn-ui/components/label";
import { Textarea } from "@repo/shadcn-ui/components/textarea";
@ -35,16 +35,12 @@ export function InterventionTemplateForm(props: InterventionTemplateFormProps) {
{...form.register("description")}
/>
</div>
<div className="flex items-center gap-2">
<Checkbox
checked={form.watch("active")}
id={`${prefix}-active`}
onCheckedChange={(value) =>
form.setValue("active", value === true, { shouldDirty: true })
}
/>
<Label htmlFor={`${prefix}-active`}>{t("active")}</Label>
</div>
<SwitchField
checked={form.watch("active")}
id={`${prefix}-active`}
label={t("active")}
onCheckedChange={(active) => form.setValue("active", active, { shouldDirty: true })}
/>
<h3 className="font-medium">{t("interventionTemplates.steps")}</h3>
{steps.fields.map((step, index) => (
<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 { Checkbox } from "@repo/shadcn-ui/components/checkbox";
import { Input } from "@repo/shadcn-ui/components/input";
import { Label } from "@repo/shadcn-ui/components/label";
import { Textarea } from "@repo/shadcn-ui/components/textarea";
@ -73,16 +73,14 @@ export function TemplateStepFields({
{...form.register(`steps.${index}.instructions`)}
/>
</div>
<div className="flex gap-2 items-center">
<Checkbox
checked={form.watch(`steps.${index}.required`)}
id={`${id}-required`}
onCheckedChange={(value) =>
form.setValue(`steps.${index}.required`, value === true, { shouldDirty: true })
}
/>
<Label htmlFor={`${id}-required`}>{t("interventionTemplates.required")}</Label>
</div>
<SwitchField
checked={form.watch(`steps.${index}.required`)}
id={`${id}-required`}
label={t("interventionTemplates.required")}
onCheckedChange={(required) =>
form.setValue(`steps.${index}.required`, required, { shouldDirty: true })
}
/>
</section>
);
}

View File

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

View File

@ -1,6 +1,6 @@
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 { Checkbox } from "@repo/shadcn-ui/components/checkbox";
import { Input } from "@repo/shadcn-ui/components/input";
import { Label } from "@repo/shadcn-ui/components/label";
import { useId } from "react";
@ -36,27 +36,21 @@ export function PersonForm(props: PersonFormProps) {
<Input id={`${id}-phone`} maxLength={50} type="tel" {...form.register("phone")} />
</div>
</div>
<div className="flex items-center gap-2">
<Checkbox
checked={form.watch("active")}
disabled={requireActive}
id={`${id}-active`}
onCheckedChange={(checked) =>
form.setValue("active", checked === true, { shouldDirty: true })
}
/>
<Label htmlFor={`${id}-active`}>{t("activePerson")}</Label>
</div>
<div className="flex items-center gap-2">
<Checkbox
checked={form.watch("isContact")}
id={`${id}-contact`}
onCheckedChange={(checked) =>
form.setValue("isContact", checked === true, { shouldDirty: true })
}
/>
<Label htmlFor={`${id}-contact`}>{t("isContact")}</Label>
</div>
<SwitchField
checked={form.watch("active")}
disabled={requireActive}
id={`${id}-active`}
label={t("activePerson")}
onCheckedChange={(active) => form.setValue("active", active, { shouldDirty: true })}
/>
<SwitchField
checked={form.watch("isContact")}
id={`${id}-contact`}
label={t("isContact")}
onCheckedChange={(isContact) =>
form.setValue("isContact", isContact, { shouldDirty: true })
}
/>
{person?.active && !form.watch("active") && (
<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 { FormActions } from "@repo/rdx-ui/components/form-dialog";
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 { Label } from "@repo/shadcn-ui/components/label";
import { Textarea } from "@repo/shadcn-ui/components/textarea";
@ -125,14 +126,13 @@ export function PrinterConfigurationForm(props: PrinterConfigurationFormProps) {
/>
</div>
</div>
<label className="flex items-center gap-2 text-sm">
<input
type="checkbox"
{...form.register("isDefault")}
disabled={!["pending", "configured"].includes(values.state)}
/>
{t("printers.default")}
</label>
<SwitchField
checked={values.isDefault}
disabled={!["pending", "configured"].includes(values.state)}
id={`${prefix}-default`}
label={t("printers.default")}
onCheckedChange={(isDefault) => form.setValue("isDefault", isDefault)}
/>
<div className="space-y-2">
<Label htmlFor={`${prefix}-notes`}>{t("mail.notes")}</Label>
<Textarea

View File

@ -1,7 +1,7 @@
import { FormActions } from "@repo/rdx-ui/components/form-dialog";
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 { Checkbox } from "@repo/shadcn-ui/components/checkbox";
import { Input } from "@repo/shadcn-ui/components/input";
import { Label } from "@repo/shadcn-ui/components/label";
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>
</div>
<div className="flex items-center gap-2">
<Checkbox
checked={form.watch("active")}
id={`${prefix}-active`}
onCheckedChange={(v) => form.setValue("active", v === true)}
/>
<Label htmlFor={`${prefix}-active`}>{t("active")}</Label>
</div>
<SwitchField
checked={form.watch("active")}
id={`${prefix}-active`}
label={t("active")}
onCheckedChange={(active) => form.setValue("active", active)}
/>
<div className="space-y-2">
<Label htmlFor={`${prefix}-notes`}>{t("mail.notes")}</Label>
<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 { FormActions } from "@repo/rdx-ui/components/form-dialog";
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 { Checkbox } from "@repo/shadcn-ui/components/checkbox";
import { Input } from "@repo/shadcn-ui/components/input";
import { Label } from "@repo/shadcn-ui/components/label";
import { Textarea } from "@repo/shadcn-ui/components/textarea";
@ -104,14 +104,12 @@ export function RemoteConnectionForm(props: RemoteConnectionFormProps) {
/>
</div>
</div>
<div className="flex items-center gap-2">
<Checkbox
checked={v.requiresVpn}
id={`${prefix}-vpn`}
onCheckedChange={(checked) => form.setValue("requiresVpn", checked === true)}
/>
<Label htmlFor={`${prefix}-vpn`}>{t("remote.requiresVpn")}</Label>
</div>
<SwitchField
checked={v.requiresVpn}
id={`${prefix}-vpn`}
label={t("remote.requiresVpn")}
onCheckedChange={(requiresVpn) => form.setValue("requiresVpn", requiresVpn)}
/>
<div className="space-y-2">
<Label htmlFor={`${prefix}-notes`}>{t("mail.notes")}</Label>
<Textarea

View File

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

View File

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

View File

@ -1,6 +1,6 @@
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 { Checkbox } from "@repo/shadcn-ui/components/checkbox";
import { Input } from "@repo/shadcn-ui/components/input";
import { Label } from "@repo/shadcn-ui/components/label";
import { useId } from "react";
@ -55,17 +55,13 @@ export function UserForm(props: UserFormProps) {
</div>
)}
{user && (
<div className="flex items-center gap-2">
<Checkbox
checked={form.watch("active")}
disabled={user.isInitialAdmin}
id={`${id}-active`}
onCheckedChange={(checked) =>
form.setValue("active", checked === true, { shouldDirty: true })
}
/>
<Label htmlFor={`${id}-active`}>{t("active")}</Label>
</div>
<SwitchField
checked={form.watch("active")}
disabled={user.isInitialAdmin}
id={`${id}-active`}
label={t("active")}
onCheckedChange={(active) => form.setValue("active", active, { shouldDirty: true })}
/>
)}
{user?.isInitialAdmin && (
<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 { 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 { Checkbox } from "@repo/shadcn-ui/components/checkbox";
import { Input } from "@repo/shadcn-ui/components/input";
import { Label } from "@repo/shadcn-ui/components/label";
import { Textarea } from "@repo/shadcn-ui/components/textarea";
@ -95,10 +95,19 @@ export function CatalogTemplateForm(props: CatalogTemplateFormProps) {
)}
</div>
<div className="flex items-center gap-2">
<Checkbox
<SwitchField
checked={binding.required}
disabled={mutation.isPending}
id={`${id}-required-${binding.fieldId}`}
label={
<>
{t("required")}
<span className="sr-only">
{": "}
{field?.values.name ?? ""}
</span>
</>
}
onCheckedChange={(checked) =>
form.setValue(
"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
aria-label={t("remove", { name: field?.values.name ?? "" })}
disabled={mutation.isPending}

View File

@ -1,3 +1,6 @@
import { ChevronDownIcon, InfoIcon } from "lucide-react";
import type { ReactNode } from "react";
export function ContextHelp({
title,
description,
@ -5,6 +8,7 @@ export function ContextHelp({
guidance,
examplesLabel,
scopeNote,
icon = <InfoIcon />,
}: {
title: string;
description: string;
@ -12,17 +16,35 @@ export function ContextHelp({
guidance: string;
examplesLabel: string;
scopeNote?: string | undefined;
icon?: ReactNode;
}) {
return (
<aside aria-label={title} className="space-y-2 rounded-lg border bg-muted/40 p-4 text-sm">
<p className="font-medium">{title}</p>
<p className="text-muted-foreground">{description}</p>
{scopeNote && <p>{scopeNote}</p>}
<details className="group">
<summary className="cursor-pointer rounded-sm font-medium text-primary focus-visible:outline-2 focus-visible:outline-ring">
<aside
aria-label={title}
className="space-y-4 rounded-xl border bg-muted/30 p-4 text-sm sm:p-5"
>
<div className="flex items-start gap-3">
<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}
</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>{guidance}</p>
</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>
);
}