feat(web): useLang + segmentClass/rowStateClass helpers; delete dead Card (#66)
This commit is contained in:
@@ -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 (
|
|
||||||
<div
|
|
||||||
data-slot="card"
|
|
||||||
data-size={size}
|
|
||||||
className={cn(
|
|
||||||
"group/card flex flex-col gap-4 overflow-hidden rounded-xl bg-card py-4 text-sm text-card-foreground ring-1 ring-foreground/10 has-data-[slot=card-footer]:pb-0 has-[>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 (
|
|
||||||
<div
|
|
||||||
data-slot="card-header"
|
|
||||||
className={cn(
|
|
||||||
"group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-4 group-data-[size=sm]/card:px-3 has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="card-title"
|
|
||||||
className={cn(
|
|
||||||
"text-base leading-snug font-medium group-data-[size=sm]/card:text-sm",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="card-description"
|
|
||||||
className={cn("text-sm text-muted-foreground", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="card-action"
|
|
||||||
className={cn(
|
|
||||||
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="card-content"
|
|
||||||
className={cn("px-4 group-data-[size=sm]/card:px-3", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="card-footer"
|
|
||||||
className={cn(
|
|
||||||
"flex items-center rounded-b-xl border-t bg-muted/50 p-4 group-data-[size=sm]/card:p-3",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
Card,
|
|
||||||
CardHeader,
|
|
||||||
CardFooter,
|
|
||||||
CardTitle,
|
|
||||||
CardAction,
|
|
||||||
CardDescription,
|
|
||||||
CardContent,
|
|
||||||
}
|
|
||||||
@@ -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");
|
||||||
|
});
|
||||||
@@ -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";
|
||||||
|
}
|
||||||
@@ -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";
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user