Apply global dark mode styling to UI (#43033)

This commit is contained in:
Luke Heath 2026-04-10 09:30:04 -05:00 committed by GitHub
parent ea9a3352df
commit 8ed339f012
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 657 additions and 100 deletions

View file

@ -252,6 +252,7 @@ const ActionsDropdown = ({
}),
menu: (provided) => ({
...provided,
backgroundColor: COLORS["core-fleet-white"],
boxShadow: "0 2px 6px rgba(0, 0, 0, 0.1)",
borderRadius: "4px",
zIndex: 6,

View file

@ -65,6 +65,10 @@
// color styles
&__white {
background-color: $core-fleet-white;
body.dark-mode & {
background-color: $ui-fleet-black-5;
}
}
&__grey {

View file

@ -1,6 +1,7 @@
.target-chip-selector {
padding: $pad-small;
background-color: $core-fleet-white;
color: $ui-fleet-black-75;
border: none;
box-shadow: inset 0 0 0 1px $ui-fleet-black-25;
border-radius: $border-radius-medium;

View file

@ -1,7 +1,7 @@
.modal {
&__background {
@include position(fixed, 0 0 0 0);
background-color: rgba($core-fleet-black, 0.4);
background-color: $core-fleet-black-overlay-40;
z-index: 101;
overflow: auto;
display: flex;

View file

@ -182,3 +182,64 @@
background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAE0lEQVQImWP4////f4bdu3f/BwAlfgctduB85QAAAABJRU5ErkJggg==)
right repeat-y;
}
/* Dark mode overrides */
body.dark-mode .ace_editor.ace-fleet {
background-color: #161819;
color: #b3b6c1;
border-color: #2e3035;
}
body.dark-mode .ace_editor.ace-fleet:hover {
border-color: #636777;
}
body.dark-mode .ace_editor.ace-fleet.ace_focus {
border-color: #7b79ff;
}
body.dark-mode .ace_editor.ace-fleet.ace_focus .ace_scroller {
box-shadow: 1px 1px 1px 1px #7b79ff;
}
body.dark-mode .ace-fleet .ace_content {
background: #111214;
}
body.dark-mode .ace-fleet .ace_gutter {
background: #111214;
color: #636777;
border-right-color: #2e3035;
}
body.dark-mode .ace-fleet .ace_gutter-active-line {
background-color: #1e2024;
}
body.dark-mode .ace-fleet .ace_cursor {
color: #b3b6c1;
}
body.dark-mode .ace-fleet .ace_keyword {
color: #b3b6c1;
}
body.dark-mode .ace-fleet .ace_marker-layer .ace_selection {
background: rgba(123, 121, 255, 0.2);
}
body.dark-mode .ace-fleet .ace_marker-layer .ace_bracket {
border-color: #3e4248;
}
body.dark-mode .ace-fleet .ace_marker-layer .ace_selected-word {
border-color: #3e4248;
}
body.dark-mode .ace-fleet .ace_invisible {
color: #3e4248;
}
body.dark-mode .ace-fleet .ace_print-margin {
background: #1e2024;
}

View file

@ -2,6 +2,10 @@
background-color: $core-fleet-white;
box-sizing: border-box;
border-left: 1px solid $ui-gray;
body.dark-mode & {
background-color: $ui-fleet-black-5;
}
min-width: 340px;
width: 340px;
padding: $pad-xxlarge;

View file

@ -23,7 +23,7 @@ $main-max-width-including-padding: 1280px + 32px + 32px; // Cannot use variables
top: 0;
right: 0;
width: $side-panel-width;
height: auto;
height: 100%;
z-index: 2;
}

View file

@ -60,6 +60,7 @@
&--selected {
font-weight: $bold;
background-color: $ui-fleet-black-5;
color: $core-fleet-black;
}
&--disabled {

View file

@ -18,17 +18,17 @@ $shadow-transition-width: 10px;
background-image:
/* Shadows */ linear-gradient(
to right,
white,
$core-fleet-white,
$transparent
),
linear-gradient(to left, white, $transparent),
linear-gradient(to left, $core-fleet-white, $transparent),
/* Shadow covers */
linear-gradient(to right, $ui-shadow, white $shadow-transition-width),
linear-gradient(to left, $ui-shadow, white $shadow-transition-width);
linear-gradient(to right, $ui-shadow, $core-fleet-white $shadow-transition-width),
linear-gradient(to left, $ui-shadow, $core-fleet-white $shadow-transition-width);
background-position: left center, right center, left center, right center;
background-repeat: no-repeat;
background-color: white;
background-color: $core-fleet-white;
background-size: $shadow-width 100%, $shadow-width 100%, 50% 100%,
50% 100%;
@ -431,7 +431,7 @@ $shadow-transition-width: 10px;
bottom: 0;
left: 0;
right: 0;
background-color: rgba(255, 255, 255, 0.8);
background-color: $loading-overlay;
z-index: 1;
}

View file

@ -201,6 +201,7 @@ const TeamsDropdown = ({
singleValue: (baseStyles) => ({
...baseStyles,
...variableSingleValueStyles,
color: COLORS["core-fleet-black"],
lineHeight: "normal",
paddingLeft: 0,
paddingRight: "8px",
@ -220,7 +221,8 @@ const TeamsDropdown = ({
}),
menu: (baseStyles) => ({
...baseStyles,
boxShadow: "0 2px 6px rgba(0, 0, 0, 0.1)",
backgroundColor: COLORS["core-fleet-white"],
boxShadow: `0 2px 6px rgba(0, 0, 0, 0.1), 0 0 0 1px ${COLORS["ui-fleet-black-10"]}`,
borderRadius: "4px",
zIndex: 6,
overflow: "hidden",

View file

@ -396,7 +396,7 @@ $base-class: "button";
&:hover,
&:focus {
background-color: rgba($core-fleet-black, 0.05);
background-color: $core-fleet-black-overlay-05;
color: $core-fleet-green-over;
svg {

View file

@ -55,7 +55,7 @@ const meta: Meta<typeof DropdownButton> = {
values: [
{
name: "header",
value: "linear-gradient(270deg, #201e43 0%, #353d62 100%)",
value: "linear-gradient(270deg, #202226 0%, #353840 100%)",
},
],
},

View file

@ -212,7 +212,8 @@
}
.Select-menu-outer {
box-shadow: 0 4px 10px rgba(52, 59, 96, 0.15);
background-color: $core-fleet-white;
box-shadow: 0 0 0 1px $ui-fleet-black-10;
z-index: 6;
overflow: hidden;
max-height: 198px; // Fits 4 options without scrolling
@ -233,6 +234,7 @@
.Select-option {
color: $core-fleet-black;
background-color: transparent;
font-size: $x-small;
margin: 0;
padding: 10px;

View file

@ -337,6 +337,7 @@ export const generateCustomDropdownStyles = (
},
singleValue: (provided) => ({
...provided,
color: COLORS["core-fleet-black"],
fontSize: "13px",
margin: 0,
padding: 0,
@ -352,7 +353,8 @@ export const generateCustomDropdownStyles = (
}),
menu: (provided) => ({
...provided,
boxShadow: "0 2px 6px rgba(0, 0, 0, 0.1)",
backgroundColor: COLORS["core-fleet-white"],
boxShadow: `0 2px 6px rgba(0, 0, 0, 0.1), 0 0 0 1px ${COLORS["ui-fleet-black-10"]}`,
borderRadius: "4px",
zIndex: 6,
overflow: "hidden",

View file

@ -10,10 +10,12 @@ class OrgLogoIcon extends Component {
static propTypes = {
className: PropTypes.string,
src: PropTypes.string.isRequired,
invertDark: PropTypes.bool,
};
static defaultProps = {
src: fleetAvatar,
invertDark: false,
};
constructor(props) {
@ -68,14 +70,16 @@ class OrgLogoIcon extends Component {
};
render() {
const { className } = this.props;
const { className, invertDark } = this.props;
const { imageSrc } = this.state;
const { onError } = this;
const classNames =
imageSrc === fleetAvatar
? classnames(baseClass, className, "default-fleet-logo")
: classnames(baseClass, className);
: classnames(baseClass, className, {
[`${baseClass}--invert-dark`]: invertDark,
});
return (
<img

View file

@ -9,3 +9,8 @@
.default-fleet-logo {
transform: scale(0.5);
}
body.dark-mode .default-fleet-logo,
body.dark-mode .org-logo-icon--invert-dark {
filter: brightness(0) invert(1);
}

View file

@ -1,9 +1,10 @@
import React, { useContext } from "react";
import React, { useContext, useState, useEffect } from "react";
import { Link } from "react-router";
import classnames from "classnames";
import { getPathWithQueryParams, QueryParams } from "utilities/url";
import { LEARN_MORE_ABOUT_BASE_LINK } from "utilities/constants";
import { isDarkMode } from "utilities/theme";
import { AppContext } from "context/app";
@ -123,6 +124,17 @@ const SiteTopNav = ({
isNoAccess,
} = useContext(AppContext);
const [darkMode, setDarkMode] = useState(() => isDarkMode());
useEffect(() => {
const onThemeChange = (e: Event) => {
setDarkMode((e as CustomEvent).detail.dark);
};
window.addEventListener("fleet-theme-change", onThemeChange);
return () =>
window.removeEventListener("fleet-theme-change", onThemeChange);
}, []);
const isActiveDetailPage = isDetailPage(currentPath);
const isActiveGlobalPage = isGlobalPage(currentPath);
@ -139,7 +151,10 @@ const SiteTopNav = ({
const renderNavItem = (navItem: INavItem) => {
const { name, iconName, withParams } = navItem;
const orgLogoURL = config.org_info.org_logo_url_light_background;
const darkLogoURL = config.org_info.org_logo_url;
const lightLogoURL = config.org_info.org_logo_url_light_background;
const hasDarkLogo = darkLogoURL && darkLogoURL !== lightLogoURL;
const orgLogoURL = darkMode && hasDarkLogo ? darkLogoURL : lightLogoURL;
const active = navItem.location.regex.test(currentPath);
const navItemBaseClass = "site-nav-item";
@ -156,7 +171,11 @@ const SiteTopNav = ({
to={navItem.location.pathname}
>
<div className={`${navItemBaseClass}__logo`}>
<OrgLogoIcon className="logo" src={orgLogoURL} />
<OrgLogoIcon
className="logo"
src={orgLogoURL}
invertDark={!hasDarkLogo}
/>
</div>
</Link>
</li>

View file

@ -224,7 +224,8 @@ const UserMenu = ({
}),
menu: (provided) => ({
...provided,
boxShadow: "0 2px 6px rgba(0, 0, 0, 0.1)",
backgroundColor: COLORS["core-fleet-white"],
boxShadow: `0 2px 6px rgba(0, 0, 0, 0.1), 0 0 0 1px ${COLORS["ui-fleet-black-10"]}`,
borderRadius: "4px",
zIndex: 6,
marginTop: "7px",
@ -250,7 +251,7 @@ const UserMenu = ({
padding: "10px 8px",
fontSize: "15px",
backgroundColor: getOptionBackgroundColor(state),
color: COLORS["tooltip-bg"],
color: COLORS["core-fleet-black"],
whiteSpace: "nowrap",
"&:hover": {
backgroundColor: COLORS["ui-fleet-black-5"],

View file

@ -0,0 +1 @@
// Styles for user-menu are defined inline via react-select's `styles` prop.

View file

@ -7,8 +7,10 @@ import "regenerator-runtime/runtime";
import "./public-path";
import routes from "./router";
import "./index.scss";
import { initTheme } from "./utilities/theme";
if (typeof window !== "undefined") {
initTheme();
const { document } = global;
const app = document.getElementById("app");
const root = createRoot(app);

View file

@ -19,6 +19,7 @@ import {
greyCell,
readableDate,
} from "utilities/helpers";
import { isDarkMode, toggleDarkMode } from "utilities/theme";
interface IAccountSidePanelProps {
currentUser: IUser;
@ -35,6 +36,7 @@ const AccountSidePanel = ({
}: IAccountSidePanelProps): JSX.Element => {
const { isPremiumTier, config } = useContext(AppContext);
const [versionData, setVersionData] = useState<IVersionData>();
const [darkMode, setDarkMode] = useState(() => isDarkMode());
useEffect(() => {
const getVersionData = async () => {
@ -74,6 +76,59 @@ const AccountSidePanel = ({
newTab
/>
</div>
<div className={`${baseClass}__theme-toggle`}>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
className={`${baseClass}__theme-icon`}
>
<circle
cx="8"
cy="8"
r="3.5"
stroke="currentColor"
strokeWidth="1.5"
/>
<path
d="M8 1v2M8 13v2M1 8h2M13 8h2M3.05 3.05l1.41 1.41M11.54 11.54l1.41 1.41M3.05 12.95l1.41-1.41M11.54 4.46l1.41-1.41"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
/>
</svg>
<button
type="button"
role="switch"
aria-checked={darkMode}
aria-label="Toggle dark mode"
className={`button button--unstyled ${baseClass}__toggle ${
darkMode ? `${baseClass}__toggle--active` : ""
}`}
onClick={() => setDarkMode(toggleDarkMode())}
>
<div
className={`${baseClass}__toggle-dot ${
darkMode ? `${baseClass}__toggle-dot--active` : ""
}`}
/>
</button>
<svg
width="14"
height="14"
viewBox="0 0 16 16"
fill="none"
className={`${baseClass}__theme-icon`}
>
<path
d="M14.3 10.7A7 7 0 0 1 5.3 1.7 7 7 0 1 0 14.3 10.7Z"
stroke="currentColor"
strokeWidth="1.5"
strokeLinejoin="round"
/>
</svg>
</div>
{isPremiumTier && (
<DataSet
title="Fleets"

View file

@ -46,6 +46,50 @@
justify-content: flex-end;
}
&__theme-toggle {
display: flex;
align-items: center;
justify-content: center;
gap: $pad-small;
}
&__theme-icon {
color: $ui-fleet-black-50;
flex-shrink: 0;
}
&__toggle {
transition: background-color 150ms ease-in-out;
background-color: $ui-fleet-black-50;
border-radius: 12px;
display: inline-block;
height: 20px;
min-width: 35px;
width: 35px;
position: relative;
flex-shrink: 0;
cursor: pointer;
&--active {
background-color: $core-fleet-green;
}
}
&__toggle-dot {
width: 16px;
height: 16px;
position: absolute;
top: 2px;
left: 2px;
transition: left 150ms ease-in-out;
border-radius: 50%;
background-color: $core-fleet-white;
&--active {
left: 17px;
}
}
&__version {
font-size: $xx-small;
text-align: center;

View file

@ -203,7 +203,8 @@ const ActivityTypeDropdown = ({
...generateCustomDropdownStyles(),
menu: (provided) => ({
...provided,
boxShadow: "0 2px 6px rgba(0, 0, 0, 0.1)",
backgroundColor: COLORS["core-fleet-white"],
boxShadow: `0 0 0 1px ${COLORS["ui-fleet-black-10"]}`,
borderRadius: "4px",
zIndex: 6,
overflow: "hidden",

View file

@ -37,6 +37,12 @@
gap: $pad-xsmall;
}
&__card-icon {
body.dark-mode & {
opacity: 0.75;
}
}
&__count {
font-size: $large;
white-space: nowrap;

View file

@ -95,6 +95,7 @@
margin-top: 0;
z-index: 2;
animation: fade-in 150ms ease-out;
background-color: $core-fleet-white;
}
.label-filter-select__menu-list {

View file

@ -8,6 +8,7 @@ html {
body {
color: $ui-fleet-black-75;
background-color: $core-fleet-white;
padding: 0;
margin: 0;
font-family: "Inter", sans-serif;
@ -16,6 +17,26 @@ body {
line-height: 1.5;
}
// Applied briefly by theme.ts during toggle to fade all colors smoothly.
// Removed after 300ms so it doesn't interfere with normal interactions.
// Only applies when the user hasn't requested reduced motion.
@media (prefers-reduced-motion: no-preference) {
body.theme-transition,
body.theme-transition *,
body.theme-transition *::before,
body.theme-transition *::after {
transition: background-color 300ms ease, color 300ms ease,
border-color 300ms ease, box-shadow 300ms ease, fill 300ms ease,
stroke 300ms ease !important;
}
// Hide the gradient image during theme switch so background-color
// can transition smoothly without the gradient snapping.
body.theme-transition .core-wrapper {
background-image: none !important;
}
}
html,
body {
height: 100%;
@ -218,30 +239,35 @@ input {
}
// syntax highlighting for pretty-printed JSON
// Pinned to dark palette code blocks stay dark in both modes.
pre {
padding: $pad-large;
background-color: $core-fleet-black;
color: $core-fleet-white;
background-color: $static-black;
color: $static-white;
border-radius: 4px;
white-space: pre-wrap;
.string {
color: $rainbow-green;
color: #63c740;
}
.number {
color: $rainbow-orange;
color: #faa669;
}
.boolean {
color: $rainbow-blue;
color: #5cabdf;
}
.null {
color: magenta;
}
.key {
color: $core-fleet-white;
color: $static-white;
}
}
body.dark-mode pre {
background-color: #111214;
}
hr {
margin-top: $pad-xlarge;
margin-bottom: $pad-xlarge;
@ -258,3 +284,64 @@ dl {
dd {
margin: 0;
}
// Dark mode: remap illustration SVG fill/stroke colors via attribute
// selectors so light-palette graphics adapt to dark backgrounds.
// Grouped by luminance band light backgrounds darken, accents stay.
// Uses [attr] on any element (path, rect, circle, etc.).
body.dark-mode .empty-table__image-wrapper {
// Very light fills (near-white backgrounds) dark surface
[fill="#F7F8FC" i],
[fill="#F0F0FF" i],
[fill="#F1F0FF" i],
[fill="#F8F8FF" i],
[fill="#F9FAFC" i],
[fill="#EDEEF2" i],
[fill="#fff" i],
[fill="#ffffff" i],
[fill="white"] {
fill: #252830;
}
// Light decorative fills dark decorative
[fill="#E1E1FF" i],
[fill="#E0E7F9" i],
[fill="#E9E9FF" i],
[fill="#F1F1FD" i],
[fill="#E3E3FE" i],
[fill="#E0E3E3" i],
[fill="#D3E8F3" i] {
fill: #282b50;
}
// Medium fills muted dark
[fill="#C9D5F3" i],
[fill="#D2D2FF" i],
[fill="#D9D9FE" i],
[fill="#E2E4EA" i],
[fill="#C5C7D1" i],
[fill="#CFCFF3" i],
[fill="#C3C3EE" i],
[fill="#E6E6F7" i],
[fill="#D7DAF5" i],
[fill="#C0CCCF" i],
[fill="#B0B2E7" i] {
fill: #363a62;
}
// Medium strokes subtle dark borders
[stroke="#D3DAE4" i],
[stroke="#CDCDF5" i],
[stroke="#C8D1F2" i],
[stroke="#CBCFF2" i],
[stroke="#CECEF5" i],
[stroke="#E2E2FE" i],
[stroke="#C5C7D1" i] {
stroke: #3e4268;
}
// Dark text fills lighten for contrast on dark bg
[fill="#8B8FA2" i] {
fill: #b3b6c1;
}
}

View file

@ -1,81 +1,279 @@
// =============================================================================
// CSS Custom Properties the single source of truth for all themed colors.
// Light mode values live on :root; dark mode overrides live on body.dark-mode.
// SCSS variables below are thin aliases so every existing stylesheet just works.
// =============================================================================
// ---------------------------------------------------------------------------
// Light mode (default)
// ---------------------------------------------------------------------------
:root {
// 2025 branding
--core-fleet-black: #192147;
--core-fleet-green: #009a7d;
--core-fleet-white: #ffffff;
--ui-fleet-black-75: #515774;
--ui-fleet-black-50: #8b8fa2;
--ui-fleet-black-33: #b3b6c1;
--ui-fleet-black-25: #c5c7d1;
--ui-fleet-black-10: #e2e4ea;
--ui-fleet-black-5: #f4f4f6;
// 2025 secondary / interaction colors
--ui-fleet-black-75-over: #454c66;
--ui-fleet-black-75-down: #3a3e59;
--core-fleet-green-over: #00886c;
--core-fleet-green-down: #00775f;
--ui-fleet-black-5-down: #f0f1f4;
// Core accent
--core-fleet-blue: #3e4771;
--core-vibrant-blue: #6a67fe;
--core-vibrant-red: #ff5c83;
--core-fleet-purple: #ae6ddf;
--core-dark-blue-grey: #506e92;
--site-nav-on-hover: #0e1533;
// UI
--ui-fleet-blue-10: #f9fafc;
--ui-dark-blue-gray: #afbec1;
--ui-blue-gray: #dbe3e5;
--ui-gray: #e3e3e3;
--ui-light-grey: #fafafa;
--ui-off-white: #f9fafc;
--ui-shadow: #e9e9e9;
--ui-vibrant-blue-50: rgba(106, 103, 254, 0.5);
--ui-vibrant-blue-25: #d9d9fe;
--ui-vibrant-blue-10: #f1f0ff;
--tooltip-bg: #3e4771;
// Notifications & status
--ui-offline: #8b8fa2;
--ui-success: #3db67b;
--ui-warning: #ebbc43;
--ui-error: #d66c7b;
--ui-info: #6a67fe;
--ui-yellow-banner: #fef7e0;
--ui-yellow-banner-outline: #ece0bb;
// TS COLORS aliases (match keys used in colors.ts for JS inline styles)
--core-fleet-red: #ff5c83;
--ui-blue-hover: #5d5ae7;
--ui-blue-pressed: #4b4ab4;
--ui-blue-50: rgba(106, 103, 254, 0.5);
--ui-blue-25: #d9d9fe;
--ui-blue-10: #f1f0ff;
--status-success: #3db67b;
--status-warning: #f8cd6b;
--status-error: #ed6e85;
// Rainbow
--rainbow-orange: #faa669;
--rainbow-green: #63c740;
--rainbow-blue: #5cabdf;
// Gradients
--gradient-background: #eff6fa;
// Button hover / active
--core-vibrant-red-over: #e93661;
--core-vibrant-red-down: #cb3559;
--core-vibrant-blue-over: #5d5ae7;
--core-vibrant-blue-down: #4b4ab4;
--ui-success-over: #36a26d;
--ui-success-down: #2b7f56;
--core-fleet-blue-over: #303860;
--core-fleet-blue-down: #192147;
// Overlay / semi-transparent (used in Modal & Button)
--core-fleet-black-overlay-40: rgba(25, 33, 71, 0.4);
--core-fleet-black-overlay-05: rgba(25, 33, 71, 0.05);
--loading-overlay: rgba(255, 255, 255, 0.8);
}
// ---------------------------------------------------------------------------
// Dark mode
// ---------------------------------------------------------------------------
body.dark-mode {
// Branding text lightens, backgrounds darken
// Surface hierarchy (darkest lightest):
// Base #181a1f Surface-0 #1e2128 Surface-1 #252830
// Surface-2 #32363e Surface-3 #42464f
--core-fleet-black: #e2e4ea;
--core-fleet-green: #009a7d;
--core-fleet-white: #181a1f;
--ui-fleet-black-75: #b3b6c1;
--ui-fleet-black-50: #8b8fa2;
--ui-fleet-black-33: #636777;
--ui-fleet-black-25: #42464f;
--ui-fleet-black-10: #32363e;
--ui-fleet-black-5: #252830;
// Secondary / interaction
--ui-fleet-black-75-over: #c5c7d1;
--ui-fleet-black-75-down: #d5d7de;
--core-fleet-green-over: #01a889;
--core-fleet-green-down: #02be9c;
--ui-fleet-black-5-down: #2c2f37;
// Core accent slightly brighter for dark-bg contrast
--core-fleet-blue: #6a6f8a;
--core-vibrant-blue: #7b79ff;
--core-vibrant-red: #ff7a9a;
--core-fleet-purple: #c08ae8;
--core-dark-blue-grey: #6e8eaf;
--site-nav-on-hover: #252830;
// UI
--ui-fleet-blue-10: #1e2128;
--ui-dark-blue-gray: #4a5560;
--ui-blue-gray: #2d313a;
--ui-gray: #32363e;
--ui-light-grey: #1e2128;
--ui-off-white: #1e2128;
--ui-shadow: #0e1014;
--ui-vibrant-blue-50: rgba(123, 121, 255, 0.3);
--ui-vibrant-blue-25: #282736;
--ui-vibrant-blue-10: #1e1f2a;
--tooltip-bg: #353940;
// Notifications & status slightly brighter
--ui-offline: #8b8fa2;
--ui-success: #4dc98b;
--ui-warning: #f0ca5e;
--ui-error: #e07888;
--ui-info: #7b79ff;
--ui-yellow-banner: #2e291d;
--ui-yellow-banner-outline: #4e4125;
// TS COLORS aliases
--core-fleet-red: #ff7a9a;
--ui-blue-hover: #8c8aff;
--ui-blue-pressed: #6b69d9;
--ui-blue-50: rgba(123, 121, 255, 0.3);
--ui-blue-25: #282736;
--ui-blue-10: #1e1f2a;
--status-success: #4dc98b;
--status-warning: #f0ca5e;
--status-error: #e07888;
// Rainbow
--rainbow-orange: #fbb580;
--rainbow-green: #75d455;
--rainbow-blue: #70bbea;
// Gradients slightly lighter top for subtle depth
--gradient-background: #1c1f25;
// Button hover / active
--core-vibrant-red-over: #ff8da5;
--core-vibrant-red-down: #e07090;
--core-vibrant-blue-over: #8c8aff;
--core-vibrant-blue-down: #6b69d9;
--ui-success-over: #5dd99b;
--ui-success-down: #4dc98b;
--core-fleet-blue-over: #7a7f96;
--core-fleet-blue-down: #e2e4ea;
// Overlays
--core-fleet-black-overlay-40: rgba(0, 0, 0, 0.6);
--core-fleet-black-overlay-05: rgba(226, 228, 234, 0.06);
--loading-overlay: rgba(24, 26, 31, 0.8);
}
// =============================================================================
// SCSS variable aliases every existing component stylesheet keeps working.
// =============================================================================
// 2025 branding
$core-fleet-black: #192147;
$core-fleet-green: #009a7d;
$core-fleet-white: #ffffff;
$ui-fleet-black-75: #515774;
$ui-fleet-black-50: #8b8fa2;
$ui-fleet-black-33: #b3b6c1;
$ui-fleet-black-25: #c5c7d1;
$ui-fleet-black-10: #e2e4ea;
$ui-fleet-black-5: #f4f4f6;
$core-focused-outline: $core-fleet-black;
$core-fleet-black: var(--core-fleet-black);
$core-fleet-green: var(--core-fleet-green);
$core-fleet-white: var(--core-fleet-white);
$ui-fleet-black-75: var(--ui-fleet-black-75);
$ui-fleet-black-50: var(--ui-fleet-black-50);
$ui-fleet-black-33: var(--ui-fleet-black-33);
$ui-fleet-black-25: var(--ui-fleet-black-25);
$ui-fleet-black-10: var(--ui-fleet-black-10);
$ui-fleet-black-5: var(--ui-fleet-black-5);
$core-focused-outline: var(--core-fleet-black);
// 2025 secondary colors
$ui-fleet-black-75-over: darken(#515774, 5%);
$ui-fleet-black-75-down: darken(#515774, 10%);
$core-fleet-green-over: darken(#009a7d, 5%);
$core-fleet-green-down: darken(#009a7d, 10%);
$ui-fleet-black-5-down: #f0f1f4; // As specified in design system
$ui-fleet-black-75-over: var(--ui-fleet-black-75-over);
$ui-fleet-black-75-down: var(--ui-fleet-black-75-down);
$core-fleet-green-over: var(--core-fleet-green-over);
$core-fleet-green-down: var(--core-fleet-green-down);
$ui-fleet-black-5-down: var(--ui-fleet-black-5-down);
// Core
$core-fleet-blue: #3e4771;
$core-vibrant-blue: #6a67fe;
$core-vibrant-red: #ff5c83;
$core-fleet-purple: #ae6ddf;
$core-dark-blue-grey: #506e92;
$site-nav-on-hover: #0e1533;
$core-fleet-blue: var(--core-fleet-blue);
$core-vibrant-blue: var(--core-vibrant-blue);
$core-vibrant-red: var(--core-vibrant-red);
$core-fleet-purple: var(--core-fleet-purple);
$core-dark-blue-grey: var(--core-dark-blue-grey);
$site-nav-on-hover: var(--site-nav-on-hover);
// UI
$ui-fleet-blue-10: #f9fafc;
$ui-dark-blue-gray: #afbec1;
$ui-blue-gray: #dbe3e5;
$ui-gray: #e3e3e3;
$ui-light-grey: #fafafa;
$ui-off-white: #f9fafc; // rgba(249, 250, 252, 1)
$ui-shadow: #e9e9e9;
$ui-vibrant-blue-50: rgba(106, 103, 254, 0.5);
$ui-vibrant-blue-25: #d9d9fe;
$ui-vibrant-blue-10: #f1f0ff; // rgba(241, 240, 255, 1)
$tooltip-bg: #3e4771;
$ui-fleet-blue-10: var(--ui-fleet-blue-10);
$ui-dark-blue-gray: var(--ui-dark-blue-gray);
$ui-blue-gray: var(--ui-blue-gray);
$ui-gray: var(--ui-gray);
$ui-light-grey: var(--ui-light-grey);
$ui-off-white: var(--ui-off-white);
$ui-shadow: var(--ui-shadow);
$ui-vibrant-blue-50: var(--ui-vibrant-blue-50);
$ui-vibrant-blue-25: var(--ui-vibrant-blue-25);
$ui-vibrant-blue-10: var(--ui-vibrant-blue-10);
$tooltip-bg: var(--tooltip-bg);
// Notifications & status & specific messages
$ui-offline: #8b8fa2;
$ui-success: #3db67b;
$ui-warning: #ebbc43;
$ui-error: #d66c7b;
$ui-info: #6a67fe;
$ui-yellow-banner: #fef7e0;
$ui-yellow-banner-outline: #ece0bb;
$ui-offline: var(--ui-offline);
$ui-success: var(--ui-success);
$ui-warning: var(--ui-warning);
$ui-error: var(--ui-error);
$ui-info: var(--ui-info);
$ui-yellow-banner: var(--ui-yellow-banner);
$ui-yellow-banner-outline: var(--ui-yellow-banner-outline);
// Rainbow
$rainbow-orange: #faa669;
$rainbow-green: #63c740;
$rainbow-blue: #5cabdf;
$rainbow-orange: var(--rainbow-orange);
$rainbow-green: var(--rainbow-green);
$rainbow-blue: var(--rainbow-blue);
// Gradients
$gradient-background: #eff6fa; // 2025 gradient start color
$gradients-dark-gradient: linear-gradient(270deg, #201e43 0%, #353d62 100%);
$gradient-background: var(--gradient-background);
// These gradients are already dark; no theme swap needed.
$gradients-dark-gradient: linear-gradient(270deg, #202226 0%, #353840 100%);
$gradients-dark-gradient-vertical: linear-gradient(
360deg,
#201e43 0%,
#353d62 100%
#202226 0%,
#353840 100%
);
$gradients-bright-gradient: linear-gradient(180deg, #ae6ddf 0%, #6a67fe 100%);
// Colors for over (hover) and down (active) buttons styles
$core-vibrant-red-over: #e93661;
$core-vibrant-red-down: #cb3559;
$core-vibrant-blue-over: #5d5ae7;
$core-vibrant-blue-down: #4b4ab4;
$ui-success-over: #36a26d;
$ui-success-down: #2b7f56;
$core-fleet-blue-over: #303860;
$core-fleet-blue-down: $core-fleet-black;
// Colors for over (hover) and down (active) button styles
$core-vibrant-red-over: var(--core-vibrant-red-over);
$core-vibrant-red-down: var(--core-vibrant-red-down);
$core-vibrant-blue-over: var(--core-vibrant-blue-over);
$core-vibrant-blue-down: var(--core-vibrant-blue-down);
$ui-success-over: var(--ui-success-over);
$ui-success-down: var(--ui-success-down);
$core-fleet-blue-over: var(--core-fleet-blue-over);
$core-fleet-blue-down: var(--core-fleet-blue-down);
$transparent: rgba(255, 255, 255, 0);
// Opaque colors for table shadows
// l = lowest color a = (255-l)/255
// Overlay aliases (replace inline rgba($core-fleet-black, ) calls)
$core-fleet-black-overlay-40: var(--core-fleet-black-overlay-40);
$core-fleet-black-overlay-05: var(--core-fleet-black-overlay-05);
$loading-overlay: var(--loading-overlay);
// Static colors not themed, same in both light and dark mode.
// Use for elements that are always dark surfaces with light text (tooltips, code blocks).
$static-white: #e8eaf0;
$static-black: #192147;
// Opaque colors for table shadows compile-time SCSS math, not themed.
// These are subtle edge effects; dark-mode polish can refine them later.
$ui-vibrant-blue-10-alpha: (255-240)/255; // rgba(241, 240, 255)
$ui-vibrant-blue-10-opaque: rgba(
(241-240) / $ui-vibrant-blue-10-alpha,

View file

@ -1,30 +1,30 @@
export type Colors = keyof typeof COLORS;
export const COLORS = {
// Static fallback values (used during SSR or if CSS custom property is missing)
const STATIC_COLORS = {
// 2025 branding
"core-fleet-black": "#192147", // Headers, thead, Field :focus outline, keyboard :focus-visible outline
"core-fleet-black": "#192147",
"core-fleet-green": "#009A7D",
"core-fleet-white": "#FFFFFF",
"ui-fleet-black-75": "#515774",
"ui-fleet-black-50": "#8B8FA2", // Field :hover borders
"ui-fleet-black-50": "#8B8FA2",
"ui-fleet-black-33": "#B3B6C1",
"ui-fleet-black-25": "#C5C7D1",
"ui-fleet-black-10": "#E2E4EA", // Field borders, card borders
"ui-fleet-black-10": "#E2E4EA",
"ui-fleet-black-5": "#F4F4F6",
// 2025 secondary colors
// Sass functions only work in SCSS, not in runtime TypeScript or JavaScript files
"ui-fleet-black-75-over": "#454C66", // darken(#515774, 5%) or color.adjust(#515774, $lightness: -5%)
"ui-fleet-black-75-down": "#3A3E59", // darken(#515774, 10%) or color.adjust(#515774, $lightness: -10%)
"core-fleet-green-over": "#00886C", // "darken(#009A7D, 5%)" or color.adjust(#009A7D, $lightness: -5%)
"core-fleet-green-down": "#00775F", // "darken(#009A7D, 10%)" or color.adjust(#009A7D, $lightness: -10%)
"ui-fleet-black-75-over": "#454C66",
"ui-fleet-black-75-down": "#3A3E59",
"core-fleet-green-over": "#00886C",
"core-fleet-green-down": "#00775F",
// core colors
"core-fleet-blue": "#6A67FE", // TODO: lots of work to correctly match scss core-fleet-blue and not ui-vibrant-blue
"core-fleet-blue": "#3E4771",
"core-fleet-red": "#FF5C83",
"core-fleet-purple": "#AE6DDF",
// ui colors
"core-vibrant-blue": "#6A67FE",
"core-vibrant-red": "#FF5C83",
"ui-off-white": "#F9FAFC",
"ui-blue-hover": "#5D5AE7",
"ui-blue-pressed": "#4B4AB4",
@ -35,6 +35,7 @@ export const COLORS = {
"ui-light-grey": "#FAFAFA",
"ui-error": "#d66c7b",
"ui-warning": "#ebbc43",
"ui-fleet-black-5-down": "#F0F1F4",
// Notifications & status
"status-success": "#3DB67B",
@ -45,4 +46,26 @@ export const COLORS = {
"core-vibrant-blue-down": "#4b4ab4",
"ui-vibrant-blue-25": "#d9d9fe",
"ui-vibrant-blue-10": "#f1f0ff",
};
} as const;
export type Colors = keyof typeof STATIC_COLORS;
// Proxy returns CSS var() references so inline JS styles automatically adapt
// to light/dark mode without calling getComputedStyle. Falls back to the
// static value when running outside a browser (e.g. SSR or tests in jsdom).
// eslint-disable-next-line import/prefer-default-export
export const COLORS: Record<Colors, string> = new Proxy(
STATIC_COLORS as Record<Colors, string>,
{
get(target, key, receiver) {
if (typeof key === "symbol") {
return Reflect.get(target, key, receiver);
}
const fallback = target[key as Colors] ?? "";
if (typeof document !== "undefined") {
return fallback ? `var(--${key}, ${fallback})` : `var(--${key})`;
}
return fallback;
},
}
);

View file

@ -221,7 +221,7 @@ $max-width: 2560px;
width: max-content;
max-width: 360px;
padding: 6px;
color: $core-fleet-white;
color: $static-white;
background-color: $tooltip-bg;
font-weight: $regular;
font-size: $xx-small;
@ -541,8 +541,8 @@ $max-width: 2560px;
}
@mixin gradient-background {
background: linear-gradient(180deg, #eff6fa 0%, #fff 100%);
background-image: linear-gradient(180deg, $gradient-background 0%, $core-fleet-white 100%);
background-size: 100% 150px; // Must do this or gradient can be different heights on different pages
background-repeat: no-repeat;
background-color: #fff; // area below gradient will just be white
background-color: $core-fleet-white; // area below gradient transitionable
}

View file

@ -0,0 +1,32 @@
const THEME_KEY = "fleet-dark-mode";
const TRANSITION_MS = 300;
export const isDarkMode = (): boolean => {
return localStorage.getItem(THEME_KEY) === "true";
};
export const toggleDarkMode = (): boolean => {
const dark = !isDarkMode();
localStorage.setItem(THEME_KEY, String(dark));
// Add a temporary class that applies a blanket transition to all elements
// so the entire UI fades smoothly instead of individual pieces snapping.
document.body.classList.add("theme-transition");
document.body.classList.toggle("dark-mode", dark);
setTimeout(() => {
document.body.classList.remove("theme-transition");
}, TRANSITION_MS);
window.dispatchEvent(
new CustomEvent("fleet-theme-change", { detail: { dark } })
);
return dark;
};
export const initTheme = (): void => {
if (isDarkMode()) {
document.body.classList.add("dark-mode");
}
};