From 6dc3d3b21ab45e967a4ecd4984ad78067dbeacfa Mon Sep 17 00:00:00 2001 From: Marco Beretta <81851188+berry-13@users.noreply.github.com> Date: Tue, 23 Jun 2026 09:41:35 +0200 Subject: [PATCH] refactor: migrate theme tokens to RGB channels for opacity support Convert semantic + palette CSS variable values in style.css from hex to bare 'R G B' channel triplets, and emit Tailwind colors as rgb(var(--token) / ) via createTailwindColors. This makes opacity modifiers (bg-surface-primary/50, bg-border-medium/60, etc.) resolve correctly and remain dark-aware, fixing ~26 existing usages that previously fell back to a hardcoded light hex. - Wrap direct var(--token) color usages in CSS rules as rgb(var(--token)) (style.css, Dropdown.css, Tooltip.css) and two inline component styles - applyTheme writes bare triplets to match the new wrapping - shadcn tokens (HSL) and the JS palette (hex) are unchanged --- .../Account/TwoFactorAuthentication.tsx | 4 +- .../Skills/tree/SkillFilePreview.tsx | 2 +- client/src/style.css | 238 +++++++++--------- packages/client/src/components/Dropdown.css | 14 +- packages/client/src/components/Tooltip.css | 4 +- packages/client/src/theme/utils/applyTheme.ts | 7 +- .../src/theme/utils/createTailwindColors.js | 15 +- 7 files changed, 143 insertions(+), 141 deletions(-) diff --git a/client/src/components/Nav/SettingsTabs/Account/TwoFactorAuthentication.tsx b/client/src/components/Nav/SettingsTabs/Account/TwoFactorAuthentication.tsx index 638b2a2743..df860b2b7f 100644 --- a/client/src/components/Nav/SettingsTabs/Account/TwoFactorAuthentication.tsx +++ b/client/src/components/Nav/SettingsTabs/Account/TwoFactorAuthentication.tsx @@ -238,7 +238,9 @@ const TwoFactorAuthentication: React.FC = () => { key={step} animate={{ color: - currentStep >= index ? 'var(--text-primary)' : 'var(--text-tertiary)', + currentStep >= index + ? 'rgb(var(--text-primary))' + : 'rgb(var(--text-tertiary))', }} className="font-medium" > diff --git a/client/src/components/Skills/tree/SkillFilePreview.tsx b/client/src/components/Skills/tree/SkillFilePreview.tsx index 285e2c9414..43868bcdd4 100644 --- a/client/src/components/Skills/tree/SkillFilePreview.tsx +++ b/client/src/components/Skills/tree/SkillFilePreview.tsx @@ -65,7 +65,7 @@ export default function SkillFilePreview({ skillId, nodeId, fileName }: SkillFil className={cn( 'flex flex-1 items-center justify-center overflow-auto p-8', isImage && - 'bg-[repeating-conic-gradient(var(--surface-tertiary)_0%_25%,transparent_0%_50%)] bg-[length:16px_16px]', + 'bg-[repeating-conic-gradient(rgb(var(--surface-tertiary))_0%_25%,transparent_0%_50%)] bg-[length:16px_16px]', )} > {isImage && downloadUrl ? ( diff --git a/client/src/style.css b/client/src/style.css index a54d087f9b..c57e4cfea4 100644 --- a/client/src/style.css +++ b/client/src/style.css @@ -8,68 +8,68 @@ /* Custom Variables */ :root { - --white: #fff; - --black: #000; - --gray-20: #ececf1; - --gray-50: #f7f7f8; - --gray-100: #ececec; - --gray-200: #e3e3e3; - --gray-300: #cdcdcd; - --gray-400: #999696; - --gray-500: #595959; - --gray-600: #424242; - --gray-700: #2f2f2f; - --gray-800: #212121; - --gray-850: #171717; - --gray-900: #0d0d0d; - --green-50: #ecfdf5; - --green-100: #d1fae5; - --green-200: #a7f3d0; - --green-300: #6ee7b7; - --green-400: #34d399; - --green-500: #10b981; - --green-600: #059669; - --green-700: #047857; - --green-800: #065f46; - --green-900: #064e3b; - --green-950: #022c22; - --red-50: #fef2f2; - --red-100: #fee2e2; - --red-200: #fecaca; - --red-300: #fca5a5; - --red-400: #f87171; - --red-500: #ef4444; - --red-600: #dc2626; - --red-700: #b91c1c; - --red-800: #991b1b; - --red-900: #7f1d1d; - --red-950: #450a0a; - --amber-50: #fffbeb; - --amber-100: #fef3c7; - --amber-200: #fde68a; - --amber-300: #fcd34d; - --amber-400: #fbbf24; - --amber-500: #f59e0b; - --amber-600: #d97706; - --amber-700: #b45309; - --amber-800: #92400e; - --amber-900: #78350f; - --amber-950: #451a03; - --blue-50: #eff6ff; - --blue-100: #dbeafe; - --blue-200: #bfdbfe; - --blue-300: #93c5fd; - --blue-400: #60a5fa; - --blue-500: #3b82f6; - --blue-600: #2563eb; - --blue-700: #1d4ed8; - --blue-800: #1e40af; - --blue-900: #1e3a8a; - --blue-950: #172554; - --brand-purple: #ab68ff; - --gizmo-gray-500: #999; - --gizmo-gray-600: #666; - --gizmo-gray-950: #0f0f0f; + --white: 255 255 255; + --black: 0 0 0; + --gray-20: 236 236 241; + --gray-50: 247 247 248; + --gray-100: 236 236 236; + --gray-200: 227 227 227; + --gray-300: 205 205 205; + --gray-400: 153 150 150; + --gray-500: 89 89 89; + --gray-600: 66 66 66; + --gray-700: 47 47 47; + --gray-800: 33 33 33; + --gray-850: 23 23 23; + --gray-900: 13 13 13; + --green-50: 236 253 245; + --green-100: 209 250 229; + --green-200: 167 243 208; + --green-300: 110 231 183; + --green-400: 52 211 153; + --green-500: 16 185 129; + --green-600: 5 150 105; + --green-700: 4 120 87; + --green-800: 6 95 70; + --green-900: 6 78 59; + --green-950: 2 44 34; + --red-50: 254 242 242; + --red-100: 254 226 226; + --red-200: 254 202 202; + --red-300: 252 165 165; + --red-400: 248 113 113; + --red-500: 239 68 68; + --red-600: 220 38 38; + --red-700: 185 28 28; + --red-800: 153 27 27; + --red-900: 127 29 29; + --red-950: 69 10 10; + --amber-50: 255 251 235; + --amber-100: 254 243 199; + --amber-200: 253 230 138; + --amber-300: 252 211 77; + --amber-400: 251 191 36; + --amber-500: 245 158 11; + --amber-600: 217 119 6; + --amber-700: 180 83 9; + --amber-800: 146 64 14; + --amber-900: 120 53 15; + --amber-950: 69 26 3; + --blue-50: 239 246 255; + --blue-100: 219 234 254; + --blue-200: 191 219 254; + --blue-300: 147 197 253; + --blue-400: 96 165 250; + --blue-500: 59 130 246; + --blue-600: 37 99 235; + --blue-700: 29 78 216; + --blue-800: 30 64 175; + --blue-900: 30 58 138; + --blue-950: 23 37 84; + --brand-purple: 171 104 255; + --gizmo-gray-500: 153 153 153; + --gizmo-gray-600: 102 102 102; + --gizmo-gray-950: 15 15 15; --font-size-xs: 0.75rem; --font-size-sm: 0.875rem; --font-size-base: 1rem; @@ -145,7 +145,7 @@ } html { - --brand-purple: #ab68ff; + --brand-purple: 171 104 255; --presentation: var(--white); --text-primary: var(--gray-800); --text-secondary: var(--gray-600); @@ -155,9 +155,9 @@ html { --text-destructive: var(--red-600); --link: var(--blue-600); --link-hover: var(--blue-700); - --link-visited: #9333ea; - --accent-primary: #126e6b; - --accent-primary-hover: #0a4f53; + --link-visited: 147 51 234; + --accent-primary: 18 110 107; + --accent-primary-hover: 10 79 83; --ring-primary: var(--gray-500); --header-primary: var(--white); --header-hover: var(--gray-50); @@ -221,7 +221,7 @@ html { --switch-unchecked: 0 0% 58%; } .dark { - --brand-purple: #ab68ff; + --brand-purple: 171 104 255; --presentation: var(--gray-800); --text-primary: var(--gray-100); --text-secondary: var(--gray-300); @@ -231,9 +231,9 @@ html { --text-destructive: var(--red-600); --link: var(--blue-400); --link-hover: var(--blue-300); - --link-visited: #c084fc; - --accent-primary: #41a79d; - --accent-primary-hover: #6dc8b9; + --link-visited: 192 132 252; + --accent-primary: 65 167 157; + --accent-primary-hover: 109 200 185; --header-primary: var(--gray-700); --header-hover: var(--gray-600); --header-button-hover: var(--gray-700); @@ -301,10 +301,10 @@ html { --surface-primary: var(--white); --surface-secondary: var(--gray-50); --surface-tertiary: var(--gray-100); - --border-light: rgba(0, 0, 0, 0.1); - --border-medium: rgba(0, 0, 0, 0.15); - --border-heavy: rgba(0, 0, 0, 0.2); - --border-xheavy: rgba(0, 0, 0, 0.25); + --border-light: 0 0 0; + --border-medium: 0 0 0; + --border-heavy: 0 0 0; + --border-xheavy: 0 0 0; } .gizmo.dark { --text-primary: var(--gray-100); @@ -313,23 +313,23 @@ html { --surface-primary: var(--gray-900); --surface-secondary: var(--gray-800); --surface-tertiary: var(--gray-700); - --border-light: rgba(217, 217, 227, 0.1); - --border-medium: rgba(217, 217, 227, 0.15); - --border-heavy: rgba(217, 217, 227, 0.2); - --border-xheavy: rgba(217, 217, 227, 0.25); + --border-light: 217 217 227; + --border-medium: 217 217 227; + --border-heavy: 217 217 227; + --border-xheavy: 217 217 227; } .text-token-text-primary { color: #171717; - color: var(--text-primary); + color: rgb(var(--text-primary)); } .text-token-text-secondary { color: #565869; - color: var(--text-secondary); + color: rgb(var(--text-secondary)); } .text-token-text-tertiary { color: #8e8ea0; - color: var(--text-tertiary); + color: rgb(var(--text-tertiary)); } .icon-sm { @@ -363,16 +363,16 @@ html { } .border-token-border-light { border-color: #ececf1; - border-color: var(--border-light); + border-color: rgb(var(--border-light)); } .border-token-border-medium { border-color: #d9d9e3; - border-color: var(--border-medium); + border-color: rgb(var(--border-medium)); } .bg-token-surface-secondary { - background-color: var(--surface-secondary); + background-color: rgb(var(--surface-secondary)); } .ellipsis { @@ -782,12 +782,12 @@ pre { color: var(--tw-prose-code); font-size: 0.875em; font-weight: 600; - background-color: var(--gray-200); + background-color: rgb(var(--gray-200)); padding: 0.125rem 0.25rem; border-radius: 0.35rem; } .dark .prose :where(code):not(:where([class~='not-prose'] *)):not(:where(pre *)) { - background-color: var(--gray-600); + background-color: rgb(var(--gray-600)); } .prose :where(a code):not(:where([class~='not-prose'] *)) { color: inherit; @@ -1030,7 +1030,7 @@ code[class*='language-'], pre[class*='language-'] { word-wrap: normal; background: none; - color: var(--gray-800); + color: rgb(var(--gray-800)); -webkit-hyphens: none; hyphens: none; font-size: 0.85rem; @@ -1052,10 +1052,10 @@ pre[class*='language-'] { white-space: normal; } .hljs-comment { - color: var(--gray-500); + color: rgb(var(--gray-500)); } .hljs-meta { - color: var(--gray-600); + color: rgb(var(--gray-600)); } .hljs-built_in, .hljs-class .hljs-title { @@ -1233,7 +1233,7 @@ button { .btn-neutral { --tw-bg-opacity: 1; --tw-text-opacity: 1; - background-color: var(--surface-secondary); + background-color: rgb(var(--surface-secondary)); border-color: rgba(0, 0, 0, 0.1); border-width: 1px; color: rgba(64, 65, 79, var(--tw-text-opacity)); @@ -1249,7 +1249,7 @@ button { .btn-neutral:focus { outline: none; - box-shadow: 0 0 0 2px var(--ring-primary); + box-shadow: 0 0 0 2px rgb(var(--ring-primary)); } .dark .btn-neutral { @@ -1267,7 +1267,7 @@ button { } .dark .btn-neutral:focus { - box-shadow: 0 0 0 2px var(--ring-primary); + box-shadow: 0 0 0 2px rgb(var(--ring-primary)); } .btn-small { padding: 0.25rem 0.5rem; @@ -1298,7 +1298,7 @@ button { overflow-x: auto; overscroll-behavior-x: contain; padding-bottom: 0.25rem; - scrollbar-color: var(--border-medium) transparent; + scrollbar-color: rgb(var(--border-medium)) transparent; scrollbar-width: thin; } @@ -1307,11 +1307,11 @@ button { } .markdown-table-wrapper::-webkit-scrollbar-thumb { - background-color: var(--border-medium); + background-color: rgb(var(--border-medium)); } .markdown-table-wrapper::-webkit-scrollbar-thumb:hover { - background-color: var(--border-heavy); + background-color: rgb(var(--border-heavy)); } /* Show scrollbar only on hover */ @@ -1321,7 +1321,7 @@ button { } .scrollbar-hover:hover { - scrollbar-color: var(--border-medium) transparent; + scrollbar-color: rgb(var(--border-medium)) transparent; } .scrollbar-hover::-webkit-scrollbar-thumb { @@ -1330,7 +1330,7 @@ button { } .scrollbar-hover:hover::-webkit-scrollbar-thumb { - background-color: var(--border-medium); + background-color: rgb(var(--border-medium)); transition-delay: 0s; } @@ -1393,7 +1393,7 @@ html { .markdown blockquote { --tw-border-opacity: 1; - border-color: var(--gray-400); + border-color: rgb(var(--gray-400)); border-left-width: 2px; line-height: 1rem; padding-left: 1rem; @@ -1563,13 +1563,13 @@ html { } .markdown blockquote { --tw-border-opacity: 1; - border-color: var(--gray-300); + border-color: rgb(var(--gray-300)); border-left-width: 2px; line-height: 1rem; padding-left: 1rem; } .dark .markdown blockquote { - border-color: var(--gray-600); + border-color: rgb(var(--gray-600)); } .markdown table { --tw-border-spacing-x: 0px; @@ -1577,20 +1577,20 @@ html { border-collapse: separate; border-spacing: var(--tw-border-spacing-x) var(--tw-border-spacing-y); width: 100%; - border-color: var(--gray-300); + border-color: rgb(var(--gray-300)); } .markdown th { - background-color: var(--gray-100); + background-color: rgb(var(--gray-100)); border-bottom-width: 1px; border-left-width: 1px; border-top-width: 1px; - border-color: var(--gray-300); + border-color: rgb(var(--gray-300)); padding: 0.25rem 0.75rem; font-weight: 600; } .dark .markdown th { - border-color: var(--gray-600); - background-color: var(--gray-600); + border-color: rgb(var(--gray-600)); + background-color: rgb(var(--gray-600)); } .markdown th:first-child { border-top-left-radius: 0.75rem; @@ -1602,18 +1602,18 @@ html { .markdown td { border-bottom-width: 1px; border-left-width: 1px; - border-color: var(--gray-300); + border-color: rgb(var(--gray-300)); padding: 0.25rem 0.75rem; } .markdown td:last-child { border-right-width: 1px; - border-color: var(--gray-300); + border-color: rgb(var(--gray-300)); } .dark .markdown td { - border-color: var(--gray-600); + border-color: rgb(var(--gray-600)); } .dark .markdown td:last-child { - border-color: var(--gray-600); + border-color: rgb(var(--gray-600)); } .markdown tbody tr:last-child td:first-child { border-bottom-left-radius: 0.75rem; @@ -1731,7 +1731,7 @@ html { -webkit-backface-visibility: hidden; backface-visibility: hidden; background-color: #0d0d0d; - background-color: var(--text-primary); + background-color: rgb(var(--text-primary)); border-radius: 50%; box-sizing: border-box; content: ' '; @@ -2152,7 +2152,7 @@ html { z-index: 150; cursor: pointer; border-radius: 0.275rem; - background-color: var(--surface-primary); + background-color: rgb(var(--surface-primary)); padding-top: 0.25rem; padding-bottom: 0.25rem; padding-left: 0.5rem; @@ -2164,7 +2164,7 @@ html { } .tooltip:where(.dark, .dark *) { - background-color: var(--surface-primary); + background-color: rgb(var(--surface-primary)); color: white; box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.35); } @@ -2192,10 +2192,10 @@ html { border-radius: 0.7rem; border-width: 1px; border-style: solid; - border-color: var(--border-light); - background-color: var(--surface-primary); + border-color: rgb(var(--border-light)); + background-color: rgb(var(--surface-primary)); padding: 0.5rem; - color: var(--text-primary); + color: rgb(var(--text-primary)); box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); @@ -2228,8 +2228,8 @@ html { } .popover-ui:where(.dark, .dark *) { - background-color: var(--surface-secondary); - color: var(--text-secondary); + background-color: rgb(var(--surface-secondary)); + color: rgb(var(--text-secondary)); box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.25), 0 4px 6px -4px rgb(0 0 0 / 0.1); @@ -2251,8 +2251,8 @@ html { } .select-item[data-active-item] { - background-color: var(--surface-hover); - color: var(--text-primary); + background-color: rgb(var(--surface-hover)); + color: rgb(var(--text-primary)); } .popover-ui[data-enter] { diff --git a/packages/client/src/components/Dropdown.css b/packages/client/src/components/Dropdown.css index 9b3026f7e5..684536966a 100644 --- a/packages/client/src/components/Dropdown.css +++ b/packages/client/src/components/Dropdown.css @@ -7,10 +7,10 @@ border-radius: 1rem; border-width: 1px; border-style: solid; - border-color: var(--border-light); - background-color: var(--surface-primary); + border-color: rgb(var(--border-light)); + background-color: rgb(var(--surface-primary)); padding: 0.5rem; - color: var(--text-primary); + color: rgb(var(--text-primary)); box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); @@ -43,8 +43,8 @@ } .popover-ui:where(.dark, .dark *) { - background-color: var(--surface-secondary); - color: var(--text-secondary); + background-color: rgb(var(--surface-secondary)); + color: rgb(var(--text-secondary)); box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.25), 0 4px 6px -4px rgb(0 0 0 / 0.1); @@ -66,8 +66,8 @@ } .select-item[data-active-item] { - background-color: var(--surface-hover); - color: var(--text-primary); + background-color: rgb(var(--surface-hover)); + color: rgb(var(--text-primary)); } .popover-ui[data-enter] { diff --git a/packages/client/src/components/Tooltip.css b/packages/client/src/components/Tooltip.css index 7d885bc90c..81b83e7788 100644 --- a/packages/client/src/components/Tooltip.css +++ b/packages/client/src/components/Tooltip.css @@ -3,7 +3,7 @@ cursor: pointer; pointer-events: auto; border-radius: 0.275rem; - background-color: var(--surface-primary); + background-color: rgb(var(--surface-primary)); padding-top: 0.25rem; padding-bottom: 0.25rem; padding-left: 0.5rem; @@ -25,7 +25,7 @@ } .tooltip:where(.dark, .dark *) { - background-color: var(--surface-primary); + background-color: rgb(var(--surface-primary)); color: white; box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.35); } diff --git a/packages/client/src/theme/utils/applyTheme.ts b/packages/client/src/theme/utils/applyTheme.ts index 93f95443e8..e0a890daa3 100644 --- a/packages/client/src/theme/utils/applyTheme.ts +++ b/packages/client/src/theme/utils/applyTheme.ts @@ -102,8 +102,9 @@ export default function applyTheme(themeRGB?: IThemeRGB): void { return; } - // Set the CSS variable as rgb() value for compatibility - // This ensures existing CSS that expects color values (not space-separated RGB) continues to work - root.style.setProperty(cssVar, `rgb(${value})`); + // Store the bare `R G B` channel triplet. The Tailwind color map wraps these + // as `rgb(var(--x) / )`, so storing raw channels is what enables + // opacity modifiers and matches the style.css defaults. + root.style.setProperty(cssVar, value); }); } diff --git a/packages/client/src/theme/utils/createTailwindColors.js b/packages/client/src/theme/utils/createTailwindColors.js index 2f014201fa..368768576a 100644 --- a/packages/client/src/theme/utils/createTailwindColors.js +++ b/packages/client/src/theme/utils/createTailwindColors.js @@ -8,13 +8,12 @@ * - packages/client/tailwind.config.js * * Semantic tokens resolve to CSS custom properties defined in - * client/src/style.css (`html`, `.dark`, `.gizmo` blocks). shadcn-compatible - * tokens hold HSL triplets, so they are wrapped in `hsl(...)`. - * - * Opacity modifiers (e.g. `bg-surface-primary/50`) are intentionally not - * supported yet: the underlying variables hold hex/`rgb()` values rather than - * bare channels. Migrating the variables to `R G B` triplets and switching to - * `rgb(var(--x) / )` is tracked as a follow-up. + * client/src/style.css (`html`, `.dark`, `.gizmo` blocks). Those variables hold + * bare `R G B` channel triplets, so `cssVar` wraps them as + * `rgb(var(--x) / )`. This makes opacity modifiers work, e.g. + * `bg-surface-primary/50`. shadcn-compatible tokens still hold HSL triplets, so + * they are wrapped in `hsl(...)`; any direct `var(--token)` usage in plain CSS + * must wrap the channel triplet itself, e.g. `color: rgb(var(--text-primary))`. */ const palette = { @@ -47,7 +46,7 @@ const palette = { }, }; -const cssVar = (name) => `var(${name})`; +const cssVar = (name) => `rgb(var(${name}) / )`; const hslVar = (name) => `hsl(var(${name}))`; /**