inspector/clients/web/src/theme/theme.ts
Cliff Hall 092165476e
web: pin Server/Client Settings modal header while body scrolls (#1753)
* web: pin Server/Client Settings modal header while body scrolls (#1698)

Expanding enough accordion sections grew the Server Settings modal past the
viewport, so the whole modal scrolled and carried the header (title + close)
out of view. The header was a custom Group inside the modal body, and the modal
used no `scrollAreaComponent`, so overflow was not confined to the body.

Switch both the Server Settings and Client Settings modals from the default
`<Modal>` (which always wraps children in `Modal.Body`) to the compound
`Modal.Root`/`Modal.Content`/`Modal.Header`/`Modal.Body` API, moving the
toggle/title/close row into `Modal.Header` (sticky by design) and setting
`scrollAreaComponent={ScrollArea.Autosize}` so only the body scrolls.
`Modal.Root` keeps the close-on-escape / close-on-click-outside / focus-trap /
scroll-lock / portal defaults; `transitionProps` restores the fade-down
animation the `<Modal>` wrapper otherwise supplies. Client Settings does not yet
have enough content to scroll but is kept consistent per the issue.

Closes #1698

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5

* docs(pr-screenshots): Server Settings modal sticky-header proof (#1698)

Two frames from the ServerSettingsModal story (all sections expanded, short
viewport): header pinned at the top of the scroll and again scrolled down,
showing it stays put while only the body scrolls.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5

* web: move Modal.Root fade-down transition into the theme (#1698 review)

Addresses the review note on PR #1753: the `transitionProps={{ transition:
"fade-down", duration: 200 }}` literal was duplicated in both settings modals to
restore the animation `<Modal>` defaults to but `Modal.Root` doesn't inherit.
Move it to a `ThemeModalRoot` (`Modal.Root.extend`) registered under the
`ModalRoot` theme key, mirroring the existing `ThemeScrollAreaAutosize`
precedent. Both call sites drop the literal; any future compound `Modal.Root`
now animates consistently with every plain `<Modal>` from one source of truth.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 19:57:03 -04:00

209 lines
4.3 KiB
TypeScript

import { createTheme, type MantineColorsTuple } from "@mantine/core";
import {
ThemeAccordion,
ThemeActionIcon,
ThemeAlert,
ThemeAppShell,
ThemeAutocomplete,
ThemeBadge,
ThemeButton,
ThemeCard,
ThemeCode,
ThemeFlex,
ThemeGroup,
ThemeInput,
ThemeModal,
ThemeModalRoot,
ThemePaper,
ThemeScrollArea,
ThemeScrollAreaAutosize,
ThemeSelect,
ThemeSwitch,
ThemeText,
ThemeTextInput,
ThemeTitle,
ThemeUnstyledButton,
} from "./index";
// Accessible light/dark palette. Each tuple runs shade 0 (lightest) → 9
// (darkest) per the Mantine convention. These tuples are the single source of
// literal hex — every `--inspector-*` token in App.css references
// `--mantine-color-*`, so improving the tuples cascades through the whole UI.
const inspector: MantineColorsTuple = [
"#eef3ff",
"#dce6ff",
"#b7ccff",
"#8facfc",
"#6b8ef8",
"#4f78f3",
"#3d68ee",
"#2f59e0",
"#2749b8",
"#16265e",
];
// Cool-slate neutrals (light-mode surfaces, borders, text).
const gray: MantineColorsTuple = [
"#f7f8fa",
"#eef0f4",
"#e2e5ec",
"#cfd4de",
"#b0b7c5",
"#8d95a6",
"#6c7486",
"#515868",
"#3a3f4b",
"#23272f",
];
// Charcoal (dark-mode surfaces 9→6, text 0→2).
const dark: MantineColorsTuple = [
"#c9cdd6",
"#aeb4c0",
"#8b93a3",
"#6a7284",
"#464c5b",
"#363b47",
"#2b2f3a",
"#23262f",
"#1a1d24",
"#131519",
];
const green: MantineColorsTuple = [
"#e6f7ee",
"#c6efd7",
"#96e2b8",
"#5fd196",
"#33bd79",
"#1aa864",
"#109655",
"#0d7d47",
"#0a6339",
"#074a2b",
];
const red: MantineColorsTuple = [
"#ffecec",
"#ffd7d7",
"#ffb3b3",
"#ff8585",
"#f95f60",
"#ef4344",
"#e03236",
"#c62328",
"#a11a1f",
"#7d1418",
];
// Amber (Mantine's `yellow` slot — warning / connecting / cancelled).
const yellow: MantineColorsTuple = [
"#fff7e6",
"#ffecc2",
"#ffdb8a",
"#fbc94f",
"#f5b722",
"#e8a800",
"#d19700",
"#a87800",
"#855f00",
"#634700",
];
// Azure (Mantine's `blue` slot — info logs / running tasks).
const blue: MantineColorsTuple = [
"#e7f1ff",
"#cfe2ff",
"#a3c8ff",
"#6fa6fb",
"#4589f5",
"#2a75ee",
"#1b66e0",
"#1553bb",
"#124399",
"#0d2f6b",
];
const teal: MantineColorsTuple = [
"#e2f7f4",
"#bfeee8",
"#8adfd4",
"#4ecabb",
"#23b2a2",
"#109e8f",
"#0d8a7d",
"#0a6f65",
"#08574f",
"#06403a",
];
// Violet accent (docs / social glyphs, direction-badge incoming).
const violet: MantineColorsTuple = [
"#faf0ff",
"#f0dcff",
"#e0bbff",
"#cb91fb",
"#b264f5",
"#a03bf5",
"#9127e6",
"#7a1fc4",
"#63189e",
"#3f0f66",
];
export const theme = createTheme({
colors: {
inspector,
gray,
dark,
green,
red,
yellow,
blue,
teal,
violet,
},
/* ── Color ──────────────────────────────────────────────── */
primaryColor: "inspector",
primaryShade: { light: 7, dark: 8 },
autoContrast: true,
/* ── Shape ──────────────────────────────────────────────── */
defaultRadius: "md",
cursorType: "pointer",
/* ── Typography ─────────────────────────────────────────── */
fontFamily: '"Fredoka", sans-serif',
fontFamilyMonospace: '"Roboto Mono", monospace',
headings: {
fontFamily: '"Fredoka", sans-serif',
fontWeight: "600",
},
/* ── Component overrides ────────────────────────────────── */
components: {
Accordion: ThemeAccordion,
ActionIcon: ThemeActionIcon,
Alert: ThemeAlert,
AppShell: ThemeAppShell,
Autocomplete: ThemeAutocomplete,
Badge: ThemeBadge,
Button: ThemeButton,
Card: ThemeCard,
Code: ThemeCode,
Flex: ThemeFlex,
Group: ThemeGroup,
Input: ThemeInput,
Modal: ThemeModal,
ModalRoot: ThemeModalRoot,
Paper: ThemePaper,
ScrollArea: ThemeScrollArea,
ScrollAreaAutosize: ThemeScrollAreaAutosize,
Select: ThemeSelect,
Switch: ThemeSwitch,
Text: ThemeText,
TextInput: ThemeTextInput,
Title: ThemeTitle,
UnstyledButton: ThemeUnstyledButton,
},
});