From 00a7ce772ed46e20ed8b81b69b019ae3cd25d935 Mon Sep 17 00:00:00 2001 From: Anders Olsson Date: Mon, 8 Jun 2026 23:41:08 +0200 Subject: [PATCH] feat(web): useLang + segmentClass/rowStateClass helpers; delete dead Card (#66) --- web/src/components/ui/card.tsx | 103 ------------------------------ web/src/lib/class-recipes.test.ts | 22 +++++++ web/src/lib/class-recipes.ts | 14 ++++ web/src/lib/use-lang.ts | 7 ++ 4 files changed, 43 insertions(+), 103 deletions(-) delete mode 100644 web/src/components/ui/card.tsx create mode 100644 web/src/lib/class-recipes.test.ts create mode 100644 web/src/lib/class-recipes.ts create mode 100644 web/src/lib/use-lang.ts diff --git a/web/src/components/ui/card.tsx b/web/src/components/ui/card.tsx deleted file mode 100644 index 9bd5a25..0000000 --- a/web/src/components/ui/card.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import * as React from "react" - -import { cn } from "@/lib/utils" - -function Card({ - className, - size = "default", - ...props -}: React.ComponentProps<"div"> & { size?: "default" | "sm" }) { - return ( -
img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl", - className - )} - {...props} - /> - ) -} - -function CardHeader({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ) -} - -function CardTitle({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ) -} - -function CardDescription({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ) -} - -function CardAction({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ) -} - -function CardContent({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ) -} - -function CardFooter({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ) -} - -export { - Card, - CardHeader, - CardFooter, - CardTitle, - CardAction, - CardDescription, - CardContent, -} diff --git a/web/src/lib/class-recipes.test.ts b/web/src/lib/class-recipes.test.ts new file mode 100644 index 0000000..1035923 --- /dev/null +++ b/web/src/lib/class-recipes.test.ts @@ -0,0 +1,22 @@ +import { expect, test } from "vitest"; + +import { rowStateClass, segmentClass } from "./class-recipes"; + +test("segmentClass active uses the primary tokens + focus ring", () => { + const cls = segmentClass(true, "px-2 py-1"); + expect(cls).toContain("bg-primary"); + expect(cls).toContain("text-primary-foreground"); + expect(cls).toContain("focus-visible:ring-ring/50"); + expect(cls).toContain("px-2"); +}); + +test("segmentClass inactive uses border, not the primary fill", () => { + const cls = segmentClass(false); + expect(cls).toContain("border"); + expect(cls).not.toContain("bg-primary"); +}); + +test("rowStateClass toggles selected vs idle-hover", () => { + expect(rowStateClass(true)).toBe("bg-primary/10"); + expect(rowStateClass(false)).toBe("hover:bg-muted"); +}); diff --git a/web/src/lib/class-recipes.ts b/web/src/lib/class-recipes.ts new file mode 100644 index 0000000..06931a6 --- /dev/null +++ b/web/src/lib/class-recipes.ts @@ -0,0 +1,14 @@ +import { cn } from "@/lib/utils"; + +import { focusRing } from "./focus-ring"; + +/** Segmented-control / filter-pill item. Unifies the active/inactive token recipe + + * focus ring; callers pass their contextual padding/size via `className`. */ +export function segmentClass(active: boolean, className?: string): string { + return cn("rounded-md", focusRing, active ? "bg-primary text-primary-foreground" : "border", className); +} + +/** Selected vs idle row background for master-detail / list rows. */ +export function rowStateClass(active: boolean): string { + return active ? "bg-primary/10" : "hover:bg-muted"; +} diff --git a/web/src/lib/use-lang.ts b/web/src/lib/use-lang.ts new file mode 100644 index 0000000..674cd68 --- /dev/null +++ b/web/src/lib/use-lang.ts @@ -0,0 +1,7 @@ +import { useTranslation } from "react-i18next"; + +/** The instance's active UI language, narrowed to the two supported locales. */ +export function useLang(): "sv" | "en" { + const { i18n } = useTranslation(); + return i18n.language.startsWith("sv") ? "sv" : "en"; +}