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

View File

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

View File

@@ -8,6 +8,8 @@
import { getNotificationRoute } from '@/features/notifications/notificationRoutes.js';
import { useContentItemsStore } from '@/features/content/stores/contentItemsStore.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 {
mdiBellOutline,
@@ -253,14 +255,14 @@
:class="{ 'brand-link-collapsed': !isExpanded }"
to="/"
>
<span class="brand-mark">S</span>
<BrandMark class="brand-mark" />
<div
v-if="isExpanded"
class="brand-copy"
>
<div class="brand-heading">
<span class="brand-name-wrap">
<span class="brand-name">Socialize</span>
<span class="brand-name">{{ branding.productName }}</span>
<span
class="brand-stage-badge"
:aria-label="t('nav.brandStageLabel')"
@@ -664,9 +666,7 @@
}
.brand-mark {
@apply flex h-11 w-11 flex-shrink-0 items-center justify-center rounded-[1.1rem] text-xl font-black;
background: var(--socialize-brand-gradient);
color: #fffaf2;
@apply h-11 w-11 flex-shrink-0 rounded-[1.1rem];
}
.brand-name-wrap {
@@ -675,7 +675,7 @@
.brand-name {
@apply min-w-0 text-lg font-black uppercase tracking-[0.18em];
color: #172033;
color: var(--h-primary);
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 config from '@/config.js';
import { createHead } from '@vueuse/head';
import { applyBranding } from '@/branding/applyBranding.js';
import { getVuetifyThemeColors } from '@/branding/branding.js';
applyBranding();
const vuetify = createVuetify({
components: {
@@ -62,17 +66,7 @@ const vuetify = createVuetify({
themes: {
socializeLight: {
dark: false,
colors: {
background: '#f4f6f3',
surface: '#fbfaf6',
primary: '#172033',
secondary: '#fff3e2',
accent: '#ff8a3d',
error: '#bc2f2f',
info: '#2563eb',
success: '#2fa58d',
warning: '#b45309',
},
colors: getVuetifyThemeColors(),
},
},
},

View File

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

View File

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