mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-07-10 01:29:41 +00:00
fix(web): complete mobile responsiveness (header overflow + a11y) (#589)
Some checks are pending
CI / semgrep (push) Waiting to run
Some checks are pending
CI / semgrep (push) Waiting to run
Adopt the more complete responsive implementation from #587 over the initial one in #582: header controls become a hidden-scrollbar strip instead of overflowing the page on phones, the sidebar drawer uses visibility:hidden when closed so its links leave the tab order, aria attributes on the toggle, 100dvh for mobile browser chrome, and 36px touch targets.
This commit is contained in:
parent
4f86e6ccfb
commit
05d4cc06e5
1 changed files with 55 additions and 37 deletions
|
|
@ -38,7 +38,7 @@ function SideLink({ active, onClick, children }: { active: boolean; onClick: ()
|
|||
type="button"
|
||||
onClick={onClick}
|
||||
className={cn(
|
||||
'flex items-center gap-2 rounded-md px-2.5 py-1.5 text-left text-[13.5px] transition-colors',
|
||||
'flex items-center gap-2 rounded-md px-2.5 py-1.5 text-left text-[13.5px] transition-colors max-md:min-h-9',
|
||||
active ? 'bg-interactive-secondary font-medium text-foreground' : 'font-light text-muted-foreground hover:text-foreground',
|
||||
)}
|
||||
>
|
||||
|
|
@ -347,6 +347,8 @@ export function App() {
|
|||
const [view, setView] = useState<string>('all')
|
||||
const [unit, setUnit] = useState<Unit>('cost')
|
||||
const [searchOpen, setSearchOpen] = useState(false)
|
||||
// Mobile only: the sidebar collapses to an off-canvas drawer below md.
|
||||
// On desktop this flag is inert (the max-md: transform classes don't apply).
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false)
|
||||
const [responded, setResponded] = useState<Set<string>>(new Set())
|
||||
|
||||
|
|
@ -426,38 +428,38 @@ export function App() {
|
|||
const label = local?.payload?.current?.label ?? ''
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-outer-background p-2.5">
|
||||
<div className="flex h-[calc(100vh-20px)] flex-col gap-2.5">
|
||||
<header className="flex h-12 shrink-0 items-center gap-4 rounded-md border border-border bg-card px-5 shadow-[0_2px_8px_rgba(0,0,0,0.03)] sticky top-0 z-40">
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSidebarOpen(!sidebarOpen)}
|
||||
className="md:hidden flex items-center justify-center w-9 h-9 rounded-md border border-border bg-interactive-secondary text-foreground"
|
||||
aria-label="Toggle sidebar"
|
||||
>
|
||||
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
||||
<line x1="3" y1="6" x2="21" y2="6" />
|
||||
<line x1="3" y1="12" x2="21" y2="12" />
|
||||
<line x1="3" y1="18" x2="21" y2="18" />
|
||||
</svg>
|
||||
</button>
|
||||
<div className="min-h-screen bg-outer-background p-2.5 max-md:min-h-[100dvh]">
|
||||
<div className="flex h-[calc(100vh-20px)] flex-col gap-2.5 max-md:h-[calc(100dvh-20px)]">
|
||||
<header className="flex h-12 shrink-0 items-center gap-4 rounded-md border border-border bg-card px-5 shadow-[0_2px_8px_rgba(0,0,0,0.03)] max-md:gap-3 max-md:px-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSidebarOpen(true)}
|
||||
aria-label="Open menu"
|
||||
aria-expanded={sidebarOpen}
|
||||
aria-controls="dashboard-sidebar"
|
||||
className="-ml-0.5 flex h-9 w-9 shrink-0 items-center justify-center rounded-md text-foreground transition-colors hover:bg-interactive-secondary md:hidden"
|
||||
>
|
||||
<svg viewBox="0 0 16 16" width="18" height="18" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round">
|
||||
<path d="M2.5 4.5h11M2.5 8h11M2.5 11.5h11" />
|
||||
</svg>
|
||||
</button>
|
||||
<div className="flex items-center gap-2 max-md:shrink-0">
|
||||
<img src="/codeburn-logo.png" alt="CodeBurn" className="h-6 w-6" />
|
||||
<span className="text-lg font-semibold tracking-[-0.02em] text-foreground">
|
||||
Code<span className="text-[#e8553a]">Burn</span>
|
||||
</span>
|
||||
<span className="ml-1 text-[11px] font-light uppercase tracking-[0.14em] text-tertiary-foreground">usage</span>
|
||||
<span className="ml-1 text-[11px] font-light uppercase tracking-[0.14em] text-tertiary-foreground max-sm:hidden">usage</span>
|
||||
</div>
|
||||
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
<div className="flex rounded-md border border-border bg-interactive-secondary p-0.5">
|
||||
<div className="ml-auto flex items-center gap-2 max-md:min-w-0 max-md:overflow-x-auto max-md:[-ms-overflow-style:none] max-md:[scrollbar-width:none] max-md:[&::-webkit-scrollbar]:hidden">
|
||||
<div className="flex rounded-md border border-border bg-interactive-secondary p-0.5 max-md:shrink-0">
|
||||
{PERIODS.map((p) => (
|
||||
<button
|
||||
key={p.key}
|
||||
type="button"
|
||||
onClick={() => setPeriod(p.key)}
|
||||
className={cn(
|
||||
'rounded-[5px] px-3 py-1 text-xs font-medium transition-colors',
|
||||
'rounded-[5px] px-3 py-1 text-xs font-medium transition-colors max-md:inline-flex max-md:min-h-9 max-md:items-center max-md:justify-center',
|
||||
period === p.key ? 'bg-active-primary text-foreground shadow-sm' : 'text-tertiary-foreground hover:text-foreground',
|
||||
)}
|
||||
>
|
||||
|
|
@ -465,14 +467,14 @@ export function App() {
|
|||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex rounded-md border border-border bg-interactive-secondary p-0.5">
|
||||
<div className="flex rounded-md border border-border bg-interactive-secondary p-0.5 max-md:shrink-0">
|
||||
{(['cost', 'tokens'] as Unit[]).map((u) => (
|
||||
<button
|
||||
key={u}
|
||||
type="button"
|
||||
onClick={() => setUnit(u)}
|
||||
className={cn(
|
||||
'rounded-[5px] px-3 py-1 text-xs font-medium transition-colors',
|
||||
'rounded-[5px] px-3 py-1 text-xs font-medium transition-colors max-md:inline-flex max-md:min-h-9 max-md:items-center max-md:justify-center',
|
||||
unit === u ? 'bg-active-primary text-foreground shadow-sm' : 'text-tertiary-foreground hover:text-foreground',
|
||||
)}
|
||||
>
|
||||
|
|
@ -483,7 +485,7 @@ export function App() {
|
|||
<select
|
||||
value={provider}
|
||||
onChange={(e) => setProvider(e.target.value)}
|
||||
className="rounded-md border border-border bg-card px-3 py-1.5 text-xs text-foreground outline-none"
|
||||
className="rounded-md border border-border bg-card px-3 py-1.5 text-xs text-foreground outline-none max-md:min-h-9 max-md:shrink-0"
|
||||
>
|
||||
<option value="all">All tools</option>
|
||||
{providerOptions.map((p) => (
|
||||
|
|
@ -495,19 +497,35 @@ export function App() {
|
|||
</div>
|
||||
</header>
|
||||
|
||||
<div className="flex min-h-0 flex-1 gap-2.5 flex-col md:flex-row">
|
||||
<div className="flex min-h-0 flex-1 gap-2.5">
|
||||
{sidebarOpen && (
|
||||
<div
|
||||
className="fixed inset-0 z-40 bg-black/40 backdrop-blur-sm md:hidden"
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Close menu"
|
||||
onClick={() => setSidebarOpen(false)}
|
||||
className="fixed inset-0 z-30 bg-black/40 md:hidden"
|
||||
/>
|
||||
)}
|
||||
<aside className={cn(
|
||||
"flex w-60 shrink-0 flex-col gap-5 overflow-y-auto rounded-md border border-border bg-card p-5",
|
||||
"fixed inset-y-0 left-0 z-50 transition-transform duration-300",
|
||||
"md:relative md:translate-x-0",
|
||||
sidebarOpen ? "translate-x-0" : "-translate-x-full md:translate-x-0"
|
||||
)}>
|
||||
<aside
|
||||
id="dashboard-sidebar"
|
||||
className={cn(
|
||||
'flex w-60 shrink-0 flex-col gap-5 overflow-y-auto rounded-md border border-border bg-card p-5',
|
||||
'max-md:fixed max-md:inset-y-0 max-md:left-0 max-md:z-40 max-md:rounded-none max-md:shadow-2xl max-md:transition-[transform,visibility] max-md:duration-200 max-md:ease-out',
|
||||
// Closed below md: slide off-canvas AND go visibility:hidden so its
|
||||
// links leave the tab order / a11y tree (not just visually hidden).
|
||||
sidebarOpen ? 'max-md:visible max-md:translate-x-0' : 'max-md:invisible max-md:-translate-x-full',
|
||||
)}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Close menu"
|
||||
onClick={() => setSidebarOpen(false)}
|
||||
className="absolute right-3 top-3 flex h-9 w-9 items-center justify-center rounded-md text-tertiary-foreground transition-colors hover:bg-interactive-secondary hover:text-foreground md:hidden"
|
||||
>
|
||||
<svg viewBox="0 0 16 16" width="15" height="15" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round">
|
||||
<path d="M4 4l8 8M12 4l-8 8" />
|
||||
</svg>
|
||||
</button>
|
||||
<div className="flex flex-col gap-1">
|
||||
<p className="mb-1 px-2.5 text-[11px] font-semibold uppercase tracking-[0.12em] text-heading">Devices</p>
|
||||
{multi && (
|
||||
|
|
@ -530,8 +548,8 @@ export function App() {
|
|||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSearchOpen(true)}
|
||||
className="flex items-center justify-center gap-2 rounded-md border border-border px-3 py-2 text-xs font-medium text-foreground transition-colors hover:bg-interactive-secondary"
|
||||
onClick={() => { setSearchOpen(true); setSidebarOpen(false) }}
|
||||
className="flex items-center justify-center gap-2 rounded-md border border-border px-3 py-2 text-xs font-medium text-foreground transition-colors hover:bg-interactive-secondary max-md:min-h-9"
|
||||
>
|
||||
<svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round">
|
||||
<circle cx="7" cy="7" r="4.5" />
|
||||
|
|
@ -545,7 +563,7 @@ export function App() {
|
|||
<button
|
||||
type="button"
|
||||
onClick={() => void toggleShare()}
|
||||
className="flex w-full items-center justify-between rounded-md px-2.5 py-1.5 text-[13.5px] text-foreground transition-colors hover:bg-interactive-secondary"
|
||||
className="flex w-full items-center justify-between rounded-md px-2.5 py-1.5 text-[13.5px] text-foreground transition-colors hover:bg-interactive-secondary max-md:min-h-9"
|
||||
>
|
||||
<span>Share this device</span>
|
||||
<Switch on={!!shareInfo?.sharing} />
|
||||
|
|
@ -612,7 +630,7 @@ export function App() {
|
|||
</div>
|
||||
</aside>
|
||||
|
||||
<main className="min-w-0 flex-1 overflow-y-auto pr-0.5 w-full">
|
||||
<main className="min-w-0 flex-1 overflow-y-auto pr-0.5">
|
||||
<div className="mb-3 flex items-baseline justify-between">
|
||||
<h1 className="font-display text-xl tracking-tight text-foreground">{viewTitle}</h1>
|
||||
<span className="text-xs text-tertiary-foreground">{label}</span>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue