From 8ed339f012ea4ce6254c53f9e7c304f3a5cd19cd Mon Sep 17 00:00:00 2001 From: Luke Heath Date: Fri, 10 Apr 2026 09:30:04 -0500 Subject: [PATCH] Apply global dark mode styling to UI (#43033) --- .../ActionsDropdown/ActionsDropdown.tsx | 1 + frontend/components/Card/_styles.scss | 4 + .../LiveQuery/TargetChipSelector/_styles.scss | 1 + frontend/components/Modal/_styles.scss | 2 +- frontend/components/SQLEditor/theme.css | 61 ++++ .../components/SidePanelContent/_styles.scss | 4 + .../components/SidePanelPage/_styles.scss | 2 +- frontend/components/TabNav/_styles.scss | 1 + .../TableContainer/DataTable/_styles.scss | 12 +- .../TeamsDropdown/TeamsDropdown.tsx | 4 +- .../components/buttons/Button/_styles.scss | 2 +- .../DropdownButton/DropdownButton.stories.tsx | 2 +- .../forms/fields/Dropdown/_styles.scss | 4 +- .../DropdownWrapper/DropdownWrapper.tsx | 4 +- .../icons/OrgLogoIcon/OrgLogoIcon.jsx | 8 +- .../components/icons/OrgLogoIcon/_styles.scss | 5 + .../top_nav/SiteTopNav/SiteTopNav.tsx | 25 +- .../components/top_nav/UserMenu/UserMenu.tsx | 5 +- .../components/top_nav/UserMenu/_styles.scss | 1 + frontend/index.jsx | 2 + .../AccountSidePanel/AccountSidePanel.tsx | 55 +++ .../AccountPage/AccountSidePanel/_styles.scss | 44 +++ .../ActivityTypeDropdown.tsx | 3 +- .../cards/HostCountCard/_styles.scss | 6 + .../components/LabelFilterSelect/_styles.scss | 1 + frontend/styles/global/_global.scss | 99 +++++- frontend/styles/var/colors.scss | 312 ++++++++++++++---- frontend/styles/var/colors.ts | 49 ++- frontend/styles/var/mixins.scss | 6 +- frontend/utilities/theme.ts | 32 ++ 30 files changed, 657 insertions(+), 100 deletions(-) create mode 100644 frontend/components/top_nav/UserMenu/_styles.scss create mode 100644 frontend/utilities/theme.ts diff --git a/frontend/components/ActionsDropdown/ActionsDropdown.tsx b/frontend/components/ActionsDropdown/ActionsDropdown.tsx index a9c33b5252..bbbe12cb83 100644 --- a/frontend/components/ActionsDropdown/ActionsDropdown.tsx +++ b/frontend/components/ActionsDropdown/ActionsDropdown.tsx @@ -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, diff --git a/frontend/components/Card/_styles.scss b/frontend/components/Card/_styles.scss index 7994f111c1..d27d3dadc0 100644 --- a/frontend/components/Card/_styles.scss +++ b/frontend/components/Card/_styles.scss @@ -65,6 +65,10 @@ // color styles &__white { background-color: $core-fleet-white; + + body.dark-mode & { + background-color: $ui-fleet-black-5; + } } &__grey { diff --git a/frontend/components/LiveQuery/TargetChipSelector/_styles.scss b/frontend/components/LiveQuery/TargetChipSelector/_styles.scss index 77c4cb8137..541a363530 100644 --- a/frontend/components/LiveQuery/TargetChipSelector/_styles.scss +++ b/frontend/components/LiveQuery/TargetChipSelector/_styles.scss @@ -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; diff --git a/frontend/components/Modal/_styles.scss b/frontend/components/Modal/_styles.scss index 5fda0439cf..e95ca8c40d 100644 --- a/frontend/components/Modal/_styles.scss +++ b/frontend/components/Modal/_styles.scss @@ -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; diff --git a/frontend/components/SQLEditor/theme.css b/frontend/components/SQLEditor/theme.css index 4393501cd1..304e4193cd 100644 --- a/frontend/components/SQLEditor/theme.css +++ b/frontend/components/SQLEditor/theme.css @@ -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; +} diff --git a/frontend/components/SidePanelContent/_styles.scss b/frontend/components/SidePanelContent/_styles.scss index c041d8a64c..6508dde7ca 100644 --- a/frontend/components/SidePanelContent/_styles.scss +++ b/frontend/components/SidePanelContent/_styles.scss @@ -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; diff --git a/frontend/components/SidePanelPage/_styles.scss b/frontend/components/SidePanelPage/_styles.scss index 3e048d3c6f..fd7cc44796 100644 --- a/frontend/components/SidePanelPage/_styles.scss +++ b/frontend/components/SidePanelPage/_styles.scss @@ -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; } diff --git a/frontend/components/TabNav/_styles.scss b/frontend/components/TabNav/_styles.scss index f7e0a4a746..a037ce9e95 100644 --- a/frontend/components/TabNav/_styles.scss +++ b/frontend/components/TabNav/_styles.scss @@ -60,6 +60,7 @@ &--selected { font-weight: $bold; background-color: $ui-fleet-black-5; + color: $core-fleet-black; } &--disabled { diff --git a/frontend/components/TableContainer/DataTable/_styles.scss b/frontend/components/TableContainer/DataTable/_styles.scss index 2019cd34ed..34704ae854 100644 --- a/frontend/components/TableContainer/DataTable/_styles.scss +++ b/frontend/components/TableContainer/DataTable/_styles.scss @@ -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; } diff --git a/frontend/components/TeamsDropdown/TeamsDropdown.tsx b/frontend/components/TeamsDropdown/TeamsDropdown.tsx index dfb634a0a7..0aa22a3145 100644 --- a/frontend/components/TeamsDropdown/TeamsDropdown.tsx +++ b/frontend/components/TeamsDropdown/TeamsDropdown.tsx @@ -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", diff --git a/frontend/components/buttons/Button/_styles.scss b/frontend/components/buttons/Button/_styles.scss index 8701fb0d21..72d9d2cba6 100644 --- a/frontend/components/buttons/Button/_styles.scss +++ b/frontend/components/buttons/Button/_styles.scss @@ -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 { diff --git a/frontend/components/buttons/DropdownButton/DropdownButton.stories.tsx b/frontend/components/buttons/DropdownButton/DropdownButton.stories.tsx index c620ed0faa..0f306b4a33 100644 --- a/frontend/components/buttons/DropdownButton/DropdownButton.stories.tsx +++ b/frontend/components/buttons/DropdownButton/DropdownButton.stories.tsx @@ -55,7 +55,7 @@ const meta: Meta = { values: [ { name: "header", - value: "linear-gradient(270deg, #201e43 0%, #353d62 100%)", + value: "linear-gradient(270deg, #202226 0%, #353840 100%)", }, ], }, diff --git a/frontend/components/forms/fields/Dropdown/_styles.scss b/frontend/components/forms/fields/Dropdown/_styles.scss index e982ea0d90..77f1d43e36 100644 --- a/frontend/components/forms/fields/Dropdown/_styles.scss +++ b/frontend/components/forms/fields/Dropdown/_styles.scss @@ -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; diff --git a/frontend/components/forms/fields/DropdownWrapper/DropdownWrapper.tsx b/frontend/components/forms/fields/DropdownWrapper/DropdownWrapper.tsx index 3338d0bd0e..058a04f8a2 100644 --- a/frontend/components/forms/fields/DropdownWrapper/DropdownWrapper.tsx +++ b/frontend/components/forms/fields/DropdownWrapper/DropdownWrapper.tsx @@ -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", diff --git a/frontend/components/icons/OrgLogoIcon/OrgLogoIcon.jsx b/frontend/components/icons/OrgLogoIcon/OrgLogoIcon.jsx index 063d808108..6ba6daa8cd 100644 --- a/frontend/components/icons/OrgLogoIcon/OrgLogoIcon.jsx +++ b/frontend/components/icons/OrgLogoIcon/OrgLogoIcon.jsx @@ -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 ( 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} >
- +
diff --git a/frontend/components/top_nav/UserMenu/UserMenu.tsx b/frontend/components/top_nav/UserMenu/UserMenu.tsx index 5718761ba2..60be9bac30 100644 --- a/frontend/components/top_nav/UserMenu/UserMenu.tsx +++ b/frontend/components/top_nav/UserMenu/UserMenu.tsx @@ -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"], diff --git a/frontend/components/top_nav/UserMenu/_styles.scss b/frontend/components/top_nav/UserMenu/_styles.scss new file mode 100644 index 0000000000..3258c85408 --- /dev/null +++ b/frontend/components/top_nav/UserMenu/_styles.scss @@ -0,0 +1 @@ +// Styles for user-menu are defined inline via react-select's `styles` prop. diff --git a/frontend/index.jsx b/frontend/index.jsx index 002730562f..d56c2516ce 100644 --- a/frontend/index.jsx +++ b/frontend/index.jsx @@ -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); diff --git a/frontend/pages/AccountPage/AccountSidePanel/AccountSidePanel.tsx b/frontend/pages/AccountPage/AccountSidePanel/AccountSidePanel.tsx index 15d197ce2c..4a3ab8a323 100644 --- a/frontend/pages/AccountPage/AccountSidePanel/AccountSidePanel.tsx +++ b/frontend/pages/AccountPage/AccountSidePanel/AccountSidePanel.tsx @@ -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(); + const [darkMode, setDarkMode] = useState(() => isDarkMode()); useEffect(() => { const getVersionData = async () => { @@ -74,6 +76,59 @@ const AccountSidePanel = ({ newTab /> +
+ + + + + + + + +
{isPremiumTier && ( ({ ...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", diff --git a/frontend/pages/DashboardPage/cards/HostCountCard/_styles.scss b/frontend/pages/DashboardPage/cards/HostCountCard/_styles.scss index 717bf4cd86..87e71e65b6 100644 --- a/frontend/pages/DashboardPage/cards/HostCountCard/_styles.scss +++ b/frontend/pages/DashboardPage/cards/HostCountCard/_styles.scss @@ -37,6 +37,12 @@ gap: $pad-xsmall; } + &__card-icon { + body.dark-mode & { + opacity: 0.75; + } + } + &__count { font-size: $large; white-space: nowrap; diff --git a/frontend/pages/hosts/ManageHostsPage/components/LabelFilterSelect/_styles.scss b/frontend/pages/hosts/ManageHostsPage/components/LabelFilterSelect/_styles.scss index b8e14f19bd..95e9490052 100644 --- a/frontend/pages/hosts/ManageHostsPage/components/LabelFilterSelect/_styles.scss +++ b/frontend/pages/hosts/ManageHostsPage/components/LabelFilterSelect/_styles.scss @@ -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 { diff --git a/frontend/styles/global/_global.scss b/frontend/styles/global/_global.scss index fbe8e65043..0fcd84407e 100644 --- a/frontend/styles/global/_global.scss +++ b/frontend/styles/global/_global.scss @@ -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; + } +} diff --git a/frontend/styles/var/colors.scss b/frontend/styles/var/colors.scss index 46c0c16df8..3a2c04d691 100644 --- a/frontend/styles/var/colors.scss +++ b/frontend/styles/var/colors.scss @@ -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, diff --git a/frontend/styles/var/colors.ts b/frontend/styles/var/colors.ts index 1a05d15c49..16f31bd146 100644 --- a/frontend/styles/var/colors.ts +++ b/frontend/styles/var/colors.ts @@ -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 = new Proxy( + STATIC_COLORS as Record, + { + 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; + }, + } +); diff --git a/frontend/styles/var/mixins.scss b/frontend/styles/var/mixins.scss index 0243c2fe33..c6d356f229 100644 --- a/frontend/styles/var/mixins.scss +++ b/frontend/styles/var/mixins.scss @@ -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 } diff --git a/frontend/utilities/theme.ts b/frontend/utilities/theme.ts new file mode 100644 index 0000000000..6c361ca69f --- /dev/null +++ b/frontend/utilities/theme.ts @@ -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"); + } +};