fix(ui) : improve control visibility while preserving header styling
CI / Verify (push) Successful in 3m0s
CI / Build immutable images and deploy (push) Successful in 2m34s

This commit is contained in:
2026-10-14 15:35:40 +07:00 Unverified
parent 8d051c9e4b
commit d229cb3942
10 changed files with 109 additions and 29 deletions
+76
View File
@@ -51,6 +51,10 @@
--color-tier-copper: var(--tier-copper);
--color-border: var(--border);
--color-input: var(--input);
--color-control: var(--control);
--color-control-hover: var(--control-hover);
--color-control-border: var(--control-border);
--color-control-border-hover: var(--control-border-hover);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
@@ -104,6 +108,10 @@
--tier-copper: oklch(0.65 0.12 55);
--border: oklch(0.96 0.01 88 / 12%);
--input: oklch(0.96 0.01 88 / 16%);
--control: oklch(0.26 0.02 274);
--control-hover: oklch(0.32 0.025 274);
--control-border: oklch(0.52 0.02 274);
--control-border-hover: oklch(0.64 0.02 274);
--ring: oklch(0.79 0.14 78);
--chart-1: oklch(0.76 0.16 76);
--chart-2: oklch(0.7 0.14 255);
@@ -156,11 +164,78 @@
text-rendering: optimizeLegibility;
}
button:not(:disabled):not([aria-disabled="true"]):not([data-disabled]),
[role="button"]:not(:disabled):not([aria-disabled="true"]):not([data-disabled]) {
cursor: pointer;
}
button:disabled,
[role="button"][aria-disabled="true"],
button[data-disabled] {
cursor: not-allowed;
}
::selection {
background: color-mix(in oklch, var(--primary) 30%, transparent);
}
}
/* Keep header actions understated while page controls use visible surfaces. */
[data-slot="header-shell"] [data-variant="ghost"] {
border-color: transparent;
background: transparent;
color: inherit;
}
[data-slot="header-shell"] [data-variant="ghost"]:hover {
background: color-mix(in oklch, var(--muted) 50%, transparent);
color: var(--foreground);
}
[data-slot="header-shell"] [data-variant="ghost"][aria-expanded="true"] {
background: var(--muted);
color: var(--foreground);
}
[data-slot="header-shell"] [data-variant="outline"] {
border-color: var(--input);
background: color-mix(in oklch, var(--input) 30%, transparent);
color: inherit;
}
[data-slot="header-shell"] [data-variant="outline"]:hover {
background: color-mix(in oklch, var(--input) 50%, transparent);
color: var(--foreground);
}
[data-slot="header-shell"] [data-variant="outline"][aria-expanded="true"] {
background: var(--muted);
color: var(--foreground);
}
[data-slot="header-shell"] [data-variant="secondary"] {
border-color: transparent;
background: var(--secondary);
color: var(--secondary-foreground);
}
[data-slot="header-shell"] [data-variant="secondary"]:hover {
background: color-mix(in oklch, var(--secondary), var(--foreground) 5%);
}
[data-slot="header-shell"] [data-variant="link"] {
text-decoration-line: none;
}
[data-slot="header-shell"] [data-variant="link"]:hover {
text-decoration-line: underline;
text-decoration-thickness: auto;
}
[data-slot="header-shell"] [data-variant]:focus-visible {
border-color: var(--ring);
}
.extra-sheet-grid {
--background-color: var(--background);
--text-color: var(--foreground);
@@ -284,6 +359,7 @@
@media (pointer: coarse) {
[data-slot="button"],
[data-slot="toggle-group-item"],
[data-slot="tabs-trigger"],
[data-slot="select-trigger"],
[data-slot="input"],
[data-slot="input-group"] {
+1 -1
View File
@@ -11,7 +11,7 @@ export function HeaderShell({ homeHref, homeLabel, children }: {
children: ReactNode;
}) {
return (
<header className="border-b bg-background/95 supports-backdrop-filter:backdrop-blur">
<header data-slot="header-shell" className="border-b bg-background/95 supports-backdrop-filter:backdrop-blur">
<div className="mx-auto flex min-h-16 w-full max-w-7xl flex-nowrap items-center gap-2 px-4 py-2 sm:px-6">
<Link
href={homeHref}
+1 -1
View File
@@ -35,7 +35,7 @@ export function ConditionSwitcher({
{items.map((_, index) => (
<Fragment key={index}>
{index > 0 ? <Separator orientation="vertical" aria-hidden="true" className="mx-1 h-7" /> : null}
<TabsTrigger value={String(index)} className="outline-primary m-2 max-sm:min-h-11">
<TabsTrigger value={String(index)} className="max-sm:min-h-11">
<ConditionTabIcon iconUrl={iconUrls?.[index]} />
<span className="font-medium text-lg">{labels?.[index] || `เงื่อนไข ${index + 1}`}</span>
</TabsTrigger>
+6 -6
View File
@@ -86,10 +86,10 @@ export function GuidePageShell({
href={baseHref}
aria-current={!section ? "page" : undefined}
className={cn(
"flex min-h-8 shrink-0 items-center gap-2 whitespace-nowrap rounded-md px-3 text-sm transition-colors outline-none focus-visible:ring-2 focus-visible:ring-ring",
"flex min-h-8 shrink-0 items-center gap-2 whitespace-nowrap rounded-md border px-3 text-sm transition-colors outline-none focus-visible:ring-2 focus-visible:ring-ring",
!section
? "bg-primary/15 font-semibold text-primary"
: "text-muted-foreground hover:bg-muted hover:text-foreground",
? "border-primary bg-primary/15 font-semibold text-primary"
: "border-control-border bg-control text-foreground hover:border-control-border-hover hover:bg-control-hover",
)}
prefetch={false}
>
@@ -108,10 +108,10 @@ export function GuidePageShell({
href={`${baseHref}/${item.slug}`}
aria-current={section === item.slug ? "page" : undefined}
className={cn(
"flex min-h-8 shrink-0 items-center gap-2 whitespace-nowrap rounded-md px-3 text-sm transition-colors outline-none focus-visible:ring-2 focus-visible:ring-ring",
"flex min-h-8 shrink-0 items-center gap-2 whitespace-nowrap rounded-md border px-3 text-sm transition-colors outline-none focus-visible:ring-2 focus-visible:ring-ring",
section === item.slug
? "bg-primary/15 font-semibold text-primary"
: "text-muted-foreground hover:bg-muted hover:text-foreground",
? "border-primary bg-primary/15 font-semibold text-primary"
: "border-control-border bg-control text-foreground hover:border-control-border-hover hover:bg-control-hover",
)}
prefetch={false}
>
+2 -2
View File
@@ -30,7 +30,7 @@ export function PublicNavigation() {
return <>
<nav aria-label="เมนูหลัก" className="hidden items-center gap-1 lg:flex">
{destinations.slice(1).map(({ href, label, icon }) =>
<Link key={href} href={href} aria-current={isCurrent(href) ? "page" : undefined}
<Link key={href} href={href} data-slot="button" data-variant="ghost" aria-current={isCurrent(href) ? "page" : undefined}
className={buttonVariants({ variant: "ghost", size: "sm" })}>
{icon ? <Image src={icon} alt="" width={24} height={24} className="size-6 object-contain" /> : <ChartNoAxesCombinedIcon data-icon="inline-start" aria-hidden="true" />}{label}
</Link>)}
@@ -47,7 +47,7 @@ export function PublicNavigation() {
</DrawerHeader>
<nav aria-label="เมนูหลักบนมือถือ" className="grid gap-1 p-4">
{destinations.map(({ href, label, icon }) =>
<DrawerClose key={href} render={<Link href={href} aria-current={isCurrent(href) ? "page" : undefined}
<DrawerClose key={href} nativeButton={false} render={<Link href={href} aria-current={isCurrent(href) ? "page" : undefined}
className="flex min-h-11 items-center gap-3 rounded-lg px-3 text-sm hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" />}>
{icon ? <Image src={icon} alt="" width={20} height={20} className="size-5 object-contain" /> : href === "/analytics" ? <ChartNoAxesCombinedIcon aria-hidden="true" /> : <BookOpenIcon aria-hidden="true" />}{label}
{isCurrent(href) ? <span className="ml-auto text-xs text-primary">ปัจจุบัน</span> : null}
+6 -2
View File
@@ -7,6 +7,7 @@ import Link from "next/link";
import { useSearchParams } from "next/navigation";
import {
ArrowDownAZIcon,
ArrowRightIcon,
ArrowUpZAIcon,
Clock3Icon,
SearchIcon,
@@ -251,7 +252,10 @@ export function StructuredDirectory({ guides }: { guides: GuideCard[] }) {
</div>
<CardHeader className="min-w-0 flex-1 p-3 sm:p-4 sm:pt-5">
<div className="min-w-0 space-y-1">
<CardTitle className="line-clamp-1">{guide.name}</CardTitle>
<CardTitle className="flex min-w-0 items-center gap-2">
<span className="truncate">{guide.name}</span>
<ArrowRightIcon aria-hidden="true" className="size-4 shrink-0 text-primary" />
</CardTitle>
<CardDescription className="line-clamp-2 sm:line-clamp-3">
{guide.overview}
</CardDescription>
@@ -305,7 +309,7 @@ function GuideToggleFilter({
<div className={showLabels ? "flex flex-col gap-2" : "shrink-0 border-l pl-2"}>
<ToggleGroup aria-label={label} variant="outline" size="sm" multiple value={value} onValueChange={onChange} className={showLabels ? "flex flex-wrap justify-start" : undefined}>
{options.map((option) => (
<ToggleGroupItem key={option} value={option} aria-label={`${label}: ${optionLabels[option] ?? option}`} title={`${label}: ${optionLabels[option] ?? option}`} className="max-sm:min-h-11 aria-pressed:border-primary aria-pressed:bg-transparent aria-pressed:text-primary">
<ToggleGroupItem key={option} value={option} aria-label={`${label}: ${optionLabels[option] ?? option}`} title={`${label}: ${optionLabels[option] ?? option}`} className="max-sm:min-h-11">
{optionIcon?.(option)}{showLabels ? optionLabels[option] ?? option : optionText?.(option)}
</ToggleGroupItem>
))}
+8 -7
View File
@@ -4,20 +4,20 @@ import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"group/button inline-flex shrink-0 items-center justify-center rounded-lg border bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/80",
default: "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
outline:
"border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
"border-control-border bg-control text-foreground hover:border-control-border-hover hover:bg-control-hover aria-expanded:border-primary aria-expanded:bg-primary/15 aria-expanded:text-primary aria-[current=page]:border-primary aria-[current=page]:bg-primary/15 aria-[current=page]:text-primary",
secondary:
"bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
"border-control-border bg-control text-secondary-foreground hover:border-control-border-hover hover:bg-control-hover aria-expanded:border-primary aria-expanded:bg-primary/15 aria-expanded:text-primary",
ghost:
"hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
"border-control-border bg-control text-foreground hover:border-control-border-hover hover:bg-control-hover aria-expanded:border-primary aria-expanded:bg-primary/15 aria-expanded:text-primary aria-[current=page]:border-primary aria-[current=page]:bg-primary/15 aria-[current=page]:text-primary",
destructive:
"bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
link: "text-primary underline-offset-4 hover:underline",
"border-destructive bg-destructive/20 text-destructive hover:bg-destructive/30 focus-visible:border-destructive focus-visible:ring-destructive/40",
link: "border-transparent text-primary underline underline-offset-4 hover:decoration-2",
},
size: {
default:
@@ -49,6 +49,7 @@ function Button({
return (
<ButtonPrimitive
data-slot="button"
data-variant={variant}
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
+1 -1
View File
@@ -41,7 +41,7 @@ function SelectTrigger({
data-slot="select-trigger"
data-size={size}
className={cn(
"flex w-fit items-center justify-between gap-1.5 rounded-lg border border-input bg-transparent py-2 pr-2 pl-2.5 text-sm whitespace-nowrap transition-colors outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-placeholder:text-muted-foreground data-[size=default]:h-8 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"flex w-fit items-center justify-between gap-1.5 rounded-lg border border-control-border bg-control text-foreground hover:border-control-border-hover hover:bg-control-hover aria-expanded:border-primary aria-expanded:bg-primary/15 py-2 pr-2 pl-2.5 text-sm whitespace-nowrap transition-colors outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-placeholder:text-muted-foreground data-[size=default]:h-8 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
+5 -6
View File
@@ -24,11 +24,11 @@ function Tabs({
}
const tabsListVariants = cva(
"group/tabs-list inline-flex w-fit items-center justify-center rounded-lg p-[3px] text-muted-foreground group-data-horizontal/tabs:h-8 group-data-vertical/tabs:h-fit group-data-vertical/tabs:flex-col data-[variant=line]:rounded-none",
"group/tabs-list inline-flex w-fit items-center justify-center gap-1 rounded-lg p-[3px] text-muted-foreground group-data-horizontal/tabs:min-h-8 group-data-vertical/tabs:h-fit group-data-vertical/tabs:flex-col data-[variant=line]:rounded-none",
{
variants: {
variant: {
default: "bg-muted",
default: "border border-border bg-card",
line: "gap-1 bg-transparent",
},
},
@@ -58,10 +58,9 @@ function TabsTrigger({ className, ...props }: TabsPrimitive.Tab.Props) {
<TabsPrimitive.Tab
data-slot="tabs-trigger"
className={cn(
"relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-1.5 py-0.5 text-sm font-medium whitespace-nowrap text-foreground/60 transition-all group-data-vertical/tabs:w-full group-data-vertical/tabs:justify-start hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 has-data-[icon=inline-end]:pr-1 has-data-[icon=inline-start]:pl-1 aria-disabled:pointer-events-none aria-disabled:opacity-50 dark:text-muted-foreground dark:hover:text-foreground group-data-[variant=default]/tabs-list:data-active:shadow-sm group-data-[variant=line]/tabs-list:data-active:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent",
"data-active:bg-background data-active:text-foreground dark:data-active:border-input dark:data-active:bg-input/30 dark:data-active:text-foreground",
"after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-horizontal/tabs:after:inset-x-0 group-data-horizontal/tabs:after:bottom-[-5px] group-data-horizontal/tabs:after:h-0.5 group-data-vertical/tabs:after:inset-y-0 group-data-vertical/tabs:after:-right-1 group-data-vertical/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100",
"relative inline-flex min-h-6 flex-1 items-center justify-center gap-1.5 rounded-md border border-control-border bg-control px-1.5 py-0.5 text-sm font-medium whitespace-nowrap text-foreground transition-all group-data-vertical/tabs:w-full group-data-vertical/tabs:justify-start hover:border-control-border-hover hover:bg-control-hover focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 has-data-[icon=inline-end]:pr-1 has-data-[icon=inline-start]:pl-1 aria-disabled:pointer-events-none aria-disabled:opacity-50 group-data-[variant=default]/tabs-list:data-active:shadow-sm group-data-[variant=line]/tabs-list:data-active:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"data-active:border-primary data-active:bg-primary/15 data-active:text-primary",
"after:absolute after:bg-primary after:opacity-0 after:transition-opacity group-data-horizontal/tabs:after:inset-x-0 group-data-horizontal/tabs:after:bottom-0 group-data-horizontal/tabs:after:h-0.5 group-data-vertical/tabs:after:inset-y-0 group-data-vertical/tabs:after:right-0 group-data-vertical/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100",
className
)}
{...props}
+3 -3
View File
@@ -6,12 +6,12 @@ import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const toggleVariants = cva(
"group/toggle inline-flex items-center justify-center gap-1 rounded-lg text-sm font-medium whitespace-nowrap transition-all outline-none hover:bg-muted hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 aria-pressed:bg-muted data-[state=on]:bg-muted dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"group/toggle inline-flex items-center justify-center gap-1 rounded-lg border text-sm font-medium whitespace-nowrap transition-all outline-none hover:border-control-border-hover hover:bg-control-hover hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 aria-pressed:border-primary aria-pressed:bg-primary/15 aria-pressed:text-primary data-[state=on]:border-primary data-[state=on]:bg-primary/15 data-[state=on]:text-primary dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
{
variants: {
variant: {
default: "bg-transparent",
outline: "border border-input bg-transparent hover:bg-muted",
default: "border-control-border bg-control text-foreground",
outline: "border-control-border bg-control text-foreground",
},
size: {
default: