diff --git a/README.md b/README.md index 012d6b1..a7c7615 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# AirCard +# AirCard-IOS -Inoffizielles Hobbyprojekt: ein Editor, mit dem man eigene Kartenmotive für https://aircard-ios.org gestalten kann. Keine Verbindung zu Apple, Visa, Mastercard, UnionPay, American Express, JCB oder Discover. +Inoffizielles Hobbyprojekt: AirCard-IOS ist ein Editor, mit dem man eigene Kartenmotive für https://aircard-ios.org gestalten kann. Keine Verbindung zu Apple, Visa, Mastercard, UnionPay, American Express, JCB oder Discover. Die Website liegt in `web/` (statisches Vite + TypeScript, ohne UI-Framework). Die Design-Spec steht in `docs/superpowers/specs/2026-10-04-aircard-website-design.md`. diff --git a/web/index.html b/web/index.html index b831992..5f63c7b 100644 --- a/web/index.html +++ b/web/index.html @@ -3,12 +3,16 @@ - AirCard – Kartenmotiv gestalten - + AirCard-IOS – Kartenmotiv gestalten + + + + +
- AirCard + AirCard-IOS diff --git a/web/public/apple-touch-icon.png b/web/public/apple-touch-icon.png new file mode 100644 index 0000000..c8fa9c4 Binary files /dev/null and b/web/public/apple-touch-icon.png differ diff --git a/web/public/favicon-32.png b/web/public/favicon-32.png new file mode 100644 index 0000000..11290a5 Binary files /dev/null and b/web/public/favicon-32.png differ diff --git a/web/public/favicon.svg b/web/public/favicon.svg index 6b6c70f..e21f455 100644 --- a/web/public/favicon.svg +++ b/web/public/favicon.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/web/src/assets/marks/aircards-symbol.svg b/web/src/assets/marks/aircards-symbol.svg index bad8629..2b07e64 100644 --- a/web/src/assets/marks/aircards-symbol.svg +++ b/web/src/assets/marks/aircards-symbol.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/web/src/assets/marks/aircards-text.svg b/web/src/assets/marks/aircards-text.svg index 85b8427..07963fd 100644 --- a/web/src/assets/marks/aircards-text.svg +++ b/web/src/assets/marks/aircards-text.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/web/src/branding.test.ts b/web/src/branding.test.ts new file mode 100644 index 0000000..ba5e1c7 --- /dev/null +++ b/web/src/branding.test.ts @@ -0,0 +1,49 @@ +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { describe, expect, it } from 'vitest'; +import indexHtml from '../index.html?raw'; + +const publicFile = (name: string) => readFileSync(resolve(import.meta.dirname, '../public', name)); + +/** Breite, Höhe und Farbtyp aus dem IHDR-Block eines PNG (Farbtyp 2 = RGB ohne Alphakanal, 6 = RGBA). */ +function pngInfo(buf: Buffer) { + expect(buf.subarray(0, 8).toString('hex')).toBe('89504e470d0a1a0a'); + expect(buf.subarray(12, 16).toString('ascii')).toBe('IHDR'); + return { width: buf.readUInt32BE(16), height: buf.readUInt32BE(20), colorType: buf[25] }; +} + +describe('Name und Kopfbereich', () => { + it('Titel und Meta-Angaben nennen AirCard-IOS', () => { + expect(indexHtml).toContain('AirCard-IOS – Kartenmotiv gestalten'); + expect(indexHtml).toContain(''); + expect(indexHtml).toContain(''); + }); + + it('Icon-Links: SVG, 32-px-PNG, Apple-Touch-Icon', () => { + expect(indexHtml).toContain(''); + expect(indexHtml).toContain(''); + expect(indexHtml).toContain(''); + }); + + it('Kopfzeile: dekoratives Logo (28 x 28) links vom Namen, beides im Markenlink', () => { + expect(indexHtml).toContain( + 'AirCard-IOS', + ); + }); + + it('Fußzeilen-Hinweis bleibt unverändert', () => { + expect(indexHtml).toContain( + 'Inoffizielles Hobbyprojekt. Keine Verbindung zu Apple, Visa, Mastercard, UnionPay, American Express, JCB oder Discover. Alle Marken gehören ihren Inhabern.', + ); + }); +}); + +describe('Icon-Dateien', () => { + it('favicon-32.png ist 32 x 32', () => { + expect(pngInfo(publicFile('favicon-32.png'))).toMatchObject({ width: 32, height: 32 }); + }); + + it('apple-touch-icon.png ist 180 x 180 und deckend (RGB ohne Alphakanal, kein transparenter Rand)', () => { + expect(pngInfo(publicFile('apple-touch-icon.png'))).toEqual({ width: 180, height: 180, colorType: 2 }); + }); +}); diff --git a/web/src/card/marks.test.ts b/web/src/card/marks.test.ts index d680ce3..34bb14b 100644 --- a/web/src/card/marks.test.ts +++ b/web/src/card/marks.test.ts @@ -29,7 +29,7 @@ describe('Registers', () => { it('Labels stehen im Wortlaut der Vorlage in Reihenfolge', () => { expect(NETWORK_MARKS.map((m) => m.label)).toEqual(NETWORK_LABELS); - expect(BRAND_MARKS.map((m) => m.label)).toEqual(['AirCards · Symbol', 'AirCards · Text', 'Apple']); + expect(BRAND_MARKS.map((m) => m.label)).toEqual(['AirCard-IOS · Symbol', 'AirCard-IOS · Text', 'Apple']); }); it('BRAND_MARKS: Logo-Gruppe, feste Reihenfolge', () => { @@ -51,7 +51,7 @@ describe('Registers', () => { it('getNetworkMark / getBrandMark finden den Eintrag', () => { for (const id of NETWORK_IDS) expect(getNetworkMark(id).id).toBe(id); expect(getBrandMark('apple').id).toBe('apple'); - expect(getBrandMark('aircards-text').label).toBe('AirCards · Text'); + expect(getBrandMark('aircards-text').label).toBe('AirCard-IOS · Text'); }); }); @@ -163,12 +163,62 @@ describe('maxH', () => { }); }); +describe('AirCard-IOS-Marken', () => { + const symbol = getBrandMark('aircards-symbol').svg; + const text = getBrandMark('aircards-text').svg; + + it('Symbol: drei Karten mit Deckkraft .45/.7/1, Wellen als Maske aus der vorderen Karte geschnitten', () => { + expect(symbol).toContain('fill-opacity=".45"'); + expect(symbol).toContain('fill-opacity=".7"'); + expect(symbol).toMatch(/]*\bid="aircards-symbol-[\w-]+"/); + expect(symbol).toMatch(/]*\bmask="url\(#aircards-symbol-[\w-]+\)"/); + expect(symbol).not.toMatch(/fill-opacity="1"/); + }); + + it('Symbol: enge viewBox um die drei Karten (680 x 730)', () => { + expect(svgAspect(symbol)).toBeCloseTo(680 / 730, 3); + }); + + it('Text: nur Pfade, ein breites Wortlogo', () => { + expect(text).toContain(', keine Schrift, keine externen Verweise', () => { + for (const svg of [symbol, text]) { + expect(svg).not.toContain(' { + for (const svg of [symbol, text]) { + const colours = [...svg.matchAll(/\b(?:fill|stroke)="([^"]+)"/g)].map((h) => h[1].toLowerCase()); + for (const c of colours) expect(['currentcolor', 'none', '#fff', '#000'], c).toContain(c); + } + expect(text).not.toMatch(/#(?:fff|000)/i); + }); +}); + describe('Favicon', () => { - it('hat feste Farben und kein currentColor', () => { + it('ist das neue Logo: 1024er Raster, feste Farben, kein currentColor, keine Metadaten', () => { expect(favicon).not.toContain('currentColor'); - expect(favicon).toContain('#E8EEF7'); - expect(favicon).toContain('#0F1828'); - expect(favicon).toMatch(/viewBox="0 0 64 64"/); + expect(favicon).toMatch(/viewBox="0 0 1024 1024"/); + expect(favicon).toContain('#0e0a24'); + expect(favicon).toContain('#8b6cff'); + for (const bad of [' { + const defined = new Set([...favicon.matchAll(/\bid="([^"]+)"/g)].map((h) => h[1])); + const refs = [...favicon.matchAll(/url\(\s*['"]?#([^)'"\s]+)/g)].map((h) => h[1]); + expect(refs.length).toBeGreaterThan(0); + for (const ref of refs) expect(defined.has(ref), ref).toBe(true); }); }); diff --git a/web/src/card/marks.ts b/web/src/card/marks.ts index d5377b1..f5c5677 100644 --- a/web/src/card/marks.ts +++ b/web/src/card/marks.ts @@ -63,8 +63,8 @@ const NETWORK_TABLE: Record = { }; const BRAND_TABLE: Record, NetworkEntry> = { - 'aircards-symbol': { group: 'Logo', label: 'AirCards · Symbol', svg: aircardsSymbol, tint: true, maxH: 100 }, - 'aircards-text': { group: 'Logo', label: 'AirCards · Text', svg: aircardsText, tint: true, maxH: 100 }, + 'aircards-symbol': { group: 'Logo', label: 'AirCard-IOS · Symbol', svg: aircardsSymbol, tint: true, maxH: 100 }, + 'aircards-text': { group: 'Logo', label: 'AirCard-IOS · Text', svg: aircardsText, tint: true, maxH: 100 }, apple: { group: 'Logo', label: 'Apple', svg: apple, tint: true, maxH: 100 }, }; diff --git a/web/src/styles.css b/web/src/styles.css index 2872b5d..5c1dbf2 100644 --- a/web/src/styles.css +++ b/web/src/styles.css @@ -57,11 +57,35 @@ a { } .brand { + display: inline-flex; + align-items: center; + gap: 10px; color: var(--text); font-size: 1.35rem; font-weight: 800; letter-spacing: 0.01em; text-decoration: none; + white-space: nowrap; +} + +.brand img { + display: block; + flex: none; + width: 28px; + height: 28px; +} + +/* Schmale Bildschirme: Marke etwas kleiner, damit sie samt „Meine Karten“ in die erste Kopfzeile passt */ +@media (max-width: 599px) { + .brand { + gap: 8px; + font-size: 1.2rem; + } + + .brand img { + width: 24px; + height: 24px; + } } .app-nav a {