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:
ClaudeandClaude Sonnet 5.5 committed 2026-10-04 13:39:09 +00:00
1 parent 1a64e12bc5
commit 25ff496548
30 files changed
+1783 -189

No files matched your search

+9 -9
View File
@@ -297,8 +297,8 @@ describe('Speichern', () => {
});
});
describe('Karte öffnen (/?karte=<id>)', () => {
const open = (id: string) => history.replaceState(null, '', `/?karte=${id}`);
describe('Karte öffnen (/gestalten?karte=<id>)', () => {
const open = (id: string) => history.replaceState(null, '', `/gestalten?karte=${id}`);
it('lädt Karte und Bild, begrenzt den Ausschnitt und setzt den Zustand', async () => {
open('k1');
@@ -531,7 +531,7 @@ describe('Karte öffnen (/?karte=<id>)', () => {
expect(store.get()).toBe(before);
expect(store.get().imageBlob).toBe(blob);
expect(store.get().image).toBe(img);
expect(location.pathname + location.search).toBe('/');
expect(location.pathname + location.search).toBe('/gestalten');
});
it('Esc im Dialog verwirft nichts', async () => {
@@ -733,7 +733,7 @@ describe('Bindung an eine gespeicherte Karte', () => {
});
it('ist nach dem Öffnen einer Karte da', async () => {
history.replaceState(null, '', '/?karte=k1');
history.replaceState(null, '', '/gestalten?karte=k1');
mocks.get.mockResolvedValue(savedCard());
mocks.loadImage.mockResolvedValue(png());
mocks.decodeImage.mockResolvedValue(image());
@@ -758,7 +758,7 @@ describe('Bindung an eine gespeicherte Karte', () => {
describe('„Neue Karte“', () => {
it('setzt den Editor zurück (frische Objekte) und entfernt ?karte= aus der Adresse', () => {
history.replaceState(null, '', '/?karte=k1');
history.replaceState(null, '', '/gestalten?karte=k1');
const { config } = boundEditor();
mount();
newCardButton().click();
@@ -774,7 +774,7 @@ describe('Bindung an eine gespeicherte Karte', () => {
expect(s.config).not.toBe(DEFAULT_CONFIG);
expect(s.config.crop).not.toBe(DEFAULT_CONFIG.crop);
expect(s.config).not.toBe(config);
expect(location.pathname + location.search).toBe('/');
expect(location.pathname + location.search).toBe('/gestalten');
expect(boundRow().hidden).toBe(true);
});
@@ -787,7 +787,7 @@ describe('Bindung an eine gespeicherte Karte', () => {
});
it('bricht eine noch ladende Karte ab', async () => {
history.replaceState(null, '', '/?karte=k2');
history.replaceState(null, '', '/gestalten?karte=k2');
boundEditor();
let finish!: (c: SavedCard) => void;
mocks.get.mockReturnValue(new Promise<SavedCard>((r) => (finish = r)));
@@ -869,7 +869,7 @@ describe('Bindung an eine gespeicherte Karte', () => {
});
it('fragt nach dem Öffnen einer Karte nicht, solange nichts geändert wurde', async () => {
history.replaceState(null, '', '/?karte=k1');
history.replaceState(null, '', '/gestalten?karte=k1');
mocks.get.mockResolvedValue(savedCard());
mocks.loadImage.mockResolvedValue(png());
mocks.decodeImage.mockResolvedValue(image(3000, 2000));
@@ -928,7 +928,7 @@ describe('Bindung an eine gespeicherte Karte', () => {
describe('Abmelden während des Ladens', () => {
it('eine noch ladende Karte landet nicht mehr im Editor', async () => {
history.replaceState(null, '', '/?karte=k1');
history.replaceState(null, '', '/gestalten?karte=k1');
let finish!: (c: SavedCard) => void;
mocks.get.mockReturnValue(new Promise<SavedCard>((r) => (finish = r)));
mount();