refactor: convert color system to RGB channels for native Tailwind opacity

Root cause fix for all broken CSS variable opacity modifiers. Converted
every hex CSS variable to space-separated RGB channels (e.g. #fda4af →
253 164 175) and updated Tailwind config to use rgb(var(...) / <alpha-value>).
This makes bg-accent-rose/10, border-border-hard/50, etc. work natively
everywhere — no more currentColor fallback. Reverted all previous explicit
rgba() workarounds back to clean Tailwind syntax. Also fixed the voice
connection wifi icon SVG (was bottom-left aligned, now centered).
This commit is contained in:
Jannis Braun
2026-03-02 17:46:21 +01:00
parent 0e235fdf1c
commit 37dc73231d
20 changed files with 97 additions and 96 deletions
@@ -42,7 +42,7 @@ export function LoginPage() {
<form onSubmit={handleSubmit}>
{error && (
<div className="mb-4 p-3 bg-[rgba(253,164,175,0.10)] border border-[rgba(253,164,175,0.30)] rounded text-txt-danger text-sm">
<div className="mb-4 p-3 bg-accent-rose/10 border border-accent-rose/30 rounded text-txt-danger text-sm">
{error}
</div>
)}
@@ -54,7 +54,7 @@ export function RegisterPage() {
<form onSubmit={handleSubmit}>
{error && (
<div className="mb-4 p-3 bg-[rgba(253,164,175,0.10)] border border-[rgba(253,164,175,0.30)] rounded text-txt-danger text-sm">
<div className="mb-4 p-3 bg-accent-rose/10 border border-accent-rose/30 rounded text-txt-danger text-sm">
{error}
</div>
)}
@@ -161,7 +161,7 @@ export function FriendsPage() {
</button>
</form>
{addStatus && (
<div className={`text-sm p-3 rounded-lg border ${addStatus.type === 'success' ? 'text-txt-positive border-[rgba(134,239,172,0.20)] bg-[rgba(134,239,172,0.05)]' : 'text-txt-danger border-[rgba(253,164,175,0.20)] bg-[rgba(253,164,175,0.05)]'}`}>
<div className={`text-sm p-3 rounded-lg border ${addStatus.type === 'success' ? 'text-txt-positive border-status-online/20 bg-status-online/5' : 'text-txt-danger border-accent-rose/20 bg-accent-rose/5'}`}>
{addStatus.message}
</div>
)}
@@ -67,8 +67,8 @@ const aetherTheme = {
...themes.oneDark,
plain: {
...themes.oneDark.plain,
backgroundColor: 'var(--bg-elevated)',
color: 'var(--text-message)',
backgroundColor: 'rgb(var(--bg-elevated))',
color: 'rgb(var(--text-message))',
},
};
+1 -1
View File
@@ -286,7 +286,7 @@ export function Message({ message, isCompact, isFirstInGroup }: MessageProps) {
key={att.id}
href={`/api/uploads/${att.filename}`}
download={att.originalName}
className="flex items-center gap-3 p-4 bg-[rgba(26,26,35,0.50)] rounded-lg border border-border-hard hover:bg-interactive-hover transition-all max-w-[400px] mt-1 group/att"
className="flex items-center gap-3 p-4 bg-surface-channel/50 rounded-lg border border-border-hard hover:bg-interactive-hover transition-all max-w-[400px] mt-1 group/att"
>
<div className="p-2 bg-surface-base rounded text-txt-tertiary group-hover/att:text-txt-primary transition-colors">
<svg className="w-8 h-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
@@ -261,7 +261,7 @@ export function MessageInput({ channelId, channelName }: MessageInputProps) {
{/* File previews */}
{files.length > 0 && (
<div className="p-4 flex flex-wrap gap-4 bg-[rgba(26,26,35,0.30)]">
<div className="p-4 flex flex-wrap gap-4 bg-surface-channel/30">
{files.map((file, i) => (
<div key={i} className="relative group bg-surface-channel rounded-lg p-2 max-w-[200px] shadow-elevation-low border border-border-hard">
{file.type.startsWith('image/') ? (
@@ -32,7 +32,7 @@ export function TypingIndicator({ channelId }: TypingIndicatorProps) {
return (
<div className="h-[24px] px-4 flex items-center text-[12px] text-txt-primary font-medium select-none pointer-events-none">
<div className="flex items-center gap-2">
<div className="flex gap-[2px] bg-[rgba(37,37,48,0.20)] rounded-full px-2 py-1">
<div className="flex gap-[2px] bg-surface-elevated/20 rounded-full px-2 py-1">
<div className="w-[5px] h-[5px] bg-txt-message rounded-full animate-bounce" style={{ animationDelay: '0ms', animationDuration: '0.8s' }} />
<div className="w-[5px] h-[5px] bg-txt-message rounded-full animate-bounce" style={{ animationDelay: '150ms', animationDuration: '0.8s' }} />
<div className="w-[5px] h-[5px] bg-txt-message rounded-full animate-bounce" style={{ animationDelay: '300ms', animationDuration: '0.8s' }} />
@@ -619,7 +619,7 @@ function UserAreaPanel({
}}
className="w-full h-1.5 rounded-full appearance-none cursor-pointer accent-accent-primary bg-surface-base [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-3 [&::-webkit-slider-thumb]:h-3 [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-white [&::-webkit-slider-thumb]:shadow-md"
style={{
background: `linear-gradient(to right, var(--accent-primary) 0%, var(--accent-primary) ${inputVolume / 2}%, var(--interactive-muted) ${inputVolume / 2}%, var(--interactive-muted) 100%)`,
background: `linear-gradient(to right, rgb(var(--accent-primary)) 0%, rgb(var(--accent-primary)) ${inputVolume / 2}%, rgb(var(--interactive-muted)) ${inputVolume / 2}%, rgb(var(--interactive-muted)) 100%)`,
}}
/>
{/* Mic level meter */}
@@ -705,7 +705,7 @@ function UserAreaPanel({
}}
className="w-full h-1.5 rounded-full appearance-none cursor-pointer bg-surface-base [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-3 [&::-webkit-slider-thumb]:h-3 [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-white [&::-webkit-slider-thumb]:shadow-md"
style={{
background: `linear-gradient(to right, var(--accent-primary) 0%, var(--accent-primary) ${outputVolume / 2}%, var(--interactive-muted) ${outputVolume / 2}%, var(--interactive-muted) 100%)`,
background: `linear-gradient(to right, rgb(var(--accent-primary)) 0%, rgb(var(--accent-primary)) ${outputVolume / 2}%, rgb(var(--interactive-muted)) ${outputVolume / 2}%, rgb(var(--interactive-muted)) 100%)`,
}}
/>
</div>
@@ -15,7 +15,7 @@ function getMemberGroup(member: MemberWithUser, ownerId: string | undefined) {
return {
key: '__owner__',
label: 'OWNER',
color: ownerRole?.color ?? 'var(--accent-rose)',
color: ownerRole?.color ?? 'rgb(var(--accent-rose))',
position: Infinity,
};
}
@@ -79,7 +79,7 @@ export function MemberSidebar() {
return { color: sorted[0]!.color };
}
if (ownerId && member.userId === ownerId) {
return { color: 'var(--accent-rose)' };
return { color: 'rgb(var(--accent-rose))' };
}
return undefined;
};
@@ -104,7 +104,7 @@ export function ChannelSettingsModal() {
</div>
{error && (
<div className="p-2 bg-[rgba(253,164,175,0.10)] border border-[rgba(253,164,175,0.30)] rounded text-txt-danger text-sm">
<div className="p-2 bg-accent-rose/10 border border-accent-rose/30 rounded text-txt-danger text-sm">
{error}
</div>
)}
@@ -139,7 +139,7 @@ export function ChannelSettingsModal() {
</div>
{isPrivate && !isFetching && (
<div className="flex items-start gap-2 p-2 bg-[rgba(17,17,24,0.50)] rounded text-xs text-txt-tertiary">
<div className="flex items-start gap-2 p-2 bg-surface-input/50 rounded text-xs text-txt-tertiary">
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" className="flex-shrink-0 mt-0.5 text-txt-secondary">
<path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm3.1-9H8.9V6c0-1.71 1.39-3.1 3.1-3.1 1.71 0 3.1 1.39 3.1 3.1v2z" />
</svg>
@@ -48,7 +48,7 @@ export function CreateChannelModal() {
<Modal isOpen={isOpen} onClose={closeModal} title="Create Channel">
<form onSubmit={handleSubmit}>
{error && (
<div className="mb-3 p-2 bg-[rgba(253,164,175,0.10)] border border-[rgba(253,164,175,0.30)] rounded text-txt-danger text-sm">
<div className="mb-3 p-2 bg-accent-rose/10 border border-accent-rose/30 rounded text-txt-danger text-sm">
{error}
</div>
)}
@@ -41,7 +41,7 @@ export function CreateServerModal() {
<Modal isOpen={isOpen} onClose={closeModal} title="Create a Server">
<form onSubmit={handleSubmit}>
{error && (
<div className="mb-3 p-2 bg-[rgba(253,164,175,0.10)] border border-[rgba(253,164,175,0.30)] rounded text-txt-danger text-sm">
<div className="mb-3 p-2 bg-accent-rose/10 border border-accent-rose/30 rounded text-txt-danger text-sm">
{error}
</div>
)}
@@ -55,7 +55,7 @@ export function InviteModal() {
Share this invite link with friends to let them join your server.
</p>
{error && (
<div className="mb-3 p-2 bg-[rgba(253,164,175,0.10)] border border-[rgba(253,164,175,0.30)] rounded text-txt-danger text-sm">
<div className="mb-3 p-2 bg-accent-rose/10 border border-accent-rose/30 rounded text-txt-danger text-sm">
{error}
</div>
)}
@@ -52,7 +52,7 @@ export function JoinServerModal() {
Enter an invite code to join an existing server.
</p>
{error && (
<div className="mb-3 p-2 bg-[rgba(253,164,175,0.10)] border border-[rgba(253,164,175,0.30)] rounded text-txt-danger text-sm">
<div className="mb-3 p-2 bg-accent-rose/10 border border-accent-rose/30 rounded text-txt-danger text-sm">
{error}
</div>
)}
@@ -189,7 +189,7 @@ function StreamingLimitsPanel() {
{/* Save / Reset */}
{saveError && (
<div className="p-2 bg-[rgba(253,164,175,0.10)] border border-[rgba(253,164,175,0.30)] rounded text-txt-danger text-sm">{saveError}</div>
<div className="p-2 bg-accent-rose/10 border border-accent-rose/30 rounded text-txt-danger text-sm">{saveError}</div>
)}
{saveSuccess && (
<div className="p-2 bg-status-online/10 border border-status-online/30 rounded text-status-online text-sm">Settings saved</div>
@@ -369,7 +369,7 @@ export function ServerSettingsModal() {
{/* Content */}
<div className="flex-1 min-w-0">
{error && (
<div className="mb-3 p-2 bg-[rgba(253,164,175,0.10)] border border-[rgba(253,164,175,0.30)] rounded text-txt-danger text-sm">{error}</div>
<div className="mb-3 p-2 bg-accent-rose/10 border border-accent-rose/30 rounded text-txt-danger text-sm">{error}</div>
)}
{tab === 'overview' && (
@@ -76,7 +76,7 @@ export function UserSettingsModal() {
</div>
{error && (
<div className="p-2 bg-[rgba(253,164,175,0.10)] border border-[rgba(253,164,175,0.30)] rounded text-txt-danger text-sm">{error}</div>
<div className="p-2 bg-accent-rose/10 border border-accent-rose/30 rounded text-txt-danger text-sm">{error}</div>
)}
{success && (
<div className="p-2 bg-status-online/10 border border-status-online/30 rounded text-status-online text-sm">{success}</div>
@@ -115,7 +115,7 @@ export function VoiceControls() {
title="Connection Info"
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor" className={qualityColor}>
<path d="M1.5 21.5a1.5 1.5 0 100-3 1.5 1.5 0 000 3zM3.14 15.75a.75.75 0 01-.09-1.06A8.46 8.46 0 0112 11a8.46 8.46 0 018.95 3.69.75.75 0 01-1.15.97A6.96 6.96 0 0012 12.5a6.96 6.96 0 00-7.8 3.16.75.75 0 01-1.06.09zM6.37 18.3a.75.75 0 01-.08-1.06A5.46 5.46 0 0112 15a5.46 5.46 0 015.71 2.24.75.75 0 01-1.14.97A3.96 3.96 0 0012 16.5a3.96 3.96 0 00-4.57 1.71.75.75 0 01-1.06.09z" />
<path d="M1 9l2 2c4.97-4.97 13.03-4.97 18 0l2-2C16.93 2.93 7.08 2.93 1 9zm8 8l3 3 3-3c-1.65-1.66-4.34-1.66-6 0zm-4-4l2 2c2.76-2.76 7.24-2.76 10 0l2-2C15.14 9.14 8.87 9.14 5 13z" />
</svg>
</button>
@@ -134,7 +134,7 @@ export function VoiceGrid({ participants }: VoiceGridProps) {
{/* Bottom strip of other tiles */}
{!stripHidden && otherTiles.length > 0 && (
<div className="h-[120px] flex-shrink-0 flex items-center justify-center gap-2 p-2 bg-[rgba(11,11,16,0.50)] overflow-x-auto no-scrollbar">
<div className="h-[120px] flex-shrink-0 flex items-center justify-center gap-2 p-2 bg-surface-base/50 overflow-x-auto no-scrollbar">
{otherTiles.map((t) => (
<div
key={t.key}
+41 -41
View File
@@ -4,55 +4,55 @@
@layer base {
:root {
/* ── Matte Surfaces ── */
--bg-base: #0b0b10;
--bg-channel: #1a1a23;
--bg-chat: #13131a;
--bg-members: #1a1a23;
--bg-elevated: #252530;
--bg-input: #111118;
/* ── Matte Surfaces (RGB channels for Tailwind opacity support) ── */
--bg-base: 11 11 16;
--bg-channel: 26 26 35;
--bg-chat: 19 19 26;
--bg-members: 26 26 35;
--bg-elevated: 37 37 48;
--bg-input: 17 17 24;
--bg-overlay: rgba(0, 0, 0, 0.85);
/* ── Borders ── */
--border-hard: #22222c;
--border-soft: #2a2a36;
--border-hard: 34 34 44;
--border-soft: 42 42 54;
/* ── Pastel Accents ── */
--accent-mint: #86efac;
--accent-peach: #fca5a5;
--accent-lavender: #c4b5fd;
--accent-sky: #7dd3fc;
--accent-amber: #fcd34d;
--accent-rose: #fda4af;
--accent-coral: #fb923c;
--accent-mint: 134 239 172;
--accent-peach: 252 165 165;
--accent-lavender: 196 181 253;
--accent-sky: 125 211 252;
--accent-amber: 252 211 77;
--accent-rose: 253 164 175;
--accent-coral: 251 146 60;
/* ── Primary Action (replaces blurple) ── */
--accent-primary: #7c6cf6;
--accent-primary-hover: #6b5ce0;
--accent-primary-active: #5f4fd0;
--accent-primary: 124 108 246;
--accent-primary-hover: 107 92 224;
--accent-primary-active: 95 79 208;
/* ── Text Hierarchy ── */
--text-primary: #efefef;
--text-secondary: #a0a0aa;
--text-tertiary: #5c5c68;
--text-category: #484854;
--text-message: #d8d8de;
--text-link: #7dd3fc;
--text-positive: #86efac;
--text-warning: #fcd34d;
--text-danger: #fca5a5;
--text-primary: 239 239 239;
--text-secondary: 160 160 170;
--text-tertiary: 92 92 104;
--text-category: 72 72 84;
--text-message: 216 216 222;
--text-link: 125 211 252;
--text-positive: 134 239 172;
--text-warning: 252 211 77;
--text-danger: 252 165 165;
/* ── Interactive ── */
--interactive-hover: rgba(255, 255, 255, 0.06);
--interactive-active: rgba(255, 255, 255, 0.12);
--interactive-selected: rgba(255, 255, 255, 0.16);
--interactive-muted: #3a3a44;
--interactive-muted: 58 58 68;
/* ── Status ── */
--status-online: #86efac;
--status-idle: #fcd34d;
--status-dnd: #fca5a5;
--status-offline: #3a3a44;
--status-online: 134 239 172;
--status-idle: 252 211 77;
--status-dnd: 252 165 165;
--status-offline: 58 58 68;
/* ── Glass Material ── */
--glass-bg: rgba(20, 20, 26, 0.52);
@@ -60,7 +60,7 @@
--glass-highlight: rgba(255, 255, 255, 0.05);
/* ── Notification ── */
--notification: #fda4af;
--notification: 253 164 175;
}
* {
@@ -70,7 +70,7 @@
::selection {
background: rgba(134, 239, 172, 0.25);
color: var(--text-primary);
color: rgb(var(--text-primary));
}
html, body, #root {
@@ -92,14 +92,14 @@
}
::-webkit-scrollbar-thumb {
background-color: var(--bg-input);
background-color: rgb(var(--bg-input));
border-radius: 9999px;
border: 2px solid transparent;
background-clip: padding-box;
}
::-webkit-scrollbar-thumb:hover {
background-color: var(--bg-channel);
background-color: rgb(var(--bg-channel));
}
.no-scrollbar::-webkit-scrollbar {
@@ -180,23 +180,23 @@
.glass {
backdrop-filter: none;
-webkit-backdrop-filter: none;
background: var(--bg-channel);
background: rgb(var(--bg-channel));
}
.glass-strip {
backdrop-filter: none;
-webkit-backdrop-filter: none;
background: var(--bg-channel);
background: rgb(var(--bg-channel));
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.20);
}
.glass-bubble {
backdrop-filter: none;
-webkit-backdrop-filter: none;
background: var(--bg-elevated);
background: rgb(var(--bg-elevated));
}
.glass-pill {
backdrop-filter: none;
-webkit-backdrop-filter: none;
background: var(--bg-elevated);
background: rgb(var(--bg-elevated));
}
}
+33 -32
View File
@@ -8,59 +8,60 @@ export default {
extend: {
colors: {
// ── AETHER DRIFT TOKENS ──
// RGB-channel variables enable Tailwind opacity modifiers (e.g. bg-accent-rose/10)
surface: {
base: 'var(--bg-base)',
channel: 'var(--bg-channel)',
chat: 'var(--bg-chat)',
members: 'var(--bg-members)',
elevated: 'var(--bg-elevated)',
input: 'var(--bg-input)',
base: 'rgb(var(--bg-base) / <alpha-value>)',
channel: 'rgb(var(--bg-channel) / <alpha-value>)',
chat: 'rgb(var(--bg-chat) / <alpha-value>)',
members: 'rgb(var(--bg-members) / <alpha-value>)',
elevated: 'rgb(var(--bg-elevated) / <alpha-value>)',
input: 'rgb(var(--bg-input) / <alpha-value>)',
overlay: 'var(--bg-overlay)',
},
border: {
hard: 'var(--border-hard)',
soft: 'var(--border-soft)',
hard: 'rgb(var(--border-hard) / <alpha-value>)',
soft: 'rgb(var(--border-soft) / <alpha-value>)',
},
accent: {
mint: 'var(--accent-mint)',
peach: 'var(--accent-peach)',
lavender: 'var(--accent-lavender)',
sky: 'var(--accent-sky)',
amber: 'var(--accent-amber)',
rose: 'var(--accent-rose)',
coral: 'var(--accent-coral)',
primary: 'var(--accent-primary)',
'primary-hover': 'var(--accent-primary-hover)',
'primary-active':'var(--accent-primary-active)',
mint: 'rgb(var(--accent-mint) / <alpha-value>)',
peach: 'rgb(var(--accent-peach) / <alpha-value>)',
lavender: 'rgb(var(--accent-lavender) / <alpha-value>)',
sky: 'rgb(var(--accent-sky) / <alpha-value>)',
amber: 'rgb(var(--accent-amber) / <alpha-value>)',
rose: 'rgb(var(--accent-rose) / <alpha-value>)',
coral: 'rgb(var(--accent-coral) / <alpha-value>)',
primary: 'rgb(var(--accent-primary) / <alpha-value>)',
'primary-hover': 'rgb(var(--accent-primary-hover) / <alpha-value>)',
'primary-active':'rgb(var(--accent-primary-active) / <alpha-value>)',
},
txt: {
primary: 'var(--text-primary)',
secondary: 'var(--text-secondary)',
tertiary: 'var(--text-tertiary)',
message: 'var(--text-message)',
link: 'var(--text-link)',
positive: 'var(--text-positive)',
warning: 'var(--text-warning)',
danger: 'var(--text-danger)',
primary: 'rgb(var(--text-primary) / <alpha-value>)',
secondary: 'rgb(var(--text-secondary) / <alpha-value>)',
tertiary: 'rgb(var(--text-tertiary) / <alpha-value>)',
message: 'rgb(var(--text-message) / <alpha-value>)',
link: 'rgb(var(--text-link) / <alpha-value>)',
positive: 'rgb(var(--text-positive) / <alpha-value>)',
warning: 'rgb(var(--text-warning) / <alpha-value>)',
danger: 'rgb(var(--text-danger) / <alpha-value>)',
},
interactive: {
hover: 'var(--interactive-hover)',
active: 'var(--interactive-active)',
selected: 'var(--interactive-selected)',
muted: 'var(--interactive-muted)',
muted: 'rgb(var(--interactive-muted) / <alpha-value>)',
},
status: {
online: 'var(--status-online)',
idle: 'var(--status-idle)',
dnd: 'var(--status-dnd)',
offline: 'var(--status-offline)',
online: 'rgb(var(--status-online) / <alpha-value>)',
idle: 'rgb(var(--status-idle) / <alpha-value>)',
dnd: 'rgb(var(--status-dnd) / <alpha-value>)',
offline: 'rgb(var(--status-offline) / <alpha-value>)',
},
glass: {
bg: 'var(--glass-bg)',
border: 'var(--glass-border)',
highlight: 'var(--glass-highlight)',
},
notification: 'var(--notification)',
notification: 'rgb(var(--notification) / <alpha-value>)',
},
boxShadow: {
'header': '0 1px 0 rgba(4, 4, 5, 0.2), 0 1.5px 0 rgba(6, 6, 7, 0.05), 0 2px 0 rgba(4, 4, 5, 0.05)',