From dda6cb2e44bc729d5eff986698696d97b2f4b2cb Mon Sep 17 00:00:00 2001 From: Jake Jarvis Date: Thu, 28 May 2026 12:27:41 -0400 Subject: [PATCH] fix: accessibility pass and miscellaneous UI polish across components - Add `aria-hidden="true"`, `aria-label`, `aria-labelledby`, `aria-expanded`, `aria-controls`, and `role="region"` attributes throughout builder, detail, docs, and search components - Fix `describeError` for `incompatible-peer` to show the module's human-readable label instead of the raw peer ID string - Replace `IconLoader2` spinner ad-hoc pattern with a shared `Spinner` component; delete unused `popover.tsx` - Add `IconExternalLink` indicator to deps table rows and make version column `tabular-nums` - Fix scripts table to use `break-words whitespace-pre-wrap` on the command column so long commands wrap instead of overflowing - Add `truncate overflow-hidden` to the file preview path header bar - Capitalize "Environment Variables", "Required", "Optional" badge labels; add trailing period to empty-state copy - Refactor `use-pointer-capability` and `use-time-ago` hooks for clarity; update chart legend, tooltip, and activity chart components --- .../src/components/builder/file-preview.tsx | 10 +-- .../src/components/builder/module-cards.tsx | 17 ++-- .../components/detail/adapter-switcher.tsx | 7 +- apps/web/src/components/detail/tables.tsx | 29 ++++--- .../src/components/detail/templates-list.tsx | 13 ++- apps/web/src/components/docs/docs-actions.tsx | 59 ++++++++------ apps/web/src/components/docs/docs-sidebar.tsx | 16 ++-- apps/web/src/components/docs/docs-toc.tsx | 7 +- .../src/components/evilcharts/ui/chart.tsx | 17 +++- .../src/components/evilcharts/ui/legend.tsx | 79 +++++++++++++------ .../src/components/evilcharts/ui/tooltip.tsx | 3 +- apps/web/src/components/footer.tsx | 4 +- apps/web/src/components/header.tsx | 4 +- apps/web/src/components/logo.tsx | 2 +- apps/web/src/components/module-logo.tsx | 4 +- .../src/components/package-manager-select.tsx | 11 +-- apps/web/src/components/route-boundaries.tsx | 6 +- .../web/src/components/search/site-search.tsx | 76 ++++++++++++------ .../src/components/stats/activity-chart.tsx | 32 ++++---- apps/web/src/components/stats/bar-list.tsx | 18 +++-- apps/web/src/components/theme-toggle.tsx | 17 ++-- apps/web/src/components/ui/badge.tsx | 2 +- apps/web/src/components/ui/copy-button.tsx | 43 +++++----- apps/web/src/components/ui/kbd.tsx | 6 +- apps/web/src/components/ui/popover.tsx | 77 ------------------ apps/web/src/components/ui/resizable.tsx | 2 +- apps/web/src/components/ui/scroll-area.tsx | 2 +- apps/web/src/components/ui/skeleton.tsx | 3 +- apps/web/src/components/ui/sonner.tsx | 14 ++-- apps/web/src/components/ui/spinner.tsx | 16 ++++ apps/web/src/components/ui/tabs.tsx | 2 +- apps/web/src/components/ui/toggle.tsx | 2 +- apps/web/src/hooks/use-pointer-capability.ts | 34 +++----- apps/web/src/hooks/use-time-ago.ts | 17 ++-- apps/web/src/routes/docs.$.tsx | 6 +- .../web/src/routes/registry.$category.$id.tsx | 24 ++++-- .../src/routes/registry.$category.index.tsx | 21 +++-- apps/web/src/routes/stats.tsx | 31 +++++--- 38 files changed, 413 insertions(+), 320 deletions(-) delete mode 100644 apps/web/src/components/ui/popover.tsx create mode 100644 apps/web/src/components/ui/spinner.tsx diff --git a/apps/web/src/components/builder/file-preview.tsx b/apps/web/src/components/builder/file-preview.tsx index c86af4e..c6a753f 100644 --- a/apps/web/src/components/builder/file-preview.tsx +++ b/apps/web/src/components/builder/file-preview.tsx @@ -1,6 +1,5 @@ import { themeToTreeStyles } from "@pierre/trees"; import { FileTree, useFileTree } from "@pierre/trees/react"; -import { IconLoader2 } from "@tabler/icons-react"; import { useLocation, useNavigate } from "@tanstack/react-router"; import type { ReactNode } from "react"; import { useCallback, useEffect, useMemo, useRef } from "react"; @@ -9,6 +8,7 @@ import { FILE_TREE_ICONS } from "@/components/builder/file-tree-icons"; import { useTheme } from "@/components/theme-provider"; import { Card } from "@/components/ui/card"; import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable"; +import { Spinner } from "@/components/ui/spinner"; import { useGracePeriod } from "@/hooks/use-grace-period"; import { useMediaQuery } from "@/hooks/use-media-query"; import type { Preview } from "@/server/highlighter"; @@ -243,9 +243,9 @@ export function FilePreview({
- +
) : null} @@ -278,14 +278,14 @@ function PreviewPane({ if (!preview || !path) { return (
- Pick a file to preview + Pick a file to preview.
); } return (
-
+
{path}
} > - +
{selected && ( - +

{m.description}

@@ -227,12 +227,17 @@ const ModuleCard = memo(function ModuleCard({ ); }); -function describeError(error: ResolveError): string { +function describeError( + error: ResolveError, + pending: Partial>, +): string { switch (error.kind) { case "missing-peer": return `Pick a ${categoryLabel(error.category)} module first.`; - case "incompatible-peer": - return `Not compatible with ${error.peer} (${categoryLabel(error.category)}).`; + case "incompatible-peer": { + const peerLabel = pending[error.category]?.label ?? error.peer; + return `Not compatible with ${peerLabel} (${categoryLabel(error.category)}).`; + } case "no-adapter": return "Not compatible with your current stack."; default: diff --git a/apps/web/src/components/detail/adapter-switcher.tsx b/apps/web/src/components/detail/adapter-switcher.tsx index 67b934a..12eff82 100644 --- a/apps/web/src/components/detail/adapter-switcher.tsx +++ b/apps/web/src/components/detail/adapter-switcher.tsx @@ -47,15 +47,20 @@ export function AdapterSwitcher({
{switchable.map(([slot, options]) => { const active = resolvedPeers[slot]; + const labelId = `adapter-switcher-${slot}`; return (
- + {categoryLabel(slot)} { const next = value[0]; diff --git a/apps/web/src/components/detail/tables.tsx b/apps/web/src/components/detail/tables.tsx index 11ca142..a9cbae5 100644 --- a/apps/web/src/components/detail/tables.tsx +++ b/apps/web/src/components/detail/tables.tsx @@ -1,4 +1,5 @@ import type { EnvVar } from "@stanza/registry"; +import { IconExternalLink } from "@tabler/icons-react"; import { Section, SectionList } from "@/components/detail/section"; import { Badge } from "@/components/ui/badge"; @@ -16,12 +17,17 @@ export function DepsTable({ title, entries }: { title: string; entries: Record - - {name} + + {name} + - {version} + {version} ))} @@ -41,10 +47,12 @@ export function ScriptsTable({ entries }: { entries: Record }) {
  • - {name} - {command} + {name} + + {command} +
  • ))} @@ -56,16 +64,19 @@ export function EnvTable({ env }: { env: EnvVar[] }) { if (env.length === 0) return null; return ( -
    +
    {env.map((e) => (
  • {e.name} - {e.required ? "required" : "optional"} + {e.required ? "Required" : "Optional"} - = {e.example} + + {"= "} + {e.example} +
    {e.description &&

    {e.description}

    }
  • diff --git a/apps/web/src/components/detail/templates-list.tsx b/apps/web/src/components/detail/templates-list.tsx index 7fa94b9..561bdd8 100644 --- a/apps/web/src/components/detail/templates-list.tsx +++ b/apps/web/src/components/detail/templates-list.tsx @@ -37,26 +37,28 @@ function TemplateRow({ }) { const [open, setOpen] = useState(false); const hasPreview = Boolean(preview); + const previewId = `template-preview-${template.dest.replaceAll(/[^a-zA-Z0-9_-]/g, "-")}`; return (
  • - {open && preview && } + {open && preview && }
  • ); } -function PreviewBlock({ preview }: { preview: Preview }) { +function PreviewBlock({ id, dest, preview }: { id: string; dest: string; preview: Preview }) { const { resolvedTheme } = useTheme(); const inner = useMemo( () => ({ __html: resolvedTheme === "dark" ? preview.dark : preview.light }), @@ -64,6 +66,9 @@ function PreviewBlock({ preview }: { preview: Preview }) { ); return (
    diff --git a/apps/web/src/components/docs/docs-actions.tsx b/apps/web/src/components/docs/docs-actions.tsx index 6728163..d89e17c 100644 --- a/apps/web/src/components/docs/docs-actions.tsx +++ b/apps/web/src/components/docs/docs-actions.tsx @@ -3,6 +3,7 @@ import { IconCheck, IconChevronDown, IconCopy } from "@tabler/icons-react"; import * as React from "react"; import type { ReactElement, SVGProps } from "react"; +import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { ButtonGroup } from "@/components/ui/button-group"; @@ -75,7 +76,14 @@ export function DocsActions({ markdownPath, pageUrl, className }: DocsActionsPro } + render={ + + } >
  • @@ -36,7 +37,7 @@ export function DocsToc({ toc }: { toc: TOCItemType[] }) { if (toc.length === 0) return null; return ( - + ); } diff --git a/apps/web/src/components/evilcharts/ui/chart.tsx b/apps/web/src/components/evilcharts/ui/chart.tsx index a438ec7..34809e9 100644 --- a/apps/web/src/components/evilcharts/ui/chart.tsx +++ b/apps/web/src/components/evilcharts/ui/chart.tsx @@ -3,6 +3,7 @@ import * as React from "react"; import * as RechartsPrimitive from "recharts"; +import { Spinner } from "@/components/ui/spinner"; import { cn } from "@/lib/utils"; // Format: { THEME_NAME: CSS_SELECTOR } @@ -138,10 +139,14 @@ function LoadingIndicator({ isLoading }: { isLoading: boolean }) { } return ( -
    +
    -
    - Loading +
    ); @@ -243,8 +248,12 @@ export function getPayloadConfigFromPayload(config: ChartConfig, payload: unknow } // Format values to percent for expanded charts +const percentFormatter = new Intl.NumberFormat(undefined, { + style: "percent", + maximumFractionDigits: 0, +}); function axisValueToPercentFormatter(value: number) { - return `${Math.round(value * 100).toFixed(0)}%`; + return percentFormatter.format(value); } // Pick the first string/number candidate as a series key, falling back to "value". diff --git a/apps/web/src/components/evilcharts/ui/legend.tsx b/apps/web/src/components/evilcharts/ui/legend.tsx index 161d2b3..27053b1 100644 --- a/apps/web/src/components/evilcharts/ui/legend.tsx +++ b/apps/web/src/components/evilcharts/ui/legend.tsx @@ -74,25 +74,36 @@ function ChartLegendContent({ // Get colors count for this item to determine gradient vs solid const colorsCount = itemConfig ? getColorsCount(itemConfig) : 1; + const itemClassName = cn( + "flex items-center gap-1.5 transition-opacity motion-reduce:transition-none [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground", + !isSelected && "opacity-30", + ); + const indicator = + itemConfig?.icon && !hideIcon ? ( + + ) : ( + + ); + if (isClickable) { + return ( + + ); + } return ( -
    svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground", - !isSelected && "opacity-30", - isClickable && "cursor-pointer", - )} - onClick={() => { - if (!isClickable) return; - - onSelectChange?.(selected === key ? null : key); - }} - > - {itemConfig?.icon && !hideIcon ? ( - - ) : ( - - )} +
    + {indicator} {itemConfig?.label}
    ); @@ -120,26 +131,44 @@ function LegendIndicator({ switch (variant) { case "square": - return
    ; + return