mirror of
https://github.com/shtorm-7/sing-box-extended.git
synced 2026-07-08 01:11:33 +03:00
Add new admin panel, failover, dns fallback, providers, limiters. Update XHTTP
This commit is contained in:
589
service/admin_panel/web/src/theme.ts
Normal file
589
service/admin_panel/web/src/theme.ts
Normal file
@@ -0,0 +1,589 @@
|
||||
import Fade from "@mui/material/Fade";
|
||||
import { alpha, createTheme, type Theme } from "@mui/material/styles";
|
||||
|
||||
// Two layered palettes, one per mode. Surface and elevated tones differ
|
||||
// by ~5 luminance steps so the eye can pick out cards/dialogs from the
|
||||
// page; the rest of the UI references this set so flipping `mode` flips
|
||||
// every component override at once.
|
||||
export type ThemeMode = "dark" | "light";
|
||||
|
||||
const DARK = {
|
||||
surface: "#141414", // page / cards / paper / tables
|
||||
elevated: "#191919", // app bar replacement, table heads, side bar
|
||||
elevated2: "#1f1f1f", // hover states for table rows
|
||||
// Borders bumped a touch (0.06 → 0.10 / 0.14 → 0.20) so outlined
|
||||
// cards and table cell borders are actually visible against the
|
||||
// very dark surface.
|
||||
border: "rgba(255,255,255,0.10)",
|
||||
borderStrong: "rgba(255,255,255,0.20)",
|
||||
textPrimary: "#f5f5f5",
|
||||
// Secondary / caption text was washing out at 0.66 — bumped to 0.80
|
||||
// so subtitles, helper text and uppercase labels stay legible.
|
||||
textSecondary: "rgba(255,255,255,0.80)",
|
||||
scrollbar: "rgba(255,255,255,0.10)",
|
||||
scrollbarHover: "rgba(255,255,255,0.18)",
|
||||
iconButtonHover: "rgba(255,255,255,0.07)",
|
||||
inputBg: "rgba(255,255,255,0.02)",
|
||||
chipBg: "rgba(255,255,255,0.06)",
|
||||
listItemHover: "rgba(255,255,255,0.04)",
|
||||
tableHeadColor: "rgba(255,255,255,0.78)",
|
||||
tooltipBg: "#222",
|
||||
tooltipText: "#fff",
|
||||
dialogShadow: "0 24px 48px rgba(0,0,0,0.55)",
|
||||
} as const;
|
||||
|
||||
const LIGHT = {
|
||||
surface: "#ffffff",
|
||||
elevated: "#f5f7fa",
|
||||
elevated2: "#e9edf3",
|
||||
// Borders deep enough that outlined cards / table rows read clearly
|
||||
// on pure-white surfaces.
|
||||
border: "rgba(15,23,42,0.18)",
|
||||
borderStrong: "rgba(15,23,42,0.32)",
|
||||
textPrimary: "#0f172a",
|
||||
// Secondary / caption text — pushed up to 0.82 so helper text,
|
||||
// subtitles and chips stop looking washed out on white.
|
||||
textSecondary: "rgba(15,23,42,0.82)",
|
||||
scrollbar: "rgba(15,23,42,0.26)",
|
||||
scrollbarHover: "rgba(15,23,42,0.40)",
|
||||
iconButtonHover: "rgba(15,23,42,0.07)",
|
||||
inputBg: "#ffffff",
|
||||
chipBg: "rgba(15,23,42,0.10)",
|
||||
listItemHover: "rgba(15,23,42,0.06)",
|
||||
tableHeadColor: "rgba(15,23,42,0.82)",
|
||||
tooltipBg: "#1e293b",
|
||||
tooltipText: "#fff",
|
||||
dialogShadow: "0 24px 48px rgba(15,23,42,0.18)",
|
||||
} as const;
|
||||
|
||||
function colors(mode: ThemeMode) {
|
||||
return mode === "light" ? LIGHT : DARK;
|
||||
}
|
||||
|
||||
// Default accent. The user can override via the in-app color picker.
|
||||
export const DEFAULT_ACCENT = "#3b82f6";
|
||||
export const DEFAULT_MODE: ThemeMode = "dark";
|
||||
|
||||
// isHexColor reports whether the string looks like a 3- or 6-digit hex CSS
|
||||
// color, e.g. "#fff" / "#7c83ff". Used to validate user-supplied accent
|
||||
// values from localStorage / the picker before feeding them to MUI.
|
||||
export function isHexColor(s: string): boolean {
|
||||
return /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(s);
|
||||
}
|
||||
|
||||
// pickContrastingTextColor returns "#0c0c0c" or "#ffffff" depending on which
|
||||
// has better contrast against the given background. Used so primary buttons
|
||||
// stay readable regardless of the chosen accent.
|
||||
export function pickContrastingTextColor(hex: string): string {
|
||||
const norm =
|
||||
hex.length === 4
|
||||
? "#" +
|
||||
hex
|
||||
.slice(1)
|
||||
.split("")
|
||||
.map((c) => c + c)
|
||||
.join("")
|
||||
: hex;
|
||||
const r = parseInt(norm.slice(1, 3), 16);
|
||||
const g = parseInt(norm.slice(3, 5), 16);
|
||||
const b = parseInt(norm.slice(5, 7), 16);
|
||||
// Relative luminance per WCAG, rough approximation.
|
||||
const lum = (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255;
|
||||
return lum > 0.55 ? "#0c0c0c" : "#ffffff";
|
||||
}
|
||||
|
||||
// ThemePalette exposes only the tokens callers actually read directly.
|
||||
// MUI's own theme covers the rest (accent goes through `var(--sb-accent)`,
|
||||
// borderStrong / elevated2 / accentSoft are only consumed inside the
|
||||
// MUI theme overrides below, never via this struct).
|
||||
export interface ThemePalette {
|
||||
mode: ThemeMode;
|
||||
surface: string;
|
||||
elevated: string;
|
||||
border: string;
|
||||
}
|
||||
|
||||
// buildTheme produces a fresh MUI theme using the given accent color and
|
||||
// theme mode. All component overrides reference the dynamic accent + mode
|
||||
// palette so the entire UI updates the moment either changes.
|
||||
export function buildTheme(
|
||||
accentInput: string = DEFAULT_ACCENT,
|
||||
modeInput: ThemeMode = DEFAULT_MODE,
|
||||
): Theme {
|
||||
const accent = isHexColor(accentInput) ? accentInput : DEFAULT_ACCENT;
|
||||
const mode: ThemeMode = modeInput === "light" ? "light" : "dark";
|
||||
const M = colors(mode);
|
||||
const accentContrast = pickContrastingTextColor(accent);
|
||||
|
||||
return createTheme({
|
||||
palette: {
|
||||
mode,
|
||||
background: { default: M.surface, paper: M.surface },
|
||||
primary: { main: accent, contrastText: accentContrast },
|
||||
secondary: { main: mode === "light" ? "#475569" : "#9aa0a6" },
|
||||
divider: M.border,
|
||||
text: {
|
||||
primary: M.textPrimary,
|
||||
secondary: M.textSecondary,
|
||||
},
|
||||
success: { main: "#22c55e" },
|
||||
warning: { main: "#f59e0b" },
|
||||
error: { main: "#ef4444" },
|
||||
},
|
||||
shape: { borderRadius: 10 },
|
||||
typography: {
|
||||
fontFamily:
|
||||
'Inter, "SF Pro Display", Roboto, system-ui, -apple-system, "Segoe UI", "Helvetica Neue", Arial, sans-serif',
|
||||
h4: { fontWeight: 600, letterSpacing: -0.4 },
|
||||
h5: { fontWeight: 600, letterSpacing: -0.3 },
|
||||
h6: { fontWeight: 600, letterSpacing: -0.2 },
|
||||
button: { fontWeight: 500 },
|
||||
},
|
||||
components: {
|
||||
MuiCssBaseline: {
|
||||
styleOverrides: {
|
||||
":root": {
|
||||
// Default accent CSS variable. AppThemeProvider overrides this
|
||||
// on <html> at runtime; declaring a fallback here means the
|
||||
// very first paint never renders with `var(--sb-accent)`
|
||||
// pointing at nothing.
|
||||
"--sb-accent": DEFAULT_ACCENT,
|
||||
// Companion variable holding the readable foreground colour
|
||||
// for content sitting on top of `--sb-accent` (e.g. the "+"
|
||||
// create IconButton on the CrudPage toolbar). Computed via
|
||||
// `pickContrastingTextColor`, kept in lockstep with
|
||||
// `--sb-accent` by AppThemeProvider on every accent change.
|
||||
"--sb-accent-contrast": accentContrast,
|
||||
// Mode-aware surface tones, exposed so component sx blocks can
|
||||
// pin sticky / overlapping elements (e.g. CrudPage's pinned
|
||||
// Actions column) to exactly the same colour the surrounding
|
||||
// Table / TableHead / hovered TableRow renders. Keeping them
|
||||
// here means flipping the mode re-emits the trio in lockstep
|
||||
// with every other component override.
|
||||
"--sb-surface": M.surface,
|
||||
"--sb-elevated": M.elevated,
|
||||
"--sb-elevated2": M.elevated2,
|
||||
},
|
||||
html: {
|
||||
// Reserve room for the vertical scrollbar even when it isn't
|
||||
// present. Without this, anything that briefly hides the
|
||||
// scrollbar (autofill, modals, even some browser quirks) shifts
|
||||
// the entire viewport horizontally by ~17px and you see a
|
||||
// "flick" in the sidebar / topbar text.
|
||||
scrollbarGutter: "stable",
|
||||
},
|
||||
body: {
|
||||
backgroundColor: M.surface,
|
||||
color: M.textPrimary,
|
||||
colorScheme: mode,
|
||||
},
|
||||
"*::-webkit-scrollbar": { width: 10, height: 10 },
|
||||
"*::-webkit-scrollbar-track": { background: "transparent" },
|
||||
"*::-webkit-scrollbar-thumb": {
|
||||
background: M.scrollbar,
|
||||
borderRadius: 8,
|
||||
border: "2px solid transparent",
|
||||
backgroundClip: "padding-box",
|
||||
},
|
||||
"*::-webkit-scrollbar-thumb:hover": {
|
||||
background: M.scrollbarHover,
|
||||
backgroundClip: "padding-box",
|
||||
},
|
||||
// Theme switching: View Transitions API circular reveal from the
|
||||
// toggle's click point. Falls back to instant change in browsers
|
||||
// without `document.startViewTransition`.
|
||||
//
|
||||
// Both pseudo-elements are static images (DOM snapshots), so we
|
||||
// override the default cross-fade animation with our own clip-path
|
||||
// reveal on the new state. The old snapshot stays at full opacity
|
||||
// beneath the new one and is wiped away by the expanding circle.
|
||||
// We deliberately do NOT disable transitions on the live DOM
|
||||
// during the view-transition — that caused a "bounce" at the end
|
||||
// when in-flight ripples / row-hover transitions snapped back.
|
||||
"::view-transition-old(root), ::view-transition-new(root)": {
|
||||
animation: "none",
|
||||
mixBlendMode: "normal",
|
||||
},
|
||||
"::view-transition-old(root)": {
|
||||
zIndex: 1,
|
||||
},
|
||||
"::view-transition-new(root)": {
|
||||
zIndex: 2,
|
||||
animation: "theme-reveal 520ms cubic-bezier(0.4, 0, 0.2, 1) forwards",
|
||||
},
|
||||
"@keyframes theme-reveal": {
|
||||
from: {
|
||||
clipPath:
|
||||
"circle(0px at var(--theme-cx, 50%) var(--theme-cy, 50%))",
|
||||
},
|
||||
to: {
|
||||
clipPath:
|
||||
"circle(var(--theme-r, 150vmax) at var(--theme-cx, 50%) var(--theme-cy, 50%))",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiPaper: {
|
||||
defaultProps: { elevation: 0 },
|
||||
styleOverrides: {
|
||||
root: { backgroundImage: "none", backgroundColor: M.surface },
|
||||
outlined: {
|
||||
borderColor: M.border,
|
||||
borderRadius: 12,
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiCard: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
backgroundColor: M.surface,
|
||||
backgroundImage: "none",
|
||||
border: `1px solid ${M.border}`,
|
||||
borderRadius: 12,
|
||||
transition:
|
||||
"transform 0.18s ease, border-color 0.18s ease, box-shadow 0.18s ease",
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiAppBar: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
backgroundImage: "none",
|
||||
backgroundColor: M.elevated,
|
||||
boxShadow: "none",
|
||||
borderBottom: `1px solid ${M.border}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiDrawer: {
|
||||
styleOverrides: {
|
||||
paper: {
|
||||
backgroundImage: "none",
|
||||
backgroundColor: M.elevated,
|
||||
borderRight: `1px solid ${M.border}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiDialog: {
|
||||
defaultProps: {
|
||||
// Don't lock body scroll — MUI's default behaviour adds
|
||||
// `overflow:hidden` + a `padding-right` to <body> equal to the
|
||||
// scrollbar width while the modal is open, which shifts every
|
||||
// `position:fixed` element (sidebar, topbar) horizontally by
|
||||
// ~17px. Keeping the scrollbar avoids that flick.
|
||||
disableScrollLock: true,
|
||||
},
|
||||
styleOverrides: {
|
||||
paper: {
|
||||
backgroundColor: M.surface,
|
||||
backgroundImage: "none",
|
||||
border: `1px solid ${M.border}`,
|
||||
boxShadow: M.dialogShadow,
|
||||
},
|
||||
},
|
||||
},
|
||||
// Same scroll-lock fix for every other Modal-based opener so popping
|
||||
// a colour picker, filter dropdown or autocomplete list doesn't yank
|
||||
// the layout sideways.
|
||||
//
|
||||
// We also override the default `Grow` transition with `Fade` for
|
||||
// every overlay element. `Grow` scales the element with
|
||||
// `scale(0.75, 0.5625) → scale(1, 1)` — an *asymmetric* stretch in
|
||||
// which the Y-axis grows from 56% to 100%. With most placements that
|
||||
// means the top edge of the popup rises noticeably as the animation
|
||||
// ends, which reads as the inner text "snapping upward" at the end
|
||||
// of the open animation. A pure opacity fade has no transform at
|
||||
// all, so the contents simply appear in their final position.
|
||||
MuiPopover: {
|
||||
defaultProps: {
|
||||
disableScrollLock: true,
|
||||
slots: { transition: Fade },
|
||||
slotProps: { transition: { timeout: 160 } },
|
||||
},
|
||||
},
|
||||
MuiMenu: {
|
||||
defaultProps: {
|
||||
disableScrollLock: true,
|
||||
slots: { transition: Fade },
|
||||
slotProps: { transition: { timeout: 160 } },
|
||||
},
|
||||
},
|
||||
MuiModal: {
|
||||
defaultProps: { disableScrollLock: true },
|
||||
},
|
||||
MuiTableContainer: {
|
||||
styleOverrides: { root: { backgroundColor: M.surface } },
|
||||
},
|
||||
MuiTable: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
backgroundColor: M.surface,
|
||||
width: "100%",
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiTableHead: {
|
||||
styleOverrides: { root: { backgroundColor: M.elevated } },
|
||||
},
|
||||
MuiTableRow: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
transition: "background-color 0.14s ease",
|
||||
"&:hover": { backgroundColor: `${M.elevated2} !important` },
|
||||
// Tone down the very last row's bottom border so the table edge
|
||||
// doesn't read as a hard double line against the Paper outline.
|
||||
"&:last-of-type td": { borderBottom: 0 },
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiTableCell: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
color: M.textPrimary,
|
||||
borderBottomColor: M.border,
|
||||
backgroundColor: "transparent",
|
||||
fontSize: 13.5,
|
||||
paddingTop: 16,
|
||||
paddingBottom: 16,
|
||||
},
|
||||
|
||||
sizeSmall: {
|
||||
paddingTop: 16,
|
||||
paddingBottom: 16,
|
||||
},
|
||||
head: {
|
||||
fontWeight: 600,
|
||||
fontSize: 11.5,
|
||||
letterSpacing: 0.6,
|
||||
textTransform: "uppercase",
|
||||
color: M.tableHeadColor,
|
||||
backgroundColor: M.elevated,
|
||||
borderBottomColor: M.borderStrong,
|
||||
// Slimmer header strip — the column-name bar shouldn't take
|
||||
// as much vertical space as a data row, just enough to read
|
||||
// the label comfortably.
|
||||
paddingTop: 8,
|
||||
paddingBottom: 8,
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiButton: {
|
||||
defaultProps: { disableElevation: true },
|
||||
styleOverrides: {
|
||||
root: {
|
||||
textTransform: "none",
|
||||
borderRadius: 8,
|
||||
paddingInline: 14,
|
||||
fontWeight: 500,
|
||||
transition:
|
||||
"background-color 0.16s ease, border-color 0.16s ease, transform 0.08s ease",
|
||||
"&:active": { transform: "translateY(0.5px)" },
|
||||
},
|
||||
// Accent-coloured rules below reference `var(--sb-accent)` so the
|
||||
// emotion class hash stays stable when the user picks a new
|
||||
// accent. With a stable class, the declared `transition` actually
|
||||
// animates the colour change instead of being voided by a class
|
||||
// swap.
|
||||
containedPrimary: {
|
||||
backgroundColor: "var(--sb-accent)",
|
||||
color: accentContrast,
|
||||
transition:
|
||||
"background-color 0.32s cubic-bezier(0.4,0,0.2,1), color 0.32s cubic-bezier(0.4,0,0.2,1)",
|
||||
"&:hover": {
|
||||
backgroundColor:
|
||||
"color-mix(in srgb, var(--sb-accent) 92%, transparent)",
|
||||
},
|
||||
},
|
||||
outlinedInherit: { borderColor: M.borderStrong },
|
||||
},
|
||||
},
|
||||
MuiIconButton: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
borderRadius: 8,
|
||||
transition: "background-color 0.14s ease, color 0.14s ease",
|
||||
// Desktop hover tint — gated on a real hover-capable
|
||||
// pointer AND a desktop-width viewport so it never
|
||||
// fires on touch devices or in Chrome DevTools' mobile
|
||||
// preset (which keeps `(hover: hover)` true because
|
||||
// the host machine still has a mouse, but does shrink
|
||||
// the viewport, so `(min-width: 600px)` catches it).
|
||||
// No companion `:active` / `:focus` rule on the mobile
|
||||
// breakpoint by design: per the user's request the
|
||||
// button gets zero post-tap visuals on a phone.
|
||||
"@media (hover: hover) and (min-width: 600px)": {
|
||||
"&:hover": { backgroundColor: M.iconButtonHover },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
// Hide MUI's TouchRipple visual on the mobile / touch
|
||||
// breakpoint for every ButtonBase-derived component
|
||||
// (IconButton, Button, MenuItem, ListItemButton, …). The
|
||||
// ripple is the growing circle that, on phones, can outlive
|
||||
// the touch when the touchend event isn't delivered cleanly
|
||||
// (drag-off, scroll, sluggish hardware) — that's the halo
|
||||
// the user kept seeing after every tap. Desktop pointers
|
||||
// fire mouseup / mouseleave reliably, so the ripple animation
|
||||
// always completes its exit phase there and we leave it
|
||||
// alone. The same `(hover: none), (max-width: 599.95px)` pair
|
||||
// is used everywhere else in the theme so the rule fires
|
||||
// both on real phones and inside Chrome DevTools'
|
||||
// mobile-emulation preset.
|
||||
MuiTouchRipple: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
"@media (hover: none), (max-width: 599.95px)": {
|
||||
display: "none",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiOutlinedInput: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
backgroundColor: M.inputBg,
|
||||
"& .MuiOutlinedInput-notchedOutline": {
|
||||
borderColor: M.border,
|
||||
transition: "border-color 0.32s cubic-bezier(0.4,0,0.2,1)",
|
||||
},
|
||||
"&:hover .MuiOutlinedInput-notchedOutline": { borderColor: M.borderStrong },
|
||||
"&.Mui-focused .MuiOutlinedInput-notchedOutline": {
|
||||
borderColor: "var(--sb-accent)",
|
||||
borderWidth: 1,
|
||||
},
|
||||
// Suppress the browser's autofill highlight (Firefox paints
|
||||
// the field with a translucent blue band, Chrome/Safari with
|
||||
// yellow). The cell's own background already reads as
|
||||
// "active", so the autofill colour is purely noise.
|
||||
//
|
||||
// WebKit (Chrome/Safari) doesn't honour normal
|
||||
// `background-color` overrides on autofilled inputs — the
|
||||
// canonical workaround is a giant inset `box-shadow` that
|
||||
// visually replaces the background, plus `text-fill-color`
|
||||
// for the foreground colour the engine forces. Firefox uses
|
||||
// the standard `:autofill` pseudo and respects ordinary
|
||||
// `background-color` / `filter` overrides since v86.
|
||||
"& input:-webkit-autofill, & input:-webkit-autofill:hover, & input:-webkit-autofill:focus, & input:-webkit-autofill:active":
|
||||
{
|
||||
WebkitBoxShadow: `0 0 0 1000px ${M.surface} inset !important`,
|
||||
WebkitTextFillColor: `${M.textPrimary} !important`,
|
||||
caretColor: `${M.textPrimary} !important`,
|
||||
// The 5000s transition delay keeps the engine from
|
||||
// re-running the autofill style transition on every
|
||||
// focus / blur, which would re-paint the highlight.
|
||||
transition: "background-color 5000s ease-in-out 0s",
|
||||
},
|
||||
"& input:autofill": {
|
||||
backgroundColor: "transparent !important",
|
||||
filter: "none !important",
|
||||
color: `${M.textPrimary} !important`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiChip: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
backgroundColor: M.chipBg,
|
||||
color: M.textPrimary,
|
||||
fontWeight: 500,
|
||||
borderRadius: 6,
|
||||
},
|
||||
// "Backlit" badge with accent glow — used for selected items in
|
||||
// multi-select boxes so they stand out without being garish.
|
||||
colorPrimary: {
|
||||
backgroundColor:
|
||||
"color-mix(in srgb, var(--sb-accent) 14%, transparent)",
|
||||
color: "var(--sb-accent)",
|
||||
border:
|
||||
"1px solid color-mix(in srgb, var(--sb-accent) 35%, transparent)",
|
||||
transition:
|
||||
"background-color 0.32s cubic-bezier(0.4,0,0.2,1), color 0.32s cubic-bezier(0.4,0,0.2,1), border-color 0.32s cubic-bezier(0.4,0,0.2,1)",
|
||||
},
|
||||
colorSuccess: { backgroundColor: alpha("#22c55e", 0.18), color: mode === "light" ? "#15803d" : "#86efac" },
|
||||
colorWarning: { backgroundColor: alpha("#f59e0b", 0.18), color: mode === "light" ? "#a16207" : "#fcd34d" },
|
||||
colorError: { backgroundColor: alpha("#ef4444", 0.18), color: mode === "light" ? "#b91c1c" : "#fca5a5" },
|
||||
},
|
||||
},
|
||||
MuiListItemButton: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
borderRadius: 8,
|
||||
marginInline: 8,
|
||||
paddingBlock: 6,
|
||||
transition: "background-color 0.32s cubic-bezier(0.4,0,0.2,1)",
|
||||
"&.Mui-selected": {
|
||||
// Mode-aware accent tint: 16% on dark, 12% on light so the
|
||||
// selected pill reads cleanly on both surfaces.
|
||||
backgroundColor: `color-mix(in srgb, var(--sb-accent) ${
|
||||
mode === "light" ? 12 : 16
|
||||
}%, transparent)`,
|
||||
"&:hover": {
|
||||
backgroundColor:
|
||||
"color-mix(in srgb, var(--sb-accent) 22%, transparent)",
|
||||
},
|
||||
"& .MuiListItemIcon-root": {
|
||||
color: "var(--sb-accent)",
|
||||
transition: "color 0.32s cubic-bezier(0.4,0,0.2,1)",
|
||||
},
|
||||
"& .MuiListItemText-primary": {
|
||||
color: mode === "light" ? "#0f172a" : "#fff",
|
||||
// Visually heavier on the active item without bumping
|
||||
// `fontWeight`. Different font weights have different glyph
|
||||
// advance widths, so toggling between 400 and 600 caused a
|
||||
// ~1px horizontal jump every time the selection changed (or
|
||||
// re-rendered on hover); painting an extra "stroke" via
|
||||
// text-shadow gives the same heaviness with zero layout
|
||||
// impact.
|
||||
textShadow: "0 0 0.6px currentColor",
|
||||
},
|
||||
},
|
||||
"&:hover": { backgroundColor: M.listItemHover },
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiListItemIcon: {
|
||||
styleOverrides: {
|
||||
root: { minWidth: 36, color: M.textSecondary },
|
||||
},
|
||||
},
|
||||
MuiTooltip: {
|
||||
defaultProps: {
|
||||
// See the MuiPopover comment above — Grow's asymmetric scale
|
||||
// makes tooltip text appear to "snap upward" at the end of the
|
||||
// open animation. A pure opacity fade has no transform so the
|
||||
// contents simply appear in their final position.
|
||||
slots: { transition: Fade },
|
||||
slotProps: { transition: { timeout: 140 } },
|
||||
},
|
||||
styleOverrides: {
|
||||
tooltip: {
|
||||
backgroundColor: M.tooltipBg,
|
||||
color: M.tooltipText,
|
||||
fontSize: 12,
|
||||
border: `1px solid ${M.border}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiAlert: {
|
||||
styleOverrides: {
|
||||
root: { borderRadius: 10 },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function buildPalette(
|
||||
modeInput: ThemeMode = DEFAULT_MODE,
|
||||
): ThemePalette {
|
||||
const mode: ThemeMode = modeInput === "light" ? "light" : "dark";
|
||||
const M = colors(mode);
|
||||
return {
|
||||
mode,
|
||||
surface: M.surface,
|
||||
elevated: M.elevated,
|
||||
border: M.border,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user