inspector/clients/web/src/App.css
Cliff Hall bb32d1db5f
fix(web): section-header hover outline + clear buttons out of tab order (#1487) (#1488)
* fix(web): section-header hover outline + clear buttons out of tab order (#1487)

Two button-behavior adjustments:

1. History section headers (Pinned / History) now use the filter-toggle
   style — a thin outline on hover instead of a background fill — by pointing
   the `sectionHeader` Group variant at the shared `.filter-toggle` class. The
   open section keeps its filled background (passed per-instance via `bg`).

2. Every input clear button added in #1448 is removed from the keyboard tab
   order via `tabIndex={-1}` (CloseButton clear buttons app-wide, plus the
   Autocomplete clear button through `theme/Autocomplete.ts` clearButtonProps),
   so tabbing through a form moves to the next field, not onto the clear button.
   The buttons stay mouse-clickable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(web): apply filter-toggle hover outline to Resources accordion headers (#1487)

The Resources screen `disclosure` accordion controls now share the same
outline-on-hover treatment as the filter toggle buttons and History section
headers: a thin border on hover instead of a background fill, plus a filled
background when the section is open (keyed off `aria-expanded`). The `.filter-
toggle` class is added to the disclosure control slot; the default-variant
accordion (Server Settings) keeps its background-fill hover via a `:not(.filter-
toggle)` guard.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(web): extract ClearButton element, replace 34 inline clear buttons

Addresses the code-review suggestion on PR #1488: the same five-line
`CloseButton` clear-affordance block (aria-label="Clear" + tabIndex={-1} +
onClick) was hand-copied across 17 files. Extract a single
`elements/ClearButton` (Mantine `CloseButton.withProps` with the aria-label and
tabIndex baked in) and replace all 34 call sites, so the clear-button contract
lives in one place. A global CloseButton theme default would be wrong here — it
would also catch tabbable modal-close X buttons and the Select clear buttons —
so a dedicated element is the right altitude.

Net -151 lines. Adds element unit tests + a Storybook play test covering the
aria-label / tabIndex / clickable contract for all sites at once.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 22:27:46 -04:00

411 lines
15 KiB
CSS

/*
* App.css — Global styles that CANNOT be expressed in the Mantine theme.
*
* Only the following belong here:
* - :root CSS custom properties (--inspector-* design tokens)
* - @keyframes animations
* - Pseudo-selectors (:hover, :focus) that cross component boundaries
* - Nested child-element selectors for third-party HTML output
* - Styles for native HTML elements (img, iframe, etc.)
*
* Everything else (margin, padding, background, border, color, font-size)
* belongs in the Mantine theme via Component.extend() or .withProps().
*/
:root {
/* ── Brand ─────────────────────────────────────────────── */
--inspector-brand-primary: var(--mantine-primary-color-filled);
/* ── Text ──────────────────────────────────────────────── */
--inspector-text-primary: var(--mantine-color-text);
--inspector-text-secondary: var(--mantine-color-dimmed);
--inspector-text-inverse: var(--mantine-color-white);
/* ── Surfaces ──────────────────────────────────────────── */
--inspector-surface-body: var(--mantine-color-dark-9);
--inspector-surface-card: var(--mantine-color-gray-0);
--inspector-surface-code: var(--mantine-color-white);
/* Subtle inset surface for modal call-outs / summary panels. Mantine's
`default-hover` already adapts to color scheme, so no dark override. */
--inspector-surface-subtle: var(--mantine-color-default-hover);
/* ── Inputs ────────────────────────────────────────────── */
--inspector-input-background: var(--mantine-color-white);
/* ── Borders ───────────────────────────────────────────── */
--inspector-border-default: var(--mantine-color-default-border);
--inspector-border-subtle: var(--mantine-color-gray-3);
/* ── Scrollbar ─────────────────────────────────────────── */
/* Matches Mantine ScrollArea's thumb so native scrollbars (used where a
ScrollArea would break flex sizing, e.g. the Resources accordion) blend in. */
--inspector-scrollbar-thumb: rgba(0, 0, 0, 0.4);
/* ── Connection status ─────────────────────────────────── */
--inspector-status-connected: var(--mantine-color-green-6);
--inspector-status-connecting: var(--mantine-color-yellow-6);
--inspector-status-disconnected: var(--mantine-color-gray-5);
--inspector-status-error: var(--mantine-color-red-6);
/* Red glow pulsed around a newly appearing tab's label (#1450) */
--inspector-glow-red: var(--mantine-color-red-6);
/* ── Log levels (RFC 5424) ─────────────────────────────── */
--inspector-log-debug: var(--mantine-color-gray-6);
--inspector-log-info: var(--mantine-color-blue-6);
--inspector-log-notice: var(--mantine-color-teal-6);
--inspector-log-warning: var(--mantine-color-yellow-6);
--inspector-log-error: var(--mantine-color-red-6);
--inspector-log-critical: var(--mantine-color-red-8);
--inspector-log-alert: var(--mantine-color-red-9);
--inspector-log-emergency: var(--mantine-color-red-9);
/* ── Task status ───────────────────────────────────────── */
--inspector-task-waiting: var(--mantine-color-gray-5);
--inspector-task-running: var(--mantine-color-blue-6);
--inspector-task-completed: var(--mantine-color-green-6);
--inspector-task-failed: var(--mantine-color-red-6);
--inspector-task-cancelled: var(--mantine-color-yellow-6);
/* ── History ───────────────────────────────────────────── */
--inspector-history-success: var(--mantine-color-green-6);
--inspector-history-error: var(--mantine-color-red-6);
}
/* Dark mode overrides — tokens that need adjustment in dark scheme */
[data-mantine-color-scheme="dark"] {
--inspector-surface-card: var(--mantine-color-gray-9);
--inspector-surface-code: var(--mantine-color-dark-9);
--inspector-border-subtle: var(--mantine-color-gray-4);
--inspector-input-background: var(--mantine-color-dark-9);
--inspector-scrollbar-thumb: rgba(255, 255, 255, 0.4);
}
/* ── Animations ──────────────────────────────────────────── */
@keyframes inspector-pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.4;
}
}
@keyframes inspector-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
/* Both motions travel downward: enter descends from above into place, exit
descends from place to below — used for the header crossfade (#1450). */
@keyframes inspector-fade-slide-in {
from {
opacity: 0;
transform: translateY(-0.5rem);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes inspector-fade-slide-out {
from {
opacity: 1;
transform: translateY(0);
}
to {
opacity: 0;
transform: translateY(0.5rem);
}
}
/* A red glow around a tab label's text that starts at full intensity the
moment a content-gated tab appears and fades out (#1450). */
@keyframes inspector-tab-glow {
from {
text-shadow:
0 0 6px var(--inspector-glow-red),
0 0 12px var(--inspector-glow-red);
}
to {
text-shadow: none;
}
}
/* ── Status dot animations ───────────────────────────────── */
.inspector-pulse {
animation: inspector-pulse 1.5s ease-in-out infinite;
}
.inspector-spin {
animation: inspector-spin 1s linear infinite;
}
/* Applied (via the `tabGlow` Text variant) to a tab label; the glow starts full
and fades, running only while `data-glow="on"` on a freshly-appeared tab. */
.tab-glow[data-glow="on"] {
animation: inspector-tab-glow 325ms ease-in;
}
/* ── Header connect/disconnect animations (#1450) ────────── */
/* Any header region that appears on connect / leaves on disconnect — the
center title↔tab-bar crossfade, the server name (left), and the status +
Disconnect controls (right). The element leaving ([data-anim="out"]) slides
down + fades out immediately; the appearing one ([data-anim="in"]) is delayed
by half the duration so they're staggered. Keep the 300ms/150ms here in sync
with HEADER_ANIM_MS in ViewHeader.tsx. */
.header-anim[data-anim="in"] {
animation: inspector-fade-slide-in 300ms ease 150ms both;
}
.header-anim[data-anim="out"] {
animation: inspector-fade-slide-out 300ms ease both;
}
/* The title and tab bar additionally share one grid cell so one replaces the
other in place during the center crossfade. */
.header-stack-cell {
grid-area: 1 / 1;
place-self: center;
}
/* Honor reduced-motion for the header animations this PR adds (#1450): skip the
fade/slide and the new-tab glow, leaving the elements at their resting state.
Selectors match the originals' specificity so they win inside the query. */
@media (prefers-reduced-motion: reduce) {
.header-anim[data-anim="in"],
.header-anim[data-anim="out"],
.tab-glow[data-glow="on"] {
animation: none;
}
}
/* ── Accordion disclosure chevron (right when closed, down when open) ──
*
* Mantine's default chevron rotation flips 180° on open (e.g. down → up). For
* the `disclosure` Accordion variant we use a right-pointing chevron and rotate
* it only 90° so closed sections point right and open sections point down — the
* standard disclosure direction, matching ResourceLink's chevron (issue #1462).
* `[data-rotate]` is set by Mantine on the chevron slot when the item is open;
* the class+attribute selector outranks Mantine's `:where()`-wrapped default. */
/* Restore a smooth rotation: the accordion sets `transitionDuration={0}` to
* disable its panel height animation (which fights the flex layout), and that
* zeroes the shared `--accordion-transition-duration` the chevron would use.
* The 200ms restates Mantine's default accordion duration — keep it in sync if
* accordion timing is ever themed. */
.disclosure-chevron {
transition: transform 200ms ease;
}
.disclosure-chevron[data-rotate] {
transform: rotate(90deg);
}
/* ── Full-height sectioned accordion (Resources) ─────────────────
*
* The `disclosure` Accordion fills its full-height card as a flex column. The
* section controls (headers) stay pinned while each OPEN section's panel scrolls
* within its own share of the remaining height — the per-item `flex` weight
* (set in ResourceControls) hands space from long sections to short ones. With
* `flex-grow: 0`, nothing expands or scrolls until the combined content
* overflows the panel, so sections never scroll while the panel has free space
* (issue #1462). Closed sections collapse to their header height.
*
* The per-section scroll uses a NATIVE scrollbar, styled below to match
* Mantine's ScrollArea thumb (#1466). A Mantine `ScrollArea` component can't be
* used here: its viewport has no intrinsic height, which would collapse the
* panel's content-based `flex-basis` that the per-section sizing above depends
* on, so the sections would no longer size to their content. */
.disclosure-sections {
display: flex;
flex-direction: column;
min-height: 0;
}
.disclosure-sections > .mantine-Accordion-item[data-active] {
display: flex;
flex-direction: column;
min-height: 0;
}
.disclosure-sections .mantine-Accordion-control {
flex: 0 0 auto;
}
.disclosure-sections
> .mantine-Accordion-item[data-active]
.mantine-Accordion-panel {
flex: 1 1 auto;
min-height: 0;
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: var(--inspector-scrollbar-thumb) transparent;
}
.disclosure-sections
> .mantine-Accordion-item[data-active]
.mantine-Accordion-panel::-webkit-scrollbar {
width: 8px;
}
.disclosure-sections
> .mantine-Accordion-item[data-active]
.mantine-Accordion-panel::-webkit-scrollbar-thumb {
background-color: var(--inspector-scrollbar-thumb);
border-radius: 8px;
}
.disclosure-sections
> .mantine-Accordion-item[data-active]
.mantine-Accordion-panel::-webkit-scrollbar-track {
background: transparent;
}
/* ── List item hover ────────────────────────────────────── */
.list-item:hover {
background-color: var(--mantine-primary-color-light);
}
[data-mantine-color-scheme="dark"] .list-item:hover {
background-color: var(--mantine-primary-color-light);
}
/* ── Filter toggle buttons (Logging / History / Network) ─────────
*
* Shared by the FilterToggleButton element, the History list's collapsible
* section headers (the `sectionHeader` Group variant), and the Resources
* disclosure accordion controls (the `disclosure` Accordion variant). The
* active (on / open) state is keyed off `aria-pressed` for the toggle buttons
* and `aria-expanded` for the accordion controls. Unlike `.list-item`,
* hover is a thin border rather than a background fill, and the active (on)
* state is the filled background. Keeping the two states distinct means
* toggling a button off while the cursor is still over it is immediately
* visible, instead of the hover background masking the removal of the active
* background (issue #1460). The 1px transparent border is reserved here (not as
* an inline style in the theme variant — inline styles outrank stylesheet
* `:hover` rules) so the hover border doesn't shift layout. */
.filter-toggle {
border: 1px solid transparent;
border-radius: var(--mantine-radius-md);
}
.filter-toggle:hover {
border-color: var(--inspector-brand-primary);
}
.filter-toggle[aria-pressed="true"],
.filter-toggle[aria-expanded="true"] {
background-color: var(--mantine-primary-color-light);
}
/* ── Select / Autocomplete dropdown / option (dark mode) ─────────
*
* Select and Autocomplete each render with their own static selector
* (`mantine-Select-*` / `mantine-Autocomplete-*`) — they share the
* Combobox primitive but don't emit a shared `mantine-Combobox-*`
* class on the dropdown root. List both so the argument-completion
* Autocompletes get the same gray surface + primary-light hover as
* the filter dropdowns. */
[data-mantine-color-scheme="dark"] .mantine-Select-dropdown,
[data-mantine-color-scheme="dark"] .mantine-Autocomplete-dropdown {
background-color: var(--mantine-color-gray-8);
}
[data-mantine-color-scheme="dark"] .mantine-Select-option:hover,
[data-mantine-color-scheme="dark"] .mantine-Autocomplete-option:hover {
background-color: var(--mantine-primary-color-light);
}
/* ── Menu dropdown / option (dark mode) ──────────────────────── */
[data-mantine-color-scheme="dark"] .mantine-Menu-dropdown {
background-color: var(--mantine-color-gray-8);
}
[data-mantine-color-scheme="dark"] .mantine-Menu-item:hover {
background-color: var(--mantine-primary-color-light);
}
/* ── Segmented control label (dark mode) ──────────────────────── */
[data-mantine-color-scheme="dark"] .mantine-SegmentedControl-indicator {
background-color: var(--mantine-primary-color-light);
}
/* ── Accordion button hover ─────────────────────────────── */
/* Default accordions (e.g. Server Settings) keep the background-fill hover.
* The Resources `disclosure` accordion opts out via `.filter-toggle` (added to
* its control slot in theme/Accordion.ts) so it gets the outline-on-hover
* treatment instead — see the `.filter-toggle` rules above. */
.mantine-Accordion-control:not(.filter-toggle):hover {
background-color: var(--mantine-primary-color-light-hover);
}
/* ── Grid alignment ─────────────────────────────────────── */
.grid-align-start {
align-items: start;
}
/* ── Markdown content (third-party HTML from react-markdown) ───── */
.markdown-content {
max-width: 100%;
overflow-wrap: anywhere;
}
.markdown-content > :first-child {
margin-top: 0;
}
.markdown-content > :last-child {
margin-bottom: 0;
}
.markdown-content pre {
max-width: 100%;
overflow-x: auto;
}
.markdown-content code {
word-break: break-word;
}
.markdown-content table {
display: block;
max-width: 100%;
overflow-x: auto;
}
.markdown-content img {
max-width: 100%;
height: auto;
}
/*
* Reorder grip on ServerCard. The grab/grabbing cursor is a draggability
* affordance that can't be expressed as a Mantine prop (and the active-press
* state is a pseudo-selector), so the whole grab-cursor unit lives here rather
* than split across the theme.
*/
.server-drag-handle {
cursor: grab;
}
.server-drag-handle:active {
cursor: grabbing;
}