feat: centralize frontend branding
This commit is contained in:
63
frontend/src/branding/applyBranding.js
Normal file
63
frontend/src/branding/applyBranding.js
Normal 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})`;
|
||||
}
|
||||
50
frontend/src/branding/branding.js
Normal file
50
frontend/src/branding/branding.js
Normal 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,
|
||||
};
|
||||
}
|
||||
35
frontend/src/components/branding/BrandLogo.vue
Normal file
35
frontend/src/components/branding/BrandLogo.vue
Normal 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>
|
||||
26
frontend/src/components/branding/BrandMark.vue
Normal file
26
frontend/src/components/branding/BrandMark.vue
Normal 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>
|
||||
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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(),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user