From d64c14eae1c4a8860f51f099d4a341d9aeb251eb Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Oct 2026 09:58:12 +0000 Subject: [PATCH] =?UTF-8?q?feat(brand):=20AirCard-IOS=20=E2=80=93=20neues?= =?UTF-8?q?=20Logo=20und=20Name?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Name AirCard-IOS in Kopfzeile, Titel, Meta-Angaben (theme-color, apple-mobile-web-app-title) und README - Website-Icon: neues Logo als favicon.svg, favicon-32.png und deckendes apple-touch-icon.png (180 x 180, ohne Rundung) - Kopfzeile zeigt das Logo (28 px, auf schmalen Schirmen 24 px) vor dem Namen - Karten-Marken neu aus dem Logo abgeleitet (einfarbig, currentColor): Symbol mit drei gestaffelten Karten und ausgeschnittenen Wellen, Text als Pfade (Nunito Sans 800); IDs unverändert - Tests: Labels, Marken-Aufbau, Favicon, Icon-Dateien, index.html Co-Authored-By: Claude Sonnet 5.5 --- README.md | 4 +- web/index.html | 10 ++-- web/public/apple-touch-icon.png | Bin 0 -> 10878 bytes web/public/favicon-32.png | Bin 0 -> 1731 bytes web/public/favicon.svg | 2 +- web/src/assets/marks/aircards-symbol.svg | 2 +- web/src/assets/marks/aircards-text.svg | 2 +- web/src/branding.test.ts | 49 ++++++++++++++++++ web/src/card/marks.test.ts | 62 ++++++++++++++++++++--- web/src/card/marks.ts | 4 +- web/src/styles.css | 24 +++++++++ 11 files changed, 143 insertions(+), 16 deletions(-) create mode 100644 web/public/apple-touch-icon.png create mode 100644 web/public/favicon-32.png create mode 100644 web/src/branding.test.ts 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 0000000000000000000000000000000000000000..c8fa9c45a0780e931b66806aa3260684ea93c12c GIT binary patch literal 10878 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 G!~PFm8$xCP literal 0 HcmV?d00001 diff --git a/web/public/favicon-32.png b/web/public/favicon-32.png new file mode 100644 index 0000000000000000000000000000000000000000..11290a5658f2e0a68b3719703498517526e532cc GIT binary patch literal 1731 zcmV;!20ZzRP)E}rR7EaIL28SwEqH-QqP}<`hD77t#8|vV6BB*V7#}pg`KBhu z7>zFyBN*eGmy)P48l|CV(BcJ438*c_S~_j#obC7TJu?gh10=%Q=Ra%hb@o~Rzb|vz z$SSO%p}oOoChl?OZgI}m+5c!!WPOoETP<7n*4SA87%6XM0S$Fa+acWo`9eF5hKSz~ zvtevvu%9k{OysF^+X>H@#ZlBD5s{YNO;2n;-7`Pd{7U5NI--WUC6A}hUM6?OEspGo zsJ?DVAA7%+{uwvU!nSDktR;^4q%4jyyrg3T@MQ55&#@K1~aFgdN9@ zD@^?^l=zR7ixT7)V6wAeWGI#u9`aYexyt%se6FaU>uFq6k4j z1aMkZ8kV;~4HfG>$C3&>=0l+JqJbk1U1b?MDKg_Nua zBv9leX~C1^&T~8+8hIawoSKhmU-iFf2S#>Ul1zIM@-i~@)c!`Udbp`<%R zH1}BZoHkx76;~l*3e%w}R*smQ`u=&bmB*{`T=DqfVcY)WYmf)x1OX|l5H+5?!fE!I zxZ2{3Rv>51ja@dUeg%anz{@HiLiGr^D<)rxOV5B}jh6^|zNtTvAGYs*djoP2BOsmf zs}MEKZet4|hgjs9sM;Fq+pT-$oz^mcAzMQ&5#d$K#|q(4;2k-|!O?t@i-f|Vy$9^u z9bees@LSA$3UWjMiHPRRUgfITf*?Rbvev%je!HyodS8HxN)QondFbXH)k|Zzg7?sk z%L5Vk{(*k`?7Qb7F3uhS1#Q8(^_^ZoCawVzkh9L#M{MZ@t9dyTI8sbPg7LMiXbk|7)QWG z%oR`~(<5$W6!VA14%)jPZ-SWFLtJ40(`3q`dFS-F7}6?MKmz1fUes%sEbg|sb4A75 znEzN5Eo6?lYXDBBP#r#s-lnF^Mn^~O{k})7z|6e>dBh-Kwaa8`Jh7I)iq+_13wj^B z(rRYK3^Ggjg!4t^<6*v^$`_H)=d0`EY^~W@XPfOGqR%^iESSza`(fZO+p_HumYo$~ z8zdk}@(5_Wk~6^mbNclal#qw+f61;`b0GsLAW)=Ku$&W$eF>IkSPU zcuqb|93q_^*gtM>f6!Yb<{a>AC`iD0jaRV(kVl9>fDYrM54>n?mn~$V3Iu4TWf4WM zyiijYd}}AO-(z-hyV=L@nH?VIk2B;6BF6tdY;SITkUAv+@@zp60C{l*B;elLp0&=) zTNzAY$Vy%$M~MO@zm`88LxX{QY{c0w-#Z%|Kmg0BnJClTSx2E!Q`)K|A;sEC@ft z8e6~SZd<+TMg~GqCIZCrWf2svL>(d;=9ul?8J50K2zlY)KZEw|&o3ZAzkfbL>C~{* z*0e=@C+z&DwOuaGejQdJXY0D}u+<&6RuEN?dLN3O3P#6&wEjIW60`i4ACHp4`k|3+ z-*^GCmU-7+5vSQLoPqh*blqt^Z5tU3F$ko<`%v&yF*dQwb_~71zEAB- + \ 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 {