feat: centralize frontend branding

This commit is contained in:
2026-05-06 14:27:09 -04:00
parent dc9a980958
commit 5c0e40db7e
14 changed files with 272 additions and 42 deletions

View File

@@ -0,0 +1,26 @@
# Task: Add global frontend branding configuration
## Goal
Centralize product branding for the frontend so product name, visible brand marks, brand assets, and theme colors can be changed from one module instead of being hardcoded across shell and auth surfaces.
## Relevant Files
- `frontend/src/branding/branding.js`
- `frontend/src/branding/applyBranding.js`
- `frontend/src/components/branding/BrandMark.vue`
- `frontend/src/components/branding/BrandLogo.vue`
- `frontend/src/main.js`
- `frontend/src/assets/main.css`
- `frontend/src/layouts/main/AppSidebar.vue`
- `frontend/src/static/components/LandingSiteMenu.vue`
- `frontend/src/features/auth/views/RegisterView.vue`
- `frontend/src/features/auth/views/LoginView.vue`
- `frontend/public/images/brand/*`
## Validation
```bash
cd frontend
npm run build
```

View File

@@ -0,0 +1,16 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 420 240" role="img" aria-labelledby="title">
<title id="title">Socialize brand illustration</title>
<defs>
<linearGradient id="brand-gradient" x1="102" y1="52" x2="324" y2="194" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#ff8a3d"/>
<stop offset="1" stop-color="#ef4444"/>
</linearGradient>
</defs>
<rect width="420" height="240" rx="36" fill="#fbfaf6"/>
<rect x="58" y="44" width="304" height="152" rx="30" fill="#f4f6f3" stroke="#c7d2cc"/>
<rect x="82" y="68" width="112" height="104" rx="28" fill="url(#brand-gradient)"/>
<path d="M113 137c5.7 5.8 13.2 8.8 22.5 8.8 10.4 0 16.3-3.4 16.3-9.4 0-5.2-4.4-7.4-16-9.1-15.2-2.3-24.2-7.8-24.2-20.2 0-12.1 10.4-20.3 25.5-20.3 10.9 0 19.2 3.3 25.4 10.1l-9.6 8.9c-4.2-4.3-9.4-6.5-15.7-6.5-6.9 0-10.7 2.7-10.7 7.2 0 4.4 4.4 6.4 15.2 8 15.9 2.4 24.6 8.5 24.6 21.1 0 13.6-11 22-29.4 22-12.8 0-23-4-30.2-12z" fill="#fffaf2"/>
<path d="M224 86h72" stroke="#172033" stroke-width="12" stroke-linecap="round"/>
<path d="M224 118h112" stroke="#2fa58d" stroke-width="12" stroke-linecap="round"/>
<path d="M224 150h84" stroke="#ff8a3d" stroke-width="12" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96" role="img" aria-labelledby="title">
<title id="title">Socialize mark</title>
<defs>
<linearGradient id="brand-gradient" x1="16" y1="12" x2="82" y2="88" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#ff8a3d"/>
<stop offset="1" stop-color="#ef4444"/>
</linearGradient>
</defs>
<rect width="96" height="96" rx="28" fill="url(#brand-gradient)"/>
<path d="M31 61.5c3.9 4.3 9.3 6.5 16.2 6.5 7.8 0 12.4-2.7 12.4-7.3 0-4.1-3.4-5.9-12.2-7.2-12.1-1.8-19-6.2-19-16.1 0-9.7 8.2-16.4 20.2-16.4 8.7 0 15.3 2.7 20.3 8.2l-8.3 8c-3.3-3.5-7.5-5.2-12.5-5.2-5.4 0-8.3 2.1-8.3 5.6 0 3.6 3.4 5.1 11.9 6.4 12.7 1.9 19.5 6.8 19.5 16.8 0 11-8.8 17.8-23.4 17.8-10.2 0-18.3-3.2-24.2-9.7z" fill="#fffaf2"/>
</svg>

After

Width:  |  Height:  |  Size: 791 B

View File

@@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 360 96" role="img" aria-labelledby="title">
<title id="title">Socialize</title>
<defs>
<linearGradient id="brand-gradient" x1="16" y1="12" x2="82" y2="88" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#ff8a3d"/>
<stop offset="1" stop-color="#ef4444"/>
</linearGradient>
</defs>
<rect width="96" height="96" rx="28" fill="url(#brand-gradient)"/>
<path d="M31 61.5c3.9 4.3 9.3 6.5 16.2 6.5 7.8 0 12.4-2.7 12.4-7.3 0-4.1-3.4-5.9-12.2-7.2-12.1-1.8-19-6.2-19-16.1 0-9.7 8.2-16.4 20.2-16.4 8.7 0 15.3 2.7 20.3 8.2l-8.3 8c-3.3-3.5-7.5-5.2-12.5-5.2-5.4 0-8.3 2.1-8.3 5.6 0 3.6 3.4 5.1 11.9 6.4 12.7 1.9 19.5 6.8 19.5 16.8 0 11-8.8 17.8-23.4 17.8-10.2 0-18.3-3.2-24.2-9.7z" fill="#fffaf2"/>
<text x="126" y="61" fill="#172033" font-family="Inter, Arial, sans-serif" font-size="35" font-weight="900" letter-spacing="6">SOCIALIZE</text>
</svg>

After

Width:  |  Height:  |  Size: 933 B

View File

@@ -0,0 +1,63 @@
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})`;
}

View File

@@ -0,0 +1,50 @@
export const branding = Object.freeze({
productName: 'Socialize',
shortName: 'S',
assets: {
logo: '/images/brand/logo.svg',
logoMark: '/images/brand/logo-mark.svg',
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,
};
}

View File

@@ -0,0 +1,35 @@
<template>
<span class="brand-logo-root">
<img
v-if="branding.assets.logo"
:src="branding.assets.logo"
:alt="branding.productName"
class="brand-logo-image"
/>
<span
v-else
class="brand-logo-text"
>
{{ branding.productName }}
</span>
</span>
</template>
<script setup>
import { branding } from '@/branding/branding.js';
</script>
<style scoped>
.brand-logo-root {
@apply inline-flex items-center;
}
.brand-logo-image {
@apply block h-auto max-h-full w-auto max-w-full;
}
.brand-logo-text {
@apply text-lg font-black uppercase tracking-[0.18em];
color: var(--h-primary);
}
</style>

View File

@@ -0,0 +1,26 @@
<template>
<span class="brand-mark-root">
<img
v-if="branding.assets.logoMark"
:src="branding.assets.logoMark"
alt=""
aria-hidden="true"
class="brand-mark-image"
/>
<span v-else>{{ branding.shortName }}</span>
</span>
</template>
<script setup>
import { branding } from '@/branding/branding.js';
</script>
<style scoped>
.brand-mark-root {
@apply inline-flex items-center justify-center overflow-hidden;
}
.brand-mark-image {
@apply h-full w-full object-contain;
}
</style>

View File

@@ -5,8 +5,7 @@
class="login-brand" class="login-brand"
to="/" to="/"
> >
<span class="login-brand-mark">S</span> <BrandLogo class="login-brand-logo" />
<span class="login-brand-text">Socialize</span>
</router-link> </router-link>
<section class="login-card"> <section class="login-card">
@@ -131,6 +130,7 @@
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { mdiEye, mdiEyeOff, mdiFacebook, mdiGoogle } from '@mdi/js'; import { mdiEye, mdiEyeOff, mdiFacebook, mdiGoogle } from '@mdi/js';
import BrandLogo from '@/components/branding/BrandLogo.vue';
const { t } = useI18n(); const { t } = useI18n();
const router = useRouter(); const router = useRouter();
@@ -212,18 +212,11 @@
} }
.login-brand { .login-brand {
@apply mx-auto flex items-center gap-3 px-4 pt-6 no-underline sm:px-0 sm:pt-0; @apply mx-auto flex items-center px-4 pt-6 no-underline sm:px-0 sm:pt-0;
color: #172033;
} }
.login-brand-mark { .login-brand-logo {
@apply flex h-11 w-11 items-center justify-center rounded-2xl text-lg font-black; @apply h-12 max-w-[180px];
background: var(--socialize-brand-gradient);
color: #fffaf2;
}
.login-brand-text {
@apply text-lg font-black uppercase tracking-[0.18em];
} }
.login-card { .login-card {

View File

@@ -7,7 +7,8 @@
> >
<img <img
:alt="t('alt')" :alt="t('alt')"
src="/images/hutopymedia/loginpage/hutopylogin.svg" :src="branding.assets.authIllustration"
class="auth-illustration"
/> />
<div class="flex flex-col gap-10 text-center"> <div class="flex flex-col gap-10 text-center">
<h1 class="login-text text-2xl font-bold text-green-600"> <h1 class="login-text text-2xl font-bold text-green-600">
@@ -41,7 +42,8 @@
> >
<img <img
:alt="t('alt')" :alt="t('alt')"
src="/images/hutopymedia/loginpage/hutopylogin.svg" :src="branding.assets.authIllustration"
class="auth-illustration"
/> />
<div class="flex flex-col gap-10"> <div class="flex flex-col gap-10">
<h1 class="login-text text-center text-2xl font-bold"> <h1 class="login-text text-center text-2xl font-bold">
@@ -134,6 +136,7 @@
import { useClient } from '@/plugins/api.js'; import { useClient } from '@/plugins/api.js';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { mdiEye, mdiEyeOff } from '@mdi/js'; import { mdiEye, mdiEyeOff } from '@mdi/js';
import { branding } from '@/branding/branding.js';
const { t } = useI18n(); const { t } = useI18n();
const clientApi = useClient(); const clientApi = useClient();
@@ -185,6 +188,10 @@
@apply z-10; @apply z-10;
} }
.auth-illustration {
@apply h-auto w-full max-w-xs;
}
/* Override Vuetify's default padding to accommodate our icon */ /* Override Vuetify's default padding to accommodate our icon */
:deep(.v-field__append-inner) { :deep(.v-field__append-inner) {
padding-inline-start: 0; padding-inline-start: 0;
@@ -195,7 +202,7 @@
{ {
"en": { "en": {
"title": "Create your account", "title": "Create your account",
"alt": "Hutopy Registration", "alt": "Socialize registration",
"name": "Full Name", "name": "Full Name",
"email": "Email", "email": "Email",
"password": "Password", "password": "Password",
@@ -215,7 +222,7 @@
}, },
"fr": { "fr": {
"title": "Créer votre compte", "title": "Créer votre compte",
"alt": "Inscription Hutopy", "alt": "Inscription Socialize",
"name": "Nom complet", "name": "Nom complet",
"email": "Email", "email": "Email",
"password": "Mot de passe", "password": "Mot de passe",

View File

@@ -8,6 +8,8 @@
import { getNotificationRoute } from '@/features/notifications/notificationRoutes.js'; import { getNotificationRoute } from '@/features/notifications/notificationRoutes.js';
import { useContentItemsStore } from '@/features/content/stores/contentItemsStore.js'; import { useContentItemsStore } from '@/features/content/stores/contentItemsStore.js';
import { useCampaignsStore } from '@/features/campaigns/stores/campaignsStore.js'; import { useCampaignsStore } from '@/features/campaigns/stores/campaignsStore.js';
import { branding } from '@/branding/branding.js';
import BrandMark from '@/components/branding/BrandMark.vue';
import SidebarUserMenu from './SidebarUserMenu.vue'; import SidebarUserMenu from './SidebarUserMenu.vue';
import { import {
mdiBellOutline, mdiBellOutline,
@@ -253,14 +255,14 @@
:class="{ 'brand-link-collapsed': !isExpanded }" :class="{ 'brand-link-collapsed': !isExpanded }"
to="/" to="/"
> >
<span class="brand-mark">S</span> <BrandMark class="brand-mark" />
<div <div
v-if="isExpanded" v-if="isExpanded"
class="brand-copy" class="brand-copy"
> >
<div class="brand-heading"> <div class="brand-heading">
<span class="brand-name-wrap"> <span class="brand-name-wrap">
<span class="brand-name">Socialize</span> <span class="brand-name">{{ branding.productName }}</span>
<span <span
class="brand-stage-badge" class="brand-stage-badge"
:aria-label="t('nav.brandStageLabel')" :aria-label="t('nav.brandStageLabel')"
@@ -664,9 +666,7 @@
} }
.brand-mark { .brand-mark {
@apply flex h-11 w-11 flex-shrink-0 items-center justify-center rounded-[1.1rem] text-xl font-black; @apply h-11 w-11 flex-shrink-0 rounded-[1.1rem];
background: var(--socialize-brand-gradient);
color: #fffaf2;
} }
.brand-name-wrap { .brand-name-wrap {
@@ -675,7 +675,7 @@
.brand-name { .brand-name {
@apply min-w-0 text-lg font-black uppercase tracking-[0.18em]; @apply min-w-0 text-lg font-black uppercase tracking-[0.18em];
color: #172033; color: var(--h-primary);
line-height: 2.75rem; line-height: 2.75rem;
} }

View File

@@ -35,6 +35,10 @@ import { useChannelsStore } from '@/features/channels/stores/channelsStore.js';
import { i18n } from '@/plugins/i18n.js'; import { i18n } from '@/plugins/i18n.js';
import config from '@/config.js'; import config from '@/config.js';
import { createHead } from '@vueuse/head'; import { createHead } from '@vueuse/head';
import { applyBranding } from '@/branding/applyBranding.js';
import { getVuetifyThemeColors } from '@/branding/branding.js';
applyBranding();
const vuetify = createVuetify({ const vuetify = createVuetify({
components: { components: {
@@ -62,17 +66,7 @@ const vuetify = createVuetify({
themes: { themes: {
socializeLight: { socializeLight: {
dark: false, dark: false,
colors: { colors: getVuetifyThemeColors(),
background: '#f4f6f3',
surface: '#fbfaf6',
primary: '#172033',
secondary: '#fff3e2',
accent: '#ff8a3d',
error: '#bc2f2f',
info: '#2563eb',
success: '#2fa58d',
warning: '#b45309',
},
}, },
}, },
}, },

View File

@@ -5,10 +5,10 @@
class="site-brand" class="site-brand"
to="/" to="/"
> >
<span class="site-brand-mark">S</span> <BrandMark class="site-brand-mark" />
<span class="site-brand-heading"> <span class="site-brand-heading">
<span class="site-brand-text-wrap"> <span class="site-brand-text-wrap">
<span class="site-brand-text">Socialize</span> <span class="site-brand-text">{{ branding.productName }}</span>
<span <span
class="site-brand-stage-badge" class="site-brand-stage-badge"
:aria-label="t('nav.brandStageLabel')" :aria-label="t('nav.brandStageLabel')"
@@ -136,6 +136,8 @@
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useAuthStore } from '@/features/auth/stores/authStore.js'; import { useAuthStore } from '@/features/auth/stores/authStore.js';
import { productFeatureItems } from '@/static/productFeatures.js'; import { productFeatureItems } from '@/static/productFeatures.js';
import { branding } from '@/branding/branding.js';
import BrandMark from '@/components/branding/BrandMark.vue';
const allowedLocales = ['en', 'fr']; const allowedLocales = ['en', 'fr'];
const localeStorageKey = 'user-locale'; const localeStorageKey = 'user-locale';
@@ -273,13 +275,11 @@
.site-brand { .site-brand {
@apply flex min-w-0 items-start gap-3 no-underline; @apply flex min-w-0 items-start gap-3 no-underline;
color: #172033; color: var(--h-primary);
} }
.site-brand-mark { .site-brand-mark {
@apply flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-2xl text-base font-black; @apply h-10 w-10 flex-shrink-0 rounded-2xl;
background: var(--socialize-brand-gradient);
color: #fffaf2;
} }
.site-brand-heading { .site-brand-heading {

View File

@@ -8,8 +8,6 @@ export default {
theme: { theme: {
extend: { extend: {
colors: { colors: {
hutopyPrimary: "var(--hutopy-primary)",
hutopySecondary: "var(--hutopy-secondary)",
hBackground: "var(--h-background)", hBackground: "var(--h-background)",
hOnBackground: "var(--h-on-background)", hOnBackground: "var(--h-on-background)",
hSurface: "var(--h-surface)", hSurface: "var(--h-surface)",
@@ -27,4 +25,3 @@ export default {
}, },
plugins: [] plugins: []
} }