Icon-System: icon()-Helper, lokale Bootstrap-Icons, icons-add.php

Inline-SVG-Ausgabe via icon('name', 'klasse', 'label?') aus
public/assets/icons/. Dekorativ (aria-hidden) oder semantisch (role=img).
.icon-Basisklasse in utilities.css (1em, currentColor). bin/icons-add.php
holt neue Icons einzeln von icons.getbootstrap.com.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KDQZ6FuchNUh2bxSW9PeNv
This commit is contained in:
2026-06-19 08:11:58 +02:00
parent c0616da257
commit 5170dcb606
11 changed files with 104 additions and 0 deletions

View File

@@ -55,6 +55,30 @@ function asset(string $path): string
return '/assets/' . $path . '?v=' . $version; return '/assets/' . $path . '?v=' . $version;
} }
/**
* Bootstrap-Icon als Inline-SVG ausgeben (lokal aus public/assets/icons/<name>.svg).
* Standard: dekorativ (aria-hidden). Mit $label wird es als beschriftetes Bild
* (role="img") ausgegeben. Größe/Farbe steuert CSS über die Klasse .icon.
* Quelle: Bootstrap Icons (MIT) — neue Icons via bin/icons-add.php hinzufügen.
*/
function icon(string $name, string $class = '', ?string $label = null): string
{
static $cache = [];
if (!array_key_exists($name, $cache)) {
$file = PUBLIC_PATH . '/assets/icons/' . basename($name) . '.svg';
$cache[$name] = is_file($file) ? trim((string) file_get_contents($file)) : '';
}
if ($cache[$name] === '') {
return '';
}
$attrs = 'class="icon' . ($class !== '' ? ' ' . e($class) : '') . '"';
$attrs .= $label !== null && $label !== ''
? ' role="img" aria-label="' . e($label) . '"'
: ' aria-hidden="true" focusable="false"';
return preg_replace('/<svg\b/', '<svg ' . $attrs, $cache[$name], 1);
}
/** /**
* JSON-Datendatei laden (data/<name>.json) mit Request-weitem Cache. * JSON-Datendatei laden (data/<name>.json) mit Request-weitem Cache.
* Wirft bei kaputtem JSON — Datenfehler sollen laut scheitern, nicht leise. * Wirft bei kaputtem JSON — Datenfehler sollen laut scheitern, nicht leise.

61
bin/icons-add.php Normal file
View File

@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
/**
* Bootstrap Icons (MIT) lokal nachladen — holt einzelne Icon-SVGs und legt sie
* minimiert unter public/assets/icons/<name>.svg ab (nur viewBox + Pfade,
* Größe/Farbe steuert CSS über die .icon-Klasse bzw. currentColor).
*
* NUR CLI/Build — nie im Request-Pfad. Besucher laden ausschließlich die lokalen
* SVGs (inline via icon()-Helper), niemals ein CDN.
*
* Aufruf: php bin/icons-add.php pause-fill play-fill arrow-right ...
* Icon-Namen siehe https://icons.getbootstrap.com (Dateiname ohne .svg).
*/
if (PHP_SAPI !== 'cli') {
exit(1);
}
require dirname(__DIR__) . '/app/bootstrap.php';
const BI_VERSION = '1.11.3';
const BI_BASE = 'https://cdn.jsdelivr.net/npm/bootstrap-icons@' . BI_VERSION . '/icons/';
$names = array_slice($argv, 1);
if ($names === []) {
fwrite(STDERR, "Usage: php bin/icons-add.php <icon-name> [<icon-name> ...]\n");
exit(1);
}
$dir = PUBLIC_PATH . '/assets/icons';
if (!is_dir($dir) && !mkdir($dir, 0775, true) && !is_dir($dir)) {
fwrite(STDERR, "Kann Verzeichnis nicht anlegen: {$dir}\n");
exit(1);
}
$failed = 0;
foreach ($names as $name) {
$name = basename(trim($name));
if ($name === '' || !preg_match('/^[a-z0-9-]+$/', $name)) {
fwrite(STDERR, "Übersprungen (ungültiger Name): {$name}\n");
$failed++;
continue;
}
$raw = @file_get_contents(BI_BASE . $name . '.svg');
if ($raw === false || !str_contains($raw, '<svg')) {
fwrite(STDERR, "Nicht gefunden: {$name}\n");
$failed++;
continue;
}
// Inneres SVG extrahieren und minimal neu aufbauen (currentColor erbt).
$inner = preg_replace(['/.*?<svg[^>]*>/s', '#</svg>\s*$#s'], '', $raw);
$svg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">' . trim((string) $inner) . '</svg>' . "\n";
file_put_contents($dir . '/' . $name . '.svg', $svg);
echo "{$name}\n";
}
exit($failed > 0 ? 1 : 0);

View File

@@ -44,6 +44,16 @@
border: 0; border: 0;
} }
/* Inline-Icons (Bootstrap Icons via icon()-Helper). Größe = font-size, Farbe = currentColor. */
.icon {
display: inline-block;
width: 1em;
height: 1em;
fill: currentColor;
vertical-align: -0.125em;
flex: none;
}
.text-center { .text-center {
text-align: center; text-align: center;
} }

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M1 8a.5.5 0 0 1 .5-.5h11.793l-3.147-3.146a.5.5 0 0 1 .708-.708l4 4a.5.5 0 0 1 0 .708l-4 4a.5.5 0 0 1-.708-.708L13.293 8.5H1.5A.5.5 0 0 1 1 8"/></svg>

After

Width:  |  Height:  |  Size: 239 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708"/></svg>

After

Width:  |  Height:  |  Size: 216 B

View File

@@ -0,0 +1,2 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="M8 3.5a.5.5 0 0 0-1 0V9a.5.5 0 0 0 .252.434l3.5 2a.5.5 0 0 0 .496-.868L8 8.71z"/>
<path d="M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m7-8A7 7 0 1 1 1 8a7 7 0 0 1 14 0"/></svg>

After

Width:  |  Height:  |  Size: 239 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="M5.5 3.5A1.5 1.5 0 0 1 7 5v6a1.5 1.5 0 0 1-3 0V5a1.5 1.5 0 0 1 1.5-1.5m5 0A1.5 1.5 0 0 1 12 5v6a1.5 1.5 0 0 1-3 0V5a1.5 1.5 0 0 1 1.5-1.5"/></svg>

After

Width:  |  Height:  |  Size: 216 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="M7 14s-1 0-1-1 1-4 5-4 5 3 5 4-1 1-1 1zm4-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6m-5.784 6A2.24 2.24 0 0 1 5 13c0-1.355.68-2.75 1.936-3.72A6.3 6.3 0 0 0 5 9c-4 0-5 3-5 4s1 1 1 1zM4.5 8a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5"/></svg>

After

Width:  |  Height:  |  Size: 289 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="m11.596 8.697-6.363 3.692c-.54.313-1.233-.066-1.233-.697V4.308c0-.63.692-1.01 1.233-.696l6.363 3.692a.802.802 0 0 1 0 1.393"/></svg>

After

Width:  |  Height:  |  Size: 202 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="M2.5.5A.5.5 0 0 1 3 0h10a.5.5 0 0 1 .5.5q0 .807-.034 1.536a3 3 0 1 1-1.133 5.89c-.79 1.865-1.878 2.777-2.833 3.011v2.173l1.425.356c.194.048.377.135.537.255L13.3 15.1a.5.5 0 0 1-.3.9H3a.5.5 0 0 1-.3-.9l1.838-1.379c.16-.12.343-.207.537-.255L6.5 13.11v-2.173c-.955-.234-2.043-1.146-2.833-3.012a3 3 0 1 1-1.132-5.89A33 33 0 0 1 2.5.5m.099 2.54a2 2 0 0 0 .72 3.935c-.333-1.05-.588-2.346-.72-3.935m10.083 3.935a2 2 0 0 0 .72-3.935c-.133 1.59-.388 2.885-.72 3.935M3.504 1q.01.775.056 1.469c.13 2.028.457 3.546.87 4.667C5.294 9.48 6.484 10 7 10a.5.5 0 0 1 .5.5v2.61a1 1 0 0 1-.757.97l-1.426.356a.5.5 0 0 0-.179.085L4.5 15h7l-.638-.479a.5.5 0 0 0-.18-.085l-1.425-.356a1 1 0 0 1-.757-.97V10.5A.5.5 0 0 1 9 10c.516 0 1.706-.52 2.57-2.864.413-1.12.74-2.64.87-4.667q.045-.694.056-1.469z"/></svg>

After

Width:  |  Height:  |  Size: 852 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="M8.051 1.999h.089c.822.003 4.987.033 6.11.335a2.01 2.01 0 0 1 1.415 1.42c.101.38.172.883.22 1.402l.01.104.022.26.008.104c.065.914.073 1.77.074 1.957v.075c-.001.194-.01 1.108-.082 2.06l-.008.105-.009.104c-.05.572-.124 1.14-.235 1.558a2.01 2.01 0 0 1-1.415 1.42c-1.16.312-5.569.334-6.18.335h-.142c-.309 0-1.587-.006-2.927-.052l-.17-.006-.087-.004-.171-.007-.171-.007c-1.11-.049-2.167-.128-2.654-.26a2.01 2.01 0 0 1-1.415-1.419c-.111-.417-.185-.986-.235-1.558L.09 9.82l-.008-.104A31 31 0 0 1 0 7.68v-.123c.002-.215.01-.958.064-1.778l.007-.103.003-.052.008-.104.022-.26.01-.104c.048-.519.119-1.023.22-1.402a2.01 2.01 0 0 1 1.415-1.42c.487-.13 1.544-.21 2.654-.26l.17-.007.172-.006.086-.003.171-.007A100 100 0 0 1 7.858 2zM6.4 5.209v4.818l4.157-2.408z"/></svg>

After

Width:  |  Height:  |  Size: 825 B