feat: standardized page banners (#3610)

* feat: standardized site banners

* fix: lint issues

* fix: deduplicate SCSS with variant map

* feat: color shades + reduced scss

* feat: fix theming

* chore: Remove shades-generator.ts

* fix: lint issues

---------

Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
This commit is contained in:
Calum H. 2025-05-16 11:12:34 +01:00 committed by GitHub
parent f19643095e
commit e225bc9f66
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 224 additions and 126 deletions

View File

@ -162,6 +162,18 @@ html {
--landing-green-label-bg: rgba(0, 216, 69, 0.15);
--landing-raw-bg: #fff;
--banner-error-bg: #fee2e2;
--banner-error-text: #991b1b;
--banner-error-border: #ef4444;
--banner-warning-bg: #ffedd5;
--banner-warning-text: #713f12;
--banner-warning-border: #f97316;
--banner-info-bg: #dbeafe;
--banner-info-text: #1e3a8a;
--banner-info-border: #3b82f6;
}
.dark,
@ -286,6 +298,18 @@ html {
--hover-filter: brightness(120%);
--active-filter: brightness(140%);
--banner-error-bg: #4c1515;
--banner-error-text: #fee2e2;
--banner-error-border: #7f1d1d;
--banner-warning-bg: #4a2a0a;
--banner-warning-text: #ffe6c0;
--banner-warning-border: #b54708;
--banner-info-bg: #1e2a44;
--banner-info-text: #dbeafe;
--banner-info-border: #2563eb;
}
.oled-mode {

View File

@ -27,76 +27,90 @@
</div>
</div>
<div ref="main_page" class="layout" :class="{ 'expanded-mobile-nav': isBrowseMenuOpen }">
<div
<PagewideBanner
v-if="auth.user && !auth.user.email_verified && route.path !== '/auth/verify-email'"
class="email-nag"
variant="warning"
>
<template v-if="auth.user.email">
<span>{{ formatMessage(verifyEmailBannerMessages.title) }}</span>
<button class="btn" @click="resendVerifyEmail">
<template #title>
<span>
{{
auth?.user?.email
? formatMessage(verifyEmailBannerMessages.title)
: formatMessage(addEmailBannerMessages.title)
}}
</span>
</template>
<template #description>
<span>
{{
auth?.user?.email
? formatMessage(verifyEmailBannerMessages.description)
: formatMessage(addEmailBannerMessages.description)
}}
</span>
</template>
<template #actions>
<button v-if="auth?.user?.email" class="btn" @click="resendVerifyEmail">
{{ formatMessage(verifyEmailBannerMessages.action) }}
</button>
</template>
<template v-else>
<span>{{ formatMessage(addEmailBannerMessages.title) }}</span>
<nuxt-link class="btn" to="/settings/account">
<nuxt-link v-else class="btn" to="/settings/account">
<SettingsIcon aria-hidden="true" />
{{ formatMessage(addEmailBannerMessages.action) }}
</nuxt-link>
</template>
</div>
<div
</PagewideBanner>
<PagewideBanner
v-if="
user &&
user.subscriptions &&
user.subscriptions.some((x) => x.status === 'payment-failed') &&
route.path !== '/settings/billing'
"
class="email-nag"
variant="error"
>
<span>{{ formatMessage(subscriptionPaymentFailedBannerMessages.title) }}</span>
<nuxt-link class="btn" to="/settings/billing">
<SettingsIcon aria-hidden="true" />
{{ formatMessage(subscriptionPaymentFailedBannerMessages.action) }}
</nuxt-link>
</div>
<div
<template #title>
<span>{{ formatMessage(subscriptionPaymentFailedBannerMessages.title) }}</span>
</template>
<template #description>
<span>{{ formatMessage(subscriptionPaymentFailedBannerMessages.description) }}</span>
</template>
<template #actions>
<nuxt-link class="btn" to="/settings/billing">
<SettingsIcon aria-hidden="true" />
{{ formatMessage(subscriptionPaymentFailedBannerMessages.action) }}
</nuxt-link>
</template>
</PagewideBanner>
<PagewideBanner
v-if="
config.public.apiBaseUrl.startsWith('https://staging-api.modrinth.com') &&
!cosmetics.hideStagingBanner
"
class="site-banner site-banner--warning [&>*]:z-[6]"
variant="warning"
>
<div class="site-banner__title">
<IssuesIcon aria-hidden="true" />
<template #title>
<span>{{ formatMessage(stagingBannerMessages.title) }}</span>
</div>
<div class="site-banner__description">
</template>
<template #description>
{{ formatMessage(stagingBannerMessages.description) }}
</div>
<div class="site-banner__actions">
<Button transparent icon-only :action="hideStagingBanner" aria-label="Close banner"
><XIcon aria-hidden="true"
/></Button>
</div>
</div>
<div
v-if="generatedStateErrors && generatedStateErrors.length > 0"
class="site-banner site-banner--warning [&>*]:z-[6]"
>
<div class="site-banner__title">
<IssuesIcon aria-hidden="true" />
</template>
<template #actions_right>
<Button transparent icon-only aria-label="Close" @click="hideStagingBanner">
<XIcon aria-hidden="true" />
</Button>
</template>
</PagewideBanner>
<PagewideBanner v-if="generatedStateErrors?.length" variant="error">
<template #title>
<span>{{ formatMessage(failedToBuildBannerMessages.title) }}</span>
</div>
<div class="site-banner__description">
</template>
<template #description>
{{
formatMessage(failedToBuildBannerMessages.description, {
errors: generatedStateErrors,
url: config.public.apiBaseUrl,
})
}}
</div>
</div>
</template>
</PagewideBanner>
<header
class="experimental-styles-within desktop-only relative z-[5] mx-auto grid max-w-[1280px] grid-cols-[1fr_auto] items-center gap-2 px-6 py-4 lg:grid-cols-[auto_1fr_auto]"
>
@ -692,7 +706,14 @@ import {
GitHubIcon,
ScaleIcon,
} from "@modrinth/assets";
import { Button, ButtonStyled, OverflowMenu, Avatar, commonMessages } from "@modrinth/ui";
import {
Button,
ButtonStyled,
OverflowMenu,
PagewideBanner,
Avatar,
commonMessages,
} from "@modrinth/ui";
import { isAdmin, isStaff } from "@modrinth/utils";
import { errors as generatedStateErrors } from "~/generated/state.json";
@ -720,8 +741,13 @@ const basePopoutId = useId();
const verifyEmailBannerMessages = defineMessages({
title: {
id: "layout.banner.verify-email.title",
defaultMessage: "For security purposes, please verify your email address on Modrinth.",
id: "layout.banner.account-action",
defaultMessage: "Account action required",
},
description: {
id: "layout.banner.verify-email.description",
defaultMessage:
"For security reasons, Modrinth needs you to verify the email address associated with your account.",
},
action: {
id: "layout.banner.verify-email.action",
@ -731,8 +757,13 @@ const verifyEmailBannerMessages = defineMessages({
const addEmailBannerMessages = defineMessages({
title: {
id: "layout.banner.add-email.title",
defaultMessage: "For security purposes, please enter your email on Modrinth.",
id: "layout.banner.account-action",
defaultMessage: "Account action required",
},
description: {
id: "layout.banner.add-email.description",
defaultMessage:
"For security reasons, Modrinth needs you to register an email address to your account.",
},
action: {
id: "layout.banner.add-email.button",
@ -743,8 +774,12 @@ const addEmailBannerMessages = defineMessages({
const subscriptionPaymentFailedBannerMessages = defineMessages({
title: {
id: "layout.banner.subscription-payment-failed.title",
defaultMessage: "Billing action required.",
},
description: {
id: "layout.banner.subscription-payment-failed.description",
defaultMessage:
"Your subscription failed to renew. Please update your payment method to prevent losing access.",
"One or more subscriptions failed to renew. Please update your payment method to prevent losing access!",
},
action: {
id: "layout.banner.subscription-payment-failed.button",
@ -755,7 +790,7 @@ const subscriptionPaymentFailedBannerMessages = defineMessages({
const stagingBannerMessages = defineMessages({
title: {
id: "layout.banner.staging.title",
defaultMessage: "Youre viewing Modrinths staging environment.",
defaultMessage: "Youre viewing Modrinths staging environment",
},
description: {
id: "layout.banner.staging.description",
@ -1347,72 +1382,6 @@ const footerLinks = [
}
}
.email-nag {
z-index: 6;
position: relative;
background-color: var(--color-raised-bg);
width: 100%;
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
padding: 0.5rem 1rem;
}
.site-banner--warning {
// On some pages, there's gradient backgrounds that seep underneath
// the banner, so we need to add a solid color underlay.
background-color: black;
border-bottom: 2px solid var(--color-red);
display: grid;
gap: 0.5rem;
grid-template: "title actions" "description actions";
padding-block: var(--gap-xl);
padding-inline: max(calc((100% - 80rem) / 2 + var(--gap-md)), var(--gap-xl));
z-index: 4;
position: relative;
&::before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: var(--color-red-bg);
z-index: 5;
}
.site-banner__title {
grid-area: title;
display: flex;
gap: 0.5rem;
align-items: center;
font-weight: bold;
font-size: var(--font-size-md);
color: var(--color-contrast);
svg {
color: var(--color-red);
width: 1.5rem;
height: 1.5rem;
flex-shrink: 0;
}
}
.site-banner__description {
grid-area: description;
}
.site-banner__actions {
grid-area: actions;
}
a {
color: var(--color-red);
}
}
@media (max-width: 1200px) {
.app-btn {
display: none;

View File

@ -344,35 +344,38 @@
"layout.avatar.alt": {
"message": "Your avatar"
},
"layout.banner.account-action": {
"message": "Account action required"
},
"layout.banner.add-email.button": {
"message": "Visit account settings"
},
"layout.banner.add-email.title": {
"message": "For security purposes, please enter your email on Modrinth."
},
"layout.banner.build-fail.description": {
"message": "This deploy of Modrinth's frontend failed to generate state from the API. This may be due to an outage or an error in configuration. Rebuild when the API is available. Error codes: {errors}; Current API URL is: {url}"
},
"layout.banner.build-fail.title": {
"message": "Error generating state from API when building."
"message": "Error generating state from API when building"
},
"layout.banner.staging.description": {
"message": "The staging environment is completely separate from the production Modrinth database. This is used for testing and debugging purposes, and may be running in-development versions of the Modrinth backend or frontend newer than the production instance."
},
"layout.banner.staging.title": {
"message": "Youre viewing Modrinths staging environment."
"message": "Youre viewing Modrinths staging environment"
},
"layout.banner.subscription-payment-failed.button": {
"message": "Update billing info"
},
"layout.banner.subscription-payment-failed.title": {
"message": "Your subscription failed to renew. Please update your payment method to prevent losing access."
"message": "Billing action required"
},
"layout.banner.subscription-payment-failed.description": {
"message": "One or more subscriptions failed to renew. Please update your payment method to prevent losing access!"
},
"layout.banner.verify-email.action": {
"message": "Re-send verification email"
},
"layout.banner.verify-email.title": {
"message": "For security purposes, please verify your email address on Modrinth."
"layout.banner.verify-email.description": {
"message": "For security reasons, Modrinth needs you to verify the email address associated with your account."
},
"layout.footer.about": {
"message": "About"

View File

@ -1,5 +1,6 @@
/** @type {import('tailwindcss').Config} */
const config = {
import type { Config } from "tailwindcss";
const config: Config = {
content: [
"./src/components/**/*.{js,vue,ts}",
"./src/layouts/**/*.vue",
@ -36,6 +37,23 @@ const config = {
purple: "var(--color-purple-bg)",
raised: "var(--color-raised-bg)",
},
banners: {
error: {
bg: "var(--banner-error-bg)",
text: "var(--banner-error-text)",
border: "var(--banner-error-border)",
},
warning: {
bg: "var(--banner-warning-bg)",
text: "var(--banner-warning-text)",
border: "var(--banner-warning-border)",
},
info: {
bg: "var(--banner-info-bg)",
text: "var(--banner-info-text)",
border: "var(--banner-info-border)",
},
},
highlight: {
DEFAULT: "var(--color-brand-highlight)",
red: "var(--color-red-highlight)",
@ -126,6 +144,7 @@ const config = {
backgroundImage: {
mazeBg: "var(--landing-maze-bg)",
mazeGradientBg: "var(--landing-maze-gradient-bg)",
// @ts-ignore
landing: {
mazeOuterBg: "var(--landing-maze-outer-bg)",
colorHeading: "var(--landing-color-heading)",

View File

@ -71,6 +71,7 @@ export { default as Breadcrumbs } from './nav/Breadcrumbs.vue'
export { default as NavItem } from './nav/NavItem.vue'
export { default as NavRow } from './nav/NavRow.vue'
export { default as NavStack } from './nav/NavStack.vue'
export { default as PagewideBanner } from './nav/PagewideBanner.vue'
// Project
export { default as NewProjectCard } from './project/NewProjectCard.vue'

View File

@ -0,0 +1,82 @@
<template>
<div
:class="['banner-grid relative border-b-2 border-solid border-0', containerClasses[variant]]"
>
<div
:class="[
'grid-area-[title] flex items-center gap-2 font-bold text-[var(--font-size-md)]',
iconClasses[variant],
]"
>
<IssuesIcon
v-if="variant === 'warning' || variant === 'error'"
aria-hidden="true"
class="w-6 h-6 flex-shrink-0"
/>
<InfoIcon v-if="variant === 'info'" aria-hidden="true" class="w-6 h-6 flex-shrink-0" />
<slot name="title" />
</div>
<div class="grid-area-[description] flex flex-col gap-[var(--gap-md)]">
<slot name="description" />
</div>
<div v-if="$slots.actions" class="grid-area-[actions]">
<slot name="actions" />
</div>
<div v-if="$slots.actions_right" class="grid-area-[actions_right]">
<slot name="actions_right" />
</div>
</div>
</template>
<script lang="ts" setup>
import { InfoIcon, IssuesIcon } from '@modrinth/assets'
defineProps<{
variant: 'error' | 'warning' | 'info'
}>()
const containerClasses = {
error: 'bg-banners-error-bg text-banners-error-text border-banners-error-border',
warning: 'bg-banners-warning-bg text-banners-warning-text border-banners-warning-border',
info: 'bg-banners-info-bg text-banners-info-text border-banners-info-border',
}
const iconClasses = {
error: 'text-brand-red',
warning: 'text-brand-orange',
info: 'text-brand-blue',
}
</script>
<style scoped>
.banner-grid {
display: grid;
gap: 0.5rem;
grid-template-areas:
'title actions_right'
'description actions_right'
'actions actions_right';
padding-block: var(--gap-xl);
padding-inline: max(calc((100% - 80rem) / 2 + var(--gap-md)), var(--gap-xl));
}
.grid-area-\[title\] {
grid-area: title;
}
.grid-area-\[description\] {
grid-area: description;
}
.grid-area-\[actions\] {
grid-area: actions;
}
.grid-area-\[actions_right\] {
grid-area: actions_right;
}
.banner-grid a {
@apply underline text-current;
}
</style>