feat(marke): Logo, Banner, Favicons und Manifest-Icons
Alle Varianten aus dem Symbol der Vorgabe per Skript (pnpm marke); Texte als Pfade, damit Wortmarke und Banner ohne installierte Schrift gleich aussehen. Closes #017 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@@ -8,6 +8,8 @@ und dieses Projekt folgt [Semantic Versioning](https://semver.org/lang/de/).
|
|||||||
## [Unveröffentlicht]
|
## [Unveröffentlicht]
|
||||||
|
|
||||||
### Hinzugefügt
|
### Hinzugefügt
|
||||||
|
- Marken-Dateien nach Vorgabe unter `public/brand/`: Symbol (dunkel/hell), kleines Icon, Avatar, Logo (dunkel/hell), Banner 1280×640, Favicons, Apple-Touch- und Manifest-Icons; erzeugt mit `pnpm marke` (#017)
|
||||||
|
- Web-App-Manifest; Favicon passt sich hellem und dunklem Browser an (#017)
|
||||||
|
|
||||||
### Geändert
|
### Geändert
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# #017 Branding-Assets
|
# #017 Branding-Assets
|
||||||
**Status:** offen
|
**Status:** erledigt
|
||||||
**Meilenstein:** v0.6.0
|
**Meilenstein:** v0.6.0
|
||||||
**Labels:** design
|
**Labels:** design
|
||||||
**Geschätzt:** 4 h **Tatsächlich:** –
|
**Geschätzt:** 4 h **Tatsächlich:** –
|
||||||
@@ -8,14 +8,21 @@
|
|||||||
Alle Logo-Varianten, Banner, Favicons, Manifest-Icons nach Vorgabe.
|
Alle Logo-Varianten, Banner, Favicons, Manifest-Icons nach Vorgabe.
|
||||||
|
|
||||||
## Aufgaben
|
## Aufgaben
|
||||||
- [ ] symbol(-light), icon-small, avatar, logo(-light), banner 1280×640
|
- [x] symbol(-light), icon-small, avatar, logo(-light), banner 1280×640 (+ banner.png für die Social Preview)
|
||||||
- [ ] Favicon 16/32, Apple-Touch-Icon, Manifest-Icons per Skript aus SVG
|
- [x] Favicon 16/32, Apple-Touch-Icon, Manifest-Icons (192, 512, maskierbar) per Skript aus SVG (`pnpm marke`)
|
||||||
|
|
||||||
## Akzeptanzkriterien
|
## Akzeptanzkriterien
|
||||||
- [ ] Darstellung in beiden Themes geprüft
|
- [x] Darstellung in beiden Themes geprüft (Vorschau-Tafel dunkel/hell, Favicons auf hellem und dunklem Tab)
|
||||||
|
|
||||||
## Abhängigkeiten
|
## Abhängigkeiten
|
||||||
#001
|
#001
|
||||||
|
|
||||||
## Notizen / Entscheidungen
|
## Notizen / Entscheidungen
|
||||||
–
|
- **Texte als Pfade:** Wortmarke und Banner sehen überall gleich aus, auch ohne installierte Schrift.
|
||||||
|
Die TTFs (JetBrains Mono 2.304, Space Grotesk 2.0.0, OFL) lädt `pnpm marke` mit SHA-256-Prüfung
|
||||||
|
nach `node_modules/.cache`; eingecheckt werden nur die Ergebnisse.
|
||||||
|
- opentype.js 1.3.5 kennt die Ligatur-Regeln von JetBrains Mono nicht und schreibt in `toPathData`
|
||||||
|
gelegentlich „NaN“ – deshalb Glyph für Glyph gesetzt und Pfaddaten selbst geschrieben.
|
||||||
|
- `icon-small.svg` passt sich per `prefers-color-scheme` an; die PNG-Favicons sind dicke Linien auf
|
||||||
|
Teal, weil 16 px mit den dünnen Linien des Symbols nicht lesbar waren.
|
||||||
|
- Unterzeile im Banner 32 statt 34 px – sonst breiter als die Ränder.
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ Details und Architektur: [../architektur.md](../architektur.md). Zeiten werden a
|
|||||||
| [#014](014-verlauf.md) | Verlauf und Wiederholen | erledigt | v0.5.0 | 4 h | 1 h 21 min |
|
| [#014](014-verlauf.md) | Verlauf und Wiederholen | erledigt | v0.5.0 | 4 h | 1 h 21 min |
|
||||||
| [#015](015-rollen-audit.md) | Rollen, Benutzer, Tokens, Audit-Log | erledigt | v0.5.0 | 6 h | 0 h 10 min |
|
| [#015](015-rollen-audit.md) | Rollen, Benutzer, Tokens, Audit-Log | erledigt | v0.5.0 | 6 h | 0 h 10 min |
|
||||||
| [#016](016-oidc.md) | Anmeldung per OIDC | erledigt | v0.5.0 | 4 h | 0 h 08 min |
|
| [#016](016-oidc.md) | Anmeldung per OIDC | erledigt | v0.5.0 | 4 h | 0 h 08 min |
|
||||||
| [#017](017-branding.md) | Branding-Assets | offen | v0.6.0 | 4 h | – |
|
| [#017](017-branding.md) | Branding-Assets | erledigt | v0.6.0 | 4 h | – |
|
||||||
| [#018](018-readme.md) | README und Dokumentation | offen | v0.6.0 | 4 h | – |
|
| [#018](018-readme.md) | README und Dokumentation | offen | v0.6.0 | 4 h | – |
|
||||||
| [#019](019-installer.md) | Docker-Image und LXC-Installer | offen | v0.6.0 | 6 h | – |
|
| [#019](019-installer.md) | Docker-Image und LXC-Installer | offen | v0.6.0 | 6 h | – |
|
||||||
| [#020](020-feldtest.md) | Feldtest 8.x und 9.x, Version 1.0.0 | offen | v1.0.0 | 6 h | – |
|
| [#020](020-feldtest.md) | Feldtest 8.x und 9.x, Version 1.0.0 | offen | v1.0.0 | 6 h | – |
|
||||||
|
|||||||
@@ -28,7 +28,8 @@
|
|||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"worker": "tsx src/worker/main.ts",
|
"worker": "tsx src/worker/main.ts",
|
||||||
"openapi": "tsx scripts/openapi.ts",
|
"openapi": "tsx scripts/openapi.ts",
|
||||||
"cli:pruefen": "scripts/cli-pruefen.sh"
|
"cli:pruefen": "scripts/cli-pruefen.sh",
|
||||||
|
"marke": "tsx scripts/marke-erzeugen.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource-variable/jetbrains-mono": "^5.3.0",
|
"@fontsource-variable/jetbrains-mono": "^5.3.0",
|
||||||
@@ -55,8 +56,10 @@
|
|||||||
"@commitlint/cli": "^21.2.3",
|
"@commitlint/cli": "^21.2.3",
|
||||||
"@commitlint/config-conventional": "^21.2.3",
|
"@commitlint/config-conventional": "^21.2.3",
|
||||||
"@playwright/test": "1.63.0",
|
"@playwright/test": "1.63.0",
|
||||||
|
"@resvg/resvg-js": "2.6.2",
|
||||||
"@tailwindcss/postcss": "^4.3.3",
|
"@tailwindcss/postcss": "^4.3.3",
|
||||||
"@types/node": "^24.13.6",
|
"@types/node": "^24.13.6",
|
||||||
|
"@types/opentype.js": "^1.3.10",
|
||||||
"@types/pg": "^8.23.1",
|
"@types/pg": "^8.23.1",
|
||||||
"@types/react": "^19.3.0",
|
"@types/react": "^19.3.0",
|
||||||
"@types/react-dom": "^19.3.0",
|
"@types/react-dom": "^19.3.0",
|
||||||
@@ -68,6 +71,7 @@
|
|||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"lint-staged": "^17.5.1",
|
"lint-staged": "^17.5.1",
|
||||||
|
"opentype.js": "1.3.5",
|
||||||
"prettier": "^3.9.9",
|
"prettier": "^3.9.9",
|
||||||
"prisma": "7.10.0",
|
"prisma": "7.10.0",
|
||||||
"tailwindcss": "^4.3.3",
|
"tailwindcss": "^4.3.3",
|
||||||
|
|||||||
@@ -233,12 +233,18 @@ importers:
|
|||||||
'@playwright/test':
|
'@playwright/test':
|
||||||
specifier: 1.63.0
|
specifier: 1.63.0
|
||||||
version: 1.63.0
|
version: 1.63.0
|
||||||
|
'@resvg/resvg-js':
|
||||||
|
specifier: 2.6.2
|
||||||
|
version: 2.6.2
|
||||||
'@tailwindcss/postcss':
|
'@tailwindcss/postcss':
|
||||||
specifier: ^4.3.3
|
specifier: ^4.3.3
|
||||||
version: 4.3.3
|
version: 4.3.3
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^24.13.6
|
specifier: ^24.13.6
|
||||||
version: 24.19.2
|
version: 24.19.2
|
||||||
|
'@types/opentype.js':
|
||||||
|
specifier: ^1.3.10
|
||||||
|
version: 1.3.10
|
||||||
'@types/pg':
|
'@types/pg':
|
||||||
specifier: ^8.23.1
|
specifier: ^8.23.1
|
||||||
version: 8.23.1
|
version: 8.23.1
|
||||||
@@ -272,6 +278,9 @@ importers:
|
|||||||
lint-staged:
|
lint-staged:
|
||||||
specifier: ^17.5.1
|
specifier: ^17.5.1
|
||||||
version: 17.6.0
|
version: 17.6.0
|
||||||
|
opentype.js:
|
||||||
|
specifier: 1.3.5
|
||||||
|
version: 1.3.5
|
||||||
prettier:
|
prettier:
|
||||||
specifier: ^3.9.9
|
specifier: ^3.9.9
|
||||||
version: 3.9.10
|
version: 3.9.10
|
||||||
@@ -1357,6 +1366,86 @@ packages:
|
|||||||
'@types/react':
|
'@types/react':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@resvg/resvg-js-android-arm-eabi@2.6.2':
|
||||||
|
resolution: {integrity: sha512-FrJibrAk6v29eabIPgcTUMPXiEz8ssrAk7TXxsiZzww9UTQ1Z5KAbFJs+Z0Ez+VZTYgnE5IQJqBcoSiMebtPHA==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [android]
|
||||||
|
|
||||||
|
'@resvg/resvg-js-android-arm64@2.6.2':
|
||||||
|
resolution: {integrity: sha512-VcOKezEhm2VqzXpcIJoITuvUS/fcjIw5NA/w3tjzWyzmvoCdd+QXIqy3FBGulWdClvp4g+IfUemigrkLThSjAQ==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [android]
|
||||||
|
|
||||||
|
'@resvg/resvg-js-darwin-arm64@2.6.2':
|
||||||
|
resolution: {integrity: sha512-nmok2LnAd6nLUKI16aEB9ydMC6Lidiiq2m1nEBDR1LaaP7FGs4AJ90qDraxX+CWlVuRlvNjyYJTNv8qFjtL9+A==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@resvg/resvg-js-darwin-x64@2.6.2':
|
||||||
|
resolution: {integrity: sha512-GInyZLjgWDfsVT6+SHxQVRwNzV0AuA1uqGsOAW+0th56J7Nh6bHHKXHBWzUrihxMetcFDmQMAX1tZ1fZDYSRsw==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@resvg/resvg-js-linux-arm-gnueabihf@2.6.2':
|
||||||
|
resolution: {integrity: sha512-YIV3u/R9zJbpqTTNwTZM5/ocWetDKGsro0SWp70eGEM9eV2MerWyBRZnQIgzU3YBnSBQ1RcxRZvY/UxwESfZIw==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@resvg/resvg-js-linux-arm64-gnu@2.6.2':
|
||||||
|
resolution: {integrity: sha512-zc2BlJSim7YR4FZDQ8OUoJg5holYzdiYMeobb9pJuGDidGL9KZUv7SbiD4E8oZogtYY42UZEap7dqkkYuA91pg==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
'@resvg/resvg-js-linux-arm64-musl@2.6.2':
|
||||||
|
resolution: {integrity: sha512-3h3dLPWNgSsD4lQBJPb4f+kvdOSJHa5PjTYVsWHxLUzH4IFTJUAnmuWpw4KqyQ3NA5QCyhw4TWgxk3jRkQxEKg==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
|
'@resvg/resvg-js-linux-x64-gnu@2.6.2':
|
||||||
|
resolution: {integrity: sha512-IVUe+ckIerA7xMZ50duAZzwf1U7khQe2E0QpUxu5MBJNao5RqC0zwV/Zm965vw6D3gGFUl7j4m+oJjubBVoftw==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
'@resvg/resvg-js-linux-x64-musl@2.6.2':
|
||||||
|
resolution: {integrity: sha512-UOf83vqTzoYQO9SZ0fPl2ZIFtNIz/Rr/y+7X8XRX1ZnBYsQ/tTb+cj9TE+KHOdmlTFBxhYzVkP2lRByCzqi4jQ==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
|
'@resvg/resvg-js-win32-arm64-msvc@2.6.2':
|
||||||
|
resolution: {integrity: sha512-7C/RSgCa+7vqZ7qAbItfiaAWhyRSoD4l4BQAbVDqRRsRgY+S+hgS3in0Rxr7IorKUpGE69X48q6/nOAuTJQxeQ==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@resvg/resvg-js-win32-ia32-msvc@2.6.2':
|
||||||
|
resolution: {integrity: sha512-har4aPAlvjnLcil40AC77YDIk6loMawuJwFINEM7n0pZviwMkMvjb2W5ZirsNOZY4aDbo5tLx0wNMREp5Brk+w==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [ia32]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@resvg/resvg-js-win32-x64-msvc@2.6.2':
|
||||||
|
resolution: {integrity: sha512-ZXtYhtUr5SSaBrUDq7DiyjOFJqBVL/dOBN7N/qmi/pO0IgiWW/f/ue3nbvu9joWE5aAKDoIzy/CxsY0suwGosQ==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@resvg/resvg-js@2.6.2':
|
||||||
|
resolution: {integrity: sha512-xBaJish5OeGmniDj9cW5PRa/PtmuVU3ziqrbr5xJj901ZDN4TosrVaNZpEiLZAxdfnhAe7uQ7QFWfjPe9d9K2Q==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
|
||||||
'@rolldown/binding-android-arm-eabi@1.2.13':
|
'@rolldown/binding-android-arm-eabi@1.2.13':
|
||||||
resolution: {integrity: sha512-Vu/NICrgdRvUOeaJZIyB2FP63s6fO/4ud5l8dXmxDx9yDyn+mN0y+gsvsVviSli+T361Aqo0h46EuMoobrwawA==}
|
resolution: {integrity: sha512-Vu/NICrgdRvUOeaJZIyB2FP63s6fO/4ud5l8dXmxDx9yDyn+mN0y+gsvsVviSli+T361Aqo0h46EuMoobrwawA==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
@@ -1628,6 +1717,9 @@ packages:
|
|||||||
'@types/node@24.19.2':
|
'@types/node@24.19.2':
|
||||||
resolution: {integrity: sha512-93d49wCWJkCyN87MxLpNN6KXPCwFw7bVUg79mGUV1OS40JGWwVCr8n61DXCSqz7CV6/VfPkb7xec7uP2M+QHWQ==}
|
resolution: {integrity: sha512-93d49wCWJkCyN87MxLpNN6KXPCwFw7bVUg79mGUV1OS40JGWwVCr8n61DXCSqz7CV6/VfPkb7xec7uP2M+QHWQ==}
|
||||||
|
|
||||||
|
'@types/opentype.js@1.3.10':
|
||||||
|
resolution: {integrity: sha512-F67EFyk6j02okHz5JCgata3ZRAcZi9GLnzmkHw/rzJq3OCc8/ZVdoKrxMTYjcQP6IYHGBz2cav1cpzkOkPiPCQ==}
|
||||||
|
|
||||||
'@types/pg@8.23.1':
|
'@types/pg@8.23.1':
|
||||||
resolution: {integrity: sha512-fKVHpikPdg4GKks3JuLEhvwSyvwzF23hnabPy6DD8ljVbC7+6J5dQzdv4arV6jqq57djnMgs1HKBxX4P8aBI3A==}
|
resolution: {integrity: sha512-fKVHpikPdg4GKks3JuLEhvwSyvwzF23hnabPy6DD8ljVbC7+6J5dQzdv4arV6jqq57djnMgs1HKBxX4P8aBI3A==}
|
||||||
|
|
||||||
@@ -3235,6 +3327,11 @@ packages:
|
|||||||
openid-client@6.8.8:
|
openid-client@6.8.8:
|
||||||
resolution: {integrity: sha512-ZsucJA5Ad04Uv7YN4ql+s4GXNmb9uAYQwyJTsJx7CH/MX3JZioLE2pVKi1SC+YrbSLA4Px3Gi30dMjgOZtb6pA==}
|
resolution: {integrity: sha512-ZsucJA5Ad04Uv7YN4ql+s4GXNmb9uAYQwyJTsJx7CH/MX3JZioLE2pVKi1SC+YrbSLA4Px3Gi30dMjgOZtb6pA==}
|
||||||
|
|
||||||
|
opentype.js@1.3.5:
|
||||||
|
resolution: {integrity: sha512-thKDiELidAApOvXlncrpwDZKJCa9fXLEKM4+FoEWI+qTLDeNb+h7EkN+8a7KQODsB1GZ+Exz9KknkoPrEdXZDw==}
|
||||||
|
deprecated: This version should be ignored, it was meant to be published as 2.0.0 but got published as 1.3.5 by mistake. As of May 6th 2026 the latest v1 release is v1.3.4
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
optionator@0.9.4:
|
optionator@0.9.4:
|
||||||
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
|
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
@@ -4888,6 +4985,57 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.3.0
|
'@types/react': 19.3.0
|
||||||
|
|
||||||
|
'@resvg/resvg-js-android-arm-eabi@2.6.2':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@resvg/resvg-js-android-arm64@2.6.2':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@resvg/resvg-js-darwin-arm64@2.6.2':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@resvg/resvg-js-darwin-x64@2.6.2':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@resvg/resvg-js-linux-arm-gnueabihf@2.6.2':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@resvg/resvg-js-linux-arm64-gnu@2.6.2':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@resvg/resvg-js-linux-arm64-musl@2.6.2':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@resvg/resvg-js-linux-x64-gnu@2.6.2':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@resvg/resvg-js-linux-x64-musl@2.6.2':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@resvg/resvg-js-win32-arm64-msvc@2.6.2':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@resvg/resvg-js-win32-ia32-msvc@2.6.2':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@resvg/resvg-js-win32-x64-msvc@2.6.2':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@resvg/resvg-js@2.6.2':
|
||||||
|
optionalDependencies:
|
||||||
|
'@resvg/resvg-js-android-arm-eabi': 2.6.2
|
||||||
|
'@resvg/resvg-js-android-arm64': 2.6.2
|
||||||
|
'@resvg/resvg-js-darwin-arm64': 2.6.2
|
||||||
|
'@resvg/resvg-js-darwin-x64': 2.6.2
|
||||||
|
'@resvg/resvg-js-linux-arm-gnueabihf': 2.6.2
|
||||||
|
'@resvg/resvg-js-linux-arm64-gnu': 2.6.2
|
||||||
|
'@resvg/resvg-js-linux-arm64-musl': 2.6.2
|
||||||
|
'@resvg/resvg-js-linux-x64-gnu': 2.6.2
|
||||||
|
'@resvg/resvg-js-linux-x64-musl': 2.6.2
|
||||||
|
'@resvg/resvg-js-win32-arm64-msvc': 2.6.2
|
||||||
|
'@resvg/resvg-js-win32-ia32-msvc': 2.6.2
|
||||||
|
'@resvg/resvg-js-win32-x64-msvc': 2.6.2
|
||||||
|
|
||||||
'@rolldown/binding-android-arm-eabi@1.2.13':
|
'@rolldown/binding-android-arm-eabi@1.2.13':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@@ -5078,6 +5226,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 7.24.6
|
undici-types: 7.24.6
|
||||||
|
|
||||||
|
'@types/opentype.js@1.3.10': {}
|
||||||
|
|
||||||
'@types/pg@8.23.1':
|
'@types/pg@8.23.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 24.19.2
|
'@types/node': 24.19.2
|
||||||
@@ -6848,6 +6998,8 @@ snapshots:
|
|||||||
jose: 6.2.12
|
jose: 6.2.12
|
||||||
oauth4webapi: 3.8.8
|
oauth4webapi: 3.8.8
|
||||||
|
|
||||||
|
opentype.js@1.3.5: {}
|
||||||
|
|
||||||
optionator@0.9.4:
|
optionator@0.9.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
deep-is: 0.1.4
|
deep-is: 0.1.4
|
||||||
|
|||||||
|
After Width: | Height: | Size: 2.9 KiB |
@@ -0,0 +1,8 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" fill="none">
|
||||||
|
<rect width="64" height="64" rx="14" fill="#2DD4BF"/>
|
||||||
|
<g transform="translate(7.039999999999999 7.039999999999999) scale(0.78)">
|
||||||
|
<path d="M32 6 L54.5 19 V45 L32 58 L9.5 45 V19 Z" stroke="#11161C" stroke-width="3.5" stroke-linejoin="round"/>
|
||||||
|
<path d="M32 41 V20 M23.5 28.5 L32 20 L40.5 28.5" stroke="#11161C" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M24 47 H40" stroke="#11161C" stroke-width="4" stroke-linecap="round"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 574 B |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 496 B |
|
After Width: | Height: | Size: 887 B |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 9.2 KiB |
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" fill="none">
|
||||||
|
<style>.h{stroke:#E6EDF3}.p{stroke:#2DD4BF}@media (prefers-color-scheme: light){.h{stroke:#11161C}.p{stroke:#0F766E}}</style>
|
||||||
|
<path class="h" d="M32 6 L54.5 19 V45 L32 58 L9.5 45 V19 Z" stroke-width="5.5" stroke-linejoin="round"/>
|
||||||
|
<path class="p" d="M32 43 V18 M22 28 L32 18 L42 28" stroke-width="7" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 458 B |
@@ -0,0 +1,8 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 282 64" width="282" height="64" fill="none">
|
||||||
|
<path d="M32 6 L54.5 19 V45 L32 58 L9.5 45 V19 Z" stroke="#11161C" stroke-width="3.5" stroke-linejoin="round"/>
|
||||||
|
<path d="M32 41 V20 M23.5 28.5 L32 20 L40.5 28.5" stroke="#0F766E" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M24 47 H40" stroke="#0F766E" stroke-width="4" stroke-linecap="round"/>
|
||||||
|
<path d="M83.46 43L80.76 43L80.76 21.1L87.81 21.1Q90.87 21.1 92.69 22.86Q94.5 24.61 94.5 27.55L94.5 27.55Q94.5 30.49 92.69 32.25Q90.87 34 87.81 34L87.81 34L83.46 34L83.46 43M83.46 23.53L83.46 31.57L87.81 31.57Q89.58 31.57 90.65 30.48Q91.71 29.38 91.71 27.55L91.71 27.55Q91.71 25.69 90.65 24.61Q89.58 23.53 87.81 23.53L87.81 23.53L83.46 23.53 M102.06 43L99.36 43L99.36 26.5L102.06 26.5L102.06 29.65L102.12 29.65Q102.33 28.06 103.47 27.13Q104.61 26.2 106.53 26.2L106.53 26.2Q109.11 26.2 110.51 27.75Q111.9 29.29 111.9 32.14L111.9 32.14L111.9 33.55L109.2 33.55L109.2 32.14Q109.2 28.54 105.69 28.54L105.69 28.54Q103.92 28.54 102.99 29.56Q102.06 30.58 102.06 32.5L102.06 32.5L102.06 43 M123 43.24L123 43.24Q120.03 43.24 118.28 41.5Q116.52 39.76 116.52 36.64L116.52 36.64L116.52 32.86Q116.52 29.71 118.26 27.99Q120 26.26 123 26.26L123 26.26Q126 26.26 127.74 27.99Q129.48 29.71 129.48 32.86L129.48 32.86L129.48 36.64Q129.48 39.76 127.73 41.5Q125.97 43.24 123 43.24M123 40.84L123 40.84Q124.77 40.84 125.78 39.85Q126.78 38.86 126.78 36.94L126.78 36.94L126.78 32.56Q126.78 30.64 125.78 29.65Q124.77 28.66 123 28.66L123 28.66Q121.26 28.66 120.24 29.65Q119.22 30.64 119.22 32.56L119.22 32.56L119.22 36.94Q119.22 38.86 120.24 39.85Q121.26 40.84 123 40.84 M136.68 43L133.5 43L139.35 34.51L133.86 26.5L137.04 26.5L140.31 31.6Q140.52 31.93 140.72 32.32Q140.91 32.71 141.03 32.95L141.03 32.95Q141.12 32.71 141.32 32.32Q141.51 31.93 141.72 31.6L141.72 31.6L144.99 26.5L148.17 26.5L142.68 34.54L148.5 43L145.32 43L141.78 37.6Q141.57 37.27 141.38 36.84Q141.18 36.4 141.03 36.13L141.03 36.13Q140.91 36.4 140.69 36.84Q140.46 37.27 140.22 37.6L140.22 37.6L136.68 43" fill="#11161C"/>
|
||||||
|
<path d="M158.76 43L152.25 43L152.25 21.1L158.76 21.1Q160.98 21.1 162.62 21.96Q164.25 22.81 165.15 24.34Q166.05 25.87 166.05 27.94L166.05 27.94L166.05 36.13Q166.05 38.2 165.15 39.74Q164.25 41.29 162.62 42.15Q160.98 43 158.76 43L158.76 43M156 24.46L156 39.64L158.76 39.64Q160.38 39.64 161.34 38.7Q162.3 37.75 162.3 36.13L162.3 36.13L162.3 27.94Q162.3 26.35 161.34 25.41Q160.38 24.46 158.76 24.46L158.76 24.46L156 24.46 M177.03 43.3L177.03 43.3Q174.93 43.3 173.37 42.49Q171.81 41.68 170.96 40.2Q170.1 38.71 170.1 36.7L170.1 36.7L170.1 32.8Q170.1 30.79 170.96 29.31Q171.81 27.82 173.37 27.01Q174.93 26.2 177.03 26.2L177.03 26.2Q179.1 26.2 180.65 27.01Q182.19 27.82 183.05 29.28Q183.9 30.73 183.9 32.68L183.9 32.68L183.9 35.68L173.7 35.68L173.7 36.82Q173.7 38.5 174.57 39.35Q175.44 40.21 177.03 40.21L177.03 40.21Q178.2 40.21 179.03 39.81Q179.85 39.4 180 38.62L180 38.62L183.72 38.62Q183.33 40.75 181.5 42.03Q179.67 43.3 177.03 43.3M173.7 32.68L173.7 32.68L173.7 33.25L180.3 33.22L180.3 32.65Q180.3 30.94 179.46 30.01Q178.62 29.08 177.03 29.08L177.03 29.08Q175.38 29.08 174.54 30.03Q173.7 30.97 173.7 32.68 M192.06 48.4L188.31 48.4L188.31 26.5L191.97 26.5L191.97 29.53Q192.42 27.97 193.59 27.09Q194.76 26.2 196.5 26.2L196.5 26.2Q198.09 26.2 199.31 26.98Q200.52 27.76 201.2 29.19Q201.87 30.61 201.87 32.5L201.87 32.5L201.87 36.97Q201.87 39.82 200.4 41.56Q198.93 43.3 196.5 43.3L196.5 43.3Q194.76 43.3 193.58 42.4Q192.39 41.5 191.94 39.88L191.94 39.88L192.06 43.78L192.06 48.4M195.09 40.06L195.09 40.06Q196.53 40.06 197.33 39.28Q198.12 38.5 198.12 36.88L198.12 36.88L198.12 32.62Q198.12 31 197.33 30.22Q196.53 29.44 195.09 29.44L195.09 29.44Q193.65 29.44 192.86 30.31Q192.06 31.18 192.06 32.77L192.06 32.77L192.06 36.73Q192.06 38.32 192.86 39.19Q193.65 40.06 195.09 40.06 M220.77 43L215.67 43Q213.21 43 211.73 41.53Q210.24 40.06 210.24 37.66L210.24 37.66L210.24 24.49L204.81 24.49L204.81 21.1L213.99 21.1L213.99 37.57Q213.99 38.5 214.52 39.06Q215.04 39.61 215.94 39.61L215.94 39.61L220.77 39.61L220.77 43 M231 43.27L231 43.27Q227.82 43.27 225.96 41.49Q224.1 39.7 224.1 36.67L224.1 36.67L224.1 32.83Q224.1 29.77 225.96 28Q227.82 26.23 231 26.23L231 26.23Q234.18 26.23 236.04 28Q237.9 29.77 237.9 32.83L237.9 32.83L237.9 36.67Q237.9 39.7 236.04 41.49Q234.18 43.27 231 43.27M231 40L231 40Q232.5 40 233.33 39.17Q234.15 38.35 234.15 36.79L234.15 36.79L234.15 32.71Q234.15 31.15 233.33 30.33Q232.5 29.5 231 29.5L231 29.5Q229.5 29.5 228.68 30.33Q227.85 31.15 227.85 32.71L227.85 32.71L227.85 36.79Q227.85 38.35 228.68 39.17Q229.5 40 231 40 M248.88 48.4L244.95 48.4L247.29 42.1L241.08 26.5L245.04 26.5L248.22 34.93Q248.49 35.68 248.76 36.57Q249.03 37.45 249.15 38.2L249.15 38.2Q249.24 37.45 249.5 36.57Q249.75 35.68 250.02 34.93L250.02 34.93L252.96 26.5L256.92 26.5L248.88 48.4" fill="#0F766E"/>
|
||||||
|
<rect x="261.60" y="19.30" width="15.60" height="27.90" fill="#0F766E"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.8 KiB |
@@ -0,0 +1,8 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 282 64" width="282" height="64" fill="none">
|
||||||
|
<path d="M32 6 L54.5 19 V45 L32 58 L9.5 45 V19 Z" stroke="#E6EDF3" stroke-width="3.5" stroke-linejoin="round"/>
|
||||||
|
<path d="M32 41 V20 M23.5 28.5 L32 20 L40.5 28.5" stroke="#2DD4BF" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M24 47 H40" stroke="#2DD4BF" stroke-width="4" stroke-linecap="round"/>
|
||||||
|
<path d="M83.46 43L80.76 43L80.76 21.1L87.81 21.1Q90.87 21.1 92.69 22.86Q94.5 24.61 94.5 27.55L94.5 27.55Q94.5 30.49 92.69 32.25Q90.87 34 87.81 34L87.81 34L83.46 34L83.46 43M83.46 23.53L83.46 31.57L87.81 31.57Q89.58 31.57 90.65 30.48Q91.71 29.38 91.71 27.55L91.71 27.55Q91.71 25.69 90.65 24.61Q89.58 23.53 87.81 23.53L87.81 23.53L83.46 23.53 M102.06 43L99.36 43L99.36 26.5L102.06 26.5L102.06 29.65L102.12 29.65Q102.33 28.06 103.47 27.13Q104.61 26.2 106.53 26.2L106.53 26.2Q109.11 26.2 110.51 27.75Q111.9 29.29 111.9 32.14L111.9 32.14L111.9 33.55L109.2 33.55L109.2 32.14Q109.2 28.54 105.69 28.54L105.69 28.54Q103.92 28.54 102.99 29.56Q102.06 30.58 102.06 32.5L102.06 32.5L102.06 43 M123 43.24L123 43.24Q120.03 43.24 118.28 41.5Q116.52 39.76 116.52 36.64L116.52 36.64L116.52 32.86Q116.52 29.71 118.26 27.99Q120 26.26 123 26.26L123 26.26Q126 26.26 127.74 27.99Q129.48 29.71 129.48 32.86L129.48 32.86L129.48 36.64Q129.48 39.76 127.73 41.5Q125.97 43.24 123 43.24M123 40.84L123 40.84Q124.77 40.84 125.78 39.85Q126.78 38.86 126.78 36.94L126.78 36.94L126.78 32.56Q126.78 30.64 125.78 29.65Q124.77 28.66 123 28.66L123 28.66Q121.26 28.66 120.24 29.65Q119.22 30.64 119.22 32.56L119.22 32.56L119.22 36.94Q119.22 38.86 120.24 39.85Q121.26 40.84 123 40.84 M136.68 43L133.5 43L139.35 34.51L133.86 26.5L137.04 26.5L140.31 31.6Q140.52 31.93 140.72 32.32Q140.91 32.71 141.03 32.95L141.03 32.95Q141.12 32.71 141.32 32.32Q141.51 31.93 141.72 31.6L141.72 31.6L144.99 26.5L148.17 26.5L142.68 34.54L148.5 43L145.32 43L141.78 37.6Q141.57 37.27 141.38 36.84Q141.18 36.4 141.03 36.13L141.03 36.13Q140.91 36.4 140.69 36.84Q140.46 37.27 140.22 37.6L140.22 37.6L136.68 43" fill="#E6EDF3"/>
|
||||||
|
<path d="M158.76 43L152.25 43L152.25 21.1L158.76 21.1Q160.98 21.1 162.62 21.96Q164.25 22.81 165.15 24.34Q166.05 25.87 166.05 27.94L166.05 27.94L166.05 36.13Q166.05 38.2 165.15 39.74Q164.25 41.29 162.62 42.15Q160.98 43 158.76 43L158.76 43M156 24.46L156 39.64L158.76 39.64Q160.38 39.64 161.34 38.7Q162.3 37.75 162.3 36.13L162.3 36.13L162.3 27.94Q162.3 26.35 161.34 25.41Q160.38 24.46 158.76 24.46L158.76 24.46L156 24.46 M177.03 43.3L177.03 43.3Q174.93 43.3 173.37 42.49Q171.81 41.68 170.96 40.2Q170.1 38.71 170.1 36.7L170.1 36.7L170.1 32.8Q170.1 30.79 170.96 29.31Q171.81 27.82 173.37 27.01Q174.93 26.2 177.03 26.2L177.03 26.2Q179.1 26.2 180.65 27.01Q182.19 27.82 183.05 29.28Q183.9 30.73 183.9 32.68L183.9 32.68L183.9 35.68L173.7 35.68L173.7 36.82Q173.7 38.5 174.57 39.35Q175.44 40.21 177.03 40.21L177.03 40.21Q178.2 40.21 179.03 39.81Q179.85 39.4 180 38.62L180 38.62L183.72 38.62Q183.33 40.75 181.5 42.03Q179.67 43.3 177.03 43.3M173.7 32.68L173.7 32.68L173.7 33.25L180.3 33.22L180.3 32.65Q180.3 30.94 179.46 30.01Q178.62 29.08 177.03 29.08L177.03 29.08Q175.38 29.08 174.54 30.03Q173.7 30.97 173.7 32.68 M192.06 48.4L188.31 48.4L188.31 26.5L191.97 26.5L191.97 29.53Q192.42 27.97 193.59 27.09Q194.76 26.2 196.5 26.2L196.5 26.2Q198.09 26.2 199.31 26.98Q200.52 27.76 201.2 29.19Q201.87 30.61 201.87 32.5L201.87 32.5L201.87 36.97Q201.87 39.82 200.4 41.56Q198.93 43.3 196.5 43.3L196.5 43.3Q194.76 43.3 193.58 42.4Q192.39 41.5 191.94 39.88L191.94 39.88L192.06 43.78L192.06 48.4M195.09 40.06L195.09 40.06Q196.53 40.06 197.33 39.28Q198.12 38.5 198.12 36.88L198.12 36.88L198.12 32.62Q198.12 31 197.33 30.22Q196.53 29.44 195.09 29.44L195.09 29.44Q193.65 29.44 192.86 30.31Q192.06 31.18 192.06 32.77L192.06 32.77L192.06 36.73Q192.06 38.32 192.86 39.19Q193.65 40.06 195.09 40.06 M220.77 43L215.67 43Q213.21 43 211.73 41.53Q210.24 40.06 210.24 37.66L210.24 37.66L210.24 24.49L204.81 24.49L204.81 21.1L213.99 21.1L213.99 37.57Q213.99 38.5 214.52 39.06Q215.04 39.61 215.94 39.61L215.94 39.61L220.77 39.61L220.77 43 M231 43.27L231 43.27Q227.82 43.27 225.96 41.49Q224.1 39.7 224.1 36.67L224.1 36.67L224.1 32.83Q224.1 29.77 225.96 28Q227.82 26.23 231 26.23L231 26.23Q234.18 26.23 236.04 28Q237.9 29.77 237.9 32.83L237.9 32.83L237.9 36.67Q237.9 39.7 236.04 41.49Q234.18 43.27 231 43.27M231 40L231 40Q232.5 40 233.33 39.17Q234.15 38.35 234.15 36.79L234.15 36.79L234.15 32.71Q234.15 31.15 233.33 30.33Q232.5 29.5 231 29.5L231 29.5Q229.5 29.5 228.68 30.33Q227.85 31.15 227.85 32.71L227.85 32.71L227.85 36.79Q227.85 38.35 228.68 39.17Q229.5 40 231 40 M248.88 48.4L244.95 48.4L247.29 42.1L241.08 26.5L245.04 26.5L248.22 34.93Q248.49 35.68 248.76 36.57Q249.03 37.45 249.15 38.2L249.15 38.2Q249.24 37.45 249.5 36.57Q249.75 35.68 250.02 34.93L250.02 34.93L252.96 26.5L256.92 26.5L248.88 48.4" fill="#2DD4BF"/>
|
||||||
|
<rect x="261.60" y="19.30" width="15.60" height="27.90" fill="#2DD4BF"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.8 KiB |
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" fill="none">
|
||||||
|
<path d="M32 6 L54.5 19 V45 L32 58 L9.5 45 V19 Z" stroke="#11161C" stroke-width="3.5" stroke-linejoin="round"/>
|
||||||
|
<path d="M32 41 V20 M23.5 28.5 L32 20 L40.5 28.5" stroke="#0F766E" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M24 47 H40" stroke="#0F766E" stroke-width="4" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 434 B |
@@ -1,4 +1,4 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" fill="none">
|
||||||
<path d="M32 6 L54.5 19 V45 L32 58 L9.5 45 V19 Z" stroke="#E6EDF3" stroke-width="3.5" stroke-linejoin="round"/>
|
<path d="M32 6 L54.5 19 V45 L32 58 L9.5 45 V19 Z" stroke="#E6EDF3" stroke-width="3.5" stroke-linejoin="round"/>
|
||||||
<path d="M32 41 V20 M23.5 28.5 L32 20 L40.5 28.5" stroke="#2DD4BF" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
<path d="M32 41 V20 M23.5 28.5 L32 20 L40.5 28.5" stroke="#2DD4BF" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
<path d="M24 47 H40" stroke="#2DD4BF" stroke-width="4" stroke-linecap="round"/>
|
<path d="M24 47 H40" stroke="#2DD4BF" stroke-width="4" stroke-linecap="round"/>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 411 B After Width: | Height: | Size: 434 B |
@@ -0,0 +1,296 @@
|
|||||||
|
/**
|
||||||
|
* Erzeugt alle Marken-Dateien unter public/brand/ aus dem Symbol der Vorgabe:
|
||||||
|
*
|
||||||
|
* symbol.svg, symbol-light.svg, icon-small.svg, avatar.svg, logo.svg, logo-light.svg, banner.svg,
|
||||||
|
* favicon-16.png, favicon-32.png, apple-touch-icon.png, icon-192.png, icon-512.png,
|
||||||
|
* icon-maskable-512.png
|
||||||
|
*
|
||||||
|
* Texte (Wortmarke, Banner) werden zu Pfaden – die SVGs sehen damit überall gleich aus, auch ohne
|
||||||
|
* installierte Schrift (README auf Gitea, Social Preview). Die Schriften (JetBrains Mono 2.304,
|
||||||
|
* Space Grotesk 2.0.0, beide SIL OFL) werden dafür einmal geladen, per SHA-256 geprüft und unter
|
||||||
|
* node_modules/.cache abgelegt; eingecheckt werden nur die Ergebnisse.
|
||||||
|
*
|
||||||
|
* pnpm marke
|
||||||
|
*/
|
||||||
|
import { execFileSync } from "node:child_process";
|
||||||
|
import { createHash } from "node:crypto";
|
||||||
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { Resvg } from "@resvg/resvg-js";
|
||||||
|
import opentype from "opentype.js";
|
||||||
|
|
||||||
|
const ZIEL = "public/brand";
|
||||||
|
const CACHE = "node_modules/.cache/proxdeploy-schriften";
|
||||||
|
|
||||||
|
const FARBE = {
|
||||||
|
ink: "#11161C",
|
||||||
|
panel: "#1A2129",
|
||||||
|
linie: "#2A333D",
|
||||||
|
muted: "#8B98A5",
|
||||||
|
paper: "#E6EDF3",
|
||||||
|
teal: "#2DD4BF",
|
||||||
|
tealDunkel: "#0F766E",
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Schriften ───────────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const ARCHIVE = {
|
||||||
|
jetbrains: {
|
||||||
|
url: "https://github.com/JetBrains/JetBrainsMono/releases/download/v2.304/JetBrainsMono-2.304.zip",
|
||||||
|
sha256: "6f6376c6ed2960ea8a963cd7387ec9d76e3f629125bc33d1fdcd7eb7012f7bbf",
|
||||||
|
},
|
||||||
|
grotesk: {
|
||||||
|
url: "https://github.com/floriankarsten/space-grotesk/releases/download/2.0.0/SpaceGrotesk-2.0.0.zip",
|
||||||
|
sha256: "53b415577d4139248555300710bea0d268c7a5be67b93de53b716a9736cabffd",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
async function archivHolen(name: keyof typeof ARCHIVE): Promise<string> {
|
||||||
|
const { url, sha256 } = ARCHIVE[name];
|
||||||
|
mkdirSync(CACHE, { recursive: true });
|
||||||
|
const pfad = join(CACHE, `${name}.zip`);
|
||||||
|
if (!existsSync(pfad)) {
|
||||||
|
const antwort = await fetch(url);
|
||||||
|
if (!antwort.ok) throw new Error(`${url}: HTTP ${antwort.status}`);
|
||||||
|
writeFileSync(pfad, Buffer.from(await antwort.arrayBuffer()));
|
||||||
|
}
|
||||||
|
const ist = createHash("sha256").update(readFileSync(pfad)).digest("hex");
|
||||||
|
if (ist !== sha256) throw new Error(`${pfad}: Prüfsumme ${ist} statt ${sha256}`);
|
||||||
|
return pfad;
|
||||||
|
}
|
||||||
|
|
||||||
|
function schriftAus(archiv: string, datei: string): opentype.Font {
|
||||||
|
const daten = execFileSync("unzip", ["-p", archiv, datei], { maxBuffer: 16 * 1024 * 1024 });
|
||||||
|
return opentype.parse(daten.buffer.slice(daten.byteOffset, daten.byteOffset + daten.byteLength));
|
||||||
|
}
|
||||||
|
|
||||||
|
type Schriften = { mono: opentype.Font; monoFett: opentype.Font; grotesk: opentype.Font };
|
||||||
|
|
||||||
|
async function schriftenLaden(): Promise<Schriften> {
|
||||||
|
const jb = await archivHolen("jetbrains");
|
||||||
|
const sg = await archivHolen("grotesk");
|
||||||
|
return {
|
||||||
|
mono: schriftAus(jb, "fonts/ttf/JetBrainsMono-Regular.ttf"),
|
||||||
|
monoFett: schriftAus(jb, "fonts/ttf/JetBrainsMono-Bold.ttf"),
|
||||||
|
grotesk: schriftAus(sg, "SpaceGrotesk-2.0.0/ttf/static/SpaceGrotesk-Regular.ttf"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pfaddaten selbst schreiben: `toPathData` von opentype.js 1.3.5 liefert an manchen Positionen
|
||||||
|
* „NaN“ (beim Runden), und resvg bricht den Pfad dort ab.
|
||||||
|
*/
|
||||||
|
function pfadDaten(pfad: opentype.Path): string {
|
||||||
|
const z = (wert: number) => {
|
||||||
|
if (!Number.isFinite(wert)) throw new Error(`Ungültige Koordinate ${wert}`);
|
||||||
|
return String(Math.round(wert * 100) / 100);
|
||||||
|
};
|
||||||
|
return pfad.commands
|
||||||
|
.map((b) => {
|
||||||
|
switch (b.type) {
|
||||||
|
case "M":
|
||||||
|
case "L":
|
||||||
|
return `${b.type}${z(b.x)} ${z(b.y)}`;
|
||||||
|
case "Q":
|
||||||
|
return `Q${z(b.x1)} ${z(b.y1)} ${z(b.x)} ${z(b.y)}`;
|
||||||
|
case "C":
|
||||||
|
return `C${z(b.x1)} ${z(b.y1)} ${z(b.x2)} ${z(b.y2)} ${z(b.x)} ${z(b.y)}`;
|
||||||
|
default:
|
||||||
|
return "Z";
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Text als Pfad; liefert Pfaddaten und die Breite. */
|
||||||
|
function text(schrift: opentype.Font, inhalt: string, x: number, y: number, groesse: number) {
|
||||||
|
for (const zeichen of inhalt) {
|
||||||
|
if (schrift.charToGlyph(zeichen).index === 0 && zeichen !== " ") {
|
||||||
|
throw new Error(`${schrift.names.fullName?.en}: kein Glyph für „${zeichen}“`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Glyph für Glyph: opentype.js 1.3.5 scheitert an den kontextuellen Ersetzungen (Ligaturen)
|
||||||
|
// von JetBrains Mono; Logo und Banner brauchen keine.
|
||||||
|
const faktor = groesse / schrift.unitsPerEm;
|
||||||
|
let lauf = x;
|
||||||
|
const teile: string[] = [];
|
||||||
|
for (const zeichen of inhalt) {
|
||||||
|
const glyph = schrift.charToGlyph(zeichen);
|
||||||
|
teile.push(pfadDaten(glyph.getPath(lauf, y, groesse)));
|
||||||
|
lauf += (glyph.advanceWidth ?? 0) * faktor;
|
||||||
|
}
|
||||||
|
return { d: teile.filter(Boolean).join(" "), breite: lauf - x };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Symbol ──────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const SECHSECK = "M32 6 L54.5 19 V45 L32 58 L9.5 45 V19 Z";
|
||||||
|
|
||||||
|
function symbolInhalt(rahmen: string, akzent: string): string {
|
||||||
|
return [
|
||||||
|
`<path d="${SECHSECK}" stroke="${rahmen}" stroke-width="3.5" stroke-linejoin="round"/>`,
|
||||||
|
`<path d="M32 41 V20 M23.5 28.5 L32 20 L40.5 28.5" stroke="${akzent}" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>`,
|
||||||
|
`<path d="M24 47 H40" stroke="${akzent}" stroke-width="4" stroke-linecap="round"/>`,
|
||||||
|
].join("\n ");
|
||||||
|
}
|
||||||
|
|
||||||
|
const svg = (breite: number, hoehe: number, inhalt: string, extra = "") =>
|
||||||
|
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${breite} ${hoehe}" width="${breite}" height="${hoehe}" fill="none"${extra}>\n ${inhalt}\n</svg>\n`;
|
||||||
|
|
||||||
|
/** Favicon-Variante: ohne Sockel, dicke Linien, passt sich dem hellen oder dunklen Browser an. */
|
||||||
|
function iconKlein(): string {
|
||||||
|
return svg(
|
||||||
|
64,
|
||||||
|
64,
|
||||||
|
[
|
||||||
|
`<style>.h{stroke:${FARBE.paper}}.p{stroke:${FARBE.teal}}@media (prefers-color-scheme: light){.h{stroke:${FARBE.ink}}.p{stroke:${FARBE.tealDunkel}}}</style>`,
|
||||||
|
`<path class="h" d="${SECHSECK}" stroke-width="5.5" stroke-linejoin="round"/>`,
|
||||||
|
`<path class="p" d="M32 43 V18 M22 28 L32 18 L42 28" stroke-width="7" stroke-linecap="round" stroke-linejoin="round"/>`,
|
||||||
|
].join("\n "),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Avatar/App-Icon: abgerundetes Quadrat in Teal, Symbol in Ink. `randlos` für maskierbare Icons. */
|
||||||
|
function avatar(randlos = false): string {
|
||||||
|
const hintergrund = randlos
|
||||||
|
? `<rect width="64" height="64" fill="${FARBE.teal}"/>`
|
||||||
|
: `<rect width="64" height="64" rx="14" fill="${FARBE.teal}"/>`;
|
||||||
|
// Maskierbar: Inhalt in der sicheren Zone (80 % in der Mitte).
|
||||||
|
const massstab = randlos ? 0.62 : 0.78;
|
||||||
|
const versatz = 32 * (1 - massstab);
|
||||||
|
return svg(
|
||||||
|
64,
|
||||||
|
64,
|
||||||
|
`${hintergrund}\n <g transform="translate(${versatz} ${versatz}) scale(${massstab})">\n ${symbolInhalt(FARBE.ink, FARBE.ink)}\n </g>`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Favicon als Rasterbild (16/32 px): dicke Linien ohne Sockel auf Teal – auf jedem Tab-Hintergrund lesbar. */
|
||||||
|
function faviconRaster(): string {
|
||||||
|
return svg(
|
||||||
|
64,
|
||||||
|
64,
|
||||||
|
[
|
||||||
|
`<rect width="64" height="64" rx="12" fill="${FARBE.teal}"/>`,
|
||||||
|
`<g transform="translate(4 4) scale(0.875)">`,
|
||||||
|
`<path d="${SECHSECK}" stroke="${FARBE.ink}" stroke-width="6" stroke-linejoin="round"/>`,
|
||||||
|
`<path d="M32 43 V18 M22 28 L32 18 L42 28" stroke="${FARBE.ink}" stroke-width="7.5" stroke-linecap="round" stroke-linejoin="round"/>`,
|
||||||
|
`</g>`,
|
||||||
|
].join("\n "),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Logo: Symbol + Wortmarke ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function wortmarke(s: Schriften, x: number, grundlinie: number, groesse: number, hell: boolean) {
|
||||||
|
const prox = text(s.mono, "Prox", x, grundlinie, groesse);
|
||||||
|
const deploy = text(s.monoFett, "Deploy", x + prox.breite, grundlinie, groesse);
|
||||||
|
const versalhoehe = (s.mono.tables["os2"]?.sCapHeight ?? 730) / s.mono.unitsPerEm;
|
||||||
|
const cursorX = x + prox.breite + deploy.breite + groesse * 0.12;
|
||||||
|
const cursorBreite = groesse * 0.52;
|
||||||
|
const cursorOben = grundlinie - versalhoehe * groesse - groesse * 0.06;
|
||||||
|
const cursorHoehe = versalhoehe * groesse + groesse * 0.2;
|
||||||
|
const text1 = hell ? FARBE.ink : FARBE.paper;
|
||||||
|
const akzent = hell ? FARBE.tealDunkel : FARBE.teal;
|
||||||
|
return {
|
||||||
|
inhalt: [
|
||||||
|
`<path d="${prox.d}" fill="${text1}"/>`,
|
||||||
|
`<path d="${deploy.d}" fill="${akzent}"/>`,
|
||||||
|
`<rect x="${cursorX.toFixed(2)}" y="${cursorOben.toFixed(2)}" width="${cursorBreite.toFixed(2)}" height="${cursorHoehe.toFixed(2)}" fill="${akzent}"/>`,
|
||||||
|
].join("\n "),
|
||||||
|
ende: cursorX + cursorBreite,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function logo(s: Schriften, hell: boolean): string {
|
||||||
|
const w = wortmarke(s, 78, 43, 30, hell);
|
||||||
|
const breite = Math.ceil(w.ende + 4);
|
||||||
|
return svg(
|
||||||
|
breite,
|
||||||
|
64,
|
||||||
|
`${symbolInhalt(hell ? FARBE.ink : FARBE.paper, hell ? FARBE.tealDunkel : FARBE.teal)}\n ${w.inhalt}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Banner 1280×640 ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function banner(s: Schriften): string {
|
||||||
|
const teile: string[] = [`<rect width="1280" height="640" fill="${FARBE.ink}"/>`];
|
||||||
|
// Großes, sehr transparentes Sechseck im Hintergrund.
|
||||||
|
teile.push(
|
||||||
|
`<g transform="translate(700 -60) scale(12)" opacity="0.07"><path d="${SECHSECK}" stroke="${FARBE.paper}" stroke-width="1.2" stroke-linejoin="round"/></g>`,
|
||||||
|
);
|
||||||
|
// Logo groß, links.
|
||||||
|
const massstab = 2;
|
||||||
|
teile.push(
|
||||||
|
`<g transform="translate(96 120) scale(${massstab})">\n ${symbolInhalt(FARBE.paper, FARBE.teal)}\n ${wortmarke(s, 78, 43, 30, false).inhalt}\n </g>`,
|
||||||
|
);
|
||||||
|
// Unterzeile in Space Grotesk.
|
||||||
|
const zeile = text(
|
||||||
|
s.grotesk,
|
||||||
|
"VMs und LXC-Container auf Proxmox VE ausrollen, per Web oder CLI.",
|
||||||
|
96,
|
||||||
|
330,
|
||||||
|
32,
|
||||||
|
);
|
||||||
|
if (zeile.breite > 1280 - 2 * 96) throw new Error(`Unterzeile zu breit (${zeile.breite})`);
|
||||||
|
teile.push(`<path d="${zeile.d}" fill="${FARBE.paper}"/>`);
|
||||||
|
// Tags als Outline-Pills.
|
||||||
|
let x = 96;
|
||||||
|
for (const tag of ["Web-GUI", "CLI", "cloud-init", "LXC", "Debian", "Ubuntu"]) {
|
||||||
|
const t = text(s.mono, tag, 0, 0, 20);
|
||||||
|
const breite = t.breite + 36;
|
||||||
|
teile.push(
|
||||||
|
`<rect x="${x}" y="378" width="${breite.toFixed(1)}" height="40" rx="20" stroke="${FARBE.linie}" stroke-width="2"/>`,
|
||||||
|
`<path d="${text(s.mono, tag, x + 18, 405, 20).d}" fill="${FARBE.muted}"/>`,
|
||||||
|
);
|
||||||
|
x += breite + 12;
|
||||||
|
}
|
||||||
|
// Kleine Terminal-Karte rechts unten.
|
||||||
|
const karte = { x: 744, y: 470, b: 440, h: 110 };
|
||||||
|
teile.push(
|
||||||
|
`<rect x="${karte.x}" y="${karte.y}" width="${karte.b}" height="${karte.h}" rx="14" fill="${FARBE.panel}" stroke="${FARBE.linie}" stroke-width="2"/>`,
|
||||||
|
`<path d="${text(s.mono, "# proxdeploy lxc dns01 --profile base", karte.x + 24, karte.y + 44, 18).d}" fill="${FARBE.muted}"/>`,
|
||||||
|
);
|
||||||
|
const haken = text(s.mono, "✓", karte.x + 24, karte.y + 80, 18);
|
||||||
|
teile.push(
|
||||||
|
`<path d="${haken.d}" fill="${FARBE.teal}"/>`,
|
||||||
|
`<path d="${text(s.mono, " dns01 bereit", karte.x + 24 + haken.breite, karte.y + 80, 18).d}" fill="${FARBE.paper}"/>`,
|
||||||
|
);
|
||||||
|
return svg(1280, 640, teile.join("\n "));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── PNG ─────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function png(quelle: string, breite: number, datei: string): void {
|
||||||
|
const bild = new Resvg(quelle, { fitTo: { mode: "width", value: breite } }).render();
|
||||||
|
writeFileSync(join(ZIEL, datei), bild.asPng());
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const s = await schriftenLaden();
|
||||||
|
mkdirSync(ZIEL, { recursive: true });
|
||||||
|
const dateien: Record<string, string> = {
|
||||||
|
"symbol.svg": svg(64, 64, symbolInhalt(FARBE.paper, FARBE.teal)),
|
||||||
|
"symbol-light.svg": svg(64, 64, symbolInhalt(FARBE.ink, FARBE.tealDunkel)),
|
||||||
|
"icon-small.svg": iconKlein(),
|
||||||
|
"avatar.svg": avatar(),
|
||||||
|
"logo.svg": logo(s, false),
|
||||||
|
"logo-light.svg": logo(s, true),
|
||||||
|
"banner.svg": banner(s),
|
||||||
|
};
|
||||||
|
for (const [name, inhalt] of Object.entries(dateien)) writeFileSync(join(ZIEL, name), inhalt);
|
||||||
|
|
||||||
|
// Raster-Icons aus dem Avatar: auf hellem und dunklem Grund gleich gut sichtbar.
|
||||||
|
png(faviconRaster(), 16, "favicon-16.png");
|
||||||
|
png(faviconRaster(), 32, "favicon-32.png");
|
||||||
|
png(dateien["avatar.svg"]!, 192, "icon-192.png");
|
||||||
|
png(dateien["avatar.svg"]!, 512, "icon-512.png");
|
||||||
|
png(avatar(true), 180, "apple-touch-icon.png");
|
||||||
|
png(avatar(true), 512, "icon-maskable-512.png");
|
||||||
|
// Vorschau des Banners (Social Preview verlangt ein Rasterbild).
|
||||||
|
png(dateien["banner.svg"]!, 1280, "banner.png");
|
||||||
|
console.log(`${Object.keys(dateien).length} SVG und 7 PNG unter ${ZIEL}/ geschrieben`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await main();
|
||||||
@@ -8,7 +8,15 @@ import { THEME_COOKIE, themeAusCookie } from "@/lib/theme";
|
|||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: { default: "ProxDeploy", template: "%s · ProxDeploy" },
|
title: { default: "ProxDeploy", template: "%s · ProxDeploy" },
|
||||||
description: "VMs und LXC-Container auf Proxmox VE ausrollen, per Web oder CLI.",
|
description: "VMs und LXC-Container auf Proxmox VE ausrollen, per Web oder CLI.",
|
||||||
icons: { icon: "/brand/symbol.svg" },
|
icons: {
|
||||||
|
// SVG passt sich hellem und dunklem Browser an; PNG für Browser ohne SVG-Favicons.
|
||||||
|
icon: [
|
||||||
|
{ url: "/brand/icon-small.svg", type: "image/svg+xml" },
|
||||||
|
{ url: "/brand/favicon-32.png", sizes: "32x32", type: "image/png" },
|
||||||
|
{ url: "/brand/favicon-16.png", sizes: "16x16", type: "image/png" },
|
||||||
|
],
|
||||||
|
apple: { url: "/brand/apple-touch-icon.png", sizes: "180x180" },
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// themeColor verträgt keine CSS-Variablen; Ink ist der Hintergrund des dunklen Themes.
|
// themeColor verträgt keine CSS-Variablen; Ink ist der Hintergrund des dunklen Themes.
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import type { MetadataRoute } from "next";
|
||||||
|
|
||||||
|
/** Web-App-Manifest unter /manifest.webmanifest – öffentlich (siehe proxy.ts). */
|
||||||
|
export default function manifest(): MetadataRoute.Manifest {
|
||||||
|
return {
|
||||||
|
name: "ProxDeploy",
|
||||||
|
short_name: "ProxDeploy",
|
||||||
|
description: "VMs und LXC-Container auf Proxmox VE ausrollen, per Web oder CLI.",
|
||||||
|
start_url: "/",
|
||||||
|
display: "standalone",
|
||||||
|
background_color: "#11161C",
|
||||||
|
theme_color: "#11161C",
|
||||||
|
lang: "de",
|
||||||
|
icons: [
|
||||||
|
{ src: "/brand/icon-192.png", sizes: "192x192", type: "image/png" },
|
||||||
|
{ src: "/brand/icon-512.png", sizes: "512x512", type: "image/png" },
|
||||||
|
{ src: "/brand/icon-maskable-512.png", sizes: "512x512", type: "image/png", purpose: "maskable" },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { Resvg } from "@resvg/resvg-js";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import manifest from "@/app/manifest";
|
||||||
|
import { metadata } from "@/app/layout";
|
||||||
|
|
||||||
|
const ORDNER = "public/brand";
|
||||||
|
|
||||||
|
/** Breite und Höhe aus dem IHDR-Block einer PNG-Datei. */
|
||||||
|
function pngGroesse(datei: string): [number, number] {
|
||||||
|
const daten = readFileSync(join(ORDNER, datei));
|
||||||
|
expect(daten.subarray(1, 4).toString()).toBe("PNG");
|
||||||
|
return [daten.readUInt32BE(16), daten.readUInt32BE(20)];
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("Marken-Dateien (pnpm marke)", () => {
|
||||||
|
it("alle SVGs der Vorgabe sind da, renderbar und ohne kaputte Koordinaten", () => {
|
||||||
|
const svgs = readdirSync(ORDNER).filter((d) => d.endsWith(".svg"));
|
||||||
|
expect(svgs.sort()).toEqual(
|
||||||
|
[
|
||||||
|
"avatar.svg",
|
||||||
|
"banner.svg",
|
||||||
|
"icon-small.svg",
|
||||||
|
"logo-light.svg",
|
||||||
|
"logo.svg",
|
||||||
|
"symbol-light.svg",
|
||||||
|
"symbol.svg",
|
||||||
|
].sort(),
|
||||||
|
);
|
||||||
|
for (const datei of svgs) {
|
||||||
|
const text = readFileSync(join(ORDNER, datei), "utf8");
|
||||||
|
expect(text, datei).not.toMatch(/NaN|Infinity/);
|
||||||
|
const bild = new Resvg(text).render();
|
||||||
|
expect(bild.width, datei).toBeGreaterThan(0);
|
||||||
|
}
|
||||||
|
const banner = new Resvg(readFileSync(join(ORDNER, "banner.svg"), "utf8")).render();
|
||||||
|
expect([banner.width, banner.height]).toEqual([1280, 640]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("kein Proxmox-Orange und nur Markenfarben", () => {
|
||||||
|
const erlaubt = new Set(["#11161C", "#1A2129", "#2A333D", "#8B98A5", "#E6EDF3", "#2DD4BF", "#0F766E"]);
|
||||||
|
for (const datei of readdirSync(ORDNER).filter((d) => d.endsWith(".svg"))) {
|
||||||
|
for (const [farbe] of readFileSync(join(ORDNER, datei), "utf8").matchAll(/#[0-9A-Fa-f]{6}\b/g)) {
|
||||||
|
expect(erlaubt.has(farbe.toUpperCase()), `${datei}: ${farbe}`).toBe(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Rasterbilder haben die richtige Größe; Layout und Manifest zeigen nur auf vorhandene Dateien", () => {
|
||||||
|
expect(pngGroesse("favicon-16.png")).toEqual([16, 16]);
|
||||||
|
expect(pngGroesse("favicon-32.png")).toEqual([32, 32]);
|
||||||
|
expect(pngGroesse("apple-touch-icon.png")).toEqual([180, 180]);
|
||||||
|
expect(pngGroesse("icon-192.png")).toEqual([192, 192]);
|
||||||
|
expect(pngGroesse("icon-512.png")).toEqual([512, 512]);
|
||||||
|
expect(pngGroesse("icon-maskable-512.png")).toEqual([512, 512]);
|
||||||
|
expect(pngGroesse("banner.png")).toEqual([1280, 640]);
|
||||||
|
|
||||||
|
const pfade = [
|
||||||
|
...(manifest().icons ?? []).map((i) => i.src),
|
||||||
|
...JSON.stringify(metadata.icons)
|
||||||
|
.matchAll(/"url":"([^"]+)"/g)
|
||||||
|
.map((t) => t[1]!),
|
||||||
|
];
|
||||||
|
expect(pfade.length).toBeGreaterThan(5);
|
||||||
|
for (const pfad of pfade) expect(existsSync(join("public", pfad)), pfad).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||