fix: re-add missing header gradient styles (#1787)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Style**
* Enhanced header banner styling: centered, non-repeating cover images
with layered gradient overlays and adjusted user-profile banner
positioning for improved layout.
* **Bug Fixes**
* Banner display logic updated so "image" is treated like "yes" for
showing banner images.
* **Tests**
  * Added unit tests covering banner/theme display behavior.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Eli Bosley
2025-11-17 11:03:37 -05:00
committed by GitHub
parent d7aca81c60
commit f8a6785e9c
5 changed files with 105 additions and 4 deletions

View File

@@ -3,6 +3,7 @@ import { Test, TestingModule } from '@nestjs/testing';
import * as fs from 'fs/promises'; import * as fs from 'fs/promises';
import * as path from 'path'; import * as path from 'path';
import type { Mock } from 'vitest';
import { plainToInstance } from 'class-transformer'; import { plainToInstance } from 'class-transformer';
import * as ini from 'ini'; import * as ini from 'ini';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
@@ -1182,4 +1183,58 @@ describe('CustomizationService - updateCfgFile', () => {
writeError writeError
); );
}); });
describe('getTheme', () => {
const mockDynamix = getters.dynamix as unknown as Mock;
const baseDisplay = {
theme: 'white',
banner: '',
showBannerGradient: 'no',
background: '123456',
headerdescription: 'yes',
headermetacolor: '789abc',
header: 'abcdef',
};
const setDisplay = (overrides: Partial<typeof baseDisplay>) => {
mockDynamix.mockReturnValue({
display: {
...baseDisplay,
...overrides,
},
});
};
it('reports showBannerImage when banner is "image"', async () => {
setDisplay({ banner: 'image' });
const theme = await service.getTheme();
expect(theme.showBannerImage).toBe(true);
});
it('reports showBannerImage when banner is "yes"', async () => {
setDisplay({ banner: 'yes' });
const theme = await service.getTheme();
expect(theme.showBannerImage).toBe(true);
});
it('disables showBannerImage when banner is empty', async () => {
setDisplay({ banner: '' });
const theme = await service.getTheme();
expect(theme.showBannerImage).toBe(false);
});
it('mirrors showBannerGradient flag from display settings', async () => {
setDisplay({ banner: 'image', showBannerGradient: 'yes' });
expect((await service.getTheme()).showBannerGradient).toBe(true);
setDisplay({ banner: 'image', showBannerGradient: 'no' });
expect((await service.getTheme()).showBannerGradient).toBe(false);
});
});
}); });

View File

@@ -458,7 +458,7 @@ export class CustomizationService implements OnModuleInit {
return { return {
name, name,
showBannerImage: banner === 'yes', showBannerImage: banner === 'image' || banner === 'yes',
showBannerGradient: bannerGradient === 'yes', showBannerGradient: bannerGradient === 'yes',
headerBackgroundColor: this.addHashtoHexField(bgColor), headerBackgroundColor: this.addHashtoHexField(bgColor),
headerPrimaryTextColor: this.addHashtoHexField(textColor), headerPrimaryTextColor: this.addHashtoHexField(textColor),

View File

@@ -156,3 +156,42 @@ iframe#progressFrame {
background-color: var(--background-color); background-color: var(--background-color);
color-scheme: light; color-scheme: light;
} }
/* Header banner compatibility tweaks */
#header.image {
background-position: center center;
background-repeat: no-repeat;
background-size: cover;
}
.has-banner-gradient #header.image {
position: relative;
overflow: hidden;
}
.has-banner-gradient #header.image::before {
content: '';
position: absolute;
inset: 0;
pointer-events: none;
background-repeat: no-repeat;
background-position: left center, right center;
background-size: min(30%, 320px) 100%, min(30%, 320px) 100%;
background-image:
linear-gradient(
90deg,
var(--color-header-gradient-end, rgba(0, 0, 0, 0.7)) 0%,
var(--color-header-gradient-start, rgba(0, 0, 0, 0)) 100%
),
linear-gradient(
270deg,
var(--color-header-gradient-end, rgba(0, 0, 0, 0.7)) 0%,
var(--color-header-gradient-start, rgba(0, 0, 0, 0)) 100%
);
z-index: 0;
}
.has-banner-gradient #header.image > * {
position: relative;
z-index: 1;
}

View File

@@ -94,11 +94,11 @@ onMounted(() => {
<template> <template>
<div <div
id="UserProfile" id="UserProfile"
class="text-foreground absolute top-0 right-0 z-20 flex h-full flex-col gap-y-1 pt-2 pr-2" class="text-foreground absolute top-0 right-0 z-20 flex h-full max-w-full flex-col items-end gap-y-1 pt-2 pr-2"
> >
<div <div
v-if="bannerGradient" v-if="bannerGradient"
class="absolute top-0 right-0 bottom-0 z-0 w-full" class="pointer-events-none absolute inset-y-0 right-0 left-0 z-0 w-full"
:style="bannerGradient" :style="bannerGradient"
/> />

View File

@@ -211,6 +211,7 @@ export const useThemeStore = defineStore(
: 'var(--header-gradient-end)'; : 'var(--header-gradient-end)';
dynamicVars['--banner-gradient'] = `linear-gradient(90deg, ${start} 0, ${end} 90%)`; dynamicVars['--banner-gradient'] = `linear-gradient(90deg, ${start} 0, ${end} 90%)`;
customClasses.push('has-banner-gradient');
} }
requestAnimationFrame(() => { requestAnimationFrame(() => {
@@ -222,7 +223,13 @@ export const useThemeStore = defineStore(
const cleanClassList = (classList: string) => const cleanClassList = (classList: string) =>
classList classList
.split(' ') .split(' ')
.filter((c) => !c.startsWith('theme-') && c !== 'dark' && !c.startsWith('has-custom-')) .filter(
(c) =>
!c.startsWith('theme-') &&
c !== 'dark' &&
!c.startsWith('has-custom-') &&
c !== 'has-banner-gradient'
)
.filter(Boolean) .filter(Boolean)
.join(' '); .join(' ');