fix(app): keep brand colour text readable on every surface (#3504)

This commit is contained in:
Hampus authored and GitHub committed 2026-10-16 03:06:24 +02:00
1 parent 3224f2113a
commit 275cc73f7a
102 files changed
+1045 -419

No files matched your search

+94 -206
View File
@@ -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<ScaleStop>;
}
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<TokenDef> = [
{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<OutputToken> {
const lastIndex = Math.max(scale.stops.length - 1, 1);
const tokens: Array<OutputToken> = [];
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<TokenDef>, scales: Record<string, Scale>): Array<OutputToken> {
@@ -881,6 +771,7 @@ function expandTokens(defs: Array<TokenDef>, scales: Record<string, Scale>): 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<string, ColorFamily>):
}
function formatValue(token: OutputToken, families: Record<string, ColorFamily>): 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<OutputToken>, families: Record<string, ColorFamily>): string {
+2 -2
View File
@@ -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;
@@ -137,5 +137,5 @@
height: 1.25rem;
border-radius: 50%;
background-color: var(--brand-primary);
color: white;
color: var(--brand-primary-fill);
}
@@ -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 {
@@ -27,5 +27,5 @@
}
:global(.theme-light) .dropIndicatorValid {
background-color: var(--brand-primary);
background-color: var(--brand-primary-foreground);
}
@@ -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);
@@ -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);
}
@@ -37,7 +37,7 @@ export const NAGBAR_TONES: Readonly<Record<NagbarToneKind, NagbarTone>> = 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'}),
@@ -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;
@@ -19,5 +19,5 @@
.iconTextSelected {
height: 1.25rem;
width: 1.25rem;
color: white;
color: var(--brand-primary-fill);
}
@@ -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"
>
<CompassIcon
@@ -269,10 +269,10 @@
:global(.theme-light) .dropIndicatorTop::before,
:global(.theme-light) .dropIndicatorBottom::after {
background: var(--brand-primary);
background: var(--brand-primary-foreground);
}
:global(.theme-light) .dropIndicatorInside .collapsedFolder,
:global(.theme-light) .dropIndicatorInside .folderHeaderButton {
outline-color: var(--brand-primary);
outline-color: var(--brand-primary-foreground);
}
@@ -15,7 +15,7 @@
width: 6rem;
height: 6rem;
margin-bottom: 1.5rem;
color: var(--brand-primary);
color: var(--brand-primary-foreground);
}
.content {
@@ -157,12 +157,12 @@
.editIcon {
height: 1.25rem;
width: 1.25rem;
color: white;
color: var(--brand-primary-fill);
}
.editLabel {
font-weight: 500;
color: white;
color: var(--brand-primary-fill);
}
.section {
@@ -32,7 +32,7 @@
padding: 0.25rem 0.5rem;
background-color: var(--brand-primary);
border-radius: 624.9375rem;
color: white;
color: var(--brand-primary-fill);
font-size: 0.875rem;
line-height: 1;
}
@@ -205,7 +205,7 @@
.item[data-selected='true'] .fluxerIcon {
border-radius: 30%;
background-color: var(--brand-primary);
color: white;
color: var(--brand-primary-fill);
}
.fluxerIconGlyph {
@@ -248,7 +248,7 @@
.item[data-selected='true'] .actionIcon {
border-radius: 30%;
background-color: var(--brand-primary);
color: white;
color: var(--brand-primary-fill);
}
.actionIconGlyph {
@@ -6,7 +6,7 @@
--account-profile-slot-width: 14.875rem;
--account-profile-instance-badge-size: 3rem;
--account-profile-state-badge-size: 2.25rem;
--account-profile-name-color: var(--text-on-brand-primary);
--account-profile-name-color: var(--text-on-dark);
--account-profile-domain-height: 0;
--account-profile-domain-gap: 0;
--account-profile-name-gap: 0.625rem;
@@ -100,7 +100,7 @@
.noAccounts {
margin-bottom: 1rem;
color: var(--text-on-brand-primary);
color: var(--text-on-dark);
font-size: 0.9375rem;
font-weight: 600;
line-height: 1.25rem;
@@ -49,7 +49,7 @@
.checkIcon {
height: 0.625rem;
width: 0.625rem;
color: white;
color: var(--brand-primary-fill);
}
.code {
@@ -157,7 +157,7 @@ img.entityIcon {
.themeIcon {
height: 2.5rem;
width: 2.5rem;
color: white;
color: var(--brand-primary-fill);
}
.form {
@@ -98,7 +98,7 @@
width: 1.5rem;
height: 1.5rem;
border: 0.1875rem solid var(--background-modifier-accent);
border-top-color: var(--brand-primary);
border-top-color: var(--brand-primary-foreground);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@@ -53,5 +53,5 @@
.audioIcon {
height: 1rem;
width: 1rem;
color: white;
color: var(--brand-primary-fill);
}
@@ -58,11 +58,11 @@
}
:global(.theme-light) .gifButton:hover {
border-color: var(--brand-primary);
border-color: var(--brand-primary-foreground);
}
:global(.theme-light) .gifButtonSelected {
border-color: var(--brand-primary);
border-color: var(--brand-primary-foreground);
}
.gifVideoWrapper {
@@ -317,5 +317,5 @@
:global(.theme-light) .dropIndicatorLeft::before,
:global(.theme-light) .dropIndicatorRight::after {
background: var(--brand-primary);
background: var(--brand-primary-foreground);
}
@@ -155,7 +155,7 @@
background: none;
border: none;
padding: 0 0.75rem;
color: white;
color: var(--brand-primary-fill);
font-size: 0.875rem;
font-weight: 500;
overflow: hidden;
@@ -172,7 +172,7 @@
background: none;
border: none;
padding: 0 0.75rem;
color: white;
color: var(--brand-primary-fill);
font-size: 0.875rem;
font-weight: 600;
flex-shrink: 0;
@@ -270,6 +270,16 @@
text-shadow: 0 0.0625rem 0.0625rem rgba(0, 0, 0, 0.06);
}
.gridItemCategoryEmpty .gridItemIcon,
.gridItemCategoryEmpty .gridItemCategoryTitleText {
color: var(--text-on-brand-primary-light);
}
.gridItemFavorites .gridItemIcon,
.gridItemFavorites .gridItemCategoryTitleText {
color: var(--text-on-brand-primary);
}
.gif {
width: 100%;
height: 100%;
@@ -445,6 +455,8 @@
color: var(--text-primary);
}
:global(.theme-light) .gridItemCategoryEmpty .gridItemCategoryTitleText,
:global(.theme-light) .gridItemCategoryEmpty .gridItemIcon,
:global(.theme-light) .gridItemFavorites .gridItemCategoryTitleText,
:global(.theme-light) .gridItemFavorites .gridItemIcon {
color: var(--text-on-brand-primary);
@@ -459,8 +471,8 @@
}
:global(.theme-light) .gridItemFocused {
border-color: var(--brand-primary);
box-shadow: inset 0 0 0 0.125rem var(--brand-primary);
border-color: var(--brand-primary-foreground);
box-shadow: inset 0 0 0 0.125rem var(--brand-primary-foreground);
}
:global(.theme-light) .gridItemFavorites .gridItemBackdrop {
@@ -195,7 +195,7 @@
}
:global(.theme-light) .progressBar {
background-color: var(--brand-primary);
background-color: var(--brand-primary-foreground);
}
.thContent {
@@ -138,7 +138,7 @@
.audioIcon {
height: 3rem;
width: 3rem;
color: white;
color: var(--brand-primary-fill);
flex-shrink: 0;
}
@@ -153,7 +153,7 @@
.audioDuration {
font-family: var(--font-mono);
font-size: 1.125rem;
color: white;
color: var(--brand-primary-fill);
}
.audioFilename {
@@ -164,14 +164,14 @@
text-align: center;
font-weight: 600;
font-size: 0.875rem;
color: rgba(255, 255, 255, 0.9);
color: color-mix(in srgb, var(--brand-primary-fill) 90%, transparent);
}
.audioBadge {
padding: 0.125rem 0.5rem;
border-radius: 0.375rem;
background-color: rgba(0, 0, 0, 0.2);
background-color: color-mix(in srgb, var(--brand-primary-shade) 20%, transparent);
font-weight: 700;
font-size: 0.75rem;
color: white;
color: var(--brand-primary-fill);
}
@@ -36,16 +36,16 @@
}
.reactionMe .reactionButton {
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);
}
:global(:where(html.window-focused, html.unfocused-fully-interactive):not(.window-focus-activation-guard))
.reactionMe
.reactionButton:hover {
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-tint-border);
}
.reactionInner {
@@ -80,7 +80,7 @@
}
.reactionMe .countWrapper {
color: var(--text-on-brand-primary);
color: var(--text-on-brand-tint);
}
:global(.theme-light) .reactionButton {
@@ -96,20 +96,20 @@
}
:global(.theme-light) .reactionMe .reactionButton {
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);
}
:global(.theme-light:where(.window-focused, .unfocused-fully-interactive):not(.window-focus-activation-guard))
.reactionMe
.reactionButton:hover {
background-color: color-mix(in srgb, var(--brand-primary) 18%, var(--background-primary) 82%);
border-color: var(--brand-primary);
background-color: var(--brand-tint-hover);
border-color: var(--brand-primary-foreground);
}
:global(.theme-light) .reactionMe .countWrapper {
color: var(--brand-primary);
color: var(--text-on-brand-tint);
}
.reactionContainerDisabled {
@@ -127,8 +127,8 @@
.reactionMe .reactionButtonDisabled,
.reactionMe .reactionButtonDisabled:hover {
background-color: color-mix(in srgb, var(--brand-primary) 36%, var(--background-secondary) 64%);
border-color: var(--brand-primary);
background-color: var(--brand-tint);
border-color: var(--brand-tint-border);
}
:global(.theme-light) .reactionButtonDisabled,
@@ -139,8 +139,8 @@
:global(.theme-light) .reactionMe .reactionButtonDisabled,
:global(.theme-light) .reactionMe .reactionButtonDisabled:hover {
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%);
background-color: var(--brand-tint);
border-color: var(--brand-tint-border);
}
.reactionPickerButton {
@@ -110,5 +110,5 @@
:global(.theme-light) .progressBarIndeterminate,
:global(.theme-light) .progressBarNormal {
background-color: var(--brand-primary);
background-color: var(--brand-primary-foreground);
}
@@ -170,7 +170,7 @@
}
:global(.theme-light) .tabButtonActive {
color: var(--brand-primary);
color: var(--brand-primary-foreground);
}
.tabButtonInactive {
@@ -367,7 +367,7 @@
}
.newGroupIconWhite {
color: white;
color: var(--brand-primary-fill);
}
.newGroupContent {
@@ -555,12 +555,13 @@
.quickActionButtonActive {
background-color: var(--brand-primary);
color: white;
color: var(--brand-primary-fill);
}
@media (hover: hover) and (pointer: fine) {
.quickActionButtonActive:hover {
background-color: var(--brand-primary-light);
color: var(--brand-primary-light-fill);
}
}
@@ -35,7 +35,7 @@
}
:global(.theme-light) .heroIcon {
color: var(--brand-primary);
color: var(--brand-primary-foreground);
}
.title {
@@ -90,7 +90,7 @@
:global(.theme-light) .dmListHeaderShortcut kbd {
background-color: hsl(220, calc(13% * var(--saturation-factor)), 18.44%);
color: var(--text-on-brand-primary);
color: var(--text-on-dark);
box-shadow: 0 0.0625rem 0 rgba(0, 0, 0, 0.4);
}
@@ -280,7 +280,7 @@
.clickableItemSelected .clickableItemIcon {
background-color: var(--brand-primary);
color: white;
color: var(--brand-primary-fill);
}
@media (hover: hover) and (pointer: fine) {
@@ -313,7 +313,7 @@
.sendIcon {
height: 1.5rem;
width: 1.5rem;
color: white;
color: var(--brand-primary-fill);
}
.clickableItemText {
@@ -725,7 +725,7 @@
.mobileSpecialButtonSelected .mobileSpecialButtonIcon {
background-color: var(--brand-primary);
color: white;
color: var(--brand-primary-fill);
}
.mobileSpecialButtonText {
@@ -403,13 +403,17 @@
.languageItemSelected {
background-color: var(--brand-primary);
color: #fff;
color: var(--brand-primary-fill);
}
.languageItemSelected.languageItemHighlighted {
background-color: var(--brand-primary);
}
.languageItemSelected .languageOptionCanonical {
color: inherit;
}
.languageItemIndicator {
position: absolute;
top: 50%;
@@ -322,7 +322,7 @@ export const collectGalleryImages = ({
};
export const getBorderColor = (color: number | undefined) => {
if (color === undefined || color === 0) {
return 'var(--brand-primary)';
return 'var(--brand-primary-foreground)';
}
return ColorUtils.int2rgb(color);
};
@@ -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;
@@ -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);
}
@@ -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);
}
@@ -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 {
@@ -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 {
@@ -41,5 +41,5 @@
}
:global(.theme-light) .progressBar {
background-color: var(--brand-primary);
background-color: var(--brand-primary-foreground);
}
@@ -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 {
@@ -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);
}
@@ -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;
}
@@ -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 {
@@ -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);
}
@@ -204,6 +204,10 @@
color: var(--brand-primary-fill);
}
.overwriteItemSelected .lockIcon {
color: inherit;
}
.roleDot {
width: 0.75rem;
height: 0.75rem;
@@ -144,7 +144,7 @@
position: absolute;
inset: 0;
margin: auto;
color: var(--text-on-brand-primary);
color: var(--text-on-dark);
}
.overflowCount {
@@ -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);
}
@@ -95,7 +95,7 @@
.breadcrumbActive .breadcrumbNumber {
background: var(--brand-primary);
color: white;
color: var(--brand-primary-fill);
}
.breadcrumbLabel {
@@ -21,7 +21,7 @@
}
:global(.theme-light) .tile {
color: var(--brand-primary);
color: var(--brand-primary-foreground);
}
.title {
@@ -143,7 +143,7 @@
}
:global(.theme-light) .checkIcon {
color: var(--brand-primary);
color: var(--brand-primary-foreground);
}
.emptyState {
@@ -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 {
@@ -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);
}
@@ -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);
}
@@ -140,7 +140,7 @@
}
:global(.theme-light) .checkIcon {
color: var(--brand-primary);
color: var(--brand-primary-foreground);
}
.emptyState {
@@ -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 {
@@ -291,7 +291,7 @@ const BackgroundItem: React.FC<BackgroundItemProps> = 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}
@@ -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 () => {
@@ -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);
}
}
@@ -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;
}
@@ -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<ScaleStop>;
}
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<string, string>;
selectedOverlay: ColorTone;
brandTint: BrandTint;
}
export const COLOR_FAMILIES: Record<string, ColorFamily> = {
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<string, Scale> = {
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<ColorSystemTheme, ThemePalette> = {
[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;
}
@@ -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<string> {
const colors: Array<string> = [];
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<Rgb>): 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%)');
});
});
@@ -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<InstanceBrandVariable, string> | 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<Rgb> {
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<Rgb>): 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<InstanceBrandVariable, string> | 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,
};
}
@@ -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<ThemeVariableDefinition> = [
{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<ThemeVariableDefinition> = [
{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<string> = [
"--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<string> = [
"--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<Record<string,
"--border-color-hover": "hsla(258, calc(10% * var(--saturation-factor)), 50%, 0.3)",
"--brand-primary": "hsl(242, calc(70% * var(--saturation-factor)), 55%)",
"--brand-primary-fill": "hsl(0, 0%, 100%)",
"--brand-primary-foreground": "hsl(242, calc(70% * var(--saturation-factor)), 55%)",
"--brand-primary-light": "hsl(242, calc(100% * var(--saturation-factor)), 84%)",
"--brand-primary-light-fill": "hsl(0, 0%, 100%)",
"--brand-primary-pressed": "color-mix(in srgb, hsl(242, calc(70% * var(--saturation-factor)), 55%) 90%, hsl(0, 0%, 0%))",
"--brand-primary-shade": "hsl(0, 0%, 0%)",
"--brand-secondary": "hsl(242, calc(60% * var(--saturation-factor)), 49%)",
"--brand-tint": "color-mix(in srgb, var(--brand-primary) 36%, var(--background-secondary) 64%)",
"--brand-tint-border": "var(--brand-primary)",
"--brand-tint-hover": "color-mix(in srgb, var(--brand-primary) 48%, var(--background-secondary) 52%)",
"--button-danger-active-fill": "hsl(359, calc(65% * var(--saturation-factor)), 45%)",
"--button-danger-fill": "hsl(359, calc(70% * var(--saturation-factor)), 54%)",
"--button-danger-outline-active-border": "transparent",
@@ -1815,6 +1852,9 @@ export const THEME_STUDIO_DARK_DEFAULT_VARIABLE_VALUES: Readonly<Record<string,
"--text-code": "color-mix(in srgb, hsl(258, calc(10% * var(--saturation-factor)), 90.36%) 82%, hsl(340, calc(50% * var(--saturation-factor)), 90%) 18%)",
"--text-link": "hsl(198, calc(92% * var(--saturation-factor)), 70%)",
"--text-on-brand-primary": "hsl(0, 0%, 98%)",
"--text-on-brand-primary-light": "hsl(0, 0%, 98%)",
"--text-on-brand-tint": "var(--text-on-brand-primary)",
"--text-on-dark": "hsl(0, 0%, 98%)",
"--text-primary": "hsl(258, calc(10% * var(--saturation-factor)), 96%)",
"--text-primary-muted": "hsl(258, calc(10% * var(--saturation-factor)), 81.42%)",
"--text-secondary": "hsl(258, calc(10% * var(--saturation-factor)), 90.36%)",
@@ -1965,8 +2005,15 @@ export const THEME_STUDIO_LIGHT_DEFAULT_VARIABLE_VALUES: Readonly<Record<string,
"--border-color-hover": "hsla(220, calc(10% * var(--saturation-factor)), 40%, 0.25)",
"--brand-primary": "hsl(242, calc(70% * var(--saturation-factor)), 55%)",
"--brand-primary-fill": "hsl(0, 0%, 100%)",
"--brand-primary-foreground": "hsl(242, calc(70% * var(--saturation-factor)), 55%)",
"--brand-primary-light": "hsl(242, calc(100% * var(--saturation-factor)), 84%)",
"--brand-primary-light-fill": "hsl(0, 0%, 100%)",
"--brand-primary-pressed": "color-mix(in srgb, hsl(242, calc(70% * var(--saturation-factor)), 55%) 90%, hsl(0, 0%, 0%))",
"--brand-primary-shade": "hsl(0, 0%, 0%)",
"--brand-secondary": "hsl(242, calc(60% * var(--saturation-factor)), 49%)",
"--brand-tint": "color-mix(in srgb, var(--brand-primary) 12%, var(--background-primary) 88%)",
"--brand-tint-border": "color-mix(in srgb, var(--brand-primary) 45%, transparent 55%)",
"--brand-tint-hover": "color-mix(in srgb, var(--brand-primary) 18%, var(--background-primary) 82%)",
"--button-danger-active-fill": "hsl(359, calc(65% * var(--saturation-factor)), 45%)",
"--button-danger-fill": "hsl(359, calc(70% * var(--saturation-factor)), 54%)",
"--button-danger-outline-active-border": "transparent",
@@ -2280,6 +2327,9 @@ export const THEME_STUDIO_LIGHT_DEFAULT_VARIABLE_VALUES: Readonly<Record<string,
"--text-code": "color-mix(in srgb, hsl(220, calc(10% * var(--saturation-factor)), 33.78%) 72%, hsl(340, calc(50% * var(--saturation-factor)), 38%) 28%)",
"--text-link": "hsl(198, calc(92% * var(--saturation-factor)), 45%)",
"--text-on-brand-primary": "hsl(0, 0%, 98%)",
"--text-on-brand-primary-light": "hsl(0, 0%, 98%)",
"--text-on-brand-tint": "var(--brand-primary)",
"--text-on-dark": "hsl(0, 0%, 98%)",
"--text-primary": "hsl(220, calc(10% * var(--saturation-factor)), 15%)",
"--text-primary-muted": "hsl(220, calc(10% * var(--saturation-factor)), 42.2%)",
"--text-secondary": "hsl(220, calc(10% * var(--saturation-factor)), 33.78%)",
@@ -24,6 +24,7 @@
--studio-fg-muted: var(--text-secondary);
--studio-fg-subtle: var(--text-tertiary);
--studio-fg-on-accent: var(--text-on-brand-primary);
--studio-fg-on-danger: var(--text-on-dark);
--studio-accent: var(--brand-primary);
--studio-accent-hover: var(--brand-secondary);
@@ -31,7 +32,7 @@
--studio-focus: var(--focus-primary);
--studio-button-primary: var(--brand-primary);
--studio-button-primary-hover: color-mix(in srgb, var(--brand-primary) 88%, var(--text-primary) 12%);
--studio-button-primary-hover: var(--studio-accent-hover);
--studio-button-secondary: var(--button-secondary-fill);
--studio-button-secondary-hover: var(--button-secondary-active-fill);
@@ -95,7 +95,7 @@ function buildEditorTheme(baseTheme: ThemeStudioBaseTheme): Extension {
},
'.cm-tooltip-autocomplete > 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)',
@@ -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 {
@@ -47,7 +47,7 @@
}
:global(.theme-light) .count {
color: var(--brand-primary);
color: var(--brand-primary-foreground);
}
.preview {
@@ -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);
}
@@ -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 {
@@ -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;
}
@@ -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 {
@@ -167,7 +167,6 @@ export const Checkbox: React.FC<CheckboxProps> = observer(
<CheckIcon
size={checkIconSizeRem}
weight="bold"
color={'#ffffff'}
className={styles.checkIcon}
data-flx="ui.checkbox.checkbox.check-icon"
/>
@@ -224,7 +223,6 @@ export const Checkbox: React.FC<CheckboxProps> = observer(
<CheckIcon
size={checkIconSizeRem}
weight="bold"
color={'#ffffff'}
className={styles.checkIcon}
data-flx="ui.checkbox.checkbox.check-icon--2"
/>
@@ -46,7 +46,7 @@
.pageButtonActive {
background-color: var(--brand-primary);
color: white;
color: var(--brand-primary-fill);
}
.pageButtonActive:hover {
@@ -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%)
);
}
@@ -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;
}
@@ -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;
}
@@ -116,6 +116,7 @@
}
.switchThumb[data-state='checked'] {
background-color: var(--brand-primary-fill);
transform: translateX(1.375rem);
}
@@ -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);
}
@@ -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 {
@@ -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;
}
@@ -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 {
@@ -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 {
@@ -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);
}
@@ -99,7 +99,7 @@
.checkIcon {
width: 0.75rem;
height: 0.75rem;
color: #fff;
color: var(--brand-primary-fill);
}
.chevronIcon {
@@ -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 {
@@ -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);
}
@@ -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);
}
@@ -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;
}
@@ -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;
}
@@ -49,7 +49,7 @@
}
:global(.theme-light) .sequenceBadge {
color: var(--brand-primary);
color: var(--brand-primary-foreground);
}
.sequenceBadgeMobile {
@@ -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%;
@@ -18,7 +18,7 @@
.new,
.experimental {
background: var(--brand-primary);
color: #fff;
color: var(--brand-primary-fill);
}
.badges {
@@ -54,6 +54,10 @@
color: white;
}
.iconContainerBrand .actionIcon {
color: var(--brand-primary-fill);
}
.actionIconSecondary {
color: var(--text-primary);
}
@@ -80,6 +80,7 @@
.playButton:hover {
background-color: var(--brand-primary-light);
color: var(--text-on-brand-primary-light);
}
.playButton:active {
@@ -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);
}
@@ -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;
@@ -66,5 +66,5 @@
}
:global(.theme-light) .active {
color: var(--brand-primary);
color: var(--brand-primary-foreground);
}
@@ -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 {
Loaded 100 of 102 files, more files were not shown because too many files have changed in this diff. Show more