From 275cc73f7a955cc6ba172d891ed7a6f50e82e218 Mon Sep 17 00:00:00 2001 From: Hampus Date: Fri, 16 Oct 2026 03:06:24 +0200 Subject: [PATCH] fix(app): keep brand colour text readable on every surface (#3504) --- fluxer_app/scripts/GenerateColorSystem.ts | 300 ++++++------------ fluxer_app/src/app/BootShell.css | 4 +- .../UserAreaAccountSwitchMenu.module.css | 2 +- .../components/layout/ChannelItem.module.css | 2 +- .../layout/DropIndicator.module.css | 2 +- .../FavoritesChannelListContent.module.css | 4 +- .../components/layout/GuildsLayout.module.css | 12 +- .../app/components/layout/NagbarTones.ts | 2 +- .../layout/NullSpaceDropIndicator.module.css | 2 +- .../sidebar_nav/DiscoveryButton.module.css | 2 +- .../layout/sidebar_nav/DiscoveryButton.tsx | 4 +- .../sidebar_nav/GuildFolderItem.module.css | 4 +- .../components/pages/NotFoundPage.module.css | 2 +- .../app/components/pages/YouPage.module.css | 4 +- .../shared/FriendSelector.module.css | 2 +- .../skeleton/GuildRailSkeleton.module.css | 4 +- .../AccountProfilePicker.module.css | 2 +- .../AccountSwitcherOverlay.module.css | 2 +- .../modals/BackupCodesModal.module.css | 2 +- .../auth/flow/AuthPageStyles.module.css | 2 +- .../auth/flow/HandoffCodeDisplay.module.css | 2 +- .../components/AutocompleteEmoji.module.css | 2 +- .../components/AutocompleteGif.module.css | 4 +- .../ChannelAttachmentArea.module.css | 2 +- .../components/ChannelMessages.module.css | 4 +- .../channel/components/GifPicker.module.css | 16 +- .../components/GuildMembersPage.module.css | 2 +- .../channel/components/MemesPicker.module.css | 10 +- .../components/MessageReactions.module.css | 32 +- .../MessageUploadProgress.module.css | 2 +- .../ChannelDetailsBottomSheet.module.css | 7 +- .../direct_message/AddFriendView.module.css | 2 +- .../DirectMessageList.module.css | 8 +- .../TextualAttachmentPreview.module.css | 6 +- .../channel_embed/ChannelEmbedShared.ts | 2 +- .../embeds/media/EmbedVideo.module.css | 2 +- .../embeds/media/GifIndicator.module.css | 2 +- .../embeds/media/MediaButtons.module.css | 4 +- .../media/VoiceMessagePlayer.module.css | 8 +- .../friends/FriendListItem.module.css | 4 +- .../gif/ImportFavoriteGifsModal.module.css | 2 +- .../stickers/StickerGridItem.module.css | 2 +- .../modals/AssetSourceModal.module.css | 4 +- .../components/modals/TagListField.module.css | 2 +- .../forum/components/Forum.module.css | 22 +- .../guild/components/GuildBadge.module.css | 10 +- .../guild_tabs/GuildRolesTab.module.css | 4 + .../modals/ForwardMessagePreview.module.css | 2 +- .../modals/UploadDropModal.module.css | 6 +- .../components/pages/ReportPage.module.css | 2 +- .../components/MacPermissionPrompt.module.css | 2 +- .../search/ChannelFilterSheet.module.css | 2 +- .../search/HasFilterSheet.module.css | 6 +- .../components/search/ScopeSheet.module.css | 6 +- .../search/SortModeSheet.module.css | 6 +- .../search/UserFilterSheet.module.css | 2 +- .../BackgroundImageGalleryModal.module.css | 12 +- .../modals/BackgroundImageGalleryModal.tsx | 2 +- .../theme/hooks/useThemeCssVariables.ts | 4 +- .../theme/styles/AttachmentFile.module.css | 2 +- .../styles/AttachmentGridItem.module.css | 4 +- .../theme/utils/ColorSystemPalette.ts | 259 +++++++++++++++ .../theme/utils/InstanceBrandColors.test.ts | 150 +++++++++ .../theme/utils/InstanceBrandColors.ts | 279 ++++++++++++++-- .../theme/variables/ThemeVariableManifest.ts | 50 +++ .../theme_studio/ThemeStudio.module.css | 3 +- .../theme_studio/sections/QuickCssEditor.tsx | 2 +- .../theme_studio/ui/StudioButton.module.css | 2 +- .../components/MessageThreadChip.module.css | 2 +- .../ui/action_menu/ContextMenu.module.css | 4 +- .../ui/action_menu/MenuItemRadio.module.css | 4 +- .../src/features/ui/button/Button.module.css | 4 +- .../features/ui/checkbox/Checkbox.module.css | 8 +- .../src/features/ui/checkbox/Checkbox.tsx | 2 - .../ui/components/Pagination.module.css | 2 +- .../features/ui/components/Slider.module.css | 12 +- .../form/DateTimePickerField.module.css | 4 +- .../components/form/FormCombobox.module.css | 6 +- .../ui/components/form/FormSwitch.module.css | 1 + .../ui/keybind_hint/KeybindHint.module.css | 2 +- .../MenuBottomSheet.module.css | 8 +- .../PlutoniumUpsell.module.css | 8 +- .../SearchableListPopout.module.css | 2 +- .../ui/radio_group/RadioGroup.module.css | 8 +- .../src/features/ui/tabs/Tabs.module.css | 4 +- .../modals/StatusChangeBottomSheet.module.css | 2 +- .../modals/UserProfileMobileSheet.module.css | 6 +- .../modals/tabs/LinkedAccountsTab.module.css | 4 +- .../tabs/appearance_tab/ThemeTab.module.css | 4 +- .../ApplicationsTab.module.css | 2 +- .../ApplicationDetail.module.css | 2 +- .../popouts/UserProfileBadges.module.css | 2 +- .../UserProfileLoadingSkeleton.module.css | 2 +- .../SettingsStatusBadge.module.css | 2 +- .../bottomsheets/VoiceLobbyShared.module.css | 4 + .../media_player/AudioPlayer.module.css | 1 + .../media_player/InlineAudioPlayer.module.css | 10 +- .../media_player/MediaPlayButton.module.css | 2 +- .../media_player/MediaPlaybackRate.module.css | 2 +- .../media_player/MediaProgressBar.module.css | 2 +- .../MediaVolumeControl.module.css | 2 +- .../media_player/VideoPlayer.module.css | 4 +- 102 files changed, 1045 insertions(+), 419 deletions(-) create mode 100644 fluxer_app/src/features/theme/utils/ColorSystemPalette.ts create mode 100644 fluxer_app/src/features/theme/utils/InstanceBrandColors.test.ts diff --git a/fluxer_app/scripts/GenerateColorSystem.ts b/fluxer_app/scripts/GenerateColorSystem.ts index 2ce0716a9..bc3ce0844 100644 --- a/fluxer_app/scripts/GenerateColorSystem.ts +++ b/fluxer_app/scripts/GenerateColorSystem.ts @@ -2,24 +2,17 @@ import {mkdirSync, writeFileSync} from 'node:fs'; import {dirname, join, relative} from 'node:path'; - -interface ColorFamily { - hue: number; - saturation: number; - useSaturationFactor: boolean; -} - -interface ScaleStop { - name: string; - position?: number; -} - -interface Scale { - family: string; - range: [number, number]; - curve: 'linear' | 'easeIn' | 'easeOut' | 'easeInOut'; - stops: Array; -} +import { + BRAND_PRESSED_RATIO, + brandTintMix, + COLOR_FAMILIES, + COLOR_SCALES, + type ColorFamily, + type Scale, + scaleStopLightness, + THEME_PALETTES, +} from '@app/features/theme/utils/ColorSystemPalette'; +import {ThemeTypes} from '@fluxer/constants/src/UserConstants'; interface TokenDef { name?: string; @@ -295,142 +288,8 @@ const LIGHT_CODE_TOKENS: Array = [ {name: '--ansi-bg-bright-white', value: 'color-mix(in srgb, hsl(0, 0%, 90%) 90%, var(--code-block-bg) 10%)'}, ]; const CONFIG: Config = { - families: { - neutralDark: {hue: 258, saturation: 10, useSaturationFactor: true}, - neutralLight: {hue: 220, saturation: 10, useSaturationFactor: true}, - brand: {hue: 242, saturation: 70, useSaturationFactor: true}, - link: {hue: 198, saturation: 92, useSaturationFactor: true}, - accentPurple: {hue: 278, saturation: 85, useSaturationFactor: true}, - statusOnline: {hue: 152, saturation: 72, useSaturationFactor: true}, - legacyDark: {hue: 220, saturation: 13, useSaturationFactor: true}, - legacyLink: {hue: 210, saturation: 100, useSaturationFactor: true}, - legacyAccentPurple: {hue: 270, saturation: 80, useSaturationFactor: true}, - legacyStatusOnline: {hue: 142, saturation: 76, useSaturationFactor: true}, - statusIdle: {hue: 45, saturation: 93, useSaturationFactor: true}, - statusDnd: {hue: 0, saturation: 84, useSaturationFactor: true}, - statusOffline: {hue: 218, saturation: 11, useSaturationFactor: true}, - statusDanger: {hue: 1, saturation: 77, useSaturationFactor: true}, - textCode: {hue: 340, saturation: 50, useSaturationFactor: true}, - brandIcon: {hue: 38, saturation: 92, useSaturationFactor: true}, - }, - scales: { - darkSurface: { - family: 'neutralDark', - range: [5, 24], - curve: 'easeOut', - stops: [ - {name: '--background-primary', position: 0}, - {name: '--background-secondary', position: 0.16}, - {name: '--background-secondary-lighter', position: 0.22}, - {name: '--background-secondary-alt', position: 0.28}, - {name: '--background-tertiary', position: 0.4}, - {name: '--background-channel-header', position: 0.34}, - {name: '--guild-list-foreground', position: 0.38}, - {name: '--background-header-secondary', position: 0.5}, - {name: '--background-header-primary', position: 0.5}, - {name: '--background-textarea', position: 0.3}, - {name: '--background-header-primary-hover', position: 0.85}, - ], - }, - coalSurface: { - family: 'neutralDark', - range: [1, 12], - curve: 'easeOut', - stops: [ - {name: '--background-primary', position: 0}, - {name: '--background-secondary', position: 0.16}, - {name: '--background-secondary-alt', position: 0.28}, - {name: '--background-tertiary', position: 0.4}, - {name: '--background-channel-header', position: 0.34}, - {name: '--guild-list-foreground', position: 0.38}, - {name: '--background-header-secondary', position: 0.5}, - {name: '--background-header-primary', position: 0.5}, - {name: '--background-textarea', position: 0.3}, - {name: '--background-header-primary-hover', position: 0.85}, - ], - }, - darkText: { - family: 'neutralDark', - range: [60, 96], - curve: 'easeInOut', - stops: [ - {name: '--text-tertiary-secondary', position: 0}, - {name: '--text-tertiary-muted', position: 0.2}, - {name: '--text-tertiary', position: 0.38}, - {name: '--text-primary-muted', position: 0.55}, - {name: '--text-chat-muted', position: 0.55}, - {name: '--text-secondary', position: 0.72}, - {name: '--text-chat', position: 0.82}, - {name: '--text-primary', position: 1}, - ], - }, - lightSurface: { - family: 'neutralLight', - range: [86, 98.5], - curve: 'easeIn', - stops: [ - {name: '--background-header-primary-hover', position: 0}, - {name: '--background-header-primary', position: 0.12}, - {name: '--background-header-secondary', position: 0.2}, - {name: '--guild-list-foreground', position: 0.35}, - {name: '--background-tertiary', position: 0.42}, - {name: '--background-channel-header', position: 0.5}, - {name: '--background-secondary-alt', position: 0.63}, - {name: '--background-secondary', position: 0.74}, - {name: '--background-secondary-lighter', position: 0.83}, - {name: '--background-textarea', position: 0.88}, - {name: '--background-primary', position: 1}, - ], - }, - lightText: { - family: 'neutralLight', - range: [15, 54], - curve: 'easeOut', - stops: [ - {name: '--text-primary', position: 0}, - {name: '--text-chat', position: 0.08}, - {name: '--text-secondary', position: 0.28}, - {name: '--text-chat-muted', position: 0.45}, - {name: '--text-primary-muted', position: 0.45}, - {name: '--text-tertiary', position: 0.6}, - {name: '--text-tertiary-secondary', position: 0.75}, - {name: '--text-tertiary-muted', position: 0.85}, - ], - }, - legacyDarkSurface: { - family: 'legacyDark', - range: [5, 26], - curve: 'easeOut', - stops: [ - {name: '--background-primary', position: 0}, - {name: '--background-secondary', position: 0.16}, - {name: '--background-secondary-lighter', position: 0.22}, - {name: '--background-secondary-alt', position: 0.28}, - {name: '--background-tertiary', position: 0.4}, - {name: '--background-channel-header', position: 0.34}, - {name: '--guild-list-foreground', position: 0.38}, - {name: '--background-header-secondary', position: 0.5}, - {name: '--background-header-primary', position: 0.5}, - {name: '--background-textarea', position: 0.3}, - {name: '--background-header-primary-hover', position: 0.85}, - ], - }, - legacyDarkText: { - family: 'legacyDark', - range: [52, 96], - curve: 'easeInOut', - stops: [ - {name: '--text-tertiary-secondary', position: 0}, - {name: '--text-tertiary-muted', position: 0.2}, - {name: '--text-tertiary', position: 0.38}, - {name: '--text-primary-muted', position: 0.55}, - {name: '--text-chat-muted', position: 0.55}, - {name: '--text-secondary', position: 0.72}, - {name: '--text-chat', position: 0.82}, - {name: '--text-primary', position: 1}, - ], - }, - }, + families: COLOR_FAMILIES, + scales: COLOR_SCALES, tokens: { root: [ {scale: 'darkSurface'}, @@ -447,7 +306,7 @@ hsl(258, calc(10% * var(--saturation-factor)), 2%) 10% {name: '--panel-control-divider', family: 'neutralDark', saturation: 30, lightness: 55, alpha: 0.35}, {name: '--panel-control-highlight', value: 'hsla(0, 0%, 100%, 0.04)'}, {name: '--background-modifier-hover', family: 'neutralDark', lightness: 100, alpha: 0.05}, - {name: '--background-modifier-selected', family: 'neutralDark', lightness: 100, alpha: 0.1}, + {name: '--background-modifier-selected', ...THEME_PALETTES[ThemeTypes.DARK].selectedOverlay}, {name: '--background-modifier-accent', family: 'neutralDark', saturation: 13, lightness: 80, alpha: 0.15}, {name: '--background-modifier-accent-focus', family: 'neutralDark', saturation: 13, lightness: 80, alpha: 0.22}, {name: '--control-button-normal-bg', value: 'transparent'}, @@ -480,6 +339,41 @@ hsl(258, calc(10% * var(--saturation-factor)), 2%) 10% lightness: 100, instanceOverride: '--instance-brand-primary-fill', }, + { + name: '--brand-primary-shade', + hue: 0, + saturation: 0, + lightness: 0, + instanceOverride: '--instance-brand-primary-shade', + }, + { + name: '--brand-primary-pressed', + value: `color-mix(in srgb, var(--brand-primary) ${BRAND_PRESSED_RATIO}%, var(--brand-primary-shade))`, + }, + { + name: '--brand-primary-foreground', + family: 'brand', + lightness: 55, + instanceOverride: '--instance-brand-primary-foreground', + }, + { + name: '--brand-primary-light-fill', + hue: 0, + saturation: 0, + lightness: 100, + instanceOverride: '--instance-brand-primary-light-fill', + }, + { + name: '--brand-tint', + value: brandTintMix(THEME_PALETTES[ThemeTypes.DARK].brandTint, 'rest'), + instanceOverride: '--instance-brand-tint', + }, + { + name: '--brand-tint-hover', + value: brandTintMix(THEME_PALETTES[ThemeTypes.DARK].brandTint, 'hover'), + instanceOverride: '--instance-brand-tint-hover', + }, + {name: '--brand-tint-border', value: 'var(--brand-primary)', instanceOverride: '--instance-brand-tint-border'}, {name: '--status-online', family: 'statusOnline', lightness: 40}, {name: '--status-idle', family: 'statusIdle', lightness: 50}, {name: '--status-dnd', family: 'statusDnd', lightness: 60}, @@ -500,6 +394,19 @@ hsl(258, calc(10% * var(--saturation-factor)), 2%) 10% lightness: 98, instanceOverride: '--instance-text-on-brand-primary', }, + { + name: '--text-on-brand-primary-light', + hue: 0, + saturation: 0, + lightness: 98, + instanceOverride: '--instance-text-on-brand-primary-light', + }, + { + name: '--text-on-brand-tint', + value: 'var(--text-on-brand-primary)', + instanceOverride: '--instance-text-on-brand-tint', + }, + {name: '--text-on-dark', hue: 0, saturation: 0, lightness: 98}, ...DARK_CODE_TOKENS, {name: '--text-selection', hue: 198, saturation: 92, useSaturationFactor: true, lightness: 70, alpha: 0.35}, {name: '--markup-mention-text', value: 'var(--text-link)'}, @@ -623,7 +530,7 @@ hsl(245, calc(100% * var(--saturation-factor)), 80%) 40% {name: '--panel-control-divider', family: 'neutralLight', saturation: 30, lightness: 35, alpha: 0.2}, {name: '--panel-control-highlight', value: 'hsla(0, 0%, 100%, 0.65)'}, {name: '--background-modifier-hover', family: 'neutralLight', saturation: 10, lightness: 10, alpha: 0.05}, - {name: '--background-modifier-selected', family: 'neutralLight', saturation: 10, lightness: 10, alpha: 0.1}, + {name: '--background-modifier-selected', ...THEME_PALETTES[ThemeTypes.LIGHT].selectedOverlay}, {name: '--background-modifier-accent', family: 'neutralLight', saturation: 10, lightness: 40, alpha: 0.22}, {name: '--background-modifier-accent-focus', family: 'neutralLight', saturation: 10, lightness: 40, alpha: 0.32}, {name: '--control-button-normal-bg', value: 'transparent'}, @@ -667,6 +574,22 @@ hsl(245, calc(100% * var(--saturation-factor)), 80%) 40% {name: '--plutonium-text', value: 'var(--text-on-brand-primary)'}, {name: '--plutonium-icon', family: 'brandIcon', lightness: 45}, {name: '--invite-verified-icon-color', value: 'var(--brand-primary)'}, + { + name: '--brand-tint', + value: brandTintMix(THEME_PALETTES[ThemeTypes.LIGHT].brandTint, 'rest'), + instanceOverride: '--instance-brand-tint', + }, + { + name: '--brand-tint-hover', + value: brandTintMix(THEME_PALETTES[ThemeTypes.LIGHT].brandTint, 'hover'), + instanceOverride: '--instance-brand-tint-hover', + }, + { + name: '--brand-tint-border', + value: 'color-mix(in srgb, var(--brand-primary) 45%, transparent 55%)', + instanceOverride: '--instance-brand-tint-border', + }, + {name: '--text-on-brand-tint', value: 'var(--brand-primary)', instanceOverride: '--instance-text-on-brand-tint'}, {name: '--border-color', family: 'neutralLight', lightness: 40, alpha: 0.15}, {name: '--border-color-hover', family: 'neutralLight', lightness: 40, alpha: 0.25}, {name: '--border-color-focus', hue: 210, saturation: 90, useSaturationFactor: true, lightness: 50, alpha: 0.4}, @@ -707,8 +630,10 @@ hsl(245, calc(100% * var(--saturation-factor)), 80%) 40% ], coal: [ {scale: 'coalSurface'}, - {name: '--background-secondary', value: 'var(--background-primary)'}, - {name: '--background-secondary-lighter', value: 'var(--background-primary)'}, + ...Object.entries(THEME_PALETTES[ThemeTypes.COAL].surfaceAliases).map(([name, target]) => ({ + name, + value: `var(${target})`, + })), { name: '--panel-control-bg', value: `color-mix( @@ -721,7 +646,7 @@ hsl(258, calc(10% * var(--saturation-factor)), 0%) 10% {name: '--panel-control-divider', family: 'neutralDark', saturation: 20, lightness: 25, alpha: 0.28}, {name: '--panel-control-highlight', value: 'hsla(0, 0%, 100%, 0.06)'}, {name: '--background-modifier-hover', family: 'neutralDark', lightness: 100, alpha: 0.04}, - {name: '--background-modifier-selected', family: 'neutralDark', lightness: 100, alpha: 0.08}, + {name: '--background-modifier-selected', ...THEME_PALETTES[ThemeTypes.COAL].selectedOverlay}, {name: '--background-modifier-accent', family: 'neutralDark', saturation: 10, lightness: 65, alpha: 0.18}, {name: '--background-modifier-accent-focus', family: 'neutralDark', saturation: 10, lightness: 70, alpha: 0.26}, {name: '--control-button-normal-bg', value: 'transparent'}, @@ -776,7 +701,7 @@ hsl(220, calc(13% * var(--saturation-factor)), 2%) 10% {name: '--panel-control-border', family: 'legacyDark', saturation: 30, lightness: 65, alpha: 0.45}, {name: '--panel-control-divider', family: 'legacyDark', saturation: 30, lightness: 55, alpha: 0.35}, {name: '--background-modifier-hover', family: 'legacyDark', lightness: 100, alpha: 0.05}, - {name: '--background-modifier-selected', family: 'legacyDark', lightness: 100, alpha: 0.1}, + {name: '--background-modifier-selected', ...THEME_PALETTES[ThemeTypes.DARK_LEGACY].selectedOverlay}, {name: '--background-modifier-accent', family: 'legacyDark', saturation: 13, lightness: 80, alpha: 0.15}, {name: '--background-modifier-accent-focus', family: 'legacyDark', saturation: 13, lightness: 80, alpha: 0.22}, {name: '--control-button-hover-bg', family: 'legacyDark', lightness: 22}, @@ -820,48 +745,13 @@ interface OutputToken { instanceOverride?: string; } -function clamp01(value: number): number { - return Math.min(1, Math.max(0, value)); -} - -function applyCurve(curve: Scale['curve'], t: number): number { - switch (curve) { - case 'easeIn': - return t * t; - case 'easeOut': - return 1 - (1 - t) * (1 - t); - case 'easeInOut': - if (t < 0.5) { - return 2 * t * t; - } - return 1 - 2 * (1 - t) * (1 - t); - default: - return t; - } -} - function buildScaleTokens(scale: Scale): Array { - const lastIndex = Math.max(scale.stops.length - 1, 1); - const tokens: Array = []; - for (let i = 0; i < scale.stops.length; i++) { - const stop = scale.stops[i]; - let pos: number; - if (stop.position !== undefined) { - pos = clamp01(stop.position); - } else { - pos = i / lastIndex; - } - const eased = applyCurve(scale.curve, pos); - let lightness = scale.range[0] + (scale.range[1] - scale.range[0]) * eased; - lightness = Math.round(lightness * 1000) / 1000; - tokens.push({ - type: 'tone', - name: stop.name, - family: scale.family, - lightness, - }); - } - return tokens; + return scale.stops.map((stop, index) => ({ + type: 'tone', + name: stop.name, + family: scale.family, + lightness: scaleStopLightness(scale, index), + })); } function expandTokens(defs: Array, scales: Record): Array { @@ -881,6 +771,7 @@ function expandTokens(defs: Array, scales: Record): Arr type: 'literal', name: def.name!, value: def.value.trim(), + instanceOverride: def.instanceOverride, }); } else { tokens.push({ @@ -945,11 +836,8 @@ function formatTone(token: OutputToken, families: Record): } function formatValue(token: OutputToken, families: Record): string { - if (token.type === 'tone') { - const tone = formatTone(token, families); - return token.instanceOverride ? `var(${token.instanceOverride}, ${tone})` : tone; - } - return token.value!.trim(); + const value = token.type === 'tone' ? formatTone(token, families) : token.value!.trim(); + return token.instanceOverride ? `var(${token.instanceOverride}, ${value})` : value; } function renderBlock(selector: string, tokens: Array, families: Record): string { diff --git a/fluxer_app/src/app/BootShell.css b/fluxer_app/src/app/BootShell.css index ad0f9ee5f..e6c31f940 100644 --- a/fluxer_app/src/app/BootShell.css +++ b/fluxer_app/src/app/BootShell.css @@ -201,7 +201,7 @@ html.platform-native .fluxer-boot-app { .fluxer-boot-item[data-selected='1'] .fluxer-boot-fluxer-icon { border-radius: 30%; background-color: var(--brand-primary); - color: white; + color: var(--brand-primary-fill); } .fluxer-boot-action-icon { display: flex; @@ -218,7 +218,7 @@ html.platform-native .fluxer-boot-app { .fluxer-boot-item[data-selected='1'] .fluxer-boot-action-icon { border-radius: 30%; background-color: var(--brand-primary); - color: white; + color: var(--brand-primary-fill); } .fluxer-boot-glyph-lg { height: 1.75rem; diff --git a/fluxer_app/src/features/app/components/floating/UserAreaAccountSwitchMenu.module.css b/fluxer_app/src/features/app/components/floating/UserAreaAccountSwitchMenu.module.css index 4cc604d7c..6309fa43e 100644 --- a/fluxer_app/src/features/app/components/floating/UserAreaAccountSwitchMenu.module.css +++ b/fluxer_app/src/features/app/components/floating/UserAreaAccountSwitchMenu.module.css @@ -137,5 +137,5 @@ height: 1.25rem; border-radius: 50%; background-color: var(--brand-primary); - color: white; + color: var(--brand-primary-fill); } diff --git a/fluxer_app/src/features/app/components/layout/ChannelItem.module.css b/fluxer_app/src/features/app/components/layout/ChannelItem.module.css index 72532742e..dcb3ae765 100644 --- a/fluxer_app/src/features/app/components/layout/ChannelItem.module.css +++ b/fluxer_app/src/features/app/components/layout/ChannelItem.module.css @@ -298,7 +298,7 @@ } .channelItemAutocompleteHighlight { - box-shadow: 0 0 0 0.125rem var(--brand-primary); + box-shadow: 0 0 0 0.125rem var(--brand-primary-foreground); } .categoryContent { diff --git a/fluxer_app/src/features/app/components/layout/DropIndicator.module.css b/fluxer_app/src/features/app/components/layout/DropIndicator.module.css index b9f5dea4a..31faf5840 100644 --- a/fluxer_app/src/features/app/components/layout/DropIndicator.module.css +++ b/fluxer_app/src/features/app/components/layout/DropIndicator.module.css @@ -27,5 +27,5 @@ } :global(.theme-light) .dropIndicatorValid { - background-color: var(--brand-primary); + background-color: var(--brand-primary-foreground); } diff --git a/fluxer_app/src/features/app/components/layout/FavoritesChannelListContent.module.css b/fluxer_app/src/features/app/components/layout/FavoritesChannelListContent.module.css index b33a6fd30..94590dcd4 100644 --- a/fluxer_app/src/features/app/components/layout/FavoritesChannelListContent.module.css +++ b/fluxer_app/src/features/app/components/layout/FavoritesChannelListContent.module.css @@ -13,7 +13,7 @@ } :global(.theme-light) .channelBadgeSelectedIcon { - color: #fff; + color: var(--brand-primary-fill); } .favoriteItemContainer { @@ -85,7 +85,7 @@ align-items: center; justify-content: center; border-radius: 624.9375rem; - background-color: white; + background-color: var(--brand-primary-fill); font-size: 0.75rem; font-weight: 600; color: var(--brand-primary); diff --git a/fluxer_app/src/features/app/components/layout/GuildsLayout.module.css b/fluxer_app/src/features/app/components/layout/GuildsLayout.module.css index 2447ddc59..ccb49d76c 100644 --- a/fluxer_app/src/features/app/components/layout/GuildsLayout.module.css +++ b/fluxer_app/src/features/app/components/layout/GuildsLayout.module.css @@ -219,7 +219,7 @@ .guildIconSelected.guildIconNoImage { border-radius: 30%; background-color: var(--brand-primary); - color: white; + color: var(--brand-primary-fill); } @media (hover: hover) and (pointer: fine) { @@ -231,7 +231,7 @@ .guildListItem.contextMenuHover .guildIconNoImage { border-radius: 30%; background-color: var(--brand-primary); - color: white; + color: var(--brand-primary-fill); } } @@ -363,7 +363,7 @@ .fluxerButtonIconSelected { background-color: var(--brand-primary); - color: white; + color: var(--brand-primary-fill); } @media (hover: hover) and (pointer: fine) { @@ -374,7 +374,7 @@ .fluxerButtonIcon:hover, .fluxerButton.contextMenuHover .fluxerButtonIcon { background-color: var(--brand-primary); - color: white; + color: var(--brand-primary-fill); } } @@ -782,10 +782,10 @@ :global(.theme-light) .dropIndicatorTop::before, :global(.theme-light) .dropIndicatorBottom::after, :global(.theme-light) .guildListDropZoneActive::after { - background: var(--brand-primary); + background: var(--brand-primary-foreground); } :global(.theme-light) .combinePreviewGrid { background-color: color-mix(in srgb, var(--background-secondary) 92%, var(--brand-primary) 8%); - outline-color: var(--brand-primary); + outline-color: var(--brand-primary-foreground); } diff --git a/fluxer_app/src/features/app/components/layout/NagbarTones.ts b/fluxer_app/src/features/app/components/layout/NagbarTones.ts index 21cd25a40..ce663d859 100644 --- a/fluxer_app/src/features/app/components/layout/NagbarTones.ts +++ b/fluxer_app/src/features/app/components/layout/NagbarTones.ts @@ -37,7 +37,7 @@ export const NAGBAR_TONES: Readonly> = Object }), [NagbarToneKind.DANGER]: Object.freeze({ backgroundColor: 'var(--status-danger)', - textColor: 'var(--text-on-brand-primary)', + textColor: 'var(--text-on-dark)', }), [NagbarToneKind.ALERT]: Object.freeze({backgroundColor: '#ea580c', textColor: '#ffffff'}), [NagbarToneKind.PREMIUM]: Object.freeze({backgroundColor: '#f97316', textColor: '#ffffff'}), diff --git a/fluxer_app/src/features/app/components/layout/NullSpaceDropIndicator.module.css b/fluxer_app/src/features/app/components/layout/NullSpaceDropIndicator.module.css index d0476d342..c04bc70f4 100644 --- a/fluxer_app/src/features/app/components/layout/NullSpaceDropIndicator.module.css +++ b/fluxer_app/src/features/app/components/layout/NullSpaceDropIndicator.module.css @@ -22,7 +22,7 @@ height: 0.125rem; width: 100%; border-radius: 624.9375rem; - background-color: var(--brand-primary); + background-color: var(--brand-primary-foreground); transition: transform 150ms, opacity 150ms; diff --git a/fluxer_app/src/features/app/components/layout/sidebar_nav/DiscoveryButton.module.css b/fluxer_app/src/features/app/components/layout/sidebar_nav/DiscoveryButton.module.css index 84e4cd833..fa9d41d26 100644 --- a/fluxer_app/src/features/app/components/layout/sidebar_nav/DiscoveryButton.module.css +++ b/fluxer_app/src/features/app/components/layout/sidebar_nav/DiscoveryButton.module.css @@ -19,5 +19,5 @@ .iconTextSelected { height: 1.25rem; width: 1.25rem; - color: white; + color: var(--brand-primary-fill); } diff --git a/fluxer_app/src/features/app/components/layout/sidebar_nav/DiscoveryButton.tsx b/fluxer_app/src/features/app/components/layout/sidebar_nav/DiscoveryButton.tsx index 188e62ea3..9a9266086 100644 --- a/fluxer_app/src/features/app/components/layout/sidebar_nav/DiscoveryButton.tsx +++ b/fluxer_app/src/features/app/components/layout/sidebar_nav/DiscoveryButton.tsx @@ -66,7 +66,9 @@ export const DiscoveryButton = observer(() => { animate={{borderRadius: isHovering || isSelected ? '30%' : '50%'}} initial={{borderRadius: isHovering || isSelected ? '30%' : '50%'}} transition={{duration: Accessibility.useReducedMotion ? 0 : 0.07, ease: 'easeOut'}} - style={isSelected ? {backgroundColor: 'var(--brand-primary)', color: 'white'} : undefined} + style={ + isSelected ? {backgroundColor: 'var(--brand-primary)', color: 'var(--brand-primary-fill)'} : undefined + } data-flx="app.sidebar-nav.discovery-button.div--2" > { if (color === undefined || color === 0) { - return 'var(--brand-primary)'; + return 'var(--brand-primary-foreground)'; } return ColorUtils.int2rgb(color); }; diff --git a/fluxer_app/src/features/channel/components/embeds/media/EmbedVideo.module.css b/fluxer_app/src/features/channel/components/embeds/media/EmbedVideo.module.css index 9db520214..0bfe89b71 100644 --- a/fluxer_app/src/features/channel/components/embeds/media/EmbedVideo.module.css +++ b/fluxer_app/src/features/channel/components/embeds/media/EmbedVideo.module.css @@ -149,7 +149,7 @@ border: none; border-radius: var(--radius-full); background-color: rgba(0, 0, 0, 0.6); - color: var(--text-on-brand-primary); + color: var(--text-on-dark); cursor: pointer; z-index: 2; transition: background-color 150ms ease; diff --git a/fluxer_app/src/features/channel/components/embeds/media/GifIndicator.module.css b/fluxer_app/src/features/channel/components/embeds/media/GifIndicator.module.css index 38507acf4..b338fe816 100644 --- a/fluxer_app/src/features/channel/components/embeds/media/GifIndicator.module.css +++ b/fluxer_app/src/features/channel/components/embeds/media/GifIndicator.module.css @@ -14,5 +14,5 @@ font-weight: 600; font-size: 0.875rem; line-height: 1; - color: var(--text-on-brand-primary); + color: var(--text-on-dark); } diff --git a/fluxer_app/src/features/channel/components/embeds/media/MediaButtons.module.css b/fluxer_app/src/features/channel/components/embeds/media/MediaButtons.module.css index a294e8d20..521a78cd1 100644 --- a/fluxer_app/src/features/channel/components/embeds/media/MediaButtons.module.css +++ b/fluxer_app/src/features/channel/components/embeds/media/MediaButtons.module.css @@ -10,7 +10,7 @@ --action-toolbar-item-size: 2rem; --action-toolbar-item-border-width: 0px; --action-toolbar-item-background: rgba(0, 0, 0, 0.75); - --action-toolbar-item-color: var(--text-on-brand-primary); + --action-toolbar-item-color: var(--text-on-dark); } .overlayButton svg { @@ -25,5 +25,5 @@ :global(:where(html.window-focused, html.unfocused-fully-interactive):not(.window-focus-activation-guard)) .overlayButton.overlayButtonHover:hover svg { - color: color-mix(in srgb, var(--text-on-brand-primary) 95%, transparent); + color: color-mix(in srgb, var(--text-on-dark) 95%, transparent); } diff --git a/fluxer_app/src/features/channel/components/embeds/media/VoiceMessagePlayer.module.css b/fluxer_app/src/features/channel/components/embeds/media/VoiceMessagePlayer.module.css index f8925bd66..ba9847398 100644 --- a/fluxer_app/src/features/channel/components/embeds/media/VoiceMessagePlayer.module.css +++ b/fluxer_app/src/features/channel/components/embeds/media/VoiceMessagePlayer.module.css @@ -46,11 +46,15 @@ inset: -0.1875rem; border-radius: var(--radius-full); border: 0.125rem solid transparent; - border-top-color: var(--text-on-brand-primary); + border-top-color: var(--brand-primary-foreground); animation: spin 0.8s linear infinite; pointer-events: none; } +.containerActive .loadingSpinner { + border-top-color: var(--text-on-brand-primary); +} + @keyframes spin { from { transform: rotate(0deg); @@ -84,7 +88,7 @@ } :global(.theme-light) .waveformBarPast { - background-color: var(--brand-primary); + background-color: var(--brand-primary-foreground); } .timestamp { diff --git a/fluxer_app/src/features/channel/components/friends/FriendListItem.module.css b/fluxer_app/src/features/channel/components/friends/FriendListItem.module.css index 528a14917..ffc4428ef 100644 --- a/fluxer_app/src/features/channel/components/friends/FriendListItem.module.css +++ b/fluxer_app/src/features/channel/components/friends/FriendListItem.module.css @@ -211,13 +211,13 @@ .actionButtonAccept { --action-toolbar-item-background: var(--friend-action-brand-background); - --action-toolbar-item-color: color-mix(in srgb, var(--brand-primary) 76%, var(--text-primary) 24%); + --action-toolbar-item-color: color-mix(in srgb, var(--brand-primary-foreground) 76%, var(--text-primary) 24%); } :global(:where(html.window-focused, html.unfocused-fully-interactive):not(.window-focus-activation-guard)) .actionButtonAccept:hover { --action-toolbar-item-background: var(--friend-action-brand-hover-background); - --action-toolbar-item-color: color-mix(in srgb, var(--brand-primary) 82%, var(--text-primary) 18%); + --action-toolbar-item-color: color-mix(in srgb, var(--brand-primary-foreground) 82%, var(--text-primary) 18%); } .actionButtonIgnore { diff --git a/fluxer_app/src/features/channel/components/pickers/gif/ImportFavoriteGifsModal.module.css b/fluxer_app/src/features/channel/components/pickers/gif/ImportFavoriteGifsModal.module.css index 3fc6b6f98..2e782773d 100644 --- a/fluxer_app/src/features/channel/components/pickers/gif/ImportFavoriteGifsModal.module.css +++ b/fluxer_app/src/features/channel/components/pickers/gif/ImportFavoriteGifsModal.module.css @@ -41,5 +41,5 @@ } :global(.theme-light) .progressBar { - background-color: var(--brand-primary); + background-color: var(--brand-primary-foreground); } diff --git a/fluxer_app/src/features/emoji/components/stickers/StickerGridItem.module.css b/fluxer_app/src/features/emoji/components/stickers/StickerGridItem.module.css index 97cea39a3..443bb6776 100644 --- a/fluxer_app/src/features/emoji/components/stickers/StickerGridItem.module.css +++ b/fluxer_app/src/features/emoji/components/stickers/StickerGridItem.module.css @@ -114,7 +114,7 @@ .actionButton:hover { border-color: var(--brand-primary); background-color: var(--brand-primary); - color: white; + color: var(--brand-primary-fill); } .deleteButton:hover { diff --git a/fluxer_app/src/features/expressions/components/modals/AssetSourceModal.module.css b/fluxer_app/src/features/expressions/components/modals/AssetSourceModal.module.css index bd8725f57..91dda1736 100644 --- a/fluxer_app/src/features/expressions/components/modals/AssetSourceModal.module.css +++ b/fluxer_app/src/features/expressions/components/modals/AssetSourceModal.module.css @@ -61,10 +61,10 @@ } :global(.theme-light) .card:hover { - border-color: var(--brand-primary); + border-color: var(--brand-primary-foreground); } :global(.theme-light) .cardIcon { background: color-mix(in srgb, var(--brand-primary) 12%, transparent); - color: var(--brand-primary); + color: var(--brand-primary-foreground); } diff --git a/fluxer_app/src/features/expressions/components/modals/TagListField.module.css b/fluxer_app/src/features/expressions/components/modals/TagListField.module.css index 9a83cbecb..a8625a8fc 100644 --- a/fluxer_app/src/features/expressions/components/modals/TagListField.module.css +++ b/fluxer_app/src/features/expressions/components/modals/TagListField.module.css @@ -36,7 +36,7 @@ padding: 0.375rem 0.75rem; border-radius: 624.9375rem; background-color: var(--brand-primary); - color: white; + color: var(--brand-primary-fill); font-size: 0.875rem; } diff --git a/fluxer_app/src/features/forum/components/Forum.module.css b/fluxer_app/src/features/forum/components/Forum.module.css index 90f83d3af..fb5643c3f 100644 --- a/fluxer_app/src/features/forum/components/Forum.module.css +++ b/fluxer_app/src/features/forum/components/Forum.module.css @@ -570,14 +570,14 @@ } :global(.theme-light) .cardUnreadLabel { - color: var(--brand-primary); + color: var(--brand-primary-foreground); } .newBadge { padding: 0 0.375rem; border-radius: 0.25rem; background-color: var(--status-danger); - color: var(--text-on-brand-primary); + color: var(--text-on-dark); font-size: 0.6875rem; font-weight: 700; line-height: 1.125rem; @@ -625,7 +625,7 @@ padding: 0.5rem; border-radius: 50%; background-color: rgb(0 0 0 / 0.6); - color: var(--text-on-brand-primary); + color: var(--text-on-dark); transform: translate(-50%, -50%); } @@ -664,14 +664,14 @@ } .reactionActive { - background-color: color-mix(in srgb, var(--brand-primary) 36%, var(--background-secondary) 64%); - border-color: var(--brand-primary); - color: var(--text-on-brand-primary); + background-color: var(--brand-tint); + border-color: var(--brand-tint-border); + color: var(--text-on-brand-tint); } .reactionActive:hover:not(:disabled) { - background-color: color-mix(in srgb, var(--brand-primary) 48%, var(--background-secondary) 52%); - border-color: var(--brand-primary); + background-color: var(--brand-tint-hover); + border-color: var(--brand-primary-foreground); } :global(.theme-light) .reaction { @@ -681,9 +681,9 @@ } :global(.theme-light) .reactionActive { - background-color: color-mix(in srgb, var(--brand-primary) 12%, var(--background-primary) 88%); - border-color: color-mix(in srgb, var(--brand-primary) 45%, transparent 55%); - color: var(--brand-primary); + background-color: var(--brand-tint); + border-color: var(--brand-tint-border); + color: var(--text-on-brand-tint); } .notice { diff --git a/fluxer_app/src/features/guild/components/GuildBadge.module.css b/fluxer_app/src/features/guild/components/GuildBadge.module.css index b22ebb3ac..c86e330b6 100644 --- a/fluxer_app/src/features/guild/components/GuildBadge.module.css +++ b/fluxer_app/src/features/guild/components/GuildBadge.module.css @@ -7,31 +7,31 @@ .badge { flex-shrink: 0; - --guild-badge-seal: white; + --guild-badge-seal: var(--brand-primary-fill); --guild-badge-icon: var(--brand-primary); } .badgeOnLightSurface { flex-shrink: 0; - --guild-badge-seal: white; + --guild-badge-seal: var(--brand-primary-fill); --guild-badge-icon: var(--brand-primary); } :global(.theme-light) .badgeOnLightSurface { --guild-badge-seal: var(--brand-primary); - --guild-badge-icon: white; + --guild-badge-icon: var(--brand-primary-fill); } .badgeBanner { flex-shrink: 0; filter: drop-shadow(0 0.0625rem 0.1875rem rgba(0, 0, 0, 0.9)); - --guild-badge-seal: white; + --guild-badge-seal: var(--brand-primary-fill); --guild-badge-icon: var(--brand-primary); } .badgeBannerDark { flex-shrink: 0; filter: drop-shadow(0 0.0625rem 0.1875rem rgba(0, 0, 0, 0.9)); - --guild-badge-seal: white; + --guild-badge-seal: var(--brand-primary-fill); --guild-badge-icon: var(--brand-primary); } diff --git a/fluxer_app/src/features/guild/components/modals/guild_tabs/GuildRolesTab.module.css b/fluxer_app/src/features/guild/components/modals/guild_tabs/GuildRolesTab.module.css index cd433a656..343680c4c 100644 --- a/fluxer_app/src/features/guild/components/modals/guild_tabs/GuildRolesTab.module.css +++ b/fluxer_app/src/features/guild/components/modals/guild_tabs/GuildRolesTab.module.css @@ -204,6 +204,10 @@ color: var(--brand-primary-fill); } +.overwriteItemSelected .lockIcon { + color: inherit; +} + .roleDot { width: 0.75rem; height: 0.75rem; diff --git a/fluxer_app/src/features/messaging/components/modals/ForwardMessagePreview.module.css b/fluxer_app/src/features/messaging/components/modals/ForwardMessagePreview.module.css index b06ef8a01..fc1e252d3 100644 --- a/fluxer_app/src/features/messaging/components/modals/ForwardMessagePreview.module.css +++ b/fluxer_app/src/features/messaging/components/modals/ForwardMessagePreview.module.css @@ -144,7 +144,7 @@ position: absolute; inset: 0; margin: auto; - color: var(--text-on-brand-primary); + color: var(--text-on-dark); } .overflowCount { diff --git a/fluxer_app/src/features/messaging/components/modals/UploadDropModal.module.css b/fluxer_app/src/features/messaging/components/modals/UploadDropModal.module.css index 78e33baf4..e43adb3fa 100644 --- a/fluxer_app/src/features/messaging/components/modals/UploadDropModal.module.css +++ b/fluxer_app/src/features/messaging/components/modals/UploadDropModal.module.css @@ -109,10 +109,14 @@ } .statusIndicatorActive .statusIcon { - color: var(--brand-primary-fill); + color: var(--brand-primary-light-fill); } :global(.theme-light) .statusIndicatorActive { border-color: var(--brand-primary); background-color: var(--brand-primary); } + +:global(.theme-light) .statusIndicatorActive .statusIcon { + color: var(--brand-primary-fill); +} diff --git a/fluxer_app/src/features/moderation/components/pages/ReportPage.module.css b/fluxer_app/src/features/moderation/components/pages/ReportPage.module.css index 50e5ca986..a10168f6e 100644 --- a/fluxer_app/src/features/moderation/components/pages/ReportPage.module.css +++ b/fluxer_app/src/features/moderation/components/pages/ReportPage.module.css @@ -95,7 +95,7 @@ .breadcrumbActive .breadcrumbNumber { background: var(--brand-primary); - color: white; + color: var(--brand-primary-fill); } .breadcrumbLabel { diff --git a/fluxer_app/src/features/permissions/system/components/MacPermissionPrompt.module.css b/fluxer_app/src/features/permissions/system/components/MacPermissionPrompt.module.css index a1ac5e8f2..27badc5ce 100644 --- a/fluxer_app/src/features/permissions/system/components/MacPermissionPrompt.module.css +++ b/fluxer_app/src/features/permissions/system/components/MacPermissionPrompt.module.css @@ -21,7 +21,7 @@ } :global(.theme-light) .tile { - color: var(--brand-primary); + color: var(--brand-primary-foreground); } .title { diff --git a/fluxer_app/src/features/search/components/search/ChannelFilterSheet.module.css b/fluxer_app/src/features/search/components/search/ChannelFilterSheet.module.css index afa06d42d..5793a18a0 100644 --- a/fluxer_app/src/features/search/components/search/ChannelFilterSheet.module.css +++ b/fluxer_app/src/features/search/components/search/ChannelFilterSheet.module.css @@ -143,7 +143,7 @@ } :global(.theme-light) .checkIcon { - color: var(--brand-primary); + color: var(--brand-primary-foreground); } .emptyState { diff --git a/fluxer_app/src/features/search/components/search/HasFilterSheet.module.css b/fluxer_app/src/features/search/components/search/HasFilterSheet.module.css index f1ee4d015..1def897ae 100644 --- a/fluxer_app/src/features/search/components/search/HasFilterSheet.module.css +++ b/fluxer_app/src/features/search/components/search/HasFilterSheet.module.css @@ -50,7 +50,7 @@ } :global(.theme-light) .optionSelected { - border-color: var(--brand-primary); + border-color: var(--brand-primary-foreground); background-color: color-mix(in srgb, var(--brand-primary) 10%, transparent); } @@ -71,7 +71,7 @@ } :global(.theme-light) .optionIconSelected { - color: var(--brand-primary); + color: var(--brand-primary-foreground); } .optionText { @@ -103,7 +103,7 @@ } :global(.theme-light) .checkIcon { - color: var(--brand-primary); + color: var(--brand-primary-foreground); } .footer { diff --git a/fluxer_app/src/features/search/components/search/ScopeSheet.module.css b/fluxer_app/src/features/search/components/search/ScopeSheet.module.css index 788b9b105..5a6b169e9 100644 --- a/fluxer_app/src/features/search/components/search/ScopeSheet.module.css +++ b/fluxer_app/src/features/search/components/search/ScopeSheet.module.css @@ -43,7 +43,7 @@ } :global(.theme-light) .optionSelected { - border-color: var(--brand-primary); + border-color: var(--brand-primary-foreground); background-color: color-mix(in srgb, var(--brand-primary) 10%, transparent); } @@ -64,7 +64,7 @@ } :global(.theme-light) .optionIconSelected { - color: var(--brand-primary); + color: var(--brand-primary-foreground); } .optionText { @@ -94,5 +94,5 @@ } :global(.theme-light) .checkIcon { - color: var(--brand-primary); + color: var(--brand-primary-foreground); } diff --git a/fluxer_app/src/features/search/components/search/SortModeSheet.module.css b/fluxer_app/src/features/search/components/search/SortModeSheet.module.css index 12d09f896..b3bdce073 100644 --- a/fluxer_app/src/features/search/components/search/SortModeSheet.module.css +++ b/fluxer_app/src/features/search/components/search/SortModeSheet.module.css @@ -34,7 +34,7 @@ } :global(.theme-light) .optionSelected { - border-color: var(--brand-primary); + border-color: var(--brand-primary-foreground); background-color: color-mix(in srgb, var(--brand-primary) 10%, transparent); } @@ -55,7 +55,7 @@ } :global(.theme-light) .optionIconSelected { - color: var(--brand-primary); + color: var(--brand-primary-foreground); } .optionText { @@ -85,5 +85,5 @@ } :global(.theme-light) .checkIcon { - color: var(--brand-primary); + color: var(--brand-primary-foreground); } diff --git a/fluxer_app/src/features/search/components/search/UserFilterSheet.module.css b/fluxer_app/src/features/search/components/search/UserFilterSheet.module.css index bd6a06900..0358e7b7d 100644 --- a/fluxer_app/src/features/search/components/search/UserFilterSheet.module.css +++ b/fluxer_app/src/features/search/components/search/UserFilterSheet.module.css @@ -140,7 +140,7 @@ } :global(.theme-light) .checkIcon { - color: var(--brand-primary); + color: var(--brand-primary-foreground); } .emptyState { diff --git a/fluxer_app/src/features/theme/components/modals/BackgroundImageGalleryModal.module.css b/fluxer_app/src/features/theme/components/modals/BackgroundImageGalleryModal.module.css index f2d4d2a47..804a61019 100644 --- a/fluxer_app/src/features/theme/components/modals/BackgroundImageGalleryModal.module.css +++ b/fluxer_app/src/features/theme/components/modals/BackgroundImageGalleryModal.module.css @@ -16,7 +16,7 @@ align-items: center; justify-content: center; border-radius: 0.5rem; - border: 0.125rem dashed var(--brand-primary); + border: 0.125rem dashed var(--brand-primary-foreground); background: color-mix(in srgb, var(--brand-primary) 10%, transparent); } @@ -28,13 +28,13 @@ } .dragIcon { - color: var(--brand-primary); + color: var(--brand-primary-foreground); } .dragText { font-weight: 500; font-size: 1.125rem; - color: var(--brand-primary); + color: var(--brand-primary-foreground); } .freeUserContainer { @@ -131,7 +131,7 @@ border: none; background: none; font-size: 0.75rem; - color: var(--brand-primary); + color: var(--brand-primary-foreground); cursor: pointer; } @@ -218,7 +218,7 @@ animation: spin 1s linear infinite; border-radius: 50%; border: 0.125rem solid var(--background-modifier-accent); - border-top-color: var(--brand-primary); + border-top-color: var(--brand-primary-foreground); } @keyframes spin { @@ -314,7 +314,7 @@ } .selectedIcon { - color: white; + color: var(--brand-primary-fill); } .statsText { diff --git a/fluxer_app/src/features/theme/components/modals/BackgroundImageGalleryModal.tsx b/fluxer_app/src/features/theme/components/modals/BackgroundImageGalleryModal.tsx index a060424be..465d6f42e 100644 --- a/fluxer_app/src/features/theme/components/modals/BackgroundImageGalleryModal.tsx +++ b/fluxer_app/src/features/theme/components/modals/BackgroundImageGalleryModal.tsx @@ -291,7 +291,7 @@ const BackgroundItem: React.FC = React.memo( ref={visibilityRef} className={styles.backgroundItem} style={{ - borderColor: isSelected ? 'var(--brand-primary)' : 'var(--background-modifier-accent)', + borderColor: isSelected ? 'var(--brand-primary-foreground)' : 'var(--background-modifier-accent)', }} onClick={handleClick} onKeyDown={handleKeyDown} diff --git a/fluxer_app/src/features/theme/hooks/useThemeCssVariables.ts b/fluxer_app/src/features/theme/hooks/useThemeCssVariables.ts index 2835f559a..82620f3f3 100644 --- a/fluxer_app/src/features/theme/hooks/useThemeCssVariables.ts +++ b/fluxer_app/src/features/theme/hooks/useThemeCssVariables.ts @@ -68,7 +68,7 @@ export function useThemeCssVariables({ ]); useLayoutEffect(() => { if (instanceThemeColor === null) return; - const variables = getInstanceBrandVariables(instanceThemeColor); + const variables = getInstanceBrandVariables(instanceThemeColor, effectiveTheme, saturationFactor); if (variables === null) return; const htmlNode = document.documentElement; for (const name of INSTANCE_BRAND_VARIABLES) { @@ -79,7 +79,7 @@ export function useThemeCssVariables({ htmlNode.style.removeProperty(name); } }; - }, [instanceThemeColor]); + }, [instanceThemeColor, effectiveTheme, saturationFactor]); useLayoutEffect(() => { const htmlNode = document.documentElement; return () => { diff --git a/fluxer_app/src/features/theme/styles/AttachmentFile.module.css b/fluxer_app/src/features/theme/styles/AttachmentFile.module.css index 1a2240a6a..cc2198ede 100644 --- a/fluxer_app/src/features/theme/styles/AttachmentFile.module.css +++ b/fluxer_app/src/features/theme/styles/AttachmentFile.module.css @@ -25,7 +25,7 @@ @media (hover: hover) { .deleteButton:hover { background-color: var(--status-danger); - color: var(--text-on-brand-primary); + color: var(--text-on-dark); } } diff --git a/fluxer_app/src/features/theme/styles/AttachmentGridItem.module.css b/fluxer_app/src/features/theme/styles/AttachmentGridItem.module.css index 4be77b008..aa1eefc05 100644 --- a/fluxer_app/src/features/theme/styles/AttachmentGridItem.module.css +++ b/fluxer_app/src/features/theme/styles/AttachmentGridItem.module.css @@ -117,7 +117,7 @@ } .playButton svg { - color: var(--text-on-brand-primary); + color: var(--text-on-dark); } .audioPlaceholder { @@ -152,6 +152,6 @@ padding: 0.25rem 0.5rem; font-size: 0.875rem; font-weight: 600; - color: var(--text-on-brand-primary); + color: var(--text-on-dark); line-height: 1; } diff --git a/fluxer_app/src/features/theme/utils/ColorSystemPalette.ts b/fluxer_app/src/features/theme/utils/ColorSystemPalette.ts new file mode 100644 index 000000000..58c15d6a3 --- /dev/null +++ b/fluxer_app/src/features/theme/utils/ColorSystemPalette.ts @@ -0,0 +1,259 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +import {ThemeTypes} from '@fluxer/constants/src/UserConstants'; + +export interface ColorFamily { + hue: number; + saturation: number; + useSaturationFactor: boolean; +} + +interface ScaleStop { + name: string; + position?: number; +} + +export interface Scale { + family: string; + range: [number, number]; + curve: 'linear' | 'easeIn' | 'easeOut' | 'easeInOut'; + stops: Array; +} + +export interface ColorTone { + family?: string; + hue?: number; + saturation?: number; + lightness?: number; + alpha?: number; + useSaturationFactor?: boolean; +} + +export type ColorSystemTheme = + | typeof ThemeTypes.DARK + | typeof ThemeTypes.DARK_LEGACY + | typeof ThemeTypes.COAL + | typeof ThemeTypes.LIGHT; + +interface BrandTint { + surface: string; + rest: number; + hover: number; +} + +interface ThemePalette { + surfaceScale: string; + surfaceAliases: Record; + selectedOverlay: ColorTone; + brandTint: BrandTint; +} + +export const COLOR_FAMILIES: Record = { + neutralDark: {hue: 258, saturation: 10, useSaturationFactor: true}, + neutralLight: {hue: 220, saturation: 10, useSaturationFactor: true}, + brand: {hue: 242, saturation: 70, useSaturationFactor: true}, + link: {hue: 198, saturation: 92, useSaturationFactor: true}, + accentPurple: {hue: 278, saturation: 85, useSaturationFactor: true}, + statusOnline: {hue: 152, saturation: 72, useSaturationFactor: true}, + legacyDark: {hue: 220, saturation: 13, useSaturationFactor: true}, + legacyLink: {hue: 210, saturation: 100, useSaturationFactor: true}, + legacyAccentPurple: {hue: 270, saturation: 80, useSaturationFactor: true}, + legacyStatusOnline: {hue: 142, saturation: 76, useSaturationFactor: true}, + statusIdle: {hue: 45, saturation: 93, useSaturationFactor: true}, + statusDnd: {hue: 0, saturation: 84, useSaturationFactor: true}, + statusOffline: {hue: 218, saturation: 11, useSaturationFactor: true}, + statusDanger: {hue: 1, saturation: 77, useSaturationFactor: true}, + textCode: {hue: 340, saturation: 50, useSaturationFactor: true}, + brandIcon: {hue: 38, saturation: 92, useSaturationFactor: true}, +}; + +export const COLOR_SCALES: Record = { + darkSurface: { + family: 'neutralDark', + range: [5, 24], + curve: 'easeOut', + stops: [ + {name: '--background-primary', position: 0}, + {name: '--background-secondary', position: 0.16}, + {name: '--background-secondary-lighter', position: 0.22}, + {name: '--background-secondary-alt', position: 0.28}, + {name: '--background-tertiary', position: 0.4}, + {name: '--background-channel-header', position: 0.34}, + {name: '--guild-list-foreground', position: 0.38}, + {name: '--background-header-secondary', position: 0.5}, + {name: '--background-header-primary', position: 0.5}, + {name: '--background-textarea', position: 0.3}, + {name: '--background-header-primary-hover', position: 0.85}, + ], + }, + coalSurface: { + family: 'neutralDark', + range: [1, 12], + curve: 'easeOut', + stops: [ + {name: '--background-primary', position: 0}, + {name: '--background-secondary', position: 0.16}, + {name: '--background-secondary-alt', position: 0.28}, + {name: '--background-tertiary', position: 0.4}, + {name: '--background-channel-header', position: 0.34}, + {name: '--guild-list-foreground', position: 0.38}, + {name: '--background-header-secondary', position: 0.5}, + {name: '--background-header-primary', position: 0.5}, + {name: '--background-textarea', position: 0.3}, + {name: '--background-header-primary-hover', position: 0.85}, + ], + }, + darkText: { + family: 'neutralDark', + range: [60, 96], + curve: 'easeInOut', + stops: [ + {name: '--text-tertiary-secondary', position: 0}, + {name: '--text-tertiary-muted', position: 0.2}, + {name: '--text-tertiary', position: 0.38}, + {name: '--text-primary-muted', position: 0.55}, + {name: '--text-chat-muted', position: 0.55}, + {name: '--text-secondary', position: 0.72}, + {name: '--text-chat', position: 0.82}, + {name: '--text-primary', position: 1}, + ], + }, + lightSurface: { + family: 'neutralLight', + range: [86, 98.5], + curve: 'easeIn', + stops: [ + {name: '--background-header-primary-hover', position: 0}, + {name: '--background-header-primary', position: 0.12}, + {name: '--background-header-secondary', position: 0.2}, + {name: '--guild-list-foreground', position: 0.35}, + {name: '--background-tertiary', position: 0.42}, + {name: '--background-channel-header', position: 0.5}, + {name: '--background-secondary-alt', position: 0.63}, + {name: '--background-secondary', position: 0.74}, + {name: '--background-secondary-lighter', position: 0.83}, + {name: '--background-textarea', position: 0.88}, + {name: '--background-primary', position: 1}, + ], + }, + lightText: { + family: 'neutralLight', + range: [15, 54], + curve: 'easeOut', + stops: [ + {name: '--text-primary', position: 0}, + {name: '--text-chat', position: 0.08}, + {name: '--text-secondary', position: 0.28}, + {name: '--text-chat-muted', position: 0.45}, + {name: '--text-primary-muted', position: 0.45}, + {name: '--text-tertiary', position: 0.6}, + {name: '--text-tertiary-secondary', position: 0.75}, + {name: '--text-tertiary-muted', position: 0.85}, + ], + }, + legacyDarkSurface: { + family: 'legacyDark', + range: [5, 26], + curve: 'easeOut', + stops: [ + {name: '--background-primary', position: 0}, + {name: '--background-secondary', position: 0.16}, + {name: '--background-secondary-lighter', position: 0.22}, + {name: '--background-secondary-alt', position: 0.28}, + {name: '--background-tertiary', position: 0.4}, + {name: '--background-channel-header', position: 0.34}, + {name: '--guild-list-foreground', position: 0.38}, + {name: '--background-header-secondary', position: 0.5}, + {name: '--background-header-primary', position: 0.5}, + {name: '--background-textarea', position: 0.3}, + {name: '--background-header-primary-hover', position: 0.85}, + ], + }, + legacyDarkText: { + family: 'legacyDark', + range: [52, 96], + curve: 'easeInOut', + stops: [ + {name: '--text-tertiary-secondary', position: 0}, + {name: '--text-tertiary-muted', position: 0.2}, + {name: '--text-tertiary', position: 0.38}, + {name: '--text-primary-muted', position: 0.55}, + {name: '--text-chat-muted', position: 0.55}, + {name: '--text-secondary', position: 0.72}, + {name: '--text-chat', position: 0.82}, + {name: '--text-primary', position: 1}, + ], + }, +}; + +const DARK_BRAND_TINT: BrandTint = {surface: '--background-secondary', rest: 36, hover: 48}; + +export const THEME_PALETTES: Record = { + [ThemeTypes.DARK]: { + surfaceScale: 'darkSurface', + surfaceAliases: {}, + selectedOverlay: {family: 'neutralDark', lightness: 100, alpha: 0.1}, + brandTint: DARK_BRAND_TINT, + }, + [ThemeTypes.COAL]: { + surfaceScale: 'coalSurface', + surfaceAliases: { + '--background-secondary': '--background-primary', + '--background-secondary-lighter': '--background-primary', + }, + selectedOverlay: {family: 'neutralDark', lightness: 100, alpha: 0.08}, + brandTint: DARK_BRAND_TINT, + }, + [ThemeTypes.DARK_LEGACY]: { + surfaceScale: 'legacyDarkSurface', + surfaceAliases: {}, + selectedOverlay: {family: 'legacyDark', lightness: 100, alpha: 0.1}, + brandTint: DARK_BRAND_TINT, + }, + [ThemeTypes.LIGHT]: { + surfaceScale: 'lightSurface', + surfaceAliases: {}, + selectedOverlay: {family: 'neutralLight', saturation: 10, lightness: 10, alpha: 0.1}, + brandTint: {surface: '--background-primary', rest: 12, hover: 18}, + }, +}; + +export const BRAND_PRESSED_RATIO = 90; + +export function getThemePalette(theme: string): ThemePalette { + return THEME_PALETTES[theme as ColorSystemTheme] ?? THEME_PALETTES[ThemeTypes.DARK]; +} + +export function brandTintMix(tint: BrandTint, state: 'rest' | 'hover'): string { + const ratio = tint[state]; + return `color-mix(in srgb, var(--brand-primary) ${ratio}%, var(${tint.surface}) ${100 - ratio}%)`; +} + +function clamp01(value: number): number { + return Math.min(1, Math.max(0, value)); +} + +function applyCurve(curve: Scale['curve'], t: number): number { + switch (curve) { + case 'easeIn': + return t * t; + case 'easeOut': + return 1 - (1 - t) * (1 - t); + case 'easeInOut': + if (t < 0.5) { + return 2 * t * t; + } + return 1 - 2 * (1 - t) * (1 - t); + default: + return t; + } +} + +export function scaleStopLightness(scale: Scale, index: number): number { + const lastIndex = Math.max(scale.stops.length - 1, 1); + const stop = scale.stops[index]; + const position = stop.position !== undefined ? clamp01(stop.position) : index / lastIndex; + const eased = applyCurve(scale.curve, position); + const lightness = scale.range[0] + (scale.range[1] - scale.range[0]) * eased; + return Math.round(lightness * 1000) / 1000; +} diff --git a/fluxer_app/src/features/theme/utils/InstanceBrandColors.test.ts b/fluxer_app/src/features/theme/utils/InstanceBrandColors.test.ts new file mode 100644 index 000000000..aac6caeae --- /dev/null +++ b/fluxer_app/src/features/theme/utils/InstanceBrandColors.test.ts @@ -0,0 +1,150 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +import {BRAND_PRESSED_RATIO, THEME_PALETTES} from '@app/features/theme/utils/ColorSystemPalette'; +import { + contrastRatio, + FOREGROUND_BRAND_TINT_RATIO, + FOREGROUND_SELF_TINT_RATIO, + getInstanceBrandVariables, + hslToRgb, + MIN_TEXT_CONTRAST, + mixRgb, + resolveThemeSurface, + resolveThemeSurfaces, +} from '@app/features/theme/utils/InstanceBrandColors'; +import {describe, expect, test} from 'vitest'; + +type Rgb = readonly [number, number, number]; + +const MIN_UI_CONTRAST = 3; +const THEMES = Object.keys(THEME_PALETTES); +const SATURATION_FACTORS = [1, 0.5, 0]; +const NAMED_COLORS = [ + '#ffffff', + '#f0f0f0', + '#ffff00', + '#00ff00', + '#00ffff', + '#ff00ff', + '#ff0000', + '#0000ff', + '#e53935', + '#4641d9', + '#7f7f7f', + '#000001', + '#0b1a3a', + '#ffd1dc', +]; + +function hex(rgb: Rgb): string { + return `#${rgb.map((channel) => Math.round(channel).toString(16).padStart(2, '0')).join('')}`; +} + +function gridColors(): Array { + const colors: Array = []; + for (let hue = 0; hue < 360; hue += 30) { + for (const saturation of [100, 35]) { + for (const lightness of [3, 15, 30, 50, 70, 85, 97]) { + colors.push(hex(hslToRgb(hue, saturation, lightness))); + } + } + } + return colors; +} + +function parseColor(value: string): Rgb { + const hsl = /^hsl\(([\d.]+), ([\d.]+)%, ([\d.]+)%\)$/.exec(value); + if (hsl) return hslToRgb(Number(hsl[1]), Number(hsl[2]), Number(hsl[3])); + const rgb = /^rgb\(([\d.]+), ([\d.]+), ([\d.]+)\)$/.exec(value); + if (rgb) return [Number(rgb[1]), Number(rgb[2]), Number(rgb[3])]; + throw new Error(`Unparseable colour ${value}`); +} + +function minContrast(foreground: Rgb, backgrounds: ReadonlyArray): number { + return Math.min(...backgrounds.map((background) => contrastRatio(foreground, background))); +} + +function sameColor(first: Rgb, second: Rgb): boolean { + return first.every((channel, index) => Math.abs(channel - second[index]) < 0.01); +} + +const COLORS = [...NAMED_COLORS, ...gridColors()]; +const CASES = THEMES.flatMap((theme) => + SATURATION_FACTORS.flatMap((saturationFactor) => COLORS.map((color) => ({theme, saturationFactor, color}))), +); + +describe('getInstanceBrandVariables', () => { + test('rejects anything but a lowercase six digit hex colour', () => { + expect(getInstanceBrandVariables('#FFF', 'dark', 1)).toBeNull(); + expect(getInstanceBrandVariables('red', 'dark', 1)).toBeNull(); + }); + + test('keeps the operator colour as the solid fill', () => { + expect(getInstanceBrandVariables('#ffff00', 'light', 1)?.['--instance-brand-primary']).toBe('hsl(60, 100%, 50%)'); + expect(getInstanceBrandVariables('#e53935', 'dark', 0.5)?.['--instance-brand-primary']).toBe( + 'hsl(1.36, 38.6%, 55.29%)', + ); + }); + + test('treats an unknown theme like the dark root palette', () => { + expect(getInstanceBrandVariables('#ffff00', 'unknown', 1)).toEqual(getInstanceBrandVariables('#ffff00', 'dark', 1)); + }); + + test.each(CASES)( + '$color in $theme at saturation $saturationFactor meets contrast minimums', + ({theme, saturationFactor, color}) => { + const variables = getInstanceBrandVariables(color, theme, saturationFactor); + expect(variables).not.toBeNull(); + const v = Object.fromEntries( + Object.entries(variables!).map(([name, value]) => [name.replace('--instance-', ''), parseColor(value)]), + ); + const surfaces = resolveThemeSurfaces(theme, saturationFactor); + const stops = surfaces.slice(0, surfaces.length / 2); + const pressed = mixRgb(v['brand-primary'], v['brand-primary-shade'], BRAND_PRESSED_RATIO); + const solidFills = [v['brand-primary'], v['brand-secondary'], pressed]; + expect(minContrast(v['text-on-brand-primary'], solidFills)).toBeGreaterThanOrEqual(MIN_TEXT_CONTRAST); + expect(minContrast(v['brand-primary-fill'], solidFills)).toBeGreaterThanOrEqual(MIN_TEXT_CONTRAST); + expect(contrastRatio(v['text-on-brand-primary-light'], v['brand-primary-light'])).toBeGreaterThanOrEqual( + MIN_TEXT_CONTRAST, + ); + expect(contrastRatio(v['brand-primary-light-fill'], v['brand-primary-light'])).toBeGreaterThanOrEqual( + MIN_TEXT_CONTRAST, + ); + for (const name of ['brand-primary-foreground', 'brand-primary-light']) { + const foreground = v[name]; + const tinted = [ + ...stops.map((stop) => mixRgb(foreground, stop, FOREGROUND_SELF_TINT_RATIO)), + ...stops.map((stop) => mixRgb(v['brand-primary'], stop, FOREGROUND_BRAND_TINT_RATIO)), + ]; + expect(minContrast(foreground, surfaces)).toBeGreaterThanOrEqual(MIN_TEXT_CONTRAST); + expect(minContrast(foreground, tinted)).toBeGreaterThanOrEqual(MIN_TEXT_CONTRAST); + } + expect(minContrast(v['brand-tint-border'], surfaces)).toBeGreaterThanOrEqual(MIN_UI_CONTRAST); + expect(minContrast(v['text-on-brand-tint'], [v['brand-tint'], v['brand-tint-hover']])).toBeGreaterThanOrEqual( + MIN_TEXT_CONTRAST, + ); + const tint = THEME_PALETTES[theme as keyof typeof THEME_PALETTES].brandTint; + const tintSurface = resolveThemeSurface(theme, tint.surface, saturationFactor); + const matchingRatio = (target: Rgb, max: number) => + Array.from({length: max + 1}, (_, ratio) => max - ratio).find((ratio) => + sameColor(target, mixRgb(v['brand-primary'], tintSurface, ratio)), + ); + expect(matchingRatio(v['brand-tint'], tint.rest)).toBeDefined(); + expect(matchingRatio(v['brand-tint-hover'], tint.hover)).toBeDefined(); + }, + ); + + test('keeps the stylesheet tint ratios whenever the text already passes', () => { + const variables = getInstanceBrandVariables('#4641d9', 'dark', 1)!; + const surface = resolveThemeSurface('dark', THEME_PALETTES.dark.brandTint.surface, 1); + const primary = parseColor(variables['--instance-brand-primary']); + expect(sameColor(parseColor(variables['--instance-brand-tint']), mixRgb(primary, surface, 36))).toBe(true); + expect(sameColor(parseColor(variables['--instance-brand-tint-hover']), mixRgb(primary, surface, 48))).toBe(true); + }); + + test('reads light text on the dark reaction tint even when the brand needs dark text', () => { + const variables = getInstanceBrandVariables('#e53935', 'dark', 1)!; + expect(variables['--instance-text-on-brand-primary']).toBe('rgb(0, 0, 0)'); + expect(variables['--instance-text-on-brand-tint']).toBe('hsl(0, 0%, 98%)'); + }); +}); diff --git a/fluxer_app/src/features/theme/utils/InstanceBrandColors.ts b/fluxer_app/src/features/theme/utils/InstanceBrandColors.ts index a5b6f60f7..8c681629b 100644 --- a/fluxer_app/src/features/theme/utils/InstanceBrandColors.ts +++ b/fluxer_app/src/features/theme/utils/InstanceBrandColors.ts @@ -1,18 +1,64 @@ // SPDX-License-Identifier: AGPL-3.0-or-later +import { + COLOR_FAMILIES, + COLOR_SCALES, + type ColorTone, + getThemePalette, + scaleStopLightness, +} from '@app/features/theme/utils/ColorSystemPalette'; + export const INSTANCE_BRAND_VARIABLES = [ '--instance-brand-primary', '--instance-brand-secondary', + '--instance-brand-primary-shade', '--instance-brand-primary-light', + '--instance-brand-primary-foreground', '--instance-brand-primary-fill', + '--instance-brand-primary-light-fill', '--instance-text-on-brand-primary', + '--instance-text-on-brand-primary-light', + '--instance-brand-tint', + '--instance-brand-tint-hover', + '--instance-brand-tint-border', + '--instance-text-on-brand-tint', ] as const; type InstanceBrandVariable = (typeof INSTANCE_BRAND_VARIABLES)[number]; -const HEX_COLOR_PATTERN = /^#[0-9a-f]{6}$/; +export const MIN_TEXT_CONTRAST = 4.5; +const TARGET_CONTRAST = MIN_TEXT_CONTRAST + 0.05; +export const FOREGROUND_SELF_TINT_RATIO = 15; +export const FOREGROUND_BRAND_TINT_RATIO = 22; -function parseHexColor(value: string): [number, number, number] | null { +type Rgb = readonly [number, number, number]; + +interface Color { + css: string; + rgb: Rgb; +} + +interface Hsl { + hue: number; + saturation: number; + lightness: number; +} + +interface OnColor { + text: Color; + fill: Color; + dark: boolean; +} + +const HEX_COLOR_PATTERN = /^#[0-9a-f]{6}$/; +const WHITE: Rgb = [255, 255, 255]; +const BLACK: Rgb = [0, 0, 0]; + +function round2(value: number): number { + return Math.round(value * 100) / 100; +} + +function parseHexColor(value: string): Rgb | null { if (!HEX_COLOR_PATTERN.test(value)) return null; const digits = value.slice(1); return [ @@ -22,7 +68,7 @@ function parseHexColor(value: string): [number, number, number] | null { ]; } -function rgbToHsl(red: number, green: number, blue: number): [number, number, number] { +function rgbToHsl([red, green, blue]: Rgb): Hsl { const r = red / 255; const g = green / 255; const b = blue / 255; @@ -30,7 +76,7 @@ function rgbToHsl(red: number, green: number, blue: number): [number, number, nu const min = Math.min(r, g, b); const lightness = (max + min) / 2; const delta = max - min; - if (delta === 0) return [0, 0, lightness * 100]; + if (delta === 0) return {hue: 0, saturation: 0, lightness: lightness * 100}; const saturation = delta / (1 - Math.abs(2 * lightness - 1)); let hue: number; if (max === r) { @@ -40,36 +86,225 @@ function rgbToHsl(red: number, green: number, blue: number): [number, number, nu } else { hue = (r - g) / delta + 4; } - return [(hue * 60 + 360) % 360, saturation * 100, lightness * 100]; + return {hue: (hue * 60 + 360) % 360, saturation: saturation * 100, lightness: lightness * 100}; } -function relativeLuminance(red: number, green: number, blue: number): number { +export function hslToRgb(hue: number, saturation: number, lightness: number): Rgb { + const s = Math.min(100, Math.max(0, saturation)) / 100; + const l = Math.min(100, Math.max(0, lightness)) / 100; + const chroma = (1 - Math.abs(2 * l - 1)) * s; + const h = (((hue % 360) + 360) % 360) / 60; + const x = chroma * (1 - Math.abs((h % 2) - 1)); + const m = l - chroma / 2; + const [r, g, b] = + h < 1 + ? [chroma, x, 0] + : h < 2 + ? [x, chroma, 0] + : h < 3 + ? [0, chroma, x] + : h < 4 + ? [0, x, chroma] + : h < 5 + ? [x, 0, chroma] + : [chroma, 0, x]; + return [(r + m) * 255, (g + m) * 255, (b + m) * 255]; +} + +export function mixRgb(first: Rgb, second: Rgb, firstPercent: number): Rgb { + const ratio = firstPercent / 100; + return [ + first[0] * ratio + second[0] * (1 - ratio), + first[1] * ratio + second[1] * (1 - ratio), + first[2] * ratio + second[2] * (1 - ratio), + ]; +} + +function relativeLuminance(rgb: Rgb): number { const linear = (channel: number) => { - const value = channel / 255; - return value <= 0.03928 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4; + const value = Math.round(channel) / 255; + return value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4; }; - return 0.2126 * linear(red) + 0.7152 * linear(green) + 0.0722 * linear(blue); + return 0.2126 * linear(rgb[0]) + 0.7152 * linear(rgb[1]) + 0.0722 * linear(rgb[2]); } -function formatNumber(value: number): string { - return String(Math.round(value * 100) / 100); +export function contrastRatio(first: Rgb, second: Rgb): number { + const a = relativeLuminance(first); + const b = relativeLuminance(second); + return (Math.max(a, b) + 0.05) / (Math.min(a, b) + 0.05); } -function hsl(hue: number, saturation: number, lightness: number): string { - return `hsl(${formatNumber(hue)}, calc(${formatNumber(saturation)}% * var(--saturation-factor)), ${formatNumber(lightness)}%)`; +function hslColor({hue, saturation, lightness}: Hsl): Color { + const h = round2(hue); + const s = round2(saturation); + const l = round2(lightness); + return {css: `hsl(${h}, ${s}%, ${l}%)`, rgb: hslToRgb(h, s, l)}; } -export function getInstanceBrandVariables(themeColor: string): Record | null { +function rgbColor(rgb: Rgb): Color { + const [r, g, b] = rgb.map(round2); + return {css: `rgb(${r}, ${g}, ${b})`, rgb: [r, g, b]}; +} + +function resolveTone(tone: ColorTone, saturationFactor: number): Rgb { + const family = tone.family ? COLOR_FAMILIES[tone.family] : undefined; + const hue = round2(tone.hue ?? family?.hue ?? 0); + const saturation = round2(tone.saturation ?? family?.saturation ?? 0); + const useFactor = tone.useSaturationFactor ?? family?.useSaturationFactor ?? false; + return hslToRgb(hue, useFactor ? saturation * saturationFactor : saturation, round2(tone.lightness ?? 0)); +} + +export function resolveThemeSurface(theme: string, name: string, saturationFactor: number): Rgb { + const palette = getThemePalette(theme); + const scale = COLOR_SCALES[palette.surfaceScale]; + const target = palette.surfaceAliases[name] ?? name; + const index = scale.stops.findIndex((stop) => stop.name === target); + if (index === -1) throw new Error(`${target} is not a stop of ${palette.surfaceScale}`); + return resolveTone({family: scale.family, lightness: scaleStopLightness(scale, index)}, saturationFactor); +} + +export function resolveThemeSurfaces(theme: string, saturationFactor: number): Array { + const palette = getThemePalette(theme); + const scale = COLOR_SCALES[palette.surfaceScale]; + const overlay = resolveTone(palette.selectedOverlay, saturationFactor); + const overlayPercent = (palette.selectedOverlay.alpha ?? 1) * 100; + const stops = scale.stops.map((_, index) => + resolveTone({family: scale.family, lightness: scaleStopLightness(scale, index)}, saturationFactor), + ); + return [...stops, ...stops.map((stop) => mixRgb(overlay, stop, overlayPercent))]; +} + +function isLightTheme(theme: string, saturationFactor: number): boolean { + return relativeLuminance(resolveThemeSurface(theme, '--background-primary', saturationFactor)) > 0.5; +} + +function passes(foreground: Rgb, backgrounds: ReadonlyArray): boolean { + return backgrounds.every((background) => contrastRatio(foreground, background) >= TARGET_CONTRAST); +} + +function shiftLightness(start: Hsl, lighter: boolean, accept: (color: Color) => boolean): Color { + const initial = hslColor(start); + if (accept(initial)) return initial; + const step = lighter ? 1 : -1; + const limit = lighter ? 10000 : 0; + let failing = Math.round(start.lightness * 100); + let passing = limit; + for (let value = failing + step * 100; lighter ? value < limit : value > limit; value += step * 100) { + if (accept(hslColor({...start, lightness: value / 100}))) { + passing = value; + break; + } + failing = value; + } + while (Math.abs(passing - failing) > 1) { + const middle = Math.round((passing + failing) / 2); + if (accept(hslColor({...start, lightness: middle / 100}))) { + passing = middle; + } else { + failing = middle; + } + } + return hslColor({...start, lightness: passing / 100}); +} + +function neutral(lightness: number): Color { + return hslColor({hue: 0, saturation: 0, lightness}); +} + +function pickOnColor(background: Rgb): OnColor { + const light = {text: neutral(98), fill: neutral(100), dark: false}; + const dark = {text: neutral(8), fill: neutral(8), dark: true}; + const best = contrastRatio(light.text.rgb, background) >= contrastRatio(dark.text.rgb, background) ? light : dark; + if (contrastRatio(best.text.rgb, background) >= TARGET_CONTRAST) return best; + const pure = rgbColor(contrastRatio(WHITE, background) >= contrastRatio(BLACK, background) ? WHITE : BLACK); + return {text: pure, fill: pure, dark: pure.rgb[0] === 0}; +} + +function readableOnSurfaces(start: Hsl, theme: string, saturationFactor: number, primary: Rgb): Color { + const surfaces = resolveThemeSurfaces(theme, saturationFactor); + const stops = surfaces.slice(0, surfaces.length / 2); + const brandTints = stops.map((stop) => mixRgb(primary, stop, FOREGROUND_BRAND_TINT_RATIO)); + return shiftLightness(start, !isLightTheme(theme, saturationFactor), (color) => { + const selfTints = stops.map((stop) => mixRgb(color.rgb, stop, FOREGROUND_SELF_TINT_RATIO)); + return passes(color.rgb, surfaces) && passes(color.rgb, selfTints) && passes(color.rgb, brandTints); + }); +} + +function largestPassingRatio(text: Rgb, primary: Rgb, surface: Rgb, ratio: number): number { + for (let candidate = ratio; candidate >= 0; candidate--) { + if (contrastRatio(text, mixRgb(primary, surface, candidate)) >= TARGET_CONTRAST) return candidate; + } + return -1; +} + +function brandTint( + brand: Hsl, + primary: Rgb, + theme: string, + saturationFactor: number, +): {rest: Color; hover: Color; text: Color} { + const tint = getThemePalette(theme).brandTint; + const surface = resolveThemeSurface(theme, tint.surface, saturationFactor); + if (isLightTheme(theme, saturationFactor)) { + const rest = rgbColor(mixRgb(primary, surface, tint.rest)); + const hover = rgbColor(mixRgb(primary, surface, tint.hover)); + const text = shiftLightness(brand, false, (color) => passes(color.rgb, [rest.rgb, hover.rgb])); + return {rest, hover, text}; + } + const options = [neutral(98), neutral(8), rgbColor(WHITE), rgbColor(BLACK)].map((text) => { + const hover = largestPassingRatio(text.rgb, primary, surface, tint.hover); + const rest = Math.min(largestPassingRatio(text.rgb, primary, surface, tint.rest), hover); + return {text, rest, hover}; + }); + const best = + options.find((option) => option.rest === tint.rest && option.hover === tint.hover) ?? + options.reduce((current, option) => (option.rest + option.hover > current.rest + current.hover ? option : current)); + return { + rest: rgbColor(mixRgb(primary, surface, best.rest)), + hover: rgbColor(mixRgb(primary, surface, best.hover)), + text: best.text, + }; +} + +export function getInstanceBrandVariables( + themeColor: string, + theme: string, + saturationFactor: number, +): Record | null { const rgb = parseHexColor(themeColor); if (rgb === null) return null; - const [hue, saturation, lightness] = rgbToHsl(...rgb); - const luminance = relativeLuminance(...rgb); - const prefersDarkText = (luminance + 0.05) / 0.05 > 1.05 / (luminance + 0.05); + const base = rgbToHsl(rgb); + const brand = {...base, saturation: base.saturation * saturationFactor}; + const primary = hslColor(brand); + const onPrimary = pickOnColor(primary.rgb); + const secondary = shiftLightness( + {hue: brand.hue, saturation: brand.saturation * (6 / 7), lightness: brand.lightness * 0.89}, + onPrimary.dark, + (color) => passes(onPrimary.text.rgb, [color.rgb]) && passes(onPrimary.fill.rgb, [color.rgb]), + ); + const shade = rgbColor(onPrimary.dark ? WHITE : BLACK); + const foreground = readableOnSurfaces(brand, theme, saturationFactor, primary.rgb); + const light = readableOnSurfaces( + {...brand, lightness: brand.lightness + (100 - brand.lightness) * 0.65}, + theme, + saturationFactor, + primary.rgb, + ); + const onLight = pickOnColor(light.rgb); + const tint = brandTint(brand, primary.rgb, theme, saturationFactor); return { - '--instance-brand-primary': hsl(hue, saturation, lightness), - '--instance-brand-secondary': hsl(hue, saturation * (6 / 7), lightness * 0.89), - '--instance-brand-primary-light': hsl(hue, saturation, lightness + (100 - lightness) * 0.65), - '--instance-brand-primary-fill': prefersDarkText ? 'hsl(0, 0%, 8%)' : 'hsl(0, 0%, 100%)', - '--instance-text-on-brand-primary': prefersDarkText ? 'hsl(0, 0%, 8%)' : 'hsl(0, 0%, 98%)', + '--instance-brand-primary': primary.css, + '--instance-brand-secondary': secondary.css, + '--instance-brand-primary-shade': shade.css, + '--instance-brand-primary-light': light.css, + '--instance-brand-primary-foreground': foreground.css, + '--instance-brand-primary-fill': onPrimary.fill.css, + '--instance-brand-primary-light-fill': onLight.fill.css, + '--instance-text-on-brand-primary': onPrimary.text.css, + '--instance-text-on-brand-primary-light': onLight.text.css, + '--instance-brand-tint': tint.rest.css, + '--instance-brand-tint-hover': tint.hover.css, + '--instance-brand-tint-border': foreground.css, + '--instance-text-on-brand-tint': tint.text.css, }; } diff --git a/fluxer_app/src/features/theme/variables/ThemeVariableManifest.ts b/fluxer_app/src/features/theme/variables/ThemeVariableManifest.ts index 22b36d3ae..990367506 100644 --- a/fluxer_app/src/features/theme/variables/ThemeVariableManifest.ts +++ b/fluxer_app/src/features/theme/variables/ThemeVariableManifest.ts @@ -106,8 +106,15 @@ export type ThemeVariableName = | "--border-color-hover" | "--brand-primary" | "--brand-primary-fill" + | "--brand-primary-foreground" | "--brand-primary-light" + | "--brand-primary-light-fill" + | "--brand-primary-pressed" + | "--brand-primary-shade" | "--brand-secondary" + | "--brand-tint" + | "--brand-tint-border" + | "--brand-tint-hover" | "--button-danger-active-fill" | "--button-danger-fill" | "--button-danger-outline-active-border" @@ -421,6 +428,9 @@ export type ThemeVariableName = | "--text-code" | "--text-link" | "--text-on-brand-primary" + | "--text-on-brand-primary-light" + | "--text-on-brand-tint" + | "--text-on-dark" | "--text-primary" | "--text-primary-muted" | "--text-secondary" @@ -570,8 +580,15 @@ export const THEME_VARIABLES: ReadonlyArray = [ {name: "--border-color-hover", kind: "color", groupId: "borders", groupLabel: "Borders & focus", source: "color-system"}, {name: "--brand-primary", kind: "color", groupId: "brand", groupLabel: "Brand & accents", source: "color-system"}, {name: "--brand-primary-fill", kind: "color", groupId: "brand", groupLabel: "Brand & accents", source: "color-system"}, + {name: "--brand-primary-foreground", kind: "color", groupId: "brand", groupLabel: "Brand & accents", source: "color-system"}, {name: "--brand-primary-light", kind: "color", groupId: "brand", groupLabel: "Brand & accents", source: "color-system"}, + {name: "--brand-primary-light-fill", kind: "color", groupId: "brand", groupLabel: "Brand & accents", source: "color-system"}, + {name: "--brand-primary-pressed", kind: "color", groupId: "brand", groupLabel: "Brand & accents", source: "color-system"}, + {name: "--brand-primary-shade", kind: "color", groupId: "brand", groupLabel: "Brand & accents", source: "color-system"}, {name: "--brand-secondary", kind: "color", groupId: "brand", groupLabel: "Brand & accents", source: "color-system"}, + {name: "--brand-tint", kind: "color", groupId: "brand", groupLabel: "Brand & accents", source: "color-system"}, + {name: "--brand-tint-border", kind: "color", groupId: "brand", groupLabel: "Brand & accents", source: "color-system"}, + {name: "--brand-tint-hover", kind: "color", groupId: "brand", groupLabel: "Brand & accents", source: "color-system"}, {name: "--button-danger-active-fill", kind: "color", groupId: "buttons", groupLabel: "Buttons", source: "color-system"}, {name: "--button-danger-fill", kind: "color", groupId: "buttons", groupLabel: "Buttons", source: "color-system"}, {name: "--button-danger-outline-active-border", kind: "color", groupId: "buttons", groupLabel: "Buttons", source: "color-system"}, @@ -885,6 +902,9 @@ export const THEME_VARIABLES: ReadonlyArray = [ {name: "--text-code", kind: "color", groupId: "code", groupLabel: "Code & terminal", source: "color-system"}, {name: "--text-link", kind: "color", groupId: "text", groupLabel: "Text", source: "color-system"}, {name: "--text-on-brand-primary", kind: "color", groupId: "text", groupLabel: "Text", source: "color-system"}, + {name: "--text-on-brand-primary-light", kind: "color", groupId: "text", groupLabel: "Text", source: "color-system"}, + {name: "--text-on-brand-tint", kind: "color", groupId: "text", groupLabel: "Text", source: "color-system"}, + {name: "--text-on-dark", kind: "color", groupId: "text", groupLabel: "Text", source: "color-system"}, {name: "--text-primary", kind: "color", groupId: "text", groupLabel: "Text", source: "color-system"}, {name: "--text-primary-muted", kind: "color", groupId: "text", groupLabel: "Text", source: "color-system"}, {name: "--text-secondary", kind: "color", groupId: "text", groupLabel: "Text", source: "color-system"}, @@ -1035,8 +1055,15 @@ export const THEME_VARIABLE_NAMES: ReadonlyArray = [ "--border-color-hover", "--brand-primary", "--brand-primary-fill", + "--brand-primary-foreground", "--brand-primary-light", + "--brand-primary-light-fill", + "--brand-primary-pressed", + "--brand-primary-shade", "--brand-secondary", + "--brand-tint", + "--brand-tint-border", + "--brand-tint-hover", "--button-danger-active-fill", "--button-danger-fill", "--button-danger-outline-active-border", @@ -1350,6 +1377,9 @@ export const THEME_VARIABLE_NAMES: ReadonlyArray = [ "--text-code", "--text-link", "--text-on-brand-primary", + "--text-on-brand-primary-light", + "--text-on-brand-tint", + "--text-on-dark", "--text-primary", "--text-primary-muted", "--text-secondary", @@ -1500,8 +1530,15 @@ export const THEME_STUDIO_DARK_DEFAULT_VARIABLE_VALUES: Readonly ul > li[aria-selected]': { backgroundColor: 'var(--studio-accent, rgba(120, 160, 220, 0.3))', - color: 'var(--studio-fg-primary)', + color: 'var(--studio-fg-on-accent)', }, '.cm-tooltip-autocomplete > ul > li': { fontFamily: 'var(--studio-font-mono)', diff --git a/fluxer_app/src/features/theme_studio/ui/StudioButton.module.css b/fluxer_app/src/features/theme_studio/ui/StudioButton.module.css index 89d36fb00..1ace2a0c6 100644 --- a/fluxer_app/src/features/theme_studio/ui/StudioButton.module.css +++ b/fluxer_app/src/features/theme_studio/ui/StudioButton.module.css @@ -80,7 +80,7 @@ .dangerSolid { --bg: var(--studio-danger); --bg-hover: var(--studio-danger-hover); - --fg: var(--studio-fg-on-accent); + --fg: var(--studio-fg-on-danger); } .leadingIcon { diff --git a/fluxer_app/src/features/threads/components/MessageThreadChip.module.css b/fluxer_app/src/features/threads/components/MessageThreadChip.module.css index 02fe99a2a..86422f27d 100644 --- a/fluxer_app/src/features/threads/components/MessageThreadChip.module.css +++ b/fluxer_app/src/features/threads/components/MessageThreadChip.module.css @@ -47,7 +47,7 @@ } :global(.theme-light) .count { - color: var(--brand-primary); + color: var(--brand-primary-foreground); } .preview { diff --git a/fluxer_app/src/features/ui/action_menu/ContextMenu.module.css b/fluxer_app/src/features/ui/action_menu/ContextMenu.module.css index 39db90862..98076ae03 100644 --- a/fluxer_app/src/features/ui/action_menu/ContextMenu.module.css +++ b/fluxer_app/src/features/ui/action_menu/ContextMenu.module.css @@ -295,7 +295,7 @@ .checkboxChecked { background-color: var(--brand-primary); - border-color: var(--brand-primary); + border-color: var(--brand-primary-foreground); } .checkboxChecked::after { @@ -305,7 +305,7 @@ top: 50%; width: 0.25rem; height: 0.5625rem; - border: solid white; + border: solid var(--brand-primary-fill); border-width: 0 0.15625rem 0.15625rem 0; transform: translate(-50%, -60%) rotate(45deg); } diff --git a/fluxer_app/src/features/ui/action_menu/MenuItemRadio.module.css b/fluxer_app/src/features/ui/action_menu/MenuItemRadio.module.css index 06a41cd6b..9cc4dd7c0 100644 --- a/fluxer_app/src/features/ui/action_menu/MenuItemRadio.module.css +++ b/fluxer_app/src/features/ui/action_menu/MenuItemRadio.module.css @@ -14,7 +14,7 @@ } .radioButtonSelected { - border-color: var(--brand-primary); + border-color: var(--brand-primary-foreground); background-color: var(--brand-primary); } @@ -27,7 +27,7 @@ height: 0.375rem; width: 0.375rem; border-radius: 50%; - background-color: white; + background-color: var(--brand-primary-fill); } .labelContent { diff --git a/fluxer_app/src/features/ui/button/Button.module.css b/fluxer_app/src/features/ui/button/Button.module.css index 71cf5da07..9b81c1ba0 100644 --- a/fluxer_app/src/features/ui/button/Button.module.css +++ b/fluxer_app/src/features/ui/button/Button.module.css @@ -129,7 +129,7 @@ } .button.primary:active:not(:disabled):not([data-disabled]) { - background-color: color-mix(in srgb, var(--brand-primary) 90%, #000); + background-color: var(--brand-primary-pressed); } .button.secondary { @@ -213,7 +213,7 @@ .button.recording { background-color: var(--accent-success, #16a34a); border-color: color-mix(in srgb, var(--accent-success, #16a34a) 80%, white 20%); - color: var(--brand-primary-fill); + color: var(--button-primary-text); animation: buttonRecordingPulse 1.1s infinite; } diff --git a/fluxer_app/src/features/ui/checkbox/Checkbox.module.css b/fluxer_app/src/features/ui/checkbox/Checkbox.module.css index 5b080ed7d..860071172 100644 --- a/fluxer_app/src/features/ui/checkbox/Checkbox.module.css +++ b/fluxer_app/src/features/ui/checkbox/Checkbox.module.css @@ -48,11 +48,11 @@ .checked { background-color: var(--brand-primary); - border: 0.0625rem solid var(--brand-primary); + border: 0.0625rem solid var(--brand-primary-foreground); } .checkIcon { - color: white; + color: var(--brand-primary-fill); } .label { @@ -80,12 +80,12 @@ .menuChecked { background-color: var(--brand-primary); - border-color: var(--brand-primary); + border-color: var(--brand-primary-foreground); } .menuChecked:hover { background-color: var(--brand-primary); - border-color: var(--brand-primary); + border-color: var(--brand-primary-foreground); } .checkboxIndicator { diff --git a/fluxer_app/src/features/ui/checkbox/Checkbox.tsx b/fluxer_app/src/features/ui/checkbox/Checkbox.tsx index 59feae072..4dfc7083b 100644 --- a/fluxer_app/src/features/ui/checkbox/Checkbox.tsx +++ b/fluxer_app/src/features/ui/checkbox/Checkbox.tsx @@ -167,7 +167,6 @@ export const Checkbox: React.FC = observer( @@ -224,7 +223,6 @@ export const Checkbox: React.FC = observer( diff --git a/fluxer_app/src/features/ui/components/Pagination.module.css b/fluxer_app/src/features/ui/components/Pagination.module.css index c901c15c6..06e7b84ca 100644 --- a/fluxer_app/src/features/ui/components/Pagination.module.css +++ b/fluxer_app/src/features/ui/components/Pagination.module.css @@ -46,7 +46,7 @@ .pageButtonActive { background-color: var(--brand-primary); - color: white; + color: var(--brand-primary-fill); } .pageButtonActive:hover { diff --git a/fluxer_app/src/features/ui/components/Slider.module.css b/fluxer_app/src/features/ui/components/Slider.module.css index de49915a1..0d8142155 100644 --- a/fluxer_app/src/features/ui/components/Slider.module.css +++ b/fluxer_app/src/features/ui/components/Slider.module.css @@ -160,7 +160,7 @@ .barFill { background: linear-gradient( 90deg, - var(--brand-primary, #5865f2), + var(--brand-primary-foreground, #5865f2), color-mix(in srgb, var(--brand-primary-light, #7983f5) 84%, #fff 16%) ); border-radius: inherit; @@ -171,7 +171,7 @@ .vertical .barFill { background: linear-gradient( 0deg, - var(--brand-primary, #5865f2), + var(--brand-primary-foreground, #5865f2), color-mix(in srgb, var(--brand-primary-light, #7983f5) 84%, #fff 16%) ); width: 100%; @@ -180,8 +180,8 @@ :global(.theme-light) .barFill { background: linear-gradient( 90deg, - var(--brand-primary, #5865f2), - color-mix(in srgb, var(--brand-primary, #5865f2) 72%, #111 28%) + var(--brand-primary-foreground, #5865f2), + color-mix(in srgb, var(--brand-primary-foreground, #5865f2) 72%, #111 28%) ); box-shadow: none; } @@ -189,8 +189,8 @@ :global(.theme-light) .vertical .barFill { background: linear-gradient( 0deg, - var(--brand-primary, #5865f2), - color-mix(in srgb, var(--brand-primary, #5865f2) 72%, #111 28%) + var(--brand-primary-foreground, #5865f2), + color-mix(in srgb, var(--brand-primary-foreground, #5865f2) 72%, #111 28%) ); } diff --git a/fluxer_app/src/features/ui/components/form/DateTimePickerField.module.css b/fluxer_app/src/features/ui/components/form/DateTimePickerField.module.css index 22d52551b..11cf96299 100644 --- a/fluxer_app/src/features/ui/components/form/DateTimePickerField.module.css +++ b/fluxer_app/src/features/ui/components/form/DateTimePickerField.module.css @@ -273,13 +273,13 @@ .rdpToday .rdpDayButton { font-weight: 700; text-decoration: underline; - text-decoration-color: var(--brand-primary); + text-decoration-color: var(--brand-primary-foreground); text-underline-offset: 0.1875rem; } .rdpSelected .rdpDayButton { background-color: var(--brand-primary); - color: #fff; + color: var(--brand-primary-fill); font-weight: 600; } diff --git a/fluxer_app/src/features/ui/components/form/FormCombobox.module.css b/fluxer_app/src/features/ui/components/form/FormCombobox.module.css index 65089daa1..0dbcccc32 100644 --- a/fluxer_app/src/features/ui/components/form/FormCombobox.module.css +++ b/fluxer_app/src/features/ui/components/form/FormCombobox.module.css @@ -364,13 +364,17 @@ .itemSelected { background-color: var(--brand-primary); - color: #fff; + color: var(--brand-primary-fill); } .itemSelected.itemHighlighted { background-color: var(--brand-primary); } +.itemSelected .itemText :where(*) { + color: inherit; +} + .itemRendered { position: relative; } diff --git a/fluxer_app/src/features/ui/components/form/FormSwitch.module.css b/fluxer_app/src/features/ui/components/form/FormSwitch.module.css index 3d123f01c..e8668e6b0 100644 --- a/fluxer_app/src/features/ui/components/form/FormSwitch.module.css +++ b/fluxer_app/src/features/ui/components/form/FormSwitch.module.css @@ -116,6 +116,7 @@ } .switchThumb[data-state='checked'] { + background-color: var(--brand-primary-fill); transform: translateX(1.375rem); } diff --git a/fluxer_app/src/features/ui/keybind_hint/KeybindHint.module.css b/fluxer_app/src/features/ui/keybind_hint/KeybindHint.module.css index 61d2ec869..a37963460 100644 --- a/fluxer_app/src/features/ui/keybind_hint/KeybindHint.module.css +++ b/fluxer_app/src/features/ui/keybind_hint/KeybindHint.module.css @@ -42,5 +42,5 @@ :global(.theme-light) { --keybind-hint-default-background: hsl(0, 0%, 18%); - --keybind-hint-default-color: var(--text-on-brand-primary); + --keybind-hint-default-color: var(--text-on-dark); } diff --git a/fluxer_app/src/features/ui/menu_bottom_sheet/MenuBottomSheet.module.css b/fluxer_app/src/features/ui/menu_bottom_sheet/MenuBottomSheet.module.css index 597c82409..f193ca711 100644 --- a/fluxer_app/src/features/ui/menu_bottom_sheet/MenuBottomSheet.module.css +++ b/fluxer_app/src/features/ui/menu_bottom_sheet/MenuBottomSheet.module.css @@ -84,14 +84,14 @@ } .checkbox.checked { - border-color: var(--brand-primary); + border-color: var(--brand-primary-foreground); background-color: var(--brand-primary); } .checkIcon { height: 0.75rem; width: 0.75rem; - color: white; + color: var(--brand-primary-fill); } .radioContainer { @@ -115,14 +115,14 @@ } .radio.radioSelected { - border-color: var(--brand-primary); + border-color: var(--brand-primary-foreground); } .radioInner { height: 0.625rem; width: 0.625rem; border-radius: 50%; - background-color: var(--brand-primary); + background-color: var(--brand-primary-foreground); } .labelColumn { diff --git a/fluxer_app/src/features/ui/plutonium_upsell/PlutoniumUpsell.module.css b/fluxer_app/src/features/ui/plutonium_upsell/PlutoniumUpsell.module.css index 11beffab9..eb2f3d502 100644 --- a/fluxer_app/src/features/ui/plutonium_upsell/PlutoniumUpsell.module.css +++ b/fluxer_app/src/features/ui/plutonium_upsell/PlutoniumUpsell.module.css @@ -12,7 +12,7 @@ .icon { flex-shrink: 0; margin-top: 0.125rem; - color: white; + color: var(--brand-primary-fill); } .content { @@ -25,7 +25,7 @@ .text { font-size: 0.8125rem; - color: white; + color: var(--brand-primary-fill); margin: 0; line-height: 1.4; } @@ -40,7 +40,7 @@ .dismissLink { font-size: 0.75rem; - color: rgba(255, 255, 255, 0.7); + color: color-mix(in srgb, var(--brand-primary-fill) 70%, transparent); background: none; border: none; padding: 0; @@ -49,6 +49,6 @@ } .dismissLink:hover { - color: white; + color: var(--brand-primary-fill); text-decoration: underline; } diff --git a/fluxer_app/src/features/ui/popover/searchable_list_popout/SearchableListPopout.module.css b/fluxer_app/src/features/ui/popover/searchable_list_popout/SearchableListPopout.module.css index b172466d9..451a9d13f 100644 --- a/fluxer_app/src/features/ui/popover/searchable_list_popout/SearchableListPopout.module.css +++ b/fluxer_app/src/features/ui/popover/searchable_list_popout/SearchableListPopout.module.css @@ -68,7 +68,7 @@ } .optionSelected { - box-shadow: inset 0 0 0 0.0625rem var(--brand-primary); + box-shadow: inset 0 0 0 0.0625rem var(--brand-primary-foreground); } .emptyState { diff --git a/fluxer_app/src/features/ui/radio_group/RadioGroup.module.css b/fluxer_app/src/features/ui/radio_group/RadioGroup.module.css index fc5be2086..c4ae8d69f 100644 --- a/fluxer_app/src/features/ui/radio_group/RadioGroup.module.css +++ b/fluxer_app/src/features/ui/radio_group/RadioGroup.module.css @@ -7,17 +7,17 @@ gap: var(--spacing-1-5); --radio-indicator-size: 1.125rem; --radio-indicator-border: color-mix(in srgb, var(--border-color) 70%, #fff 30%); - --radio-indicator-border-selected: var(--brand-primary); + --radio-indicator-border-selected: var(--brand-primary-foreground); --radio-dot-bg: var(--brand-primary); - --radio-dot-fill: #fff; + --radio-dot-fill: var(--brand-primary-fill); --radio-transition: var(--transition-normal, 150ms ease); } :global(.theme-light) .group { --radio-indicator-border: color-mix(in srgb, var(--text-secondary) 70%, #000 30%); - --radio-indicator-border-selected: color-mix(in srgb, var(--brand-primary) 85%, var(--text-primary) 15%); + --radio-indicator-border-selected: color-mix(in srgb, var(--brand-primary-foreground) 85%, var(--text-primary) 15%); --radio-dot-bg: var(--brand-primary); - --radio-dot-fill: #fff; + --radio-dot-fill: var(--brand-primary-fill); } .radioGroupOption { diff --git a/fluxer_app/src/features/ui/tabs/Tabs.module.css b/fluxer_app/src/features/ui/tabs/Tabs.module.css index 8b5b3d24e..653c1f86b 100644 --- a/fluxer_app/src/features/ui/tabs/Tabs.module.css +++ b/fluxer_app/src/features/ui/tabs/Tabs.module.css @@ -34,6 +34,6 @@ } :global(.theme-light) .tab.selected { - color: var(--brand-primary); - border-bottom-color: var(--brand-primary); + color: var(--brand-primary-foreground); + border-bottom-color: var(--brand-primary-foreground); } diff --git a/fluxer_app/src/features/user/components/modals/StatusChangeBottomSheet.module.css b/fluxer_app/src/features/user/components/modals/StatusChangeBottomSheet.module.css index 3a6ec9586..b398363ec 100644 --- a/fluxer_app/src/features/user/components/modals/StatusChangeBottomSheet.module.css +++ b/fluxer_app/src/features/user/components/modals/StatusChangeBottomSheet.module.css @@ -99,7 +99,7 @@ .checkIcon { width: 0.75rem; height: 0.75rem; - color: #fff; + color: var(--brand-primary-fill); } .chevronIcon { diff --git a/fluxer_app/src/features/user/components/modals/UserProfileMobileSheet.module.css b/fluxer_app/src/features/user/components/modals/UserProfileMobileSheet.module.css index 90d3dcf1f..96084b396 100644 --- a/fluxer_app/src/features/user/components/modals/UserProfileMobileSheet.module.css +++ b/fluxer_app/src/features/user/components/modals/UserProfileMobileSheet.module.css @@ -185,12 +185,12 @@ .editProfileIcon { height: 1.25rem; width: 1.25rem; - color: white; + color: var(--brand-primary-fill); } .editProfileText { font-weight: 500; - color: white; + color: var(--brand-primary-fill); } .actionButtonsContainer { @@ -234,7 +234,7 @@ .actionIcon { height: 1.5rem; width: 1.5rem; - color: white; + color: var(--brand-primary-fill); } .actionIconSecondary { diff --git a/fluxer_app/src/features/user/components/modals/tabs/LinkedAccountsTab.module.css b/fluxer_app/src/features/user/components/modals/tabs/LinkedAccountsTab.module.css index 1552cff14..660644048 100644 --- a/fluxer_app/src/features/user/components/modals/tabs/LinkedAccountsTab.module.css +++ b/fluxer_app/src/features/user/components/modals/tabs/LinkedAccountsTab.module.css @@ -143,9 +143,9 @@ } .dropIndicatorTop { - border-top: 0.125rem solid var(--brand-primary); + border-top: 0.125rem solid var(--brand-primary-foreground); } .dropIndicatorBottom { - border-bottom: 0.125rem solid var(--brand-primary); + border-bottom: 0.125rem solid var(--brand-primary-foreground); } diff --git a/fluxer_app/src/features/user/components/modals/tabs/appearance_tab/ThemeTab.module.css b/fluxer_app/src/features/user/components/modals/tabs/appearance_tab/ThemeTab.module.css index c5a86af19..610de273e 100644 --- a/fluxer_app/src/features/user/components/modals/tabs/appearance_tab/ThemeTab.module.css +++ b/fluxer_app/src/features/user/components/modals/tabs/appearance_tab/ThemeTab.module.css @@ -20,7 +20,7 @@ } .themeButtonSelected { - border-color: var(--brand-primary); + border-color: var(--brand-primary-foreground); } .themeButtonLight { @@ -59,5 +59,5 @@ } .themeButtonCheckmarkIcon { - color: white; + color: var(--brand-primary-fill); } diff --git a/fluxer_app/src/features/user/components/modals/tabs/applications_tab/ApplicationsTab.module.css b/fluxer_app/src/features/user/components/modals/tabs/applications_tab/ApplicationsTab.module.css index 6afdcb757..d2f11fe35 100644 --- a/fluxer_app/src/features/user/components/modals/tabs/applications_tab/ApplicationsTab.module.css +++ b/fluxer_app/src/features/user/components/modals/tabs/applications_tab/ApplicationsTab.module.css @@ -86,7 +86,7 @@ display: flex; align-items: center; justify-content: center; - color: white; + color: var(--brand-primary-fill); font-weight: 600; font-size: 0.875rem; } diff --git a/fluxer_app/src/features/user/components/modals/tabs/applications_tab/application_detail/ApplicationDetail.module.css b/fluxer_app/src/features/user/components/modals/tabs/applications_tab/application_detail/ApplicationDetail.module.css index 363a3fc4f..6b0e4671c 100644 --- a/fluxer_app/src/features/user/components/modals/tabs/applications_tab/application_detail/ApplicationDetail.module.css +++ b/fluxer_app/src/features/user/components/modals/tabs/applications_tab/application_detail/ApplicationDetail.module.css @@ -288,7 +288,7 @@ color-mix(in srgb, var(--brand-primary) 90%, transparent), color-mix(in srgb, var(--brand-secondary) 85%, transparent) ); - color: white; + color: var(--brand-primary-fill); font-weight: 800; font-size: 1.85rem; } diff --git a/fluxer_app/src/features/user/components/popouts/UserProfileBadges.module.css b/fluxer_app/src/features/user/components/popouts/UserProfileBadges.module.css index 6ad8ffa89..42845fe33 100644 --- a/fluxer_app/src/features/user/components/popouts/UserProfileBadges.module.css +++ b/fluxer_app/src/features/user/components/popouts/UserProfileBadges.module.css @@ -49,7 +49,7 @@ } :global(.theme-light) .sequenceBadge { - color: var(--brand-primary); + color: var(--brand-primary-foreground); } .sequenceBadgeMobile { diff --git a/fluxer_app/src/features/user/components/profile/UserProfileLoadingSkeleton.module.css b/fluxer_app/src/features/user/components/profile/UserProfileLoadingSkeleton.module.css index eca839bf2..86e0730e0 100644 --- a/fluxer_app/src/features/user/components/profile/UserProfileLoadingSkeleton.module.css +++ b/fluxer_app/src/features/user/components/profile/UserProfileLoadingSkeleton.module.css @@ -4,7 +4,7 @@ --profile-skeleton-bg: var(--background-primary); --profile-skeleton-fill: var(--background-secondary-alt); --profile-skeleton-fill-strong: var(--background-tertiary); - --profile-skeleton-border-color: var(--brand-primary); + --profile-skeleton-border-color: var(--brand-primary-foreground); --profile-skeleton-radius: 0.375rem; --profile-skeleton-border-width: 0px; --profile-skeleton-width: 100%; diff --git a/fluxer_app/src/features/user/components/settings_utils/SettingsStatusBadge.module.css b/fluxer_app/src/features/user/components/settings_utils/SettingsStatusBadge.module.css index 08db3b714..6903bb69f 100644 --- a/fluxer_app/src/features/user/components/settings_utils/SettingsStatusBadge.module.css +++ b/fluxer_app/src/features/user/components/settings_utils/SettingsStatusBadge.module.css @@ -18,7 +18,7 @@ .new, .experimental { background: var(--brand-primary); - color: #fff; + color: var(--brand-primary-fill); } .badges { diff --git a/fluxer_app/src/features/voice/components/bottomsheets/VoiceLobbyShared.module.css b/fluxer_app/src/features/voice/components/bottomsheets/VoiceLobbyShared.module.css index bbb459fa2..7a553913b 100644 --- a/fluxer_app/src/features/voice/components/bottomsheets/VoiceLobbyShared.module.css +++ b/fluxer_app/src/features/voice/components/bottomsheets/VoiceLobbyShared.module.css @@ -54,6 +54,10 @@ color: white; } +.iconContainerBrand .actionIcon { + color: var(--brand-primary-fill); +} + .actionIconSecondary { color: var(--text-primary); } diff --git a/fluxer_app/src/features/voice/components/media_player/AudioPlayer.module.css b/fluxer_app/src/features/voice/components/media_player/AudioPlayer.module.css index a7ca8f4a3..b344c1c7f 100644 --- a/fluxer_app/src/features/voice/components/media_player/AudioPlayer.module.css +++ b/fluxer_app/src/features/voice/components/media_player/AudioPlayer.module.css @@ -80,6 +80,7 @@ .playButton:hover { background-color: var(--brand-primary-light); + color: var(--text-on-brand-primary-light); } .playButton:active { diff --git a/fluxer_app/src/features/voice/components/media_player/InlineAudioPlayer.module.css b/fluxer_app/src/features/voice/components/media_player/InlineAudioPlayer.module.css index d8dd7b03d..3a5b947ea 100644 --- a/fluxer_app/src/features/voice/components/media_player/InlineAudioPlayer.module.css +++ b/fluxer_app/src/features/voice/components/media_player/InlineAudioPlayer.module.css @@ -56,7 +56,7 @@ inset: -0.1875rem; border-radius: var(--radius-full); border: 0.125rem solid transparent; - border-top-color: var(--text-on-brand-primary); + border-top-color: var(--brand-primary-foreground); animation: spin 0.8s linear infinite; pointer-events: none; } @@ -201,11 +201,15 @@ inset: -0.1875rem; border-radius: var(--radius-full); border: 0.125rem solid transparent; - border-top-color: var(--text-on-brand-primary); + border-top-color: var(--brand-primary-foreground); animation: spin 0.8s linear infinite; pointer-events: none; } +.mobileContainerActive .loadingSpinner { + border-top-color: var(--text-on-brand-primary); +} + @keyframes spin { from { transform: rotate(0deg); @@ -292,5 +296,5 @@ } :global(.theme-light) .mobileProgressFill:not(.mobileProgressFillActive) { - background-color: var(--brand-primary); + background-color: var(--brand-primary-foreground); } diff --git a/fluxer_app/src/features/voice/components/media_player/MediaPlayButton.module.css b/fluxer_app/src/features/voice/components/media_player/MediaPlayButton.module.css index c6d5a0809..bed0fb9fa 100644 --- a/fluxer_app/src/features/voice/components/media_player/MediaPlayButton.module.css +++ b/fluxer_app/src/features/voice/components/media_player/MediaPlayButton.module.css @@ -56,7 +56,7 @@ background-color: rgba(0, 0, 0, 0.6); border-radius: 50%; padding: 1rem; - color: var(--text-on-brand-primary); + color: var(--text-on-dark); transition: background-color 150ms ease, transform 150ms ease; diff --git a/fluxer_app/src/features/voice/components/media_player/MediaPlaybackRate.module.css b/fluxer_app/src/features/voice/components/media_player/MediaPlaybackRate.module.css index 73f081193..696281524 100644 --- a/fluxer_app/src/features/voice/components/media_player/MediaPlaybackRate.module.css +++ b/fluxer_app/src/features/voice/components/media_player/MediaPlaybackRate.module.css @@ -66,5 +66,5 @@ } :global(.theme-light) .active { - color: var(--brand-primary); + color: var(--brand-primary-foreground); } diff --git a/fluxer_app/src/features/voice/components/media_player/MediaProgressBar.module.css b/fluxer_app/src/features/voice/components/media_player/MediaProgressBar.module.css index 8b68d3946..0e8544db3 100644 --- a/fluxer_app/src/features/voice/components/media_player/MediaProgressBar.module.css +++ b/fluxer_app/src/features/voice/components/media_player/MediaProgressBar.module.css @@ -144,7 +144,7 @@ :global(.theme-light) .fill, :global(.theme-light) .thumb { - background-color: var(--brand-primary); + background-color: var(--brand-primary-foreground); } .compact .thumb { diff --git a/fluxer_app/src/features/voice/components/media_player/MediaVolumeControl.module.css b/fluxer_app/src/features/voice/components/media_player/MediaVolumeControl.module.css index 705d346fd..e7b223e38 100644 --- a/fluxer_app/src/features/voice/components/media_player/MediaVolumeControl.module.css +++ b/fluxer_app/src/features/voice/components/media_player/MediaVolumeControl.module.css @@ -118,5 +118,5 @@ :global(.theme-light) .sliderFill, :global(.theme-light) .sliderThumb { - background-color: var(--brand-primary); + background-color: var(--brand-primary-foreground); } diff --git a/fluxer_app/src/features/voice/components/media_player/VideoPlayer.module.css b/fluxer_app/src/features/voice/components/media_player/VideoPlayer.module.css index e5ae9056b..4823ad0ec 100644 --- a/fluxer_app/src/features/voice/components/media_player/VideoPlayer.module.css +++ b/fluxer_app/src/features/voice/components/media_player/VideoPlayer.module.css @@ -80,7 +80,7 @@ background-color: rgba(0, 0, 0, 0.6); border: none; border-radius: 50%; - color: var(--text-on-brand-primary); + color: var(--text-on-dark); cursor: pointer; transition: background-color 0.1s ease; z-index: 1; @@ -141,7 +141,7 @@ height: 3rem; background-color: rgba(0, 0, 0, 0.6); border-radius: 50%; - color: var(--text-on-brand-primary); + color: var(--text-on-dark); pointer-events: none; }