refactor: mejorar la navegación de configuración y agregar iconos a las entradas de menú

This commit is contained in:
David Arranz 2026-09-04 19:35:20 +02:00
parent 5ad446be00
commit 685472b3ef
8 changed files with 85 additions and 37 deletions

View File

@ -32,7 +32,7 @@ export const SettingsLayout = ({ sections }: SettingsLayoutProps) => {
/>
<div className="grid gap-6 lg:grid-cols-[18rem_minmax(0,1fr)]">
<aside className="rounded-xl border border-border bg-card p-4 shadow-sm lg:sticky lg:top-6 lg:self-start">
<aside className="min-w-0 lg:sticky lg:top-6 lg:self-start">
<SettingsNavigation sections={sections} />
</aside>

View File

@ -1,5 +1,8 @@
import { i18n } from "@repo/i18next";
import { Card, CardContent, CardFooter, CardHeader, Separator } from "@repo/shadcn-ui/components";
import { cn } from "@repo/shadcn-ui/lib/utils";
import { ChevronRightIcon, SettingsIcon } from "lucide-react";
import { Fragment } from "react";
import { NavLink } from "react-router-dom";
import type { SettingsNavigationSection } from "@/settings/build-settings-navigation";
@ -20,43 +23,70 @@ const resolveEntryLabel = (labelKey: string, moduleName: string) => {
export const SettingsNavigation = ({ sections }: SettingsNavigationProps) => {
const { t } = useAppTranslation();
if (sections.length === 0) {
return (
<Card className="gap-5" size="sm">
<CardHeader>
<h2 className="px-3 text-lg font-semibold tracking-tight pb-2">
{t("web.settings.title")}
</h2>
</CardHeader>
<CardContent>
<nav aria-label={t("web.settings.title")} className="space-y-4 ">
{sections.length === 0 ? (
<p className="text-sm leading-6 text-muted-foreground">
{t("web.settings.empty.navigation")}
</p>
);
}
) : (
sections.map((section, index) => (
<Fragment key={section.id}>
{index > 0 && <Separator />}
<div className="space-y-2">
<h3 className="px-3 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
{t(section.labelKey)}
</h3>
<ul className="space-y-1">
{section.entries.map((entry) => {
const Icon = entry.icon ?? SettingsIcon;
return (
<nav aria-label={t("web.settings.title")} className="space-y-6">
{sections.map((section) => (
<div className="space-y-2" key={section.id}>
<h2 className="text-xs font-semibold uppercase tracking-[0.14em] text-muted-foreground">
{t(section.labelKey)}
</h2>
<ul className="space-y-1">
{section.entries.map((entry) => (
<li key={`${entry.moduleName}:${entry.id}`}>
<NavLink
className={({ isActive }) =>
cn(
"block rounded-lg px-3 py-2 text-sm font-medium text-foreground/80 transition-colors",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
"hover:bg-muted hover:text-foreground",
isActive && "bg-muted text-foreground"
"group flex min-h-12 w-full items-center gap-3 rounded-lg px-3 py-2 text-base transition-colors",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-card",
isActive
? "bg-primary/5 font-medium text-primary"
: "bg-transparent font-normal text-foreground hover:bg-muted/50"
)
}
to={entry.path}
>
<Icon
aria-hidden="true"
className="size-5 shrink-0 text-muted-foreground group-aria-[current=page]:text-primary"
/>
<span className="min-w-0 flex-1 break-words">
{resolveEntryLabel(entry.labelKey, entry.moduleName)}
</span>
<ChevronRightIcon
aria-hidden="true"
className="size-5 shrink-0 text-muted-foreground group-aria-[current=page]:text-primary"
/>
</NavLink>
</li>
))}
);
})}
</ul>
</div>
))}
</Fragment>
))
)}
</nav>
</CardContent>
<CardFooter>
<></>
</CardFooter>
</Card>
);
};

View File

@ -1,6 +1,8 @@
import assert from "node:assert/strict";
import test from "node:test";
import { CalendarClockIcon, CreditCardIcon, LandmarkIcon } from "lucide-react";
import { CatalogsModuleManifest } from "./manifest";
test("publica la entry de Settings para payment methods", () => {
@ -8,18 +10,21 @@ test("publica la entry de Settings para payment methods", () => {
entries: [
{
id: "payment-methods",
icon: CreditCardIcon,
sectionId: "general",
labelKey: "settings.paymentMethods.title",
path: "/settings/general/payment-methods",
},
{
id: "payment-terms",
icon: CalendarClockIcon,
sectionId: "general",
labelKey: "settings.paymentTerms.title",
path: "/settings/general/payment-terms",
},
{
id: "tax-regimes",
icon: LandmarkIcon,
sectionId: "general",
labelKey: "settings.taxRegimes.title",
path: "/settings/general/tax-regimes",

View File

@ -1,4 +1,5 @@
import type { IModuleClient } from "@erp/core/client";
import { CalendarClockIcon, CreditCardIcon, LandmarkIcon } from "lucide-react";
import { CatalogsRoutes } from "./catalogs-routes";
import { ensureModuleTranslations } from "./i18n";
@ -18,18 +19,21 @@ export const CatalogsModuleManifest: IModuleClient = {
entries: [
{
id: "payment-methods",
icon: CreditCardIcon,
sectionId: "general",
labelKey: "settings.paymentMethods.title",
path: "/settings/general/payment-methods",
},
{
id: "payment-terms",
icon: CalendarClockIcon,
sectionId: "general",
labelKey: "settings.paymentTerms.title",
path: "/settings/general/payment-terms",
},
{
id: "tax-regimes",
icon: LandmarkIcon,
sectionId: "general",
labelKey: "settings.taxRegimes.title",
path: "/settings/general/tax-regimes",

View File

@ -1,4 +1,5 @@
import type { IModuleClient } from "@erp/core/client";
import { Building2Icon } from "lucide-react";
import { CompanyRoutes } from "./company-routes";
import { ensureModuleTranslations } from "./i18n";
@ -18,6 +19,7 @@ export const CompaniesModuleManifest: IModuleClient = {
entries: [
{
id: "company-details",
icon: Building2Icon,
sectionId: "company",
labelKey: "settings.companyDetails.title",
path: "/settings/company/details",

View File

@ -1,3 +1,4 @@
import type { LucideIcon } from "lucide-react";
import type { ReactNode } from "react";
import type { RouteObject } from "react-router-dom";
@ -10,6 +11,7 @@ export type ModuleRouteLayout = "app-sidebar" | "auth" | "app-fullscreen";
export interface ModuleSettingsEntryContribution {
id: string;
sectionId: string;
icon?: LucideIcon;
labelKey: string;
path: string;
}

View File

@ -1,6 +1,8 @@
import assert from "node:assert/strict";
import test from "node:test";
import { ShieldCheckIcon } from "lucide-react";
import { CustomerInvoicesModuleManifest } from "./manifest";
test("publica la entry de Settings para VeriFactu", () => {
@ -8,6 +10,7 @@ test("publica la entry de Settings para VeriFactu", () => {
entries: [
{
id: "verifactu",
icon: ShieldCheckIcon,
sectionId: "billing",
labelKey: "settings.verifactu.title",
path: "/settings/billing/verifactu",

View File

@ -1,4 +1,5 @@
import type { IModuleClient, ModuleClientParams } from "@erp/core/client";
import { ShieldCheckIcon } from "lucide-react";
import { CustomerInvoiceRoutes } from "./customer-invoice-routes";
import { ensureModuleTranslations } from "./i18n";
@ -21,6 +22,7 @@ export const CustomerInvoicesModuleManifest: IModuleClient = {
entries: [
{
id: "verifactu",
icon: ShieldCheckIcon,
sectionId: "billing",
labelKey: "settings.verifactu.title",
path: "/settings/billing/verifactu",