From 0be5abd954a675a8dfbcb45fd7e91f0ede749662 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Oct 2026 20:31:36 +0000 Subject: [PATCH] feat(card): Marken-Logos entfernt, Community-Logo unten rechts im Renderer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - card/config.ts: network und das Apple-Logo entfallen. CardConfig.logo ist die ID eines Community-Logos (Muster ID_PATTERN, sonst null), DEFAULT_CONFIG.logo = null, serializeConfig schreibt {v, template, logo, details, brand, crop}. Ein altes network wird ignoriert, brand 'apple' fällt auf den Standard. ID_PATTERN steht jetzt hier (die reine Konfiguration prüft die Logo-ID, ohne Appwrite zu laden), account/cards.ts reicht sie weiter; single-sources.test.ts sorgt für genau eine Definition. - card/geometry.ts: fitLogo und LOGO_MAX_H (125) ersetzen fitNetwork, Layout.network heißt Layout.logo. - card/marks.ts: nur noch die AirCard-IOS-Marken (BRAND_MARKS, getBrandMark, svgAspect, tintSvg, loadMarkImage). Die 19 SVGs der Netzwerke und von Apple sind per git rm gelöscht, ein Test hält assets/marks auf die zwei AirCard-Dateien. - card/render.ts: LogoAsset, CardAssets.logo statt network, LoadedAssets.logoMissing. loadCardAssets(config, image, loadLogo = loadLogoAsset): ohne Logo kein Aufruf, null vom Lader ergibt logoMissing, ein Ladefehler wirft. Der Renderer zeichnet das Logo mit fitLogo(w / h) unten rechts, wenn Vorlage und Bilder eines haben. - logos/logo-asset.ts (neu): loadLogoAsset(id, tone) holt Zeile (getLogo) und PNG (loadLogoBlob) und dekodiert mit createImageBitmap, sonst über ein Image. Einfarbige Logos werden in einem eigenen Canvas mit source-in in der Details-Farbe gefüllt, mehrfarbige bleiben. Fehlende Zeile, ausgeblendetes Logo (auch für Hochlader und Admins), Datei 404, unlesbare Datei und mehr als 1 000 000 Pixel ergeben null. Je (id, tone) ein Ladevorgang, ein Fehler bleibt nicht im Cache. rememberLogo, forgetLogo und LOGO_MISSING nach der Schnittstelle des Plans. - editor/options-panel.ts: keine Netzwerk-Kacheln mehr, Überschrift „Logo oben links“ mit Symbol, Text und Kein Logo (ohne Apple), dazu logoSlot als leerer Platz oben für Task 8 (editor.css: Details rückt bei leerem Platz wie bisher nach oben). - editor/editor-page.ts: logoKey aus Logo, Details und Marke, der Logo-Zustand hält missing. Fehlt das Logo, wird die Karte ohne es gezeichnet und der Hinweis LOGO_MISSING gezeigt, solange das so bleibt; ein Wechsel auf ein vorhandenes Logo oder auf kein Logo nimmt nur diesen Hinweis weg (PNG-Größe und „Bild fehlt“ stehen getrennt und bleiben). Kein LOGO_ERROR bei fehlendem Logo, nur bei echtem Ladefehler, auch beim Download. - community/publish.ts: LOGO_MISSING_PUBLISH, renderForGallery (jetzt exportiert) bricht bei logoMissing vor dem Rendern ab, es wird nichts hochgeladen. publish-dialog.ts lässt den Text im Wortlaut durch. - i18n/en.ts: englische Texte zu LOGO_MISSING, LOGO_MISSING_PUBLISH und „Logo oben links“. - Tests: config, geometry, marks, render und die Fixtures von cards, theme, cards-page auf das neue Modell; neu logo-asset.test.ts (28) und options-panel.test.ts (10); editor-page (14 neue), publish und publish-dialog erweitert. Vitest 45 Dateien, 1319 Tests grün, tsc und Build sauber. Co-Authored-By: Claude Opus 5.5 --- web/src/account/cards.test.ts | 4 +- web/src/account/cards.ts | 6 +- web/src/assets/marks/amex-blue.svg | 1 - web/src/assets/marks/amex-outline.svg | 1 - web/src/assets/marks/amex-select.svg | 1 - web/src/assets/marks/apple.svg | 1 - web/src/assets/marks/discover.svg | 1 - web/src/assets/marks/jcb.svg | 1 - web/src/assets/marks/mc-classic.svg | 1 - web/src/assets/marks/mc-color.svg | 1 - web/src/assets/marks/mc-silver-text.svg | 1 - web/src/assets/marks/mc-silver.svg | 1 - web/src/assets/marks/mc-wordmark.svg | 1 - web/src/assets/marks/up-color.svg | 1 - web/src/assets/marks/up-english.svg | 1 - web/src/assets/marks/up-silver.svg | 1 - web/src/assets/marks/up-white-frame.svg | 1 - web/src/assets/marks/visa-auto.svg | 1 - web/src/assets/marks/visa-blue.svg | 1 - web/src/assets/marks/visa-navy.svg | 1 - web/src/assets/marks/visa-silver.svg | 1 - web/src/card/config.test.ts | 71 ++-- web/src/card/config.ts | 33 +- web/src/card/geometry.test.ts | 67 ++-- web/src/card/geometry.ts | 30 +- web/src/card/marks.test.ts | 157 +++------ web/src/card/marks.ts | 62 +--- web/src/card/render.test.ts | 130 ++++++-- web/src/card/render.ts | 49 ++- web/src/community/publish-dialog.test.ts | 22 +- web/src/community/publish-dialog.ts | 4 +- web/src/community/publish.test.ts | 95 +++++- web/src/community/publish.ts | 9 +- web/src/editor/editor-page.test.ts | 233 ++++++++++++- web/src/editor/editor-page.ts | 55 ++- web/src/editor/editor.css | 9 +- web/src/editor/options-panel.test.ts | 101 ++++++ web/src/editor/options-panel.ts | 43 ++- web/src/i18n/en.ts | 6 + web/src/logos/logo-asset.test.ts | 406 +++++++++++++++++++++++ web/src/logos/logo-asset.ts | 142 ++++++++ web/src/pages/cards-page.test.ts | 4 +- web/src/shell/theme.test.ts | 6 +- web/src/single-sources.test.ts | 4 + 44 files changed, 1401 insertions(+), 366 deletions(-) delete mode 100644 web/src/assets/marks/amex-blue.svg delete mode 100644 web/src/assets/marks/amex-outline.svg delete mode 100644 web/src/assets/marks/amex-select.svg delete mode 100644 web/src/assets/marks/apple.svg delete mode 100644 web/src/assets/marks/discover.svg delete mode 100644 web/src/assets/marks/jcb.svg delete mode 100644 web/src/assets/marks/mc-classic.svg delete mode 100644 web/src/assets/marks/mc-color.svg delete mode 100644 web/src/assets/marks/mc-silver-text.svg delete mode 100644 web/src/assets/marks/mc-silver.svg delete mode 100644 web/src/assets/marks/mc-wordmark.svg delete mode 100644 web/src/assets/marks/up-color.svg delete mode 100644 web/src/assets/marks/up-english.svg delete mode 100644 web/src/assets/marks/up-silver.svg delete mode 100644 web/src/assets/marks/up-white-frame.svg delete mode 100644 web/src/assets/marks/visa-auto.svg delete mode 100644 web/src/assets/marks/visa-blue.svg delete mode 100644 web/src/assets/marks/visa-navy.svg delete mode 100644 web/src/assets/marks/visa-silver.svg create mode 100644 web/src/editor/options-panel.test.ts create mode 100644 web/src/logos/logo-asset.test.ts create mode 100644 web/src/logos/logo-asset.ts diff --git a/web/src/account/cards.test.ts b/web/src/account/cards.test.ts index f943a65..dc08f9b 100644 --- a/web/src/account/cards.test.ts +++ b/web/src/account/cards.test.ts @@ -350,7 +350,7 @@ describe('save: bestehende Karte', () => { it('ohne Bildwechsel: nur neue Vorschau, Update, dann alte Vorschau löschen', async () => { const fake = fakeApi([existing()]); - const c = config({ network: 'jcb' }); + const c = config({ logo: 'abc123' }); const saved = await service(fake).save( newInput({ name: 'Neu', config: c, imageChanged: false, previous: previous() }), ); @@ -370,7 +370,7 @@ describe('save: bestehende Karte', () => { expect(saved.id).toBe('row1'); expect(saved.imageFileId).toBe('oldImage'); expect(saved.previewFileId).toBe('file1'); - expect(saved.config.network).toBe('jcb'); + expect(saved.config.logo).toBe('abc123'); }); it('veralteter Schnappschuss (Bild in anderem Tab ersetzt): das Update schickt kein altes imageFileId, die Zeile behält das neue Bild', async () => { diff --git a/web/src/account/cards.ts b/web/src/account/cards.ts index 6e32312..89b24a9 100644 --- a/web/src/account/cards.ts +++ b/web/src/account/cards.ts @@ -1,7 +1,7 @@ // Gespeicherte Karten: Dienst (Regeln fürs Speichern, Aufräumen, Laden) und Anschluss an Appwrite. // Der Dienst kennt nur das schmale `CardsApi`-Interface und lässt sich dadurch ohne Netzwerk testen. import { AppwriteException, ID, Permission, Query, Role, type Models } from 'appwrite'; -import { parseConfig, serializeConfig, type CardConfig } from '../card/config'; +import { ID_PATTERN, parseConfig, serializeConfig, type CardConfig } from '../card/config'; import type { SavedCardRef } from '../editor/state'; import { APPWRITE_ENDPOINT, IDS, client, storage, tables } from './appwrite'; import { currentUserId } from './auth'; @@ -46,8 +46,8 @@ export interface SaveInput { export const DEFAULT_CARD_NAME = 'Meine Karte'; const NAME_MAX = 80; -/** Zeilen- und Datei-IDs von Appwrite: höchstens 36 Zeichen, kein Sonderzeichen am Anfang. */ -export const ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,35}$/; +/** Zeilen- und Datei-IDs von Appwrite (definiert in card/config.ts, dort braucht sie auch die Logo-ID der Konfiguration). */ +export { ID_PATTERN }; /** Trimmt, fasst Leerraum zusammen und kürzt auf 80 Zeichen (Codepoints); leer ergibt „Meine Karte“. */ export function normalizeName(name: string): string { diff --git a/web/src/assets/marks/amex-blue.svg b/web/src/assets/marks/amex-blue.svg deleted file mode 100644 index ccb37e6..0000000 --- a/web/src/assets/marks/amex-blue.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/web/src/assets/marks/amex-outline.svg b/web/src/assets/marks/amex-outline.svg deleted file mode 100644 index a569042..0000000 --- a/web/src/assets/marks/amex-outline.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/web/src/assets/marks/amex-select.svg b/web/src/assets/marks/amex-select.svg deleted file mode 100644 index ccb37e6..0000000 --- a/web/src/assets/marks/amex-select.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/web/src/assets/marks/apple.svg b/web/src/assets/marks/apple.svg deleted file mode 100644 index 31b057a..0000000 --- a/web/src/assets/marks/apple.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/web/src/assets/marks/discover.svg b/web/src/assets/marks/discover.svg deleted file mode 100644 index d55863a..0000000 --- a/web/src/assets/marks/discover.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/web/src/assets/marks/jcb.svg b/web/src/assets/marks/jcb.svg deleted file mode 100644 index 133310f..0000000 --- a/web/src/assets/marks/jcb.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/web/src/assets/marks/mc-classic.svg b/web/src/assets/marks/mc-classic.svg deleted file mode 100644 index 37a8330..0000000 --- a/web/src/assets/marks/mc-classic.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/web/src/assets/marks/mc-color.svg b/web/src/assets/marks/mc-color.svg deleted file mode 100644 index 1aef221..0000000 --- a/web/src/assets/marks/mc-color.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/web/src/assets/marks/mc-silver-text.svg b/web/src/assets/marks/mc-silver-text.svg deleted file mode 100644 index 4aa4250..0000000 --- a/web/src/assets/marks/mc-silver-text.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/web/src/assets/marks/mc-silver.svg b/web/src/assets/marks/mc-silver.svg deleted file mode 100644 index fb19c12..0000000 --- a/web/src/assets/marks/mc-silver.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/web/src/assets/marks/mc-wordmark.svg b/web/src/assets/marks/mc-wordmark.svg deleted file mode 100644 index c24ddcc..0000000 --- a/web/src/assets/marks/mc-wordmark.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/web/src/assets/marks/up-color.svg b/web/src/assets/marks/up-color.svg deleted file mode 100644 index 8d51b91..0000000 --- a/web/src/assets/marks/up-color.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/web/src/assets/marks/up-english.svg b/web/src/assets/marks/up-english.svg deleted file mode 100644 index 8c92134..0000000 --- a/web/src/assets/marks/up-english.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/web/src/assets/marks/up-silver.svg b/web/src/assets/marks/up-silver.svg deleted file mode 100644 index 9d9dd36..0000000 --- a/web/src/assets/marks/up-silver.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/web/src/assets/marks/up-white-frame.svg b/web/src/assets/marks/up-white-frame.svg deleted file mode 100644 index 4660669..0000000 --- a/web/src/assets/marks/up-white-frame.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/web/src/assets/marks/visa-auto.svg b/web/src/assets/marks/visa-auto.svg deleted file mode 100644 index 1f64047..0000000 --- a/web/src/assets/marks/visa-auto.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/web/src/assets/marks/visa-blue.svg b/web/src/assets/marks/visa-blue.svg deleted file mode 100644 index 9fe1811..0000000 --- a/web/src/assets/marks/visa-blue.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/web/src/assets/marks/visa-navy.svg b/web/src/assets/marks/visa-navy.svg deleted file mode 100644 index 739c9bb..0000000 --- a/web/src/assets/marks/visa-navy.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/web/src/assets/marks/visa-silver.svg b/web/src/assets/marks/visa-silver.svg deleted file mode 100644 index 673abb0..0000000 --- a/web/src/assets/marks/visa-silver.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/web/src/card/config.test.ts b/web/src/card/config.test.ts index 07ebd07..e807ae4 100644 --- a/web/src/card/config.test.ts +++ b/web/src/card/config.test.ts @@ -2,7 +2,7 @@ import { describe, it, expect } from 'vitest'; import { DEFAULT_CONFIG, DEFAULT_CROP, - NETWORK_IDS, + ID_PATTERN, TONE_COLOR, parseConfig, serializeConfig, @@ -12,9 +12,9 @@ import { const custom: CardConfig = { v: 1, template: 'classic', - network: 'jcb', + logo: 'abc123', details: 'dark', - brand: 'apple', + brand: 'aircards-text', crop: { x: -120.5, y: 33, zoom: 2.5 }, }; @@ -23,7 +23,7 @@ describe('Standardwerte', () => { expect(DEFAULT_CONFIG).toEqual({ v: 1, template: 'signature', - network: 'visa-auto', + logo: null, details: 'light', brand: 'aircards-symbol', crop: { x: 0, y: 0, zoom: 1 }, @@ -36,16 +36,13 @@ describe('Standardwerte', () => { expect(DEFAULT_CONFIG.crop).not.toBe(DEFAULT_CROP); }); - it('NETWORK_IDS: 18 Logos in der vereinbarten Reihenfolge', () => { - expect(NETWORK_IDS.length).toBe(18); - expect([...NETWORK_IDS]).toEqual([ - 'visa-auto', 'visa-blue', 'visa-navy', 'visa-silver', - 'mc-color', 'mc-silver', 'mc-wordmark', 'mc-silver-text', 'mc-classic', - 'up-color', 'up-silver', 'up-white-frame', 'up-english', - 'amex-blue', 'amex-outline', 'amex-select', - 'jcb', 'discover', - ]); - expect(new Set(NETWORK_IDS).size).toBe(18); + it('eine neue Karte hat kein Logo unten rechts', () => { + expect(DEFAULT_CONFIG.logo).toBeNull(); + }); + + it('ID_PATTERN: Appwrite-IDs mit höchstens 36 Zeichen und ohne Sonderzeichen am Anfang', () => { + for (const ok of ['abc123', 'A', '0', 'a.b_c-d', 'x'.repeat(36)]) expect(ID_PATTERN.test(ok), ok).toBe(true); + for (const bad of ['', 'kaputt id', '-abc', '_abc', '.abc', 'ä', 'a/b', 'x'.repeat(37)]) expect(ID_PATTERN.test(bad), bad).toBe(false); }); it('TONE_COLOR: Hell weiß, Dunkel fast schwarz', () => { @@ -72,9 +69,31 @@ describe('parseConfig / serializeConfig', () => { } }); - it('unbekanntes Netzwerk setzt nur dieses Feld auf den Standard', () => { - const raw = JSON.stringify({ ...custom, network: 'paypal' }); - expect(parseConfig(raw)).toEqual({ ...custom, network: DEFAULT_CONFIG.network }); + it('eine alte Konfiguration mit network und Apple-Logo: network entfällt, Logo oben links fällt auf den Standard', () => { + const raw = '{"v":1,"template":"classic","network":"mc-color","brand":"apple","details":"dark"}'; + expect(parseConfig(raw)).toEqual({ + v: 1, + template: 'classic', + logo: null, + details: 'dark', + brand: 'aircards-symbol', + crop: { x: 0, y: 0, zoom: 1 }, + }); + // andere unbekannte Marken ebenso + expect(parseConfig('{"brand":"visa"}').brand).toBe('aircards-symbol'); + }); + + it('logo: eine gültige ID bleibt, alles andere ergibt null', () => { + expect(parseConfig('{"logo":"abc123"}').logo).toBe('abc123'); + expect(parseConfig(JSON.stringify({ logo: 'x'.repeat(36) })).logo).toBe('x'.repeat(36)); + for (const bad of ['kaputt id', '', '-abc', 'x'.repeat(37), 42, true, null, ['abc'], { id: 'abc' }]) { + expect(parseConfig(JSON.stringify({ logo: bad })).logo, JSON.stringify(bad)).toBeNull(); + } + expect(parseConfig('{}').logo).toBeNull(); + }); + + it('das gewählte Logo bleibt nur dem Feld logo zugeordnet: ein altes network wird nie zum Logo', () => { + expect(parseConfig('{"network":"abc123"}').logo).toBeNull(); }); it('jedes Feld wird einzeln validiert', () => { @@ -88,11 +107,11 @@ describe('parseConfig / serializeConfig', () => { }); it('Namen aus Object.prototype gelten nicht als gültige Werte', () => { - const raw = JSON.stringify({ ...custom, network: 'constructor', template: '__proto__', brand: 'toString' }); + const raw = JSON.stringify({ ...custom, template: '__proto__', details: 'constructor', brand: 'toString' }); expect(parseConfig(raw)).toEqual({ ...custom, - network: DEFAULT_CONFIG.network, template: DEFAULT_CONFIG.template, + details: DEFAULT_CONFIG.details, brand: DEFAULT_CONFIG.brand, }); }); @@ -137,21 +156,29 @@ describe('parseConfig / serializeConfig', () => { const raw = JSON.stringify({ ...custom, evil: '