From 6ac05e1a101f5462497ade3bbc215580354e6007 Mon Sep 17 00:00:00 2001 From: Jonathan Bourdon Date: Thu, 7 May 2026 16:35:47 -0400 Subject: [PATCH] refactor: simplify frontend theme setup --- frontend/src/assets/main.css | 111 +++++------------- frontend/src/branding/applyBranding.js | 63 ---------- frontend/src/branding/branding.js | 40 ------- .../src/components/branding/BrandLogo.vue | 2 +- .../components/FeedbackFloatingButton.vue | 10 +- frontend/src/layouts/main/AppSidebar.vue | 2 +- frontend/src/main.js | 10 +- frontend/src/plugins/theme.js | 29 +++++ .../src/static/components/LandingSiteMenu.vue | 2 +- 9 files changed, 70 insertions(+), 199 deletions(-) delete mode 100644 frontend/src/branding/applyBranding.js create mode 100644 frontend/src/plugins/theme.js diff --git a/frontend/src/assets/main.css b/frontend/src/assets/main.css index 822c34f9..7e7cebdd 100644 --- a/frontend/src/assets/main.css +++ b/frontend/src/assets/main.css @@ -2,80 +2,25 @@ @custom-variant dark (&:where(.dark, .dark *)); @theme inline { - --color-hBackground: var(--h-background); - --color-hOnBackground: var(--h-on-background); - --color-hSurface: var(--h-surface); - --color-hOnSurface: var(--h-on-surface); - --color-hPrimary: var(--h-primary); - --color-hOnPrimary: var(--h-on-primary); - --color-hSecondary: var(--h-secondary); - --color-hOnSecondary: var(--h-on-secondary); - --color-hTertiary: var(--h-tertiary); - --color-hOnTertiary: var(--h-on-tertiary); - --color-hError: var(--h-error); - --color-hOnError: var(--h-on-error); -} - -:root { - --socialize-primary: #172033; - --socialize-accent: #ff8a3d; - --socialize-accent-strong: #ef4444; - --socialize-brand-gradient: linear-gradient(135deg, var(--socialize-accent) 0%, var(--socialize-accent-strong) 100%); - --socialize-accent-shadow: rgba(255, 138, 61, 0.28); - --socialize-accent-strong-shadow: rgba(239, 68, 68, 0.28); - --socialize-highlight: #2fa58d; - --h-background: #f4f6f3; - --h-on-background: #172033; - --h-surface: #fbfaf6; - --h-surface-muted: #f1f5f2; - --h-on-surface: #172033; - --h-control: #eef3ef; - --h-control-hover: #e7eee9; - --h-control-focus: #ffffff; - --h-border: #c7d2cc; - --h-border-strong: #94a39d; - --h-primary: #172033; - --h-on-primary: #fbfaf6; - --h-secondary: #fff3e2; - --h-on-secondary: #172033; - --h-tertiary: #d9f6ee; - --h-on-tertiary: #0f766e; - --h-error: #bc2f2f; - --h-on-error: #ffffff; + --color-hBackground: rgb(var(--v-theme-background)); + --color-hOnBackground: rgb(var(--v-theme-on-background)); + --color-hSurface: rgb(var(--v-theme-surface)); + --color-hOnSurface: rgb(var(--v-theme-on-surface)); + --color-hPrimary: rgb(var(--v-theme-primary)); + --color-hOnPrimary: rgb(var(--v-theme-on-primary)); + --color-hSecondary: rgb(var(--v-theme-secondary)); + --color-hOnSecondary: rgb(var(--v-theme-on-secondary)); + --color-hTertiary: rgb(var(--v-theme-tertiary)); + --color-hOnTertiary: rgb(var(--v-theme-on-tertiary)); + --color-hError: rgb(var(--v-theme-error)); + --color-hOnError: rgb(var(--v-theme-on-error)); } html, body, #app { min-height: 100%; - background: var(--h-background); -} - -input:not([type='checkbox']):not([type='radio']):not([type='range']):not([type='file']), -select, -textarea { - background-color: var(--h-control) !important; - border-color: var(--h-border) !important; - color: var(--h-on-surface); -} - -input:not([type='checkbox']):not([type='radio']):not([type='range']):not([type='file']):hover, -select:hover, -textarea:hover { - background-color: var(--h-control-hover) !important; - border-color: var(--h-border-strong) !important; -} - -input:not([type='checkbox']):not([type='radio']):not([type='range']):not([type='file']):focus, -select:focus, -textarea:focus, -input:not([type='checkbox']):not([type='radio']):not([type='range']):not([type='file']):focus-visible, -select:focus-visible, -textarea:focus-visible { - background-color: var(--h-control-focus) !important; - border-color: var(--socialize-highlight) !important; - box-shadow: 0 0 0 3px rgba(47, 165, 141, 0.16); - outline: none; + background: #f4f6f3; } input::placeholder, @@ -85,8 +30,8 @@ textarea::placeholder { } .v-application { - background: var(--h-background) !important; - color: var(--h-on-background); + background: rgb(var(--v-theme-background)) !important; + color: rgb(var(--v-theme-on-background)); } .v-card, @@ -94,41 +39,47 @@ textarea::placeholder { .v-list, .v-menu > .v-overlay__content, .v-dialog > .v-overlay__content { - background-color: var(--h-surface) !important; - border: 1px solid var(--h-border); + background-color: rgb(var(--v-theme-surface)) !important; + border: 1px solid rgb(var(--v-theme-border)); } .v-field { - background-color: var(--h-control) !important; - color: var(--h-on-surface); + background-color: rgb(var(--v-theme-control)) !important; + color: rgb(var(--v-theme-on-surface)); } .v-field:hover { - background-color: var(--h-control-hover) !important; + background-color: rgb(var(--v-theme-control-hover)) !important; } .v-field--focused { - background-color: var(--h-control-focus) !important; + background-color: rgb(var(--v-theme-control-focus)) !important; } .v-field__outline { - color: var(--h-border-strong); + color: rgb(var(--v-theme-border-strong)); } .v-field--focused .v-field__outline { - color: var(--socialize-highlight); + color: rgb(var(--v-theme-highlight)); } .v-field__input, .v-field-label { - color: var(--h-on-surface); + color: rgb(var(--v-theme-on-surface)); +} + +.v-select .v-field .v-field__input > input, +.v-select .v-field .v-field__input > input::placeholder { + color: transparent !important; + caret-color: transparent; } .panel, [class$='-panel'], [class$='-card'], div.card { - border-color: var(--h-border) !important; + border-color: rgb(var(--v-theme-border)) !important; } @layer components { diff --git a/frontend/src/branding/applyBranding.js b/frontend/src/branding/applyBranding.js deleted file mode 100644 index 8825d0a6..00000000 --- a/frontend/src/branding/applyBranding.js +++ /dev/null @@ -1,63 +0,0 @@ -import { branding } from './branding.js'; - -const cssVariableMap = { - '--socialize-primary': 'primary', - '--socialize-accent': 'accent', - '--socialize-accent-strong': 'accentStrong', - '--socialize-highlight': 'highlight', - '--h-background': 'background', - '--h-on-background': 'onBackground', - '--h-surface': 'surface', - '--h-surface-muted': 'surfaceMuted', - '--h-on-surface': 'onSurface', - '--h-control': 'control', - '--h-control-hover': 'controlHover', - '--h-control-focus': 'controlFocus', - '--h-border': 'border', - '--h-border-strong': 'borderStrong', - '--h-primary': 'primary', - '--h-on-primary': 'onPrimary', - '--h-secondary': 'secondary', - '--h-on-secondary': 'onSecondary', - '--h-tertiary': 'tertiary', - '--h-on-tertiary': 'onTertiary', - '--h-error': 'error', - '--h-on-error': 'onError', -}; - -export function applyBranding(target = getDefaultTarget()) { - if (!target) { - return; - } - - Object.entries(cssVariableMap).forEach(([variableName, colorKey]) => { - target.style.setProperty(variableName, branding.colors[colorKey]); - }); - - target.style.setProperty( - '--socialize-brand-gradient', - `linear-gradient(135deg, ${branding.colors.accent} 0%, ${branding.colors.accentStrong} 100%)` - ); - target.style.setProperty('--socialize-accent-shadow', getRgbShadow(branding.colors.accent, 0.28)); - target.style.setProperty('--socialize-accent-strong-shadow', getRgbShadow(branding.colors.accentStrong, 0.28)); -} - -function getDefaultTarget() { - return typeof document === 'undefined' - ? null - : document.documentElement; -} - -function getRgbShadow(hexColor, opacity) { - const normalizedHex = hexColor.replace('#', ''); - - if (normalizedHex.length !== 6) { - return hexColor; - } - - const red = Number.parseInt(normalizedHex.slice(0, 2), 16); - const green = Number.parseInt(normalizedHex.slice(2, 4), 16); - const blue = Number.parseInt(normalizedHex.slice(4, 6), 16); - - return `rgba(${red}, ${green}, ${blue}, ${opacity})`; -} diff --git a/frontend/src/branding/branding.js b/frontend/src/branding/branding.js index ca6d5528..62de3ce8 100644 --- a/frontend/src/branding/branding.js +++ b/frontend/src/branding/branding.js @@ -7,44 +7,4 @@ export const branding = Object.freeze({ authIllustration: '/images/brand/auth-illustration.svg', favicon: '/favicon.ico', }, - colors: { - background: '#f4f6f3', - onBackground: '#172033', - surface: '#fbfaf6', - surfaceMuted: '#f1f5f2', - onSurface: '#172033', - control: '#eef3ef', - controlHover: '#e7eee9', - controlFocus: '#ffffff', - border: '#c7d2cc', - borderStrong: '#94a39d', - primary: '#172033', - onPrimary: '#fbfaf6', - secondary: '#fff3e2', - onSecondary: '#172033', - tertiary: '#d9f6ee', - onTertiary: '#0f766e', - accent: '#ff8a3d', - accentStrong: '#ef4444', - highlight: '#2fa58d', - error: '#bc2f2f', - onError: '#ffffff', - info: '#2563eb', - success: '#2fa58d', - warning: '#b45309', - }, }); - -export function getVuetifyThemeColors() { - return { - background: branding.colors.background, - surface: branding.colors.surface, - primary: branding.colors.primary, - secondary: branding.colors.secondary, - accent: branding.colors.accent, - error: branding.colors.error, - info: branding.colors.info, - success: branding.colors.success, - warning: branding.colors.warning, - }; -} diff --git a/frontend/src/components/branding/BrandLogo.vue b/frontend/src/components/branding/BrandLogo.vue index 2dde5c40..6bf320a8 100644 --- a/frontend/src/components/branding/BrandLogo.vue +++ b/frontend/src/components/branding/BrandLogo.vue @@ -31,6 +31,6 @@ .brand-logo-text { @apply text-lg font-black uppercase tracking-[0.18em]; - color: var(--h-primary); + color: rgb(var(--v-theme-primary)); } diff --git a/frontend/src/features/feedback/components/FeedbackFloatingButton.vue b/frontend/src/features/feedback/components/FeedbackFloatingButton.vue index 07b4ee0a..59166bbe 100644 --- a/frontend/src/features/feedback/components/FeedbackFloatingButton.vue +++ b/frontend/src/features/feedback/components/FeedbackFloatingButton.vue @@ -36,19 +36,19 @@ .feedback-entry-button { @apply flex h-12 items-center gap-2 rounded-full border px-4 text-sm font-bold shadow-lg transition-colors; - background: var(--socialize-accent-strong); + background: rgb(var(--v-theme-accent-strong)); border-color: rgba(255, 255, 255, 0.55); color: #ffffff; - box-shadow: 0 16px 34px var(--socialize-accent-strong-shadow); + box-shadow: 0 16px 34px rgb(var(--v-theme-accent-strong) / 0.28); } .feedback-entry-button:hover { - background: color-mix(in srgb, var(--socialize-accent-strong) 82%, var(--socialize-primary)); - box-shadow: 0 18px 38px var(--socialize-accent-strong-shadow); + background: color-mix(in srgb, rgb(var(--v-theme-accent-strong)) 82%, rgb(var(--v-theme-primary))); + box-shadow: 0 18px 38px rgb(var(--v-theme-accent-strong) / 0.28); } .feedback-entry-button:focus-visible { - outline: 3px solid color-mix(in srgb, var(--socialize-accent) 35%, transparent); + outline: 3px solid color-mix(in srgb, rgb(var(--v-theme-accent)) 35%, transparent); outline-offset: 3px; } diff --git a/frontend/src/layouts/main/AppSidebar.vue b/frontend/src/layouts/main/AppSidebar.vue index 50586870..80c0cb93 100644 --- a/frontend/src/layouts/main/AppSidebar.vue +++ b/frontend/src/layouts/main/AppSidebar.vue @@ -676,7 +676,7 @@ .brand-name { @apply min-w-0 text-lg font-black uppercase tracking-[0.18em]; - color: var(--h-primary); + color: rgb(var(--v-theme-primary)); line-height: 2.75rem; } diff --git a/frontend/src/main.js b/frontend/src/main.js index 973b55bc..312f6dac 100644 --- a/frontend/src/main.js +++ b/frontend/src/main.js @@ -35,10 +35,7 @@ import { useChannelsStore } from '@/features/channels/stores/channelsStore.js'; import { i18n } from '@/plugins/i18n.js'; import config from '@/config.js'; import { createHead } from '@vueuse/head'; -import { applyBranding } from '@/branding/applyBranding.js'; -import { getVuetifyThemeColors } from '@/branding/branding.js'; - -applyBranding(); +import { socializeTheme } from '@/plugins/theme.js'; const vuetify = createVuetify({ components: { @@ -64,10 +61,7 @@ const vuetify = createVuetify({ theme: { defaultTheme: 'socializeLight', themes: { - socializeLight: { - dark: false, - colors: getVuetifyThemeColors(), - }, + socializeLight: socializeTheme, }, }, }); diff --git a/frontend/src/plugins/theme.js b/frontend/src/plugins/theme.js new file mode 100644 index 00000000..6969810d --- /dev/null +++ b/frontend/src/plugins/theme.js @@ -0,0 +1,29 @@ +export const socializeTheme = { + dark: false, + colors: { + background: '#f4f6f3', + 'on-background': '#172033', + surface: '#fbfaf6', + 'surface-muted': '#f1f5f2', + 'on-surface': '#172033', + control: '#eef3ef', + 'control-hover': '#e7eee9', + 'control-focus': '#ffffff', + border: '#c7d2cc', + 'border-strong': '#94a39d', + primary: '#172033', + 'on-primary': '#fbfaf6', + secondary: '#fff3e2', + 'on-secondary': '#172033', + tertiary: '#d9f6ee', + 'on-tertiary': '#0f766e', + accent: '#ff8a3d', + 'accent-strong': '#ef4444', + highlight: '#2fa58d', + error: '#bc2f2f', + 'on-error': '#ffffff', + info: '#2563eb', + success: '#2fa58d', + warning: '#b45309', + }, +}; diff --git a/frontend/src/static/components/LandingSiteMenu.vue b/frontend/src/static/components/LandingSiteMenu.vue index 9284b48e..19a7517e 100644 --- a/frontend/src/static/components/LandingSiteMenu.vue +++ b/frontend/src/static/components/LandingSiteMenu.vue @@ -276,7 +276,7 @@ .site-brand { @apply flex min-w-0 items-start gap-3 no-underline; - color: var(--h-primary); + color: rgb(var(--v-theme-primary)); } .site-brand-mark {