refactor(web): kit consistency — focusRing, PageTitle, Badge, size-4, icon buttons (#66)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,7 @@ import { useConfig } from "../config/config-context";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { PageTitle } from "@/components/ui/page-title";
|
||||||
|
|
||||||
/** Accept only a single-leading-slash local path; reject protocol-relative
|
/** Accept only a single-leading-slash local path; reject protocol-relative
|
||||||
* ("//host") and absolute URLs to avoid an open redirect. */
|
* ("//host") and absolute URLs to avoid an open redirect. */
|
||||||
@@ -46,7 +47,7 @@ export function LoginPage() {
|
|||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen items-center justify-center p-4">
|
<div className="flex min-h-screen items-center justify-center p-4">
|
||||||
<form onSubmit={onSubmit} className="w-full max-w-sm space-y-4">
|
<form onSubmit={onSubmit} className="w-full max-w-sm space-y-4">
|
||||||
<h1 className="text-2xl font-semibold">{app_name}</h1>
|
<PageTitle>{app_name}</PageTitle>
|
||||||
{sessionExpired && (
|
{sessionExpired && (
|
||||||
<p className="text-sm text-muted-foreground">{t("auth.sessionExpired")}</p>
|
<p className="text-sm text-muted-foreground">{t("auth.sessionExpired")}</p>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -96,9 +96,9 @@ export function FieldList({
|
|||||||
>
|
>
|
||||||
<span className="font-medium">{labelText(def.labels, lang)}</span>
|
<span className="font-medium">{labelText(def.labels, lang)}</span>
|
||||||
<span className="text-xs text-muted-foreground">{def.key}</span>
|
<span className="text-xs text-muted-foreground">{def.key}</span>
|
||||||
<span className="rounded-md bg-muted px-1.5 py-0.5 text-xs text-muted-foreground">
|
<Badge variant="secondary">
|
||||||
{t(`fields.types.${def.data_type}`)}
|
{t(`fields.types.${def.data_type}`)}
|
||||||
</span>
|
</Badge>
|
||||||
{def.required && (
|
{def.required && (
|
||||||
<span
|
<span
|
||||||
className="text-xs text-destructive"
|
className="text-xs text-destructive"
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { X } from "lucide-react";
|
import { X } from "lucide-react";
|
||||||
|
|
||||||
import { Drawer, DrawerClose, DrawerContent } from "@/components/ui/drawer";
|
import { Drawer, DrawerClose, DrawerContent } from "@/components/ui/drawer";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Narrow-viewport object detail: the nested <Outlet/> inside a Base UI Drawer that
|
* Narrow-viewport object detail: the nested <Outlet/> inside a Base UI Drawer that
|
||||||
@@ -30,7 +31,7 @@ export function ObjectDetailDrawer({
|
|||||||
<div className="flex justify-end border-b p-2">
|
<div className="flex justify-end border-b p-2">
|
||||||
<DrawerClose
|
<DrawerClose
|
||||||
aria-label={t("actions.closeDetail")}
|
aria-label={t("actions.closeDetail")}
|
||||||
className="rounded-md p-1 text-muted-foreground hover:bg-muted hover:text-foreground"
|
render={<Button variant="ghost" size="icon-sm" />}
|
||||||
>
|
>
|
||||||
<X className="size-4" aria-hidden="true" />
|
<X className="size-4" aria-hidden="true" />
|
||||||
</DrawerClose>
|
</DrawerClose>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { ObjectsTable } from "./objects-table";
|
|||||||
import { useMediaQuery } from "../lib/use-media-query";
|
import { useMediaQuery } from "../lib/use-media-query";
|
||||||
import { useDocumentTitle } from "../lib/use-document-title";
|
import { useDocumentTitle } from "../lib/use-document-title";
|
||||||
import { useBreadcrumb } from "../shell/use-breadcrumb";
|
import { useBreadcrumb } from "../shell/use-breadcrumb";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
import { PageTitle } from "@/components/ui/page-title";
|
import { PageTitle } from "@/components/ui/page-title";
|
||||||
|
|
||||||
const ObjectDetailDrawer = lazy(() =>
|
const ObjectDetailDrawer = lazy(() =>
|
||||||
@@ -47,14 +48,14 @@ export function ObjectsPage() {
|
|||||||
{open && (
|
{open && (
|
||||||
<div className="flex h-full flex-col overflow-hidden border-l">
|
<div className="flex h-full flex-col overflow-hidden border-l">
|
||||||
<div className="flex justify-end border-b p-2">
|
<div className="flex justify-end border-b p-2">
|
||||||
<button
|
<Button
|
||||||
type="button"
|
variant="ghost"
|
||||||
|
size="icon-sm"
|
||||||
onClick={closeDetail}
|
onClick={closeDetail}
|
||||||
aria-label={t("actions.closeDetail")}
|
aria-label={t("actions.closeDetail")}
|
||||||
className="rounded-md p-1 text-muted-foreground hover:bg-muted hover:text-foreground"
|
|
||||||
>
|
>
|
||||||
<X className="size-4" aria-hidden="true" />
|
<X className="size-4" aria-hidden="true" />
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 overflow-auto">
|
<div className="flex-1 overflow-auto">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export function HeaderSearch() {
|
|||||||
<form onSubmit={onSubmit} className="hidden sm:block">
|
<form onSubmit={onSubmit} className="hidden sm:block">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Search
|
<Search
|
||||||
className="pointer-events-none absolute top-1/2 left-2 h-4 w-4 -translate-y-1/2 text-muted-foreground"
|
className="pointer-events-none absolute top-1/2 left-2 size-4 -translate-y-1/2 text-muted-foreground"
|
||||||
aria-hidden
|
aria-hidden
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
import type { LucideIcon } from "lucide-react";
|
import type { LucideIcon } from "lucide-react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { focusRing } from "../lib/focus-ring";
|
||||||
import { Tooltip } from "@/components/ui/tooltip";
|
import { Tooltip } from "@/components/ui/tooltip";
|
||||||
import { useMediaQuery } from "@/lib/use-media-query";
|
import { useMediaQuery } from "@/lib/use-media-query";
|
||||||
import { useConfig } from "../config/config-context";
|
import { useConfig } from "../config/config-context";
|
||||||
@@ -43,7 +44,7 @@ function navLinkClass(collapsed: boolean) {
|
|||||||
return ({ isActive }: { isActive: boolean }) =>
|
return ({ isActive }: { isActive: boolean }) =>
|
||||||
cn(
|
cn(
|
||||||
"flex items-center gap-2 rounded-md px-2 py-1 outline-none",
|
"flex items-center gap-2 rounded-md px-2 py-1 outline-none",
|
||||||
"focus-visible:ring-3 focus-visible:ring-ring/50",
|
focusRing,
|
||||||
collapsed && "justify-center",
|
collapsed && "justify-center",
|
||||||
isActive && "bg-accent font-medium",
|
isActive && "bg-accent font-medium",
|
||||||
);
|
);
|
||||||
@@ -85,7 +86,8 @@ export function Sidebar() {
|
|||||||
title={t(collapsed ? "nav.expandSidebar" : "nav.collapseSidebar")}
|
title={t(collapsed ? "nav.expandSidebar" : "nav.collapseSidebar")}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center justify-center rounded-md p-1 outline-none",
|
"flex items-center justify-center rounded-md p-1 outline-none",
|
||||||
"hover:bg-accent focus-visible:ring-3 focus-visible:ring-ring/50",
|
"hover:bg-accent",
|
||||||
|
focusRing,
|
||||||
"disabled:pointer-events-none disabled:opacity-50",
|
"disabled:pointer-events-none disabled:opacity-50",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export function ThemeSwitch() {
|
|||||||
: "text-muted-foreground hover:text-foreground",
|
: "text-muted-foreground hover:text-foreground",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Icon className="h-4 w-4" aria-hidden />
|
<Icon className="size-4" aria-hidden />
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export function UserMenu() {
|
|||||||
<MenuTrigger
|
<MenuTrigger
|
||||||
render={
|
render={
|
||||||
<Button variant="ghost" size="sm" className="max-w-44">
|
<Button variant="ghost" size="sm" className="max-w-44">
|
||||||
<CircleUser className="h-4 w-4" aria-hidden />
|
<CircleUser className="size-4" aria-hidden />
|
||||||
<span className="truncate">{me.email}</span>
|
<span className="truncate">{me.email}</span>
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user