diff --git a/.jules/palette.md b/.jules/palette.md index c05638899..91300fb20 100644 --- a/.jules/palette.md +++ b/.jules/palette.md @@ -1,3 +1,7 @@ -## 2024-05-19 - Replace HTML disabled with aria-disabled="true" for Accessible Tooltips -**Learning:** Native HTML `disabled` attributes completely hide elements from screen readers and block all pointer/hover events, preventing tooltips from functioning for disabled elements. -**Action:** Replace `disabled` with `aria-disabled="true"`, enforce block click handlers via `e.preventDefault()`, and add a title tooltip directly to the element to maintain full tooltip accessibility and keyboard focus support for visually impaired and mouse users. +## 2026-09-09 - Accessible Tooltips on Disabled Elements +**Learning:** `aria-disabled="true"` exposes unavailable state but does not suppress activation. Icon-only controls therefore need an accessible name independent of the tooltip, and unavailable activation must still be blocked in code. +**Action:** Keep unavailable icon buttons focusable with `aria-disabled="true"`, give the trigger an `aria-label` matching the Tooltip text, and route pointer or keyboard-generated clicks through `preventUnavailableAction`. Use the design-system Tooltip only to explain the unavailable state; do not log or expose user-controlled values through tooltip copy. + +## 2026-09-11 - Preserve doctoring security boundaries during regeneration +**Learning:** This Tooltip slice cites external MDN, W3C, and Base UI documentation. Two later branch regenerations removed the reviewed `Security Notes` paragraph even though the runtime trust/network boundary had not changed, repeatedly reopening the same governance finding. +**Action:** When updating `docs/doctoring/sidebar-disabled-tooltips.md`, preserve its `Security Notes` section whenever external references remain. The note must keep those URLs documentation-only and state that this Tooltip slice adds no runtime fetch/navigation, subprocess, IPC, updater, model-download, credential, or trust-boundary path. If that security boundary truly changes, update the note and tests/review evidence explicitly rather than deleting it as formatting or generated-copy cleanup. diff --git a/apps/desktop/src/App.test.tsx b/apps/desktop/src/App.test.tsx index 3eed386f8..58d217e7e 100644 --- a/apps/desktop/src/App.test.tsx +++ b/apps/desktop/src/App.test.tsx @@ -225,7 +225,7 @@ describe("App", () => { for (const name of ["Import", "Export"]) { const navButton = within(primaryNav).getByRole("button", { name }); expect(navButton).toHaveAttribute("aria-disabled", "true"); - expect(navButton).toHaveAttribute("title", "Coming soon"); + expect(navButton).not.toHaveAttribute("title"); expect(navButton).not.toBeDisabled(); } fireEvent.click(within(primaryNav).getByRole("button", { name: "Import" })); @@ -236,7 +236,7 @@ describe("App", () => { for (const name of ["Import", "Export"]) { const navButton = within(compactNav).getByRole("button", { name: `${name} compact view` }); expect(navButton).toHaveAttribute("aria-disabled", "true"); - expect(navButton).toHaveAttribute("title", "Coming soon"); + expect(navButton).not.toHaveAttribute("title"); expect(navButton).not.toBeDisabled(); } fireEvent.click(within(compactNav).getByRole("button", { name: "Import compact view" })); diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx index f3d678454..42b28cb9e 100644 --- a/apps/desktop/src/App.tsx +++ b/apps/desktop/src/App.tsx @@ -24,6 +24,7 @@ import { X, type LucideIcon, } from "lucide-react"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { SUPPORTED_AUDIO_FORMATS, type AnalysisJobRequest, @@ -565,25 +566,43 @@ export function App() { const { label, enabled, active, title } = navButtonState(item); const { icon: Icon, view } = item; - return ( + const buttonClass = `flex min-h-11 w-full items-center gap-3 rounded-xl px-3 text-left text-sm font-semibold transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cyan-300 ${ + active + ? "bg-blue-600/70 text-white shadow-[0_12px_30px_rgba(37,99,235,0.32)]" + : enabled + ? "text-slate-200 hover:bg-white/5" + : "cursor-not-allowed text-slate-500 opacity-70" + }`; + + const buttonContent = ( + <> +