Phase 7: polish — overview health, reduced-motion, mobile, a11y, copy-ids
- Overview health hero derives from subsystem probes when the backend gives no overall status, so it never says "Unknown" next to an all-Healthy Subsystems card (the audit's most jarring contradiction). Home error copy now uses the Phase-2 describeError instead of raw backend strings. - Global prefers-reduced-motion guard (transitions, loops, aurora field) + motion-reduce on the sidebar width transition. - Mobile: content reserves a top row (pt-16 md:p-6) so the floating hamburger never overlaps the page H1. - A11y: collapsed rail items get an explicit aria-label. - Monitoring: disk "busiest mount" tile shows the % as the value and the long mount path as a truncated caption, so it no longer clips mid-word. - New CopyId component (click-to-copy UUIDs), wired into Profile. Typecheck 36→36, 24-route sweep clean. Remaining items (lib a11y labels, table aria-sort, dev-only monitoring data) noted in the spec. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
51
app/components/copy-id.tsx
Normal file
51
app/components/copy-id.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import { useState } from "react"
|
||||
import { Check, Copy } from "lucide-react"
|
||||
|
||||
/**
|
||||
* A monospace id (UUID, slug, token name) that copies to the clipboard on
|
||||
* click, with a brief checkmark. Raw ids are common in an admin console and
|
||||
* useless if you can't get them into a support ticket or a CLI.
|
||||
*/
|
||||
export function CopyId({
|
||||
value,
|
||||
label,
|
||||
className = "",
|
||||
dataAction,
|
||||
}: {
|
||||
value: string
|
||||
/** What's being copied, for the aria-label. Defaults to "id". */
|
||||
label?: string
|
||||
className?: string
|
||||
dataAction?: string
|
||||
}) {
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
const copy = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(value)
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 1200)
|
||||
} catch {
|
||||
// Clipboard blocked (insecure context / permissions) — no-op; the value
|
||||
// is still selectable by hand.
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={copy}
|
||||
data-action={dataAction}
|
||||
aria-label={`Copy ${label ?? "id"}`}
|
||||
title="Copy"
|
||||
className={`group inline-flex max-w-full items-center gap-1 rounded bg-muted px-1.5 py-0.5 font-mono text-xs transition-colors hover:bg-accent ${className}`}
|
||||
>
|
||||
<span className="truncate">{value}</span>
|
||||
{copied ? (
|
||||
<Check className="size-3 shrink-0 text-emerald-500" />
|
||||
) : (
|
||||
<Copy className="size-3 shrink-0 text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100" />
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
@@ -360,7 +360,7 @@ export function AppShell({
|
||||
data-slot="sidebar"
|
||||
data-expanded={expanded ? "true" : "false"}
|
||||
className={[
|
||||
"sticky top-0 z-30 hidden h-svh shrink-0 flex-col border-r bg-sidebar transition-[width] duration-base ease-standard md:flex",
|
||||
"sticky top-0 z-30 hidden h-svh shrink-0 flex-col border-r bg-sidebar transition-[width] duration-base ease-standard motion-reduce:transition-none md:flex",
|
||||
expanded ? "w-60" : "w-16",
|
||||
].join(" ")}
|
||||
>
|
||||
@@ -656,7 +656,10 @@ export function AppShell({
|
||||
one floating card in a sea of black. The floating actions
|
||||
pill is fixed to the viewport edge and lives outside this
|
||||
column, so it stays clear regardless of cap width. */}
|
||||
<div className="mx-auto flex w-full max-w-[1180px] flex-1 flex-col gap-6 p-6 [&>*:first-child]:lg:pr-72">
|
||||
{/* pt-16 on mobile reserves a row for the floating hamburger (left)
|
||||
and actions pill (right) so they never overlap the page H1; the
|
||||
desktop rail/pill sit outside this column, so pt drops to p-6. */}
|
||||
<div className="mx-auto flex w-full max-w-[1180px] flex-1 flex-col gap-6 px-4 pb-6 pt-16 md:p-6 [&>*:first-child]:lg:pr-72">
|
||||
<RouteGuard>{children}</RouteGuard>
|
||||
</div>
|
||||
</div>
|
||||
@@ -693,6 +696,9 @@ function NavRow({
|
||||
to={item.to}
|
||||
end={item.end}
|
||||
title={expanded ? undefined : item.label}
|
||||
// When collapsed there's no visible label text, only an icon — give
|
||||
// screen readers the name explicitly (title alone isn't reliably read).
|
||||
aria-label={expanded ? undefined : item.label}
|
||||
onClick={onNavigate}
|
||||
data-action={`${prefix}${item.label.toLowerCase()}`}
|
||||
className={({ isActive }) =>
|
||||
|
||||
Reference in New Issue
Block a user