chore(web): veraltete Kommentare, Theme-Skript in IIFE, CARD_W/CARD_H und reportsLabel nur aus einer Quelle

- publish.ts und editor-page.ts: Kommentare zum Aufräumen bzw. zur Adresse /gestalten?karte= stimmen wieder.
- index.html: Inline-Skript für Hell/Dunkel in einer sofort aufgerufenen Funktion (keine globale Variable theme);
  Test führt es aus und vergleicht mit initialTheme().
- CARD_W/CARD_H nur noch aus card/geometry.ts (Startseite, Detailseite, Raster), Vorschauhöhe des Rasters daraus abgeleitet;
  reportsLabel nur aus pages/format.ts. Ein Test sorgt dafür, dass es dabei bleibt.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-04 16:40:17 +00:00
1 parent f6b9917b07
commit a3523d3316
11 files changed
+116 -22

No files matched your search

+3
View File
@@ -11,6 +11,8 @@
<link rel="apple-touch-icon" href="/apple-touch-icon.png?v=2" />
<script>
// Hell/Dunkel vor dem ersten Paint setzen (kein Aufblitzen); dieselbe Regel wie initialTheme() in src/shell/theme.ts.
// In einer sofort aufgerufenen Funktion, damit keine globale Variable `theme` entsteht.
(function () {
try {
var theme = null;
try {
@@ -21,6 +23,7 @@
}
document.documentElement.setAttribute('data-theme', theme);
} catch (e) {}
})();
</script>
</head>
<body>
+53
View File
@@ -1,7 +1,9 @@
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { runInNewContext } from 'node:vm';
import { describe, expect, it } from 'vitest';
import indexHtml from '../index.html?raw';
import { initialTheme } from './shell/theme';
const publicFile = (name: string) => readFileSync(resolve(import.meta.dirname, '../public', name));
@@ -47,3 +49,54 @@ describe('Icon-Dateien', () => {
expect(pngInfo(publicFile('apple-touch-icon.png'))).toEqual({ width: 180, height: 180, colorType: 2 });
});
});
describe('Inline-Skript für Hell/Dunkel in index.html', () => {
const script = /<script>([\s\S]*?)<\/script>/.exec(indexHtml)?.[1] ?? '';
/** Führt das Skript in einer leeren Umgebung aus (kein Browser) und liefert, was es gesetzt hat. */
function run(stored: string | null, prefersDark: boolean, storageThrows = false) {
const attributes: Record<string, string> = {};
const sandbox: Record<string, unknown> = {
localStorage: {
getItem: () => {
if (storageThrows) throw new Error('gesperrt');
return stored;
},
},
window: { matchMedia: () => ({ matches: prefersDark }) },
document: { documentElement: { setAttribute: (name: string, value: string) => (attributes[name] = value) } },
};
runInNewContext(script, sandbox);
return { attributes, sandbox };
}
it('ist da und läuft ohne Browser-Umgebung durch', () => {
expect(script).toContain('data-theme');
expect(() => run(null, true)).not.toThrow();
});
it('legt keine globale Variable an (Skript steckt in einer sofort aufgerufenen Funktion)', () => {
const { sandbox } = run('dark', false);
expect(Object.keys(sandbox).sort()).toEqual(['document', 'localStorage', 'window']);
expect('theme' in sandbox).toBe(false);
});
it.each([
['dark', false],
['light', true],
['dark', true],
['light', false],
['blau', true],
['blau', false],
['', true],
[null, true],
[null, false],
] as const)('gleiche Regel wie initialTheme(): gespeichert %j, Systemvorgabe dunkel %s', (stored, prefersDark) => {
expect(run(stored, prefersDark).attributes['data-theme']).toBe(initialTheme(stored, prefersDark));
});
it('gesperrter localStorage: es gilt die Systemvorgabe', () => {
expect(run(null, true, true).attributes['data-theme']).toBe('dark');
expect(run(null, false, true).attributes['data-theme']).toBe('light');
});
});
+3
View File
@@ -126,6 +126,9 @@ describe('mountGalleryGrid: Vorschaubilder', () => {
expect(load.mock.calls.map((c) => (c as unknown as [GalleryCard])[0].id)).toEqual(['g1', 'g2']);
const srcs = [...host.querySelectorAll<HTMLImageElement>('.gallery-thumb img')].map((i) => i.getAttribute('src'));
expect(srcs).toEqual(['blob:t-1', 'blob:t-2']);
// Vorschau 480 breit, Höhe nach 1536 : 969 (wie exportCard sie rendert)
const first = host.querySelector('.gallery-thumb img');
expect([first?.getAttribute('width'), first?.getAttribute('height')]).toEqual(['480', '303']);
});
it('gibt alle Object-URLs beim Aufräumen frei', async () => {
+4 -7
View File
@@ -1,7 +1,9 @@
// Kachelraster der Community-Galerie: Vorschau, Titel, Herausgeber, bis zu drei Tags.
// Die Vorschaubilder kommen über `load` (WebP 480 px aus dem Galerie-Bucket, mit Pixelgrenze geprüft: loadGalleryImageChecked)
// als fertige Object-URL; `dispose()` gibt alle URLs wieder frei. Alle Texte gehen als Text in den DOM, nie als HTML.
import { CARD_H, CARD_W, PREVIEW_WIDTH } from '../card/geometry';
import { el } from '../editor/dom';
import { reportsLabel } from '../pages/format';
import type { CheckedImage, GalleryCard } from './gallery';
export interface GalleryGridOptions {
@@ -22,13 +24,8 @@ export interface GalleryGrid {
const PARALLEL = 6;
const MAX_TAGS = 3;
const THUMB_W = 480;
const THUMB_H = 303; // 480 x 1536:969
/** „1 Meldung“, „3 Meldungen“. */
function reportsLabel(count: number): string {
return count === 1 ? '1 Meldung' : `${count} Meldungen`;
}
const THUMB_W = PREVIEW_WIDTH;
const THUMB_H = Math.round((THUMB_W * CARD_H) / CARD_W); // wie exportCard: 480 x 1536:969 = 303
export function mountGalleryGrid(container: HTMLElement, opts: GalleryGridOptions): GalleryGrid {
const list = el('ul', { class: 'gallery-grid' });
+2 -1
View File
@@ -1,5 +1,6 @@
// Eine gespeicherte Karte in der Community-Galerie veröffentlichen: rendern, zwei Dateien hochladen, API rufen.
// Scheitert danach irgendetwas, löscht der Client genau die zwei Dateien dieses Versuchs wieder.
// Aufgeräumt wird nur bei eindeutigem Scheitern (Upload-Fehler, eindeutige Ablehnung der API): dann löscht der Client genau
// die zwei Dateien dieses Versuchs wieder. Bleibt der Ausgang offen, bleiben sie liegen (siehe publishSavedCard).
import { ID, Permission, Role } from 'appwrite';
import { IDS, storage } from '../account/appwrite';
import { cardsService, requireUserId, toUploadFile, type SavedCard } from '../account/cards';
+1 -1
View File
@@ -205,7 +205,7 @@ export function mountEditor(root: HTMLElement, store: EditorStore): () => void {
onStatus: onSaveStatus,
});
// Karte öffnen (/?karte=<id>): nur die zuletzt angeforderte zählt.
// Karte öffnen (/gestalten?karte=<id>): nur die zuletzt angeforderte zählt.
let loadToken = 0;
function cancelCardLoad(): void {
loadToken += 1;
+2 -9
View File
@@ -16,10 +16,11 @@ import {
type GalleryReport,
} from '../community/gallery';
import { REPORT_REASONS, reportDialog } from '../community/report-dialog';
import { CARD_H, CARD_W } from '../card/geometry';
import { confirmDialog, type ConfirmOptions } from '../editor/confirm-dialog';
import { el } from '../editor/dom';
import { downloadFileName, saveBlobAs } from '../editor/download';
import { formatDate } from './format';
import { formatDate, reportsLabel } from './format';
import { navigate, titleFor } from './router';
/** Alles, was die Seite von außen braucht (für Tests austauschbar). */
@@ -56,9 +57,6 @@ export const defaultDeps: CommunityDeps = {
withLogin: (fn) => withLogin(fn),
};
const CARD_W = 1536;
const CARD_H = 969;
const UNAVAILABLE = 'Diese Karte ist nicht (mehr) verfügbar.';
const LOADING = 'Karte wird geladen …';
const LOAD_ERROR = 'Die Karte konnte gerade nicht geladen werden.';
@@ -76,11 +74,6 @@ interface ReportsState {
list: GalleryReport[];
}
/** „1 Meldung“, „3 Meldungen“. */
function reportsLabel(count: number): string {
return count === 1 ? '1 Meldung' : `${count} Meldungen`;
}
/** Der Text für den Nutzer: Servertexte (deutsch) unverändert, alles andere über errorMessage. */
function failureText(err: unknown): string {
if (err instanceof LoginCancelled) return LOGIN_NEEDED;
+10 -1
View File
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest';
import { formatDate } from './format';
import { formatDate, reportsLabel } from './format';
describe('formatDate', () => {
it('schreibt dd.mm.yyyy', () => {
@@ -11,3 +11,12 @@ describe('formatDate', () => {
expect(formatDate('kaputt')).toBe('');
});
});
describe('reportsLabel', () => {
it('„1 Meldung“, sonst „n Meldungen“', () => {
expect(reportsLabel(1)).toBe('1 Meldung');
expect(reportsLabel(0)).toBe('0 Meldungen');
expect(reportsLabel(2)).toBe('2 Meldungen');
expect(reportsLabel(12)).toBe('12 Meldungen');
});
});
+6 -1
View File
@@ -1,4 +1,4 @@
// Gemeinsame Formatierung der Seiten (Meine Karten, Community-Detailseite).
// Gemeinsame Formatierung der Seiten (Meine Karten, Community-Detailseite, Galerie-Raster).
/** `dd.mm.yyyy` in der Zeitzone des Betrachters; leer, wenn das Datum unlesbar ist. */
export function formatDate(iso: string): string {
@@ -7,3 +7,8 @@ export function formatDate(iso: string): string {
const pad = (n: number) => String(n).padStart(2, '0');
return `${pad(date.getDate())}.${pad(date.getMonth() + 1)}.${date.getFullYear()}`;
}
/** „1 Meldung“, „3 Meldungen“. */
export function reportsLabel(count: number): string {
return count === 1 ? '1 Meldung' : `${count} Meldungen`;
}
+1 -2
View File
@@ -11,6 +11,7 @@ import {
type GalleryFilter,
type GalleryOptions,
} from '../community/gallery';
import { CARD_H, CARD_W } from '../card/geometry';
import { el } from '../editor/dom';
import { navigate, scrollToHash } from './router';
@@ -38,8 +39,6 @@ export const defaultDeps: HomeDeps = {
const EXAMPLE_IMAGE = '/hero-beispiel.png';
const EXAMPLE_ALT = 'Beispielmotiv: violetter Verlauf mit Wellen und dem AirCard-IOS-Symbol';
const CARD_W = 1536;
const CARD_H = 969;
const LOADING = 'Karten werden geladen …';
const LOAD_ERROR = 'Die Community-Karten konnten gerade nicht geladen werden.';
+31
View File
@@ -0,0 +1,31 @@
import { readdirSync, readFileSync, statSync } from 'node:fs';
import { join, relative } from 'node:path';
import { describe, expect, it } from 'vitest';
/** Alle Quelldateien (ohne Tests) unter src/ als [Pfad relativ zu src, Text]. */
function sources(dir = import.meta.dirname): Array<[string, string]> {
const out: Array<[string, string]> = [];
for (const name of readdirSync(dir)) {
const path = join(dir, name);
if (statSync(path).isDirectory()) out.push(...sources(path));
else if (/\.ts$/.test(name) && !/\.test\.ts$/.test(name)) out.push([relative(import.meta.dirname, path), readFileSync(path, 'utf8')]);
}
return out;
}
const definedIn = (pattern: RegExp) =>
sources()
.filter(([, text]) => pattern.test(text))
.map(([path]) => path)
.sort();
describe('Einzelne Quellen', () => {
it('CARD_W und CARD_H werden nur in card/geometry.ts definiert', () => {
expect(definedIn(/\b(?:const|let|var)\s+CARD_W\b/)).toEqual(['card/geometry.ts']);
expect(definedIn(/\b(?:const|let|var)\s+CARD_H\b/)).toEqual(['card/geometry.ts']);
});
it('reportsLabel wird nur in pages/format.ts definiert', () => {
expect(definedIn(/\bfunction\s+reportsLabel\b/)).toEqual(['pages/format.ts']);
});
});