dark mode
Some checks failed
Angular Build / build (push) Has been cancelled

This commit is contained in:
2026-04-27 23:42:53 +02:00
parent 5dffcf8cd2
commit 8b3647b023
8 changed files with 372 additions and 23 deletions

View File

@@ -22,7 +22,9 @@
"base": "dist/wgenerator"
},
"index": "src/index.html",
"polyfills": ["src/polyfills.ts"],
"polyfills": [
"src/polyfills.ts"
],
"tsConfig": "tsconfig.app.json",
"inlineStyleLanguage": "less",
"assets": [
@@ -38,9 +40,17 @@
"src/assets",
"src/manifest.webmanifest"
],
"styles": ["src/custom-theme.scss", "src/styles/styles.less", "src/styles/shadow.less"],
"styles": [
"src/custom-theme.scss",
"src/styles/styles.less",
"src/styles/shadow.less"
],
"scripts": [],
"allowedCommonJsDependencies": ["lodash", "docx", "qrcode"]
"allowedCommonJsDependencies": [
"lodash",
"docx",
"qrcode"
]
},
"configurations": {
"production": {
@@ -58,8 +68,8 @@
},
{
"type": "anyComponentStyle",
"maximumWarning": "4kB",
"maximumError": "8kB"
"maximumWarning": "40kB",
"maximumError": "80kB"
}
],
"outputHashing": "all"
@@ -89,14 +99,19 @@
"options": {
"runner": "vitest",
"tsConfig": "tsconfig.spec.json",
"setupFiles": ["src/test-vitest.ts"],
"setupFiles": [
"src/test-vitest.ts"
],
"runnerConfig": true
}
},
"lint": {
"builder": "@angular-eslint/builder:lint",
"options": {
"lintFilePatterns": ["src/**/*.ts", "src/**/*.html"]
"lintFilePatterns": [
"src/**/*.ts",
"src/**/*.html"
]
}
}
}

View File

@@ -1,7 +1,8 @@
import {ChangeDetectionStrategy, Component, OnInit} from '@angular/core';
import {ChangeDetectionStrategy, Component, OnInit, inject} from '@angular/core';
import {fader} from './animations';
import {RouterOutlet} from '@angular/router';
import {NavigationComponent} from './widget-modules/components/application-frame/navigation/navigation.component';
import {ThemeService} from './services/theme/theme.service';
@Component({
selector: 'app-root',
@@ -12,6 +13,10 @@ import {NavigationComponent} from './widget-modules/components/application-frame
imports: [RouterOutlet, NavigationComponent],
})
export class AppComponent implements OnInit {
public constructor() {
inject(ThemeService);
}
public ngOnInit(): void {
setTimeout(() => document.querySelector('#load-bg')?.classList.add('hidden'), 1000);
setTimeout(() => document.querySelector('#load-bg')?.remove(), 5000);

View File

@@ -0,0 +1,48 @@
import {DOCUMENT} from '@angular/common';
import {Injectable, inject, signal} from '@angular/core';
type ThemeMode = 'light' | 'dark';
@Injectable({providedIn: 'root'})
export class ThemeService {
private readonly document = inject(DOCUMENT);
private readonly storageKey = 'wgenerator-theme';
public readonly theme = signal<ThemeMode>('light');
public readonly isDarkMode = signal(false);
public constructor() {
this.initializeTheme();
}
public toggleTheme(): void {
this.setTheme(this.isDarkMode() ? 'light' : 'dark');
}
public setTheme(theme: ThemeMode): void {
this.theme.set(theme);
this.isDarkMode.set(theme === 'dark');
this.applyTheme(theme);
if (typeof localStorage !== 'undefined') {
localStorage.setItem(this.storageKey, theme);
}
}
private initializeTheme(): void {
const storedTheme = typeof localStorage !== 'undefined' ? localStorage.getItem(this.storageKey) : null;
if (storedTheme === 'light' || storedTheme === 'dark') {
this.setTheme(storedTheme);
return;
}
const prefersDarkMode = typeof window !== 'undefined'
&& typeof window.matchMedia === 'function'
&& window.matchMedia('(prefers-color-scheme: dark)').matches;
this.setTheme(prefersDarkMode ? 'dark' : 'light');
}
private applyTheme(theme: ThemeMode): void {
this.document.body.classList.toggle('theme-dark', theme === 'dark');
this.document.documentElement.style.colorScheme = theme;
}
}

View File

@@ -6,7 +6,17 @@
<app-link *appRole="['presenter']" [icon]="faPresentation" link="/presentation" text="Präsentation"></app-link>
<app-link [icon]="faUser" link="/user" text="Benutzer"></app-link>
</div>
<div *appRole="['user', 'presenter', 'leader']" class="actions">
<div class="actions">
<button
mat-icon-button
class="theme-toggle"
[attr.aria-label]="themeService.isDarkMode() ? 'Lightmode aktivieren' : 'Darkmode aktivieren'"
[attr.title]="themeService.isDarkMode() ? 'Zum Lightmode wechseln' : 'Zum Darkmode wechseln'"
(click)="themeService.toggleTheme()">
<fa-icon [icon]="themeService.isDarkMode() ? faLightMode : faDarkMode"></fa-icon>
</button>
</div>
<div *appRole="['user', 'presenter', 'leader']" class="actions actions-search">
<app-filter></app-filter>
</div>
</nav>

View File

@@ -32,9 +32,25 @@ nav {
display: flex;
height: 100%;
align-items: center;
padding-right: 12px;
}
.actions-search {
padding-right: 20px;
}
.theme-toggle {
color: var(--text-inverse);
transition: var(--transition-fast);
&:hover {
background: var(--hover-background);
}
}
.theme-toggle fa-icon {
font-size: 14px;
}
.links {
display: flex;

View File

@@ -1,5 +1,7 @@
import {Component} from '@angular/core';
import {faChalkboard, faMusic, faPersonBooth, faUserCog} from '@fortawesome/free-solid-svg-icons';
import {Component, inject} from '@angular/core';
import {MatIconButton} from '@angular/material/button';
import {FaIconComponent} from '@fortawesome/angular-fontawesome';
import {faChalkboard, faMoon, faMusic, faPersonBooth, faSun, faUserCog} from '@fortawesome/free-solid-svg-icons';
import {fromEvent, Observable} from 'rxjs';
import {distinctUntilChanged, map, shareReplay, startWith} from 'rxjs/operators';
import {BrandComponent} from './brand/brand.component';
@@ -8,18 +10,22 @@ import {RoleDirective} from '../../../../services/user/role.directive';
import {LinkComponent} from './link/link.component';
import {FilterComponent} from './filter/filter.component';
import {AsyncPipe} from '@angular/common';
import {ThemeService} from '../../../../services/theme/theme.service';
@Component({
selector: 'app-navigation',
templateUrl: './navigation.component.html',
styleUrls: ['./navigation.component.less'],
imports: [BrandComponent, RouterLink, RoleDirective, LinkComponent, FilterComponent, AsyncPipe],
imports: [BrandComponent, RouterLink, RoleDirective, LinkComponent, FilterComponent, AsyncPipe, MatIconButton, FaIconComponent],
})
export class NavigationComponent {
public readonly themeService = inject(ThemeService);
public faSongs = faMusic;
public faShows = faPersonBooth;
public faUser = faUserCog;
public faPresentation = faChalkboard;
public faDarkMode = faMoon;
public faLightMode = faSun;
public readonly windowScroll$: Observable<number> = fromEvent(window, 'scroll').pipe(
map(() => window.scrollY),

View File

@@ -5,18 +5,123 @@
@include mat.elevation-classes();
@include mat.app-background();
$wgenerator-primary: mat.m2-define-palette(mat.$m2-indigo-palette);
$wgenerator-accent: mat.m2-define-palette(mat.$m2-pink-palette, A200, A100, A400);
$wgenerator-warn: mat.m2-define-palette(mat.$m2-red-palette);
$wgenerator-theme: mat.m2-define-light-theme((
$wgenerator-material-theme: mat.define-theme((
color: (
primary: $wgenerator-primary,
accent: $wgenerator-accent,
warn: $wgenerator-warn,
theme-type: light,
primary: mat.$azure-palette,
tertiary: mat.$blue-palette,
use-system-variables: true,
),
typography: (
use-system-variables: true,
),
density: (
scale: -2,
),
typography: mat.m2-define-typography-config(),
density: -2,
));
@include mat.all-component-themes($wgenerator-theme);
@include mat.all-component-themes($wgenerator-material-theme);
:root {
@include mat.theme((
color: (
theme-type: light,
primary: mat.$azure-palette,
tertiary: mat.$blue-palette,
),
typography: Roboto,
density: -2,
));
@include mat.theme-overrides((
background: var(--surface),
error: var(--danger),
error-container: color-mix(in srgb, var(--danger) 18%, var(--surface-strong)),
inverse-on-surface: var(--text-inverse),
inverse-primary: var(--primary-hover),
inverse-surface: var(--bg-deep),
on-background: var(--text),
on-error: var(--text-inverse),
on-error-container: var(--text),
on-primary: var(--text-inverse),
on-primary-container: var(--text),
on-secondary: var(--text-inverse),
on-secondary-container: var(--text),
on-surface: var(--text),
on-surface-variant: var(--text-soft),
on-tertiary: var(--text-inverse),
on-tertiary-container: var(--text),
outline: var(--surface-border),
outline-variant: var(--divider),
primary: var(--primary-color),
primary-container: color-mix(in srgb, var(--primary-color) 22%, var(--surface-strong)),
secondary: var(--primary-hover),
secondary-container: color-mix(in srgb, var(--primary-hover) 20%, var(--surface-strong)),
scrim: rgba(18, 24, 37, 0.62),
shadow: rgba(18, 24, 37, 0.28),
surface: var(--surface),
surface-bright: var(--surface-strong),
surface-container: color-mix(in srgb, var(--surface) 88%, white),
surface-container-high: color-mix(in srgb, var(--surface-strong) 92%, var(--primary-hover)),
surface-container-highest: color-mix(in srgb, var(--surface-strong) 84%, var(--primary-hover)),
surface-container-low: color-mix(in srgb, var(--surface) 96%, white),
surface-container-lowest: var(--surface-strong),
surface-dim: color-mix(in srgb, var(--surface) 78%, var(--surface-dark)),
surface-tint: var(--primary-color),
surface-variant: color-mix(in srgb, var(--surface) 72%, var(--primary-hover)),
tertiary: var(--accent-color),
tertiary-container: color-mix(in srgb, var(--accent-color) 18%, var(--surface-strong)),
));
}
body.theme-dark {
@include mat.theme((
color: (
theme-type: dark,
primary: mat.$azure-palette,
tertiary: mat.$blue-palette,
),
typography: Roboto,
density: -2,
));
@include mat.theme-overrides((
background: var(--surface-dark),
error: var(--danger),
error-container: color-mix(in srgb, var(--danger) 24%, rgba(255, 255, 255, 0.18)),
inverse-on-surface: var(--text),
inverse-primary: var(--primary-color),
inverse-surface: var(--text-inverse),
on-background: var(--text),
on-error: var(--text-inverse),
on-error-container: var(--text),
on-primary: #07111b,
on-primary-container: var(--text),
on-secondary: #07111b,
on-secondary-container: var(--text),
on-surface: var(--text),
on-surface-variant: color-mix(in srgb, var(--text) 78%, white),
on-tertiary: #07111b,
on-tertiary-container: var(--text),
outline: rgba(206, 223, 229, 0.34),
outline-variant: rgba(206, 223, 229, 0.22),
primary: var(--primary-hover),
primary-container: color-mix(in srgb, var(--primary-color) 36%, rgba(255, 255, 255, 0.18)),
secondary: color-mix(in srgb, var(--primary-hover) 92%, white),
secondary-container: color-mix(in srgb, var(--primary-active) 32%, rgba(255, 255, 255, 0.18)),
scrim: rgba(4, 10, 18, 0.82),
shadow: rgba(0, 0, 0, 0.45),
surface: rgba(36, 54, 72, 0.96),
surface-bright: rgba(46, 68, 89, 0.98),
surface-container: rgba(41, 61, 80, 0.97),
surface-container-high: rgba(48, 71, 93, 0.98),
surface-container-highest: rgba(57, 82, 106, 0.98),
surface-container-low: rgba(32, 48, 63, 0.94),
surface-container-lowest: rgba(25, 38, 50, 0.94),
surface-dim: rgba(20, 31, 42, 0.92),
surface-tint: var(--primary-hover),
surface-variant: rgba(52, 75, 96, 0.95),
tertiary: var(--accent-color),
tertiary-container: color-mix(in srgb, var(--accent-color) 26%, rgba(255, 255, 255, 0.18)),
));
}

View File

@@ -50,6 +50,150 @@
--gap-s: calc(var(--gap-m) / 1.618);
}
body.theme-dark {
--bg-deep: #07111b;
--bg-mid: #102435;
--bg-soft: #1d3d4a;
--surface: rgba(13, 22, 34, 0.82);
--surface-strong: rgba(18, 31, 46, 0.94);
--surface-dark: rgba(4, 10, 18, 0.88);
--surface-border: rgba(167, 198, 206, 0.24);
--surface-subtle: rgba(255, 255, 255, 0.06);
--surface-muted: rgba(167, 198, 206, 0.08);
--text: #e8f0f4;
--text-soft: #b7c9cf;
--text-inverse: #f7fbff;
--color-primary-dark: #89acb5;
--color-primary: #7ea2ab;
--color-primary-light: #a7c6ce;
--primary-color-sat: #73bbc7;
--primary-color: #7ea2ab;
--primary-hover: #a7c6ce;
--primary-active: #638892;
--accent-color: #8ce3ca;
--navigation-background: rgba(4, 10, 18, 0.82);
--hover-background: rgba(126, 162, 171, 0.18);
--overlay: rgba(4, 10, 18, 0.68);
--overlay-strong: rgba(4, 10, 18, 0.9);
--divider: rgba(232, 240, 244, 0.12);
--link-color: var(--primary-hover);
--focus-ring: 0 0 0 2px rgba(126, 162, 171, 0.34);
--icon-button-color: var(--primary-hover);
--icon-button-hover-color: var(--text-inverse);
--mat-button-text-label-text-color: var(--text);
--mat-option-label-text-color: var(--text);
--mat-option-selected-state-label-text-color: var(--primary-hover);
--mat-optgroup-label-text-color: var(--text-soft);
--mat-select-enabled-trigger-text-color: var(--text);
--mat-select-disabled-trigger-text-color: rgba(232, 240, 244, 0.38);
--mat-select-placeholder-text-color: var(--text-soft);
--mat-select-enabled-arrow-color: var(--text-soft);
--mat-select-focused-arrow-color: var(--primary-hover);
--mat-form-field-enabled-select-arrow-color: var(--text-soft);
--mat-form-field-focus-select-arrow-color: var(--primary-hover);
--mat-form-field-outlined-input-text-color: var(--text);
--mat-form-field-outlined-label-text-color: var(--text-soft);
--mat-form-field-outlined-hover-label-text-color: var(--text);
--mat-form-field-outlined-focus-label-text-color: var(--primary-hover);
--mat-form-field-outlined-outline-color: rgba(206, 223, 229, 0.22);
--mat-form-field-outlined-hover-outline-color: rgba(206, 223, 229, 0.42);
--mat-form-field-outlined-focus-outline-color: var(--primary-hover);
--mat-form-field-outlined-caret-color: var(--primary-hover);
--mat-form-field-filled-input-text-color: var(--text);
--mat-form-field-filled-label-text-color: var(--text-soft);
--mat-form-field-filled-hover-label-text-color: var(--text);
--mat-form-field-filled-focus-label-text-color: var(--primary-hover);
--mat-form-field-filled-container-color: rgba(36, 54, 72, 0.96);
--mat-form-field-filled-active-indicator-color: rgba(206, 223, 229, 0.28);
--mat-form-field-filled-hover-active-indicator-color: rgba(206, 223, 229, 0.48);
--mat-form-field-filled-focus-active-indicator-color: var(--primary-hover);
--mat-form-field-input-text-placeholder-color: var(--text-soft);
--mat-form-field-filled-input-text-placeholder-color: var(--text-soft);
--mat-form-field-outlined-input-text-placeholder-color: var(--text-soft);
--mat-dialog-container-color: rgba(41, 61, 80, 0.98);
--mat-dialog-subhead-color: var(--text);
--mat-dialog-supporting-text-color: var(--text);
--mat-select-panel-background-color: rgba(41, 61, 80, 0.98);
--mat-autocomplete-background-color: rgba(41, 61, 80, 0.98);
--mat-menu-container-color: rgba(41, 61, 80, 0.98);
--mat-menu-item-label-text-color: var(--text);
--mat-menu-item-icon-color: var(--text-soft);
--mat-menu-item-hover-state-layer-color: rgba(255, 255, 255, 0.08);
--mat-menu-item-focus-state-layer-color: rgba(255, 255, 255, 0.12);
--mat-menu-divider-color: rgba(206, 223, 229, 0.16);
}
body.theme-dark .mat-mdc-text-field-wrapper {
background-color: rgba(36, 54, 72, 0.96);
}
body.theme-dark .mdc-notched-outline__leading,
body.theme-dark .mdc-notched-outline__notch,
body.theme-dark .mdc-notched-outline__trailing {
border-color: rgba(206, 223, 229, 0.22) !important;
}
body.theme-dark .mat-mdc-form-field:hover .mdc-notched-outline__leading,
body.theme-dark .mat-mdc-form-field:hover .mdc-notched-outline__notch,
body.theme-dark .mat-mdc-form-field:hover .mdc-notched-outline__trailing {
border-color: rgba(206, 223, 229, 0.42) !important;
}
body.theme-dark .mat-mdc-form-field.mat-focused .mdc-notched-outline__leading,
body.theme-dark .mat-mdc-form-field.mat-focused .mdc-notched-outline__notch,
body.theme-dark .mat-mdc-form-field.mat-focused .mdc-notched-outline__trailing {
border-color: var(--primary-hover) !important;
}
body.theme-dark .mat-mdc-input-element,
body.theme-dark .mat-mdc-select-value,
body.theme-dark .mat-mdc-select-min-line {
color: var(--text) !important;
}
body.theme-dark .mat-mdc-form-field .mat-mdc-floating-label,
body.theme-dark .mat-mdc-form-field .mat-mdc-select-placeholder {
color: var(--text-soft) !important;
}
body.theme-dark .mat-mdc-select-arrow svg {
fill: var(--text-soft) !important;
}
body.theme-dark .cdk-overlay-container .mat-mdc-select-panel,
body.theme-dark .cdk-overlay-container .mat-mdc-autocomplete-panel,
body.theme-dark .cdk-overlay-container .mat-mdc-menu-panel {
background: rgba(41, 61, 80, 0.98) !important;
color: var(--text) !important;
}
body.theme-dark .cdk-overlay-container .mat-mdc-select-panel,
body.theme-dark .cdk-overlay-container .mat-mdc-autocomplete-panel {
--mat-select-panel-background-color: rgba(41, 61, 80, 0.98);
--mat-autocomplete-background-color: rgba(41, 61, 80, 0.98);
}
body.theme-dark .cdk-overlay-container .mat-mdc-option .mdc-list-item__primary-text,
body.theme-dark .cdk-overlay-container .mat-mdc-option .mat-pseudo-checkbox,
body.theme-dark .cdk-overlay-container .mat-mdc-menu-item .mat-mdc-menu-item-text {
color: var(--text) !important;
}
body.theme-dark .cdk-overlay-container .mat-mdc-option.mdc-list-item--selected .mdc-list-item__primary-text {
color: var(--primary-hover) !important;
}
body.theme-dark .cdk-overlay-container .mat-mdc-option:hover:not(.mdc-list-item--disabled),
body.theme-dark .cdk-overlay-container .mat-mdc-option.mdc-list-item--selected:not(.mdc-list-item--disabled),
body.theme-dark .cdk-overlay-container .mat-mdc-menu-item:hover:not([disabled]) {
background: rgba(255, 255, 255, 0.08) !important;
}
html {
scroll-behavior: auto;
}