fix: style
This commit is contained in:
+21
-25
@@ -2,21 +2,19 @@
|
|||||||
<router-outlet></router-outlet>
|
<router-outlet></router-outlet>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (isLanguageConfirmed()) {
|
<button
|
||||||
<button
|
type="button"
|
||||||
type="button"
|
class="language-switcher"
|
||||||
class="language-switcher"
|
[attr.aria-label]="content().app.changeLanguageAriaLabel"
|
||||||
[attr.aria-label]="content().app.changeLanguageAriaLabel"
|
[title]="content().app.changeLanguageAriaLabel"
|
||||||
[title]="content().app.changeLanguageAriaLabel"
|
(click)="reopenLanguageSelector()"
|
||||||
(click)="reopenLanguageSelector()"
|
>
|
||||||
>
|
<span class="language-switcher-tag">{{ currentLanguageCode().toUpperCase() }}</span>
|
||||||
<span class="language-switcher-tag">{{ currentLanguageCode().toUpperCase() }}</span>
|
<span class="language-switcher-copy">
|
||||||
<span class="language-switcher-copy">
|
<strong>{{ content().app.changeLanguage }}</strong>
|
||||||
<strong>{{ content().app.changeLanguage }}</strong>
|
<span>{{ languageSwitcherLabel() }}</span>
|
||||||
<span>{{ languageSwitcherLabel() }}</span>
|
</span>
|
||||||
</span>
|
</button>
|
||||||
</button>
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (isLanguageSelectorOpen()) {
|
@if (isLanguageSelectorOpen()) {
|
||||||
<div
|
<div
|
||||||
@@ -32,16 +30,14 @@
|
|||||||
<h2 id="language-title">{{ dialogContent().app.selectorTitle }}</h2>
|
<h2 id="language-title">{{ dialogContent().app.selectorTitle }}</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (isLanguageConfirmed()) {
|
<button
|
||||||
<button
|
type="button"
|
||||||
type="button"
|
class="language-close"
|
||||||
class="language-close"
|
[attr.aria-label]="dialogContent().app.closeSelector"
|
||||||
[attr.aria-label]="dialogContent().app.closeSelector"
|
(click)="closeLanguageSelector()"
|
||||||
(click)="closeLanguageSelector()"
|
>
|
||||||
>
|
×
|
||||||
x
|
</button>
|
||||||
</button>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="language-options" role="radiogroup" [attr.aria-label]="dialogContent().app.selectorTitle">
|
<div class="language-options" role="radiogroup" [attr.aria-label]="dialogContent().app.selectorTitle">
|
||||||
|
|||||||
@@ -25,9 +25,9 @@ describe('AppComponent', (): void => {
|
|||||||
expect(component).toBeTruthy();
|
expect(component).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render the language selector on first visit', (): void => {
|
it('should keep the language selector closed on first visit', (): void => {
|
||||||
const el = fixture.nativeElement as HTMLElement;
|
const el = fixture.nativeElement as HTMLElement;
|
||||||
expect(el.querySelector('.language-overlay')).not.toBeNull();
|
expect(el.querySelector('.language-overlay')).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should preselect English by default', (): void => {
|
it('should preselect English by default', (): void => {
|
||||||
@@ -35,6 +35,11 @@ describe('AppComponent', (): void => {
|
|||||||
expect(comp.selectedLanguage()).toBe('en');
|
expect(comp.selectedLanguage()).toBe('en');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should render the language switcher on first visit', (): void => {
|
||||||
|
const el = fixture.nativeElement as HTMLElement;
|
||||||
|
expect(el.querySelector('.language-switcher')).not.toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
it('should always render a router-outlet element', (): void => {
|
it('should always render a router-outlet element', (): void => {
|
||||||
const el = fixture.nativeElement as HTMLElement;
|
const el = fixture.nativeElement as HTMLElement;
|
||||||
expect(el.querySelector('router-outlet')).not.toBeNull();
|
expect(el.querySelector('router-outlet')).not.toBeNull();
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import { RouterOutlet } from '@angular/router';
|
|||||||
|
|
||||||
import type { LanguageCode } from '../languages/language.types';
|
import type { LanguageCode } from '../languages/language.types';
|
||||||
import type { LanguageOption } from './app.types';
|
import type { LanguageOption } from './app.types';
|
||||||
import { KofiWidgetService } from './services/kofi-widget.service';
|
|
||||||
import { LanguageService } from './services/language.service';
|
import { LanguageService } from './services/language.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -24,13 +23,12 @@ import { LanguageService } from './services/language.service';
|
|||||||
})
|
})
|
||||||
export class AppComponent implements OnInit, OnDestroy {
|
export class AppComponent implements OnInit, OnDestroy {
|
||||||
private readonly document = inject(DOCUMENT);
|
private readonly document = inject(DOCUMENT);
|
||||||
private readonly kofiWidgetService = inject(KofiWidgetService);
|
|
||||||
private readonly languageService = inject(LanguageService);
|
private readonly languageService = inject(LanguageService);
|
||||||
|
|
||||||
protected readonly content = this.languageService.content;
|
protected readonly content = this.languageService.content;
|
||||||
protected readonly currentLanguageCode = this.languageService.languageCode;
|
protected readonly currentLanguageCode = this.languageService.languageCode;
|
||||||
protected readonly isLanguageConfirmed = this.languageService.isLanguageConfirmed;
|
protected readonly isLanguageConfirmed = this.languageService.isLanguageConfirmed;
|
||||||
protected readonly isLanguageSelectorOpen = signal(true);
|
protected readonly isLanguageSelectorOpen = signal(false);
|
||||||
protected readonly selectedLanguage = signal<LanguageCode>('en');
|
protected readonly selectedLanguage = signal<LanguageCode>('en');
|
||||||
protected readonly languageOptions: readonly LanguageOption[] = [
|
protected readonly languageOptions: readonly LanguageOption[] = [
|
||||||
{ code: 'en', accent: 'EN' },
|
{ code: 'en', accent: 'EN' },
|
||||||
@@ -54,8 +52,6 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
const initialLanguage = this.languageService.initializeFromStorage();
|
const initialLanguage = this.languageService.initializeFromStorage();
|
||||||
this.selectedLanguage.set(initialLanguage);
|
this.selectedLanguage.set(initialLanguage);
|
||||||
this.isLanguageSelectorOpen.set(!this.isLanguageConfirmed());
|
|
||||||
this.kofiWidgetService.scheduleLoad();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected chooseLanguage(language: LanguageCode): void {
|
protected chooseLanguage(language: LanguageCode): void {
|
||||||
@@ -66,7 +62,6 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||||||
protected confirmLanguage(): void {
|
protected confirmLanguage(): void {
|
||||||
this.languageService.confirmLanguage(this.selectedLanguage());
|
this.languageService.confirmLanguage(this.selectedLanguage());
|
||||||
this.isLanguageSelectorOpen.set(false);
|
this.isLanguageSelectorOpen.set(false);
|
||||||
this.kofiWidgetService.prioritizeLoad();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected reopenLanguageSelector(): void {
|
protected reopenLanguageSelector(): void {
|
||||||
@@ -75,13 +70,8 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected closeLanguageSelector(): void {
|
protected closeLanguageSelector(): void {
|
||||||
if (!this.isLanguageConfirmed()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.selectedLanguage.set(this.currentLanguageCode());
|
this.selectedLanguage.set(this.currentLanguageCode());
|
||||||
this.isLanguageSelectorOpen.set(false);
|
this.isLanguageSelectorOpen.set(false);
|
||||||
this.kofiWidgetService.prioritizeLoad();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected closeLanguageSelectorOnBackdrop(event: MouseEvent): void {
|
protected closeLanguageSelectorOnBackdrop(event: MouseEvent): void {
|
||||||
@@ -95,7 +85,6 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.kofiWidgetService.teardown();
|
|
||||||
this.document.body.style.overflow = '';
|
this.document.body.style.overflow = '';
|
||||||
this.document.documentElement.style.overflow = '';
|
this.document.documentElement.style.overflow = '';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,350 +0,0 @@
|
|||||||
import { DOCUMENT, isPlatformBrowser } from '@angular/common';
|
|
||||||
import { inject, Injectable, PLATFORM_ID } from '@angular/core';
|
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
|
||||||
export class KofiWidgetService {
|
|
||||||
private readonly document = inject(DOCUMENT);
|
|
||||||
private readonly platformId = inject(PLATFORM_ID);
|
|
||||||
|
|
||||||
private readonly widgetScriptUrl = 'https://storage.ko-fi.com/cdn/scripts/overlay-widget.js';
|
|
||||||
private readonly widgetIframeTitle = 'Ko-fi support chat';
|
|
||||||
private readonly fallbackDelayMs = 2500;
|
|
||||||
private readonly mobileBreakpointPx = 700;
|
|
||||||
private readonly mobileViewportOffsetPx = 24;
|
|
||||||
|
|
||||||
private loadTimeoutId: number | null = null;
|
|
||||||
private loadIdleCallbackId: number | null = null;
|
|
||||||
private loadListenerAbortController: AbortController | null = null;
|
|
||||||
private resizeListenerAbortController: AbortController | null = null;
|
|
||||||
private iframeObserver: MutationObserver | null = null;
|
|
||||||
private isWidgetScheduled = false;
|
|
||||||
private isWidgetLoaded = false;
|
|
||||||
private isWidgetDrawn = false;
|
|
||||||
|
|
||||||
scheduleLoad(): void {
|
|
||||||
if (!isPlatformBrowser(this.platformId) || this.isWidgetScheduled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.isWidgetScheduled = true;
|
|
||||||
|
|
||||||
const windowRef = this.document.defaultView;
|
|
||||||
if (windowRef === null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const queueWidgetLoad = (): void => {
|
|
||||||
if (this.isWidgetLoaded || this.isWidgetDrawn) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadWidget = (): void => {
|
|
||||||
this.loadIdleCallbackId = null;
|
|
||||||
void this.loadWidget();
|
|
||||||
};
|
|
||||||
|
|
||||||
if (typeof windowRef.requestIdleCallback === 'function') {
|
|
||||||
this.loadIdleCallbackId = windowRef.requestIdleCallback(loadWidget, { timeout: 2500 });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.loadTimeoutId = windowRef.setTimeout(loadWidget, 1800);
|
|
||||||
};
|
|
||||||
|
|
||||||
const triggerWidgetLoad = (): void => {
|
|
||||||
this.clearStartListeners();
|
|
||||||
queueWidgetLoad();
|
|
||||||
};
|
|
||||||
|
|
||||||
const scheduleFallbackWidgetLoad = (): void => {
|
|
||||||
if (this.isWidgetLoaded || this.isWidgetDrawn) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.loadTimeoutId = windowRef.setTimeout((): void => {
|
|
||||||
this.loadTimeoutId = null;
|
|
||||||
triggerWidgetLoad();
|
|
||||||
}, this.fallbackDelayMs);
|
|
||||||
};
|
|
||||||
|
|
||||||
this.loadListenerAbortController = new AbortController();
|
|
||||||
const listenerOptions: AddEventListenerOptions = {
|
|
||||||
passive: true,
|
|
||||||
signal: this.loadListenerAbortController.signal,
|
|
||||||
};
|
|
||||||
|
|
||||||
windowRef.addEventListener('pointerdown', triggerWidgetLoad, listenerOptions);
|
|
||||||
windowRef.addEventListener('keydown', triggerWidgetLoad, listenerOptions);
|
|
||||||
windowRef.addEventListener('touchstart', triggerWidgetLoad, listenerOptions);
|
|
||||||
windowRef.addEventListener('scroll', triggerWidgetLoad, listenerOptions);
|
|
||||||
|
|
||||||
if (this.document.readyState === 'complete') {
|
|
||||||
scheduleFallbackWidgetLoad();
|
|
||||||
} else {
|
|
||||||
windowRef.addEventListener('load', scheduleFallbackWidgetLoad, {
|
|
||||||
...listenerOptions,
|
|
||||||
once: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
prioritizeLoad(): void {
|
|
||||||
if (!isPlatformBrowser(this.platformId) || this.isWidgetLoaded || this.isWidgetDrawn) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const windowRef = this.document.defaultView;
|
|
||||||
if (windowRef === null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.clearStartListeners();
|
|
||||||
this.clearPendingLoadTimers();
|
|
||||||
|
|
||||||
const loadWidget = (): void => {
|
|
||||||
this.loadIdleCallbackId = null;
|
|
||||||
this.loadTimeoutId = null;
|
|
||||||
void this.loadWidget();
|
|
||||||
};
|
|
||||||
|
|
||||||
if (typeof windowRef.requestIdleCallback === 'function') {
|
|
||||||
this.loadIdleCallbackId = windowRef.requestIdleCallback(loadWidget, { timeout: 1200 });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.loadTimeoutId = windowRef.setTimeout(loadWidget, 200);
|
|
||||||
}
|
|
||||||
|
|
||||||
teardown(): void {
|
|
||||||
this.clearStartListeners();
|
|
||||||
this.resizeListenerAbortController?.abort();
|
|
||||||
this.resizeListenerAbortController = null;
|
|
||||||
this.clearPendingLoadTimers();
|
|
||||||
|
|
||||||
this.iframeObserver?.disconnect();
|
|
||||||
this.iframeObserver = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private clearStartListeners(): void {
|
|
||||||
this.loadListenerAbortController?.abort();
|
|
||||||
this.loadListenerAbortController = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private clearPendingLoadTimers(): void {
|
|
||||||
const windowRef = this.document.defaultView;
|
|
||||||
|
|
||||||
if (windowRef !== null && this.loadTimeoutId !== null) {
|
|
||||||
windowRef.clearTimeout(this.loadTimeoutId);
|
|
||||||
this.loadTimeoutId = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
windowRef !== null &&
|
|
||||||
this.loadIdleCallbackId !== null &&
|
|
||||||
typeof windowRef.cancelIdleCallback === 'function'
|
|
||||||
) {
|
|
||||||
windowRef.cancelIdleCallback(this.loadIdleCallbackId);
|
|
||||||
this.loadIdleCallbackId = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async loadWidget(): Promise<void> {
|
|
||||||
if (this.isWidgetLoaded || this.isWidgetDrawn) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const windowRef = this.document.defaultView;
|
|
||||||
if (windowRef === null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (windowRef.kofiWidgetOverlay !== undefined) {
|
|
||||||
this.isWidgetLoaded = true;
|
|
||||||
this.drawWidget();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const existingScript = this.document.querySelector<HTMLScriptElement>(
|
|
||||||
`script[src="${this.widgetScriptUrl}"]`,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (existingScript !== null) {
|
|
||||||
existingScript.addEventListener(
|
|
||||||
'load',
|
|
||||||
(): void => {
|
|
||||||
this.isWidgetLoaded = true;
|
|
||||||
this.drawWidget();
|
|
||||||
},
|
|
||||||
{ once: true },
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await new Promise<void>((resolve, reject): void => {
|
|
||||||
const scriptElement = this.document.createElement('script');
|
|
||||||
scriptElement.src = this.widgetScriptUrl;
|
|
||||||
scriptElement.async = true;
|
|
||||||
scriptElement.crossOrigin = 'anonymous';
|
|
||||||
scriptElement.addEventListener(
|
|
||||||
'load',
|
|
||||||
(): void => {
|
|
||||||
this.isWidgetLoaded = true;
|
|
||||||
resolve();
|
|
||||||
},
|
|
||||||
{ once: true },
|
|
||||||
);
|
|
||||||
scriptElement.addEventListener(
|
|
||||||
'error',
|
|
||||||
(): void => reject(new Error('Failed to load the Ko-fi widget script.')),
|
|
||||||
{ once: true },
|
|
||||||
);
|
|
||||||
this.document.body.appendChild(scriptElement);
|
|
||||||
}).catch((): void => {
|
|
||||||
this.isWidgetLoaded = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.drawWidget();
|
|
||||||
}
|
|
||||||
|
|
||||||
private drawWidget(): void {
|
|
||||||
if (this.isWidgetDrawn) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const windowRef = this.document.defaultView;
|
|
||||||
if (windowRef?.kofiWidgetOverlay === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
windowRef.kofiWidgetOverlay.draw('fraujulian', {
|
|
||||||
type: 'floating-chat',
|
|
||||||
'floating-chat.donateButton.text': 'Coffee',
|
|
||||||
'floating-chat.donateButton.background-color': '#ff5f5f',
|
|
||||||
'floating-chat.donateButton.text-color': '#fff',
|
|
||||||
});
|
|
||||||
|
|
||||||
this.isWidgetDrawn = true;
|
|
||||||
this.ensureAccessibleIframeTitles();
|
|
||||||
this.applyResponsiveWidgetLayout();
|
|
||||||
this.observeInjectedIframes();
|
|
||||||
this.observeViewportChanges();
|
|
||||||
}
|
|
||||||
|
|
||||||
private ensureAccessibleIframeTitles(): void {
|
|
||||||
this.document
|
|
||||||
.querySelectorAll<HTMLIFrameElement>('iframe[id^="kofi-wo-container"]')
|
|
||||||
.forEach((iframe: HTMLIFrameElement): void => {
|
|
||||||
iframe.title = this.widgetIframeTitle;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private applyResponsiveWidgetLayout(): void {
|
|
||||||
const windowRef = this.document.defaultView;
|
|
||||||
if (windowRef === null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isMobileViewport: boolean = windowRef.innerWidth <= this.mobileBreakpointPx;
|
|
||||||
const mobileWidthPx: number = Math.max(windowRef.innerWidth - this.mobileViewportOffsetPx, 280);
|
|
||||||
const mobileHeightPx: number = Math.max(
|
|
||||||
Math.min(Math.round(windowRef.innerHeight * 0.68), 520),
|
|
||||||
360,
|
|
||||||
);
|
|
||||||
|
|
||||||
this.document
|
|
||||||
.querySelectorAll<HTMLElement>('[id*="kofi-widget-overlay"]')
|
|
||||||
.forEach((overlay: HTMLElement): void => {
|
|
||||||
if (isMobileViewport) {
|
|
||||||
overlay.style.left = '12px';
|
|
||||||
overlay.style.right = '12px';
|
|
||||||
overlay.style.bottom = '12px';
|
|
||||||
overlay.style.width = 'auto';
|
|
||||||
overlay.style.maxWidth = `${mobileWidthPx}px`;
|
|
||||||
} else {
|
|
||||||
overlay.style.removeProperty('left');
|
|
||||||
overlay.style.removeProperty('right');
|
|
||||||
overlay.style.removeProperty('width');
|
|
||||||
overlay.style.removeProperty('max-width');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.document
|
|
||||||
.querySelectorAll<HTMLElement>(
|
|
||||||
'.floatingchat-container-wrap, .floatingchat-container-wrap-mobi',
|
|
||||||
)
|
|
||||||
.forEach((container: HTMLElement): void => {
|
|
||||||
if (isMobileViewport) {
|
|
||||||
container.style.width = `${mobileWidthPx}px`;
|
|
||||||
container.style.maxWidth = 'calc(100vw - 24px)';
|
|
||||||
container.style.left = '0';
|
|
||||||
container.style.right = '0';
|
|
||||||
} else {
|
|
||||||
container.style.removeProperty('width');
|
|
||||||
container.style.removeProperty('max-width');
|
|
||||||
container.style.removeProperty('left');
|
|
||||||
container.style.removeProperty('right');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.document
|
|
||||||
.querySelectorAll<HTMLIFrameElement>('iframe[id^="kofi-wo-container"]')
|
|
||||||
.forEach((iframe: HTMLIFrameElement): void => {
|
|
||||||
iframe.title = this.widgetIframeTitle;
|
|
||||||
|
|
||||||
if (isMobileViewport) {
|
|
||||||
iframe.style.width = `${mobileWidthPx}px`;
|
|
||||||
iframe.style.maxWidth = 'calc(100vw - 24px)';
|
|
||||||
iframe.style.height = `${mobileHeightPx}px`;
|
|
||||||
iframe.style.maxHeight = '68vh';
|
|
||||||
iframe.style.borderRadius = '18px';
|
|
||||||
} else {
|
|
||||||
iframe.style.removeProperty('width');
|
|
||||||
iframe.style.removeProperty('max-width');
|
|
||||||
iframe.style.removeProperty('height');
|
|
||||||
iframe.style.removeProperty('max-height');
|
|
||||||
iframe.style.removeProperty('border-radius');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private observeInjectedIframes(): void {
|
|
||||||
if (this.iframeObserver !== null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.iframeObserver = new MutationObserver((): void => {
|
|
||||||
this.ensureAccessibleIframeTitles();
|
|
||||||
this.applyResponsiveWidgetLayout();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.iframeObserver.observe(this.document.body, {
|
|
||||||
childList: true,
|
|
||||||
subtree: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private observeViewportChanges(): void {
|
|
||||||
if (this.resizeListenerAbortController !== null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const windowRef = this.document.defaultView;
|
|
||||||
if (windowRef === null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.resizeListenerAbortController = new AbortController();
|
|
||||||
const listenerOptions: AddEventListenerOptions = {
|
|
||||||
passive: true,
|
|
||||||
signal: this.resizeListenerAbortController.signal,
|
|
||||||
};
|
|
||||||
|
|
||||||
windowRef.addEventListener(
|
|
||||||
'resize',
|
|
||||||
(): void => {
|
|
||||||
this.applyResponsiveWidgetLayout();
|
|
||||||
},
|
|
||||||
listenerOptions,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -36,7 +36,7 @@ const deShellLanguage: LanguageShellPack = {
|
|||||||
export class LanguageService {
|
export class LanguageService {
|
||||||
private static readonly storageKey = 'portfolio-language';
|
private static readonly storageKey = 'portfolio-language';
|
||||||
private readonly currentLanguageCode = signal<LanguageCode>('en');
|
private readonly currentLanguageCode = signal<LanguageCode>('en');
|
||||||
private readonly languageConfirmed = signal(false);
|
private readonly languageConfirmed = signal(true);
|
||||||
|
|
||||||
readonly languageCode = this.currentLanguageCode.asReadonly();
|
readonly languageCode = this.currentLanguageCode.asReadonly();
|
||||||
readonly isLanguageConfirmed = this.languageConfirmed.asReadonly();
|
readonly isLanguageConfirmed = this.languageConfirmed.asReadonly();
|
||||||
@@ -52,7 +52,6 @@ export class LanguageService {
|
|||||||
const storedLanguage = window.localStorage.getItem(LanguageService.storageKey);
|
const storedLanguage = window.localStorage.getItem(LanguageService.storageKey);
|
||||||
if (storedLanguage === 'de' || storedLanguage === 'en') {
|
if (storedLanguage === 'de' || storedLanguage === 'en') {
|
||||||
this.currentLanguageCode.set(storedLanguage);
|
this.currentLanguageCode.set(storedLanguage);
|
||||||
this.languageConfirmed.set(true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.currentLanguageCode();
|
return this.currentLanguageCode();
|
||||||
|
|||||||
Vendored
-9
@@ -1,9 +0,0 @@
|
|||||||
interface KofiWidgetOverlay {
|
|
||||||
draw(userName: string, options: Record<string, string>): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Window {
|
|
||||||
kofiWidgetOverlay?: KofiWidgetOverlay;
|
|
||||||
requestIdleCallback?: (callback: IdleRequestCallback, options?: IdleRequestOptions) => number;
|
|
||||||
cancelIdleCallback?: (handle: number) => void;
|
|
||||||
}
|
|
||||||
+156
-74
@@ -4,6 +4,7 @@
|
|||||||
--surface: rgba(20, 18, 22, 0.62);
|
--surface: rgba(20, 18, 22, 0.62);
|
||||||
--surface-strong: rgba(15, 14, 18, 0.78);
|
--surface-strong: rgba(15, 14, 18, 0.78);
|
||||||
--surface-soft: rgba(255, 255, 255, 0.05);
|
--surface-soft: rgba(255, 255, 255, 0.05);
|
||||||
|
--panel-background: linear-gradient(180deg, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0.03));
|
||||||
--border-soft: rgba(255, 255, 255, 0.1);
|
--border-soft: rgba(255, 255, 255, 0.1);
|
||||||
--text-primary: #f7f1ea;
|
--text-primary: #f7f1ea;
|
||||||
--text-muted: rgba(247, 241, 234, 0.74);
|
--text-muted: rgba(247, 241, 234, 0.74);
|
||||||
@@ -95,7 +96,7 @@ hr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.panel {
|
.panel {
|
||||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0.03));
|
background: var(--panel-background);
|
||||||
border: 1px solid var(--border-soft);
|
border: 1px solid var(--border-soft);
|
||||||
border-radius: var(--radius-xl);
|
border-radius: var(--radius-xl);
|
||||||
box-shadow: var(--shadow);
|
box-shadow: var(--shadow);
|
||||||
@@ -194,6 +195,14 @@ hr {
|
|||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hero-section > *,
|
||||||
|
.story-grid > *,
|
||||||
|
.project-entry > *,
|
||||||
|
.project-header > *,
|
||||||
|
.footer-inner > * {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.hero-copy {
|
.hero-copy {
|
||||||
grid-column: 1;
|
grid-column: 1;
|
||||||
align-self: start;
|
align-self: start;
|
||||||
@@ -201,6 +210,25 @@ hr {
|
|||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hero-copy,
|
||||||
|
.hero-copy-main,
|
||||||
|
.hero-actions,
|
||||||
|
.hero-action-links,
|
||||||
|
.hero-meta,
|
||||||
|
.hero-visual,
|
||||||
|
.portrait-media,
|
||||||
|
.story-copy,
|
||||||
|
.story-detail,
|
||||||
|
.project-copy,
|
||||||
|
.project-title-wrap,
|
||||||
|
.project-languages,
|
||||||
|
.legal-contact-box,
|
||||||
|
.language-copy,
|
||||||
|
.language-switcher-copy {
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.hero-copy-main {
|
.hero-copy-main {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
@@ -209,11 +237,29 @@ hr {
|
|||||||
.legal-intro,
|
.legal-intro,
|
||||||
.project-description,
|
.project-description,
|
||||||
.story-copy p,
|
.story-copy p,
|
||||||
.detail-panel p {
|
.story-detail p,
|
||||||
|
.detail-panel p,
|
||||||
|
.legal-contact-box p {
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
line-height: 1.75;
|
line-height: 1.75;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hero-intro,
|
||||||
|
.legal-intro,
|
||||||
|
.project-description,
|
||||||
|
.story-copy p,
|
||||||
|
.story-detail p,
|
||||||
|
.detail-panel p,
|
||||||
|
.legal-contact-box p,
|
||||||
|
.legal-provider-name,
|
||||||
|
.project-link,
|
||||||
|
.project-language-chip,
|
||||||
|
.portrait-caption,
|
||||||
|
.portrait-click-hint,
|
||||||
|
.inline-link {
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
.hero-intro {
|
.hero-intro {
|
||||||
max-width: 34rem;
|
max-width: 34rem;
|
||||||
margin: var(--space-4) 0 0;
|
margin: var(--space-4) 0 0;
|
||||||
@@ -239,6 +285,7 @@ hr {
|
|||||||
display: grid;
|
display: grid;
|
||||||
justify-items: center;
|
justify-items: center;
|
||||||
gap: 0.45rem;
|
gap: 0.45rem;
|
||||||
|
width: min(100%, 22rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero-contact-title {
|
.hero-contact-title {
|
||||||
@@ -252,7 +299,12 @@ hr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.hero-contact-area {
|
.hero-contact-area {
|
||||||
|
display: grid;
|
||||||
|
grid-auto-flow: column;
|
||||||
|
grid-auto-columns: minmax(0, 1fr);
|
||||||
gap: 0.6rem;
|
gap: 0.6rem;
|
||||||
|
width: 100%;
|
||||||
|
justify-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
#contact-links {
|
#contact-links {
|
||||||
@@ -260,20 +312,25 @@ hr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.hero-contact-area a {
|
.hero-contact-area a {
|
||||||
width: 2.8rem;
|
width: min(100%, 2.8rem);
|
||||||
height: 2.8rem;
|
height: auto;
|
||||||
|
aspect-ratio: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-link {
|
.action-link {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
max-width: 100%;
|
||||||
min-height: 3rem;
|
min-height: 3rem;
|
||||||
padding: 0 1.25rem;
|
padding: 0 1.25rem;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
border: 1px solid var(--border-soft);
|
border: 1px solid var(--border-soft);
|
||||||
font: inherit;
|
font: inherit;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
|
line-height: 1.35;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
white-space: normal;
|
||||||
background: rgba(255, 255, 255, 0.04);
|
background: rgba(255, 255, 255, 0.04);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition:
|
transition:
|
||||||
@@ -444,6 +501,7 @@ hr {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
right: 1.1rem;
|
right: 1.1rem;
|
||||||
bottom: 1.1rem;
|
bottom: 1.1rem;
|
||||||
|
max-width: calc(100% - 2.2rem);
|
||||||
padding: 0.5rem 0.9rem;
|
padding: 0.5rem 0.9rem;
|
||||||
border-radius: 0.65rem;
|
border-radius: 0.65rem;
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
@@ -457,6 +515,7 @@ hr {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
left: 1.1rem;
|
left: 1.1rem;
|
||||||
top: 1.1rem;
|
top: 1.1rem;
|
||||||
|
max-width: calc(100% - 2.2rem);
|
||||||
padding: 0.5rem 0.9rem;
|
padding: 0.5rem 0.9rem;
|
||||||
border-radius: 0.65rem;
|
border-radius: 0.65rem;
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
@@ -543,11 +602,15 @@ hr {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--space-2);
|
gap: var(--space-2);
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
|
max-width: 100%;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
padding: 0.9rem 1.1rem;
|
padding: 0.9rem 1.1rem;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
|
line-height: 1.35;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
white-space: normal;
|
||||||
background: rgba(255, 255, 255, 0.06);
|
background: rgba(255, 255, 255, 0.06);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition:
|
transition:
|
||||||
@@ -624,17 +687,22 @@ hr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.contact-area {
|
.contact-area {
|
||||||
display: flex;
|
display: grid;
|
||||||
flex-wrap: wrap;
|
grid-auto-flow: column;
|
||||||
|
grid-auto-columns: minmax(0, 1fr);
|
||||||
gap: var(--space-3);
|
gap: var(--space-3);
|
||||||
|
width: 100%;
|
||||||
|
justify-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.contact-area a {
|
.contact-area a {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 3.3rem;
|
width: min(100%, 3.3rem);
|
||||||
height: 3.3rem;
|
height: auto;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
min-width: 0;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: rgba(255, 255, 255, 0.06);
|
background: rgba(255, 255, 255, 0.06);
|
||||||
border: 1px solid var(--border-soft);
|
border: 1px solid var(--border-soft);
|
||||||
@@ -1094,10 +1162,6 @@ hr {
|
|||||||
backdrop-filter: none;
|
backdrop-filter: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel {
|
|
||||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0.025)), rgba(12, 12, 16, 0.88);
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-entry,
|
.project-entry,
|
||||||
.about-scroll-anim-enabled .story-copy,
|
.about-scroll-anim-enabled .story-copy,
|
||||||
.about-scroll-anim-enabled .story-detail {
|
.about-scroll-anim-enabled .story-detail {
|
||||||
@@ -1192,20 +1256,24 @@ hr {
|
|||||||
place-items: center;
|
place-items: center;
|
||||||
padding: 1.25rem;
|
padding: 1.25rem;
|
||||||
background:
|
background:
|
||||||
radial-gradient(circle at top, rgba(239, 191, 157, 0.16), transparent 28%),
|
radial-gradient(circle at 50% 18%, rgba(217, 162, 140, 0.2), transparent 24%),
|
||||||
linear-gradient(180deg, rgba(4, 4, 6, 0.7), rgba(7, 7, 10, 0.9));
|
linear-gradient(180deg, rgba(3, 3, 5, 0.78), rgba(7, 7, 10, 0.94));
|
||||||
backdrop-filter: blur(14px);
|
backdrop-filter: blur(18px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.language-card {
|
.language-card {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 1rem;
|
gap: 1.15rem;
|
||||||
width: min(100%, 520px);
|
width: min(100%, 500px);
|
||||||
padding: clamp(1rem, 4vw, 1.35rem);
|
padding: clamp(1.1rem, 4vw, 1.45rem);
|
||||||
background:
|
background:
|
||||||
radial-gradient(circle at top left, rgba(239, 191, 157, 0.16), transparent 26%),
|
radial-gradient(circle at top left, rgba(217, 162, 140, 0.18), transparent 28%),
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0.02)), rgba(10, 10, 14, 0.62);
|
linear-gradient(180deg, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0.02)), rgba(11, 10, 14, 0.82);
|
||||||
|
border: 1px solid rgba(239, 191, 157, 0.16);
|
||||||
|
box-shadow:
|
||||||
|
0 24px 60px rgba(0, 0, 0, 0.32),
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.04);
|
||||||
}
|
}
|
||||||
|
|
||||||
.language-card-header {
|
.language-card-header {
|
||||||
@@ -1217,7 +1285,7 @@ hr {
|
|||||||
|
|
||||||
.language-copy {
|
.language-copy {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0;
|
gap: 0.18rem;
|
||||||
padding-right: 3.4rem;
|
padding-right: 3.4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1228,29 +1296,30 @@ hr {
|
|||||||
|
|
||||||
.language-copy h2 {
|
.language-copy h2 {
|
||||||
font-size: clamp(1.85rem, 4vw, 2.5rem);
|
font-size: clamp(1.85rem, 4vw, 2.5rem);
|
||||||
line-height: 1;
|
line-height: 0.98;
|
||||||
letter-spacing: -0.04em;
|
letter-spacing: -0.04em;
|
||||||
|
text-wrap: balance;
|
||||||
}
|
}
|
||||||
|
|
||||||
.language-options {
|
.language-options {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
gap: 0.75rem;
|
gap: 0.85rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.language-option {
|
.language-option {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.45rem;
|
gap: 0.55rem;
|
||||||
justify-items: center;
|
justify-items: start;
|
||||||
align-content: center;
|
align-content: center;
|
||||||
min-height: 8.5rem;
|
min-height: 7.4rem;
|
||||||
padding: 1rem 0.9rem;
|
padding: 1rem 1rem 1.05rem;
|
||||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
border: 1px solid rgba(255, 255, 255, 0.09);
|
||||||
border-radius: 20px;
|
border-radius: 22px;
|
||||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.07), rgba(255, 255, 255, 0.03)), rgba(10, 10, 14, 0.34);
|
background: linear-gradient(180deg, rgba(255, 255, 255, 0.045), rgba(255, 255, 255, 0.018)), rgba(14, 13, 18, 0.72);
|
||||||
color: inherit;
|
color: inherit;
|
||||||
text-align: center;
|
text-align: left;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition:
|
transition:
|
||||||
transform var(--transition-fast),
|
transform var(--transition-fast),
|
||||||
@@ -1261,23 +1330,23 @@ hr {
|
|||||||
|
|
||||||
.language-option:hover {
|
.language-option:hover {
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
border-color: rgba(255, 255, 255, 0.28);
|
border-color: rgba(239, 191, 157, 0.24);
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 0 0 1px rgba(255, 255, 255, 0.1),
|
0 0 0 1px rgba(239, 191, 157, 0.08),
|
||||||
0 14px 28px rgba(0, 0, 0, 0.16);
|
0 16px 30px rgba(0, 0, 0, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.language-option-active {
|
.language-option-active {
|
||||||
border-color: rgba(239, 191, 157, 0.64);
|
border-color: rgba(239, 191, 157, 0.5);
|
||||||
background: linear-gradient(180deg, rgba(239, 191, 157, 0.14), rgba(255, 255, 255, 0.05)), rgba(10, 10, 14, 0.48);
|
background: linear-gradient(180deg, rgba(217, 162, 140, 0.16), rgba(255, 255, 255, 0.03)), rgba(17, 14, 18, 0.84);
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 0 0 1px rgba(239, 191, 157, 0.22),
|
0 0 0 1px rgba(239, 191, 157, 0.18),
|
||||||
0 18px 34px rgba(0, 0, 0, 0.2);
|
0 18px 34px rgba(0, 0, 0, 0.22);
|
||||||
}
|
}
|
||||||
|
|
||||||
.language-option strong {
|
.language-option strong {
|
||||||
font-size: 1.32rem;
|
font-size: 1.18rem;
|
||||||
line-height: 1.08;
|
line-height: 1.04;
|
||||||
letter-spacing: -0.03em;
|
letter-spacing: -0.03em;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1297,8 +1366,8 @@ hr {
|
|||||||
|
|
||||||
.language-option-accent {
|
.language-option-accent {
|
||||||
color: var(--accent-strong);
|
color: var(--accent-strong);
|
||||||
background: rgba(239, 191, 157, 0.12);
|
background: rgba(239, 191, 157, 0.1);
|
||||||
border: 1px solid rgba(239, 191, 157, 0.24);
|
border: 1px solid rgba(239, 191, 157, 0.18);
|
||||||
}
|
}
|
||||||
|
|
||||||
.language-close {
|
.language-close {
|
||||||
@@ -1306,14 +1375,14 @@ hr {
|
|||||||
top: 1rem;
|
top: 1rem;
|
||||||
right: 1rem;
|
right: 1rem;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
width: 2.8rem;
|
width: 2.5rem;
|
||||||
height: 2.8rem;
|
height: 2.5rem;
|
||||||
border: 1px solid var(--border-soft);
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
background: rgba(255, 255, 255, 0.04);
|
background: rgba(255, 255, 255, 0.03);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
font: inherit;
|
font: inherit;
|
||||||
font-size: 1.4rem;
|
font-size: 1.25rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition:
|
transition:
|
||||||
transform var(--transition-fast),
|
transform var(--transition-fast),
|
||||||
@@ -1323,8 +1392,8 @@ hr {
|
|||||||
|
|
||||||
.language-close:hover {
|
.language-close:hover {
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
border-color: rgba(255, 255, 255, 0.42);
|
border-color: rgba(239, 191, 157, 0.28);
|
||||||
background: rgba(255, 255, 255, 0.08);
|
background: rgba(239, 191, 157, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
.language-switcher {
|
.language-switcher {
|
||||||
@@ -1334,32 +1403,43 @@ hr {
|
|||||||
z-index: 2000;
|
z-index: 2000;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.7rem;
|
gap: 0.6rem;
|
||||||
min-height: 3.5rem;
|
min-height: 3.15rem;
|
||||||
padding: 0.55rem 0.8rem 0.55rem 0.55rem;
|
padding: 0.42rem 0.78rem 0.42rem 0.42rem;
|
||||||
border: 1px solid rgba(255, 255, 255, 0.22);
|
border: 1px solid rgba(239, 191, 157, 0.18);
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.09), rgba(255, 255, 255, 0.04)), rgba(12, 12, 16, 0.82);
|
background:
|
||||||
|
radial-gradient(circle at top left, rgba(217, 162, 140, 0.15), transparent 58%),
|
||||||
|
linear-gradient(180deg, rgba(255, 255, 255, 0.055), rgba(255, 255, 255, 0.018)), rgba(12, 12, 16, 0.86);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
box-shadow: var(--shadow-soft);
|
box-shadow:
|
||||||
backdrop-filter: blur(18px);
|
0 14px 32px rgba(5, 5, 8, 0.24),
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.04);
|
||||||
|
backdrop-filter: blur(14px);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition:
|
transition:
|
||||||
transform var(--transition-fast),
|
transform var(--transition-fast),
|
||||||
border-color var(--transition-fast),
|
border-color var(--transition-fast),
|
||||||
background var(--transition-fast);
|
background var(--transition-fast),
|
||||||
|
box-shadow var(--transition-fast);
|
||||||
}
|
}
|
||||||
|
|
||||||
.language-switcher:hover {
|
.language-switcher:hover {
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
border-color: rgba(255, 255, 255, 0.52);
|
border-color: rgba(239, 191, 157, 0.34);
|
||||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.12), rgba(255, 255, 255, 0.05)), rgba(18, 18, 24, 0.9);
|
background:
|
||||||
|
radial-gradient(circle at top left, rgba(217, 162, 140, 0.2), transparent 58%),
|
||||||
|
linear-gradient(180deg, rgba(255, 255, 255, 0.07), rgba(255, 255, 255, 0.022)), rgba(16, 15, 20, 0.92);
|
||||||
|
box-shadow:
|
||||||
|
0 18px 38px rgba(5, 5, 8, 0.28),
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.language-switcher-tag {
|
.language-switcher-tag {
|
||||||
min-width: 2.3rem;
|
min-width: 2.15rem;
|
||||||
color: #111214;
|
color: #1a1514;
|
||||||
background: #f7f1ea;
|
background: linear-gradient(180deg, #f0c2a7, #d9a28c);
|
||||||
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.language-switcher-copy {
|
.language-switcher-copy {
|
||||||
@@ -1369,14 +1449,16 @@ hr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.language-switcher-copy strong {
|
.language-switcher-copy strong {
|
||||||
font-size: 0.78rem;
|
font-size: 0.72rem;
|
||||||
letter-spacing: 0.12em;
|
letter-spacing: 0.12em;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
|
color: rgba(247, 241, 234, 0.78);
|
||||||
}
|
}
|
||||||
|
|
||||||
.language-switcher-copy span {
|
.language-switcher-copy span {
|
||||||
color: var(--text-muted);
|
color: var(--text-primary);
|
||||||
font-size: 0.9rem;
|
font-size: 0.86rem;
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-content[aria-hidden='true'] {
|
.app-content[aria-hidden='true'] {
|
||||||
@@ -1386,7 +1468,7 @@ hr {
|
|||||||
|
|
||||||
@media (max-width: 700px) {
|
@media (max-width: 700px) {
|
||||||
.language-card {
|
.language-card {
|
||||||
width: min(100%, 460px);
|
width: min(100%, 440px);
|
||||||
gap: 0.9rem;
|
gap: 0.9rem;
|
||||||
padding: 0.95rem;
|
padding: 0.95rem;
|
||||||
padding-top: 1.15rem;
|
padding-top: 1.15rem;
|
||||||
@@ -1405,30 +1487,30 @@ hr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.language-option {
|
.language-option {
|
||||||
min-height: auto;
|
min-height: 5.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.language-switcher {
|
.language-switcher {
|
||||||
right: 0.75rem;
|
right: 0.75rem;
|
||||||
top: 0.75rem;
|
top: 0.75rem;
|
||||||
gap: 0.45rem;
|
gap: 0.45rem;
|
||||||
min-height: 2.75rem;
|
min-height: 2.6rem;
|
||||||
padding: 0.4rem 0.65rem 0.4rem 0.4rem;
|
padding: 0.34rem 0.58rem 0.34rem 0.34rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.language-switcher-tag {
|
.language-switcher-tag {
|
||||||
min-width: 1.95rem;
|
min-width: 1.85rem;
|
||||||
min-height: 1.65rem;
|
min-height: 1.58rem;
|
||||||
padding: 0.14rem 0.55rem;
|
padding: 0.12rem 0.5rem;
|
||||||
font-size: 0.68rem;
|
font-size: 0.68rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.language-switcher-copy strong {
|
.language-switcher-copy strong {
|
||||||
font-size: 0.68rem;
|
font-size: 0.62rem;
|
||||||
letter-spacing: 0.1em;
|
letter-spacing: 0.1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.language-switcher-copy span {
|
.language-switcher-copy span {
|
||||||
font-size: 0.78rem;
|
font-size: 0.74rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user