feat(shell): Routen, Kopfzeile, Hell/Dunkel, Spenden-Banner
- Router: Routen home, gestalten, cards, community/<id>, reset; Umleitung /?karte=<id> nach /gestalten?karte=<id>; Titel je Route (titleFor); Hash-Ziele (/#community) scrollen den Anker ins Bild, nur-Hash-Wechsel bauen die Seite nicht neu auf. - Kopfzeile: Navigation, Suche (Enter -> /?q=...#community), Spenden-Knopf, Hell/Dunkel-Schalter, Menue unter 900 px, Konto als Avatar-Kreis mit Menue. - Theme: data-theme auf <html> (Skript im <head> gegen Aufblitzen), helle Tokens, danger-/warn-/ok-text-Tokens statt fester Farben. - Spenden-Banner (schliessbar); ohne DONATE_URL bleiben Banner und Knopf weg. - User.labels und isAdmin; Platzhalter fuer Startseite und Detailseite. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1a64e12bc5
commit
25ff496548
30 files changed
+1783
-189
No files matched your search
@@ -1,4 +1,5 @@
|
||||
// Konto-Menü im Seitenkopf: abgemeldet „Anmelden“, angemeldet E-Mail und „Abmelden“.
|
||||
// Konto-Menü im Seitenkopf: abgemeldet „Anmelden“, angemeldet ein Avatar-Kreis mit Initiale
|
||||
// und einem Menü (E-Mail, „Meine Karten“, „Abmelden“).
|
||||
import './account.css';
|
||||
import { el } from '../editor/dom';
|
||||
import { currentUser, logout, onAuthChange } from './auth';
|
||||
@@ -13,8 +14,12 @@ export interface AccountMenu {
|
||||
|
||||
export function mountAccountMenu(slot: HTMLElement): AccountMenu {
|
||||
let isReady = false;
|
||||
// Hebt die Dokument-Listener des gerade offenen Menüs wieder auf (null: kein Menü gezeichnet).
|
||||
let detach: (() => void) | null = null;
|
||||
|
||||
function render(): void {
|
||||
detach?.();
|
||||
detach = null;
|
||||
if (!isReady) {
|
||||
slot.replaceChildren();
|
||||
return;
|
||||
@@ -26,9 +31,52 @@ export function mountAccountMenu(slot: HTMLElement): AccountMenu {
|
||||
slot.replaceChildren(el('div', { class: 'account-menu' }, signIn));
|
||||
return;
|
||||
}
|
||||
|
||||
const initial = Array.from(user.email)[0]?.toUpperCase() ?? '?';
|
||||
const avatar = el(
|
||||
'button',
|
||||
{
|
||||
type: 'button',
|
||||
class: 'account-avatar',
|
||||
'aria-label': 'Konto-Menü',
|
||||
'aria-expanded': 'false',
|
||||
'aria-controls': 'account-dropdown',
|
||||
},
|
||||
initial,
|
||||
);
|
||||
const email = el('span', { class: 'account-email', title: user.email }, user.email);
|
||||
const signOut = el('button', { type: 'button', class: 'auth-btn auth-btn-small' }, 'Abmelden');
|
||||
const myCards = el('a', { class: 'account-item', href: '/karten', 'data-link': true }, 'Meine Karten');
|
||||
const signOut = el('button', { type: 'button', class: 'account-item' }, 'Abmelden');
|
||||
const error = el('span', { class: 'account-error', role: 'alert' });
|
||||
const dropdown = el('div', { class: 'account-dropdown', id: 'account-dropdown', hidden: true }, email, myCards, signOut, error);
|
||||
const wrap = el('div', { class: 'account-menu', role: 'group', 'aria-label': 'Konto' }, avatar, dropdown);
|
||||
|
||||
let isOpen = false;
|
||||
function setOpen(open: boolean): void {
|
||||
isOpen = open;
|
||||
dropdown.hidden = !open;
|
||||
avatar.setAttribute('aria-expanded', String(open));
|
||||
}
|
||||
avatar.addEventListener('click', () => setOpen(!isOpen));
|
||||
|
||||
const onDocumentClick = (event: MouseEvent) => {
|
||||
if (!isOpen) return;
|
||||
const target = event.target as Element | null;
|
||||
if (target && wrap.contains(target) && !target.closest('a')) return; // nur Links schließen das Menü selbst
|
||||
setOpen(false);
|
||||
};
|
||||
const onKeydown = (event: KeyboardEvent) => {
|
||||
if (event.key !== 'Escape' || !isOpen) return;
|
||||
setOpen(false);
|
||||
avatar.focus();
|
||||
};
|
||||
document.addEventListener('click', onDocumentClick);
|
||||
document.addEventListener('keydown', onKeydown);
|
||||
detach = () => {
|
||||
document.removeEventListener('click', onDocumentClick);
|
||||
document.removeEventListener('keydown', onKeydown);
|
||||
};
|
||||
|
||||
signOut.addEventListener('click', () => {
|
||||
signOut.disabled = true;
|
||||
error.textContent = '';
|
||||
@@ -38,14 +86,7 @@ export function mountAccountMenu(slot: HTMLElement): AccountMenu {
|
||||
signOut.disabled = false;
|
||||
});
|
||||
});
|
||||
slot.replaceChildren(
|
||||
el(
|
||||
'div',
|
||||
{ class: 'account-menu', role: 'group', 'aria-label': 'Konto' },
|
||||
el('div', { class: 'account-row' }, email, signOut),
|
||||
error,
|
||||
),
|
||||
);
|
||||
slot.replaceChildren(wrap);
|
||||
}
|
||||
|
||||
const off = onAuthChange(render);
|
||||
@@ -58,6 +99,8 @@ export function mountAccountMenu(slot: HTMLElement): AccountMenu {
|
||||
},
|
||||
dispose() {
|
||||
off();
|
||||
detach?.();
|
||||
detach = null;
|
||||
slot.replaceChildren();
|
||||
},
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user