* 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>
411 lines
15 KiB
CSS
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;
|
|
}
|