adasd
All checks were successful
Deploy / deploy (push) Successful in 1m5s

This commit is contained in:
2026-07-29 20:57:47 +02:00
parent 0e73147886
commit 37bea188d5
7 changed files with 110 additions and 208 deletions

View File

@@ -8,7 +8,7 @@ Aktueller Projektstand
- Der Avatar-Proxy liegt unter `public/api/avatar.php`. - Der Avatar-Proxy liegt unter `public/api/avatar.php`.
- Die visuelle Avatar-Konfiguration liegt im Dashboard-Modal unter `partials/landing/account/dashboard.php` und rendert Komponenten-Vorschauen live nach. - Die visuelle Avatar-Konfiguration liegt im Dashboard-Modal unter `partials/landing/account/dashboard.php` und rendert Komponenten-Vorschauen live nach.
- Der Generator speichert `avatar_style`, `avatar_seed` und `avatar_config_json` generisch im Profil. - Der Generator speichert `avatar_style`, `avatar_seed` und `avatar_config_json` generisch im Profil.
- Der Generator zeigt pro Style auch die von DiceBear unterstützten Farbfelder inklusive Fuellung, Stopps und Winkel und speichert diese im gleichen Config-JSON. - Der Generator zeigt pro Style die von DiceBear unterstützten Farbfelder direkt in den passenden Komponentenabschnitten und speichert diese im gleichen Config-JSON.
- Gespeicherte Benutzer-Avatare werden lokal unter `public/assets/avatars/users/ptk-<userId>.svg` abgelegt und bei Änderungen überschrieben. - Gespeicherte Benutzer-Avatare werden lokal unter `public/assets/avatars/users/ptk-<userId>.svg` abgelegt und bei Änderungen überschrieben.
- Der Generator besitzt einen Button `Random Profilbild`, der Seed und Varianten des aktiven Styles zufällig neu setzt. - Der Generator besitzt einen Button `Random Profilbild`, der Seed und Varianten des aktiven Styles zufällig neu setzt.
- Beim Oeffnen des Modals werden nur die Bilder des aktiven Styles und Tabs geladen; weitere Tabs laden ihre Varianten erst beim Wechsel nach. - Beim Oeffnen des Modals werden nur die Bilder des aktiven Styles und Tabs geladen; weitere Tabs laden ihre Varianten erst beim Wechsel nach.

View File

@@ -26,7 +26,7 @@ Papa-Kind-Treff ist eine PHP-basierte Plattform für Väter. Kernbereiche sind l
- Standortfunktionen sind relevant für lokale Treffen und werden künftig weiter ausgebaut. - Standortfunktionen sind relevant für lokale Treffen und werden künftig weiter ausgebaut.
- Profilbilder werden aktuell über die Mehr-Style-Architektur serverseitig erzeugt; aktiv sind derzeit `lorelei` und `croodles`. - Profilbilder werden aktuell über die Mehr-Style-Architektur serverseitig erzeugt; aktiv sind derzeit `lorelei` und `croodles`.
- Stil, Seed und Varianten werden generisch im Profil gespeichert; das fertige SVG wird lokal pro Benutzer unter `ptk-<userId>.svg` abgelegt. - Stil, Seed und Varianten werden generisch im Profil gespeichert; das fertige SVG wird lokal pro Benutzer unter `ptk-<userId>.svg` abgelegt.
- Der Avatar-Editor bietet visuelle Komponenten-Vorschauen statt reiner Text-Dropdowns, laedt nur den aktiven Style/Tab nach und zeigt pro Style die unterstützten Farb-, Fuellungs-, Stopps- und Winkelparameter an. - Der Avatar-Editor bietet visuelle Komponenten-Vorschauen statt reiner Text-Dropdowns, laedt nur den aktiven Style/Tab nach und zeigt die Farbfelder direkt im passenden Komponentenabschnitt an.
## Navigation / Konto ## Navigation / Konto
- Eingeloggte Nutzer sehen rechts ein Profil-Menü statt einzelner `Dashboard`-/`Logout`-Buttons. - Eingeloggte Nutzer sehen rechts ein Profil-Menü statt einzelner `Dashboard`-/`Logout`-Buttons.

View File

@@ -19,9 +19,9 @@ Papa-Kind-Treff ist eine PHP-basierte Plattform für Väter mit Fokus auf lokale
- eigener Einstellungsbereich für Standortfreigabe, Browser-Hinweise und Consent-Verwaltung - eigener Einstellungsbereich für Standortfreigabe, Browser-Hinweise und Consent-Verwaltung
- modernes Avatar-System mit zentraler Mehr-Style-Architektur auf Basis von DiceBear - modernes Avatar-System mit zentraler Mehr-Style-Architektur auf Basis von DiceBear
- aktive Avatar-Styles aktuell: `lorelei` und `croodles`; weitere Styles können über `config/avatar.php` freigeschaltet werden - aktive Avatar-Styles aktuell: `lorelei` und `croodles`; weitere Styles können über `config/avatar.php` freigeschaltet werden
- Profilbild-Generator mit visueller Komponenten-Auswahl, generischem Style-/Seed-/Config-Speichermodell, Style-Wechsel und vollständigen Farb-, Füllungs-, Stopps- und Winkeloptionen pro Style - Profilbild-Generator mit visueller Komponenten-Auswahl, generischem Style-/Seed-/Config-Speichermodell, Style-Wechsel und komponentenbezogenen Farbfeldern pro Style
- jedes gespeicherte Profilbild wird als lokale Benutzer-SVG unter `public/assets/avatars/users/ptk-<userId>.svg` abgelegt - jedes gespeicherte Profilbild wird als lokale Benutzer-SVG unter `public/assets/avatars/users/ptk-<userId>.svg` abgelegt
- `config/avatar.php` enthaelt jetzt pro Style auch explizite Beispielpaletten und Beispielprofile als Einbindungsreferenz - `config/avatar.php` enthaelt jetzt pro Style auch explizite Beispielpaletten, Farbgruppen pro Komponente und Beispielprofile als Einbindungsreferenz
- Consent-Manager für Analyse und externe Dienste - Consent-Manager für Analyse und externe Dienste
- Navigation aktuell mit `Home`, `Suche`, `Community` - Navigation aktuell mit `Home`, `Suche`, `Community`
- eingeloggte Nutzer sehen rechts ein Profil-Menü mit Direktlinks zu `Profil`, `Kinder`, `Termine`, `Community`, `Einstellungen` - eingeloggte Nutzer sehen rechts ein Profil-Menü mit Direktlinks zu `Profil`, `Kinder`, `Termine`, `Community`, `Einstellungen`

View File

@@ -23,6 +23,15 @@ return [
'label' => 'Lorelei', 'label' => 'Lorelei',
'class' => \App\Avatar\Lorelei::class, 'class' => \App\Avatar\Lorelei::class,
'fixed_colors' => [], 'fixed_colors' => [],
'color_groups' => [
'eyesVariant' => ['eyesColor', 'skinColor', 'backgroundColor'],
'eyebrowsVariant' => ['eyebrowsColor'],
'mouthVariant' => ['mouthColor', 'noseColor', 'frecklesColor'],
'glassesVariant' => ['glassesColor'],
'hairVariant' => ['hairColor', 'hairAccessoriesColor'],
'beardVariant' => [],
'earringsVariant' => ['earringsColor'],
],
'component_controls' => [ 'component_controls' => [
'eyesVariant', 'eyesVariant',
'eyebrowsVariant', 'eyebrowsVariant',
@@ -38,99 +47,66 @@ return [
'default' => '#d6b36b', 'default' => '#d6b36b',
'samples' => ['#d6b36b', '#b08d57', '#d9d9d9', '#8c6c4a'], 'samples' => ['#d6b36b', '#b08d57', '#d9d9d9', '#8c6c4a'],
'allow_custom' => true, 'allow_custom' => true,
'allow_fill' => true,
'allow_fill_stops' => true,
'allow_angle' => true,
], ],
'eyebrowsColor' => [ 'eyebrowsColor' => [
'label' => 'Augenbrauen', 'label' => 'Augenbrauen',
'default' => '#4a3227', 'default' => '#4a3227',
'samples' => ['#4a3227', '#6a4938', '#2c221d', '#8a6651'], 'samples' => ['#4a3227', '#6a4938', '#2c221d', '#8a6651'],
'allow_custom' => true, 'allow_custom' => true,
'allow_fill' => true,
'allow_fill_stops' => true,
'allow_angle' => true,
], ],
'eyesColor' => [ 'eyesColor' => [
'label' => 'Augen', 'label' => 'Augen',
'default' => '#2a2522', 'default' => '#2a2522',
'samples' => ['#2a2522', '#3f4f66', '#6b4d35', '#1f3a5f'], 'samples' => ['#2a2522', '#3f4f66', '#6b4d35', '#1f3a5f'],
'allow_custom' => true, 'allow_custom' => true,
'allow_fill' => true,
'allow_fill_stops' => true,
'allow_angle' => true,
], ],
'frecklesColor' => [ 'frecklesColor' => [
'label' => 'Sommersprossen', 'label' => 'Sommersprossen',
'default' => '#b67852', 'default' => '#b67852',
'samples' => ['#b67852', '#a16645', '#c48a66', '#8f5a3e'], 'samples' => ['#b67852', '#a16645', '#c48a66', '#8f5a3e'],
'allow_custom' => true, 'allow_custom' => true,
'allow_fill' => true,
'allow_fill_stops' => true,
'allow_angle' => true,
], ],
'glassesColor' => [ 'glassesColor' => [
'label' => 'Brille', 'label' => 'Brille',
'default' => '#26313f', 'default' => '#26313f',
'samples' => ['#26313f', '#4d5f73', '#1b1f27', '#7a5d42'], 'samples' => ['#26313f', '#4d5f73', '#1b1f27', '#7a5d42'],
'allow_custom' => true, 'allow_custom' => true,
'allow_fill' => true,
'allow_fill_stops' => true,
'allow_angle' => true,
], ],
'hairColor' => [ 'hairColor' => [
'label' => 'Haare', 'label' => 'Haare',
'default' => '#4b3128', 'default' => '#4b3128',
'samples' => ['#4b3128', '#6f4b38', '#221916', '#b38a52'], 'samples' => ['#4b3128', '#6f4b38', '#221916', '#b38a52'],
'allow_custom' => true, 'allow_custom' => true,
'allow_fill' => true,
'allow_fill_stops' => true,
'allow_angle' => true,
], ],
'hairAccessoriesColor' => [ 'hairAccessoriesColor' => [
'label' => 'Haar-Accessoires', 'label' => 'Haar-Accessoires',
'default' => '#b96f54', 'default' => '#b96f54',
'samples' => ['#b96f54', '#c98664', '#8d5a9e', '#5e8aa8'], 'samples' => ['#b96f54', '#c98664', '#8d5a9e', '#5e8aa8'],
'allow_custom' => true, 'allow_custom' => true,
'allow_fill' => true,
'allow_fill_stops' => true,
'allow_angle' => true,
], ],
'mouthColor' => [ 'mouthColor' => [
'label' => 'Mund', 'label' => 'Mund',
'default' => '#9f5a56', 'default' => '#9f5a56',
'samples' => ['#9f5a56', '#b66b66', '#804444', '#c98a7a'], 'samples' => ['#9f5a56', '#b66b66', '#804444', '#c98a7a'],
'allow_custom' => true, 'allow_custom' => true,
'allow_fill' => true,
'allow_fill_stops' => true,
'allow_angle' => true,
], ],
'noseColor' => [ 'noseColor' => [
'label' => 'Nase', 'label' => 'Nase',
'default' => '#a56f58', 'default' => '#a56f58',
'samples' => ['#a56f58', '#bf8569', '#8d5f49', '#d3a17b'], 'samples' => ['#a56f58', '#bf8569', '#8d5f49', '#d3a17b'],
'allow_custom' => true, 'allow_custom' => true,
'allow_fill' => true,
'allow_fill_stops' => true,
'allow_angle' => true,
], ],
'skinColor' => [ 'skinColor' => [
'label' => 'Haut', 'label' => 'Haut',
'default' => '#e7ba9a', 'default' => '#e7ba9a',
'samples' => ['#f4d2ba', '#e7ba9a', '#c98f69', '#8a5638'], 'samples' => ['#f4d2ba', '#e7ba9a', '#c98f69', '#8a5638'],
'allow_custom' => true, 'allow_custom' => true,
'allow_fill' => true,
'allow_fill_stops' => true,
'allow_angle' => true,
], ],
'backgroundColor' => [ 'backgroundColor' => [
'label' => 'Hintergrund', 'label' => 'Hintergrund',
'default' => '#f5efe5', 'default' => '#f5efe5',
'samples' => ['#f5efe5', '#e9dfd1', '#d7e6ef', '#efe2d2'], 'samples' => ['#f5efe5', '#e9dfd1', '#d7e6ef', '#efe2d2'],
'allow_custom' => true, 'allow_custom' => true,
'allow_fill' => true,
'allow_fill_stops' => true,
'allow_angle' => true,
], ],
], ],
'example_profile' => [ 'example_profile' => [
@@ -156,6 +132,15 @@ return [
'label' => 'Croodles', 'label' => 'Croodles',
'class' => \App\Avatar\Croodles::class, 'class' => \App\Avatar\Croodles::class,
'fixed_colors' => [], 'fixed_colors' => [],
'color_groups' => [
'headVariant' => ['baseColor', 'backgroundColor'],
'eyesVariant' => ['eyepatchColor', 'glassesColor'],
'noseVariant' => [],
'mouthVariant' => [],
'topVariant' => ['topColor'],
'beardVariant' => [],
'mustacheVariant' => [],
],
'component_controls' => [ 'component_controls' => [
'headVariant', 'headVariant',
'eyesVariant', 'eyesVariant',
@@ -171,45 +156,30 @@ return [
'default' => '#e7ba9a', 'default' => '#e7ba9a',
'samples' => ['#f4d2ba', '#e7ba9a', '#c98f69', '#8a5638'], 'samples' => ['#f4d2ba', '#e7ba9a', '#c98f69', '#8a5638'],
'allow_custom' => true, 'allow_custom' => true,
'allow_fill' => true,
'allow_fill_stops' => true,
'allow_angle' => true,
], ],
'eyepatchColor' => [ 'eyepatchColor' => [
'label' => 'Augenklappe', 'label' => 'Augenklappe',
'default' => '#283341', 'default' => '#283341',
'samples' => ['#283341', '#1a222d', '#4a5a6f', '#77583f'], 'samples' => ['#283341', '#1a222d', '#4a5a6f', '#77583f'],
'allow_custom' => true, 'allow_custom' => true,
'allow_fill' => true,
'allow_fill_stops' => true,
'allow_angle' => true,
], ],
'glassesColor' => [ 'glassesColor' => [
'label' => 'Brille', 'label' => 'Brille',
'default' => '#283341', 'default' => '#283341',
'samples' => ['#283341', '#4a5a6f', '#1a222d', '#74614a'], 'samples' => ['#283341', '#4a5a6f', '#1a222d', '#74614a'],
'allow_custom' => true, 'allow_custom' => true,
'allow_fill' => true,
'allow_fill_stops' => true,
'allow_angle' => true,
], ],
'topColor' => [ 'topColor' => [
'label' => 'Top-Farbe', 'label' => 'Top-Farbe',
'default' => '#4f6ea8', 'default' => '#4f6ea8',
'samples' => ['#4f6ea8', '#6d8fc2', '#36507a', '#6f8c55'], 'samples' => ['#4f6ea8', '#6d8fc2', '#36507a', '#6f8c55'],
'allow_custom' => true, 'allow_custom' => true,
'allow_fill' => true,
'allow_fill_stops' => true,
'allow_angle' => true,
], ],
'backgroundColor' => [ 'backgroundColor' => [
'label' => 'Hintergrund', 'label' => 'Hintergrund',
'default' => '#f2eadf', 'default' => '#f2eadf',
'samples' => ['#f2eadf', '#e6dccd', '#d9e8ef', '#efe0d0'], 'samples' => ['#f2eadf', '#e6dccd', '#d9e8ef', '#efe0d0'],
'allow_custom' => true, 'allow_custom' => true,
'allow_fill' => true,
'allow_fill_stops' => true,
'allow_angle' => true,
], ],
], ],
'example_profile' => [ 'example_profile' => [

View File

@@ -378,110 +378,64 @@ $sectionLinks = [
</label> </label>
<?php endforeach; ?> <?php endforeach; ?>
</div> </div>
<?php $componentColors = $component['colors'] ?? []; ?>
<?php if ($componentColors): ?>
<div class="form-grid" style="margin-top:18px;">
<?php foreach ($componentColors as $colorField => $colorMeta): ?>
<?php
$colorValue = (string)($colorMeta['value'] ?? '');
$colorDefault = (string)($colorMeta['default'] ?? '#c4a484');
$samples = is_array($colorMeta['samples'] ?? null) ? $colorMeta['samples'] : [];
?>
<div class="stack gap-6">
<label class="label" for="avatarColor<?= htmlspecialchars($styleKey . $colorField, ENT_QUOTES) ?>"><?= htmlspecialchars((string)($colorMeta['label'] ?? $colorField), ENT_QUOTES) ?></label>
<input
type="hidden"
name="avatar_config[<?= htmlspecialchars($styleKey, ENT_QUOTES) ?>][<?= htmlspecialchars((string)$colorField, ENT_QUOTES) ?>]"
value="<?= htmlspecialchars($colorValue, ENT_QUOTES) ?>"
data-avatar-style="<?= htmlspecialchars($styleKey, ENT_QUOTES) ?>"
data-avatar-color-input="<?= htmlspecialchars((string)$colorField, ENT_QUOTES) ?>"
>
<div class="flex gap-12 center-y" style="flex-wrap:wrap;">
<input
id="avatarColor<?= htmlspecialchars($styleKey . $colorField, ENT_QUOTES) ?>"
type="color"
value="<?= htmlspecialchars($colorValue !== '' ? $colorValue : $colorDefault, ENT_QUOTES) ?>"
data-avatar-style="<?= htmlspecialchars($styleKey, ENT_QUOTES) ?>"
data-avatar-color-picker="<?= htmlspecialchars((string)$colorField, ENT_QUOTES) ?>"
data-avatar-color-default="<?= htmlspecialchars($colorDefault, ENT_QUOTES) ?>"
style="width:64px; height:44px; padding:4px;"
>
<button
class="btn ghost"
type="button"
data-avatar-style="<?= htmlspecialchars($styleKey, ENT_QUOTES) ?>"
data-avatar-color-reset="<?= htmlspecialchars((string)$colorField, ENT_QUOTES) ?>"
>Standard</button>
</div>
<?php if ($samples): ?>
<div class="flex gap-8 center-y" style="flex-wrap:wrap;">
<?php foreach ($samples as $sampleColor): ?>
<button
type="button"
class="avatar-color-sample"
title="<?= htmlspecialchars((string)$sampleColor, ENT_QUOTES) ?>"
aria-label="<?= htmlspecialchars((string)$sampleColor, ENT_QUOTES) ?>"
data-avatar-style="<?= htmlspecialchars($styleKey, ENT_QUOTES) ?>"
data-avatar-color-sample-field="<?= htmlspecialchars((string)$colorField, ENT_QUOTES) ?>"
data-avatar-color-sample-value="<?= htmlspecialchars((string)$sampleColor, ENT_QUOTES) ?>"
style="width:32px; height:32px; border-radius:10px; border:1px solid var(--color-border); background: <?= htmlspecialchars((string)$sampleColor, ENT_QUOTES) ?>;"
></button>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</section> </section>
<?php endforeach; ?> <?php endforeach; ?>
</div> </div>
<?php $styleColors = $styleMeta['colors'] ?? []; ?>
<?php if ($styleColors): ?>
<div class="account-panel" style="margin-top:18px;">
<div class="account-panel__head">
<h3>Farben</h3>
<p class="muted small">Alle aktuell für diesen DiceBear-Style unterstützten Farbfelder.</p>
</div>
<div class="account-panel__body">
<div class="form-grid">
<?php foreach ($styleColors as $colorField => $colorMeta): ?>
<?php
$colorValue = (string)($colorMeta['value'] ?? '');
$colorDefault = (string)($colorMeta['default'] ?? '#c4a484');
?>
<div class="stack gap-6">
<label class="label" for="avatarColor<?= htmlspecialchars($styleKey . $colorField, ENT_QUOTES) ?>"><?= htmlspecialchars((string)($colorMeta['label'] ?? $colorField), ENT_QUOTES) ?></label>
<input
type="hidden"
name="avatar_config[<?= htmlspecialchars($styleKey, ENT_QUOTES) ?>][<?= htmlspecialchars((string)$colorField, ENT_QUOTES) ?>]"
value="<?= htmlspecialchars($colorValue, ENT_QUOTES) ?>"
data-avatar-style="<?= htmlspecialchars($styleKey, ENT_QUOTES) ?>"
data-avatar-color-input="<?= htmlspecialchars((string)$colorField, ENT_QUOTES) ?>"
>
<div class="flex gap-12 center-y" style="flex-wrap:wrap;">
<input
id="avatarColor<?= htmlspecialchars($styleKey . $colorField, ENT_QUOTES) ?>"
type="color"
value="<?= htmlspecialchars($colorValue !== '' ? $colorValue : $colorDefault, ENT_QUOTES) ?>"
data-avatar-style="<?= htmlspecialchars($styleKey, ENT_QUOTES) ?>"
data-avatar-color-picker="<?= htmlspecialchars((string)$colorField, ENT_QUOTES) ?>"
data-avatar-color-default="<?= htmlspecialchars($colorDefault, ENT_QUOTES) ?>"
style="width:64px; height:44px; padding:4px;"
>
<button
class="btn ghost"
type="button"
data-avatar-style="<?= htmlspecialchars($styleKey, ENT_QUOTES) ?>"
data-avatar-color-reset="<?= htmlspecialchars((string)$colorField, ENT_QUOTES) ?>"
>Standard</button>
<span class="muted small" data-avatar-color-label="<?= htmlspecialchars($styleKey . ':' . (string)$colorField, ENT_QUOTES) ?>">
<?= $colorValue !== '' ? htmlspecialchars($colorValue, ENT_QUOTES) : 'DiceBear-Standard' ?>
</span>
</div>
<div class="form-grid" style="margin-top:6px;">
<div class="stack gap-6">
<label class="label" for="avatarFill<?= htmlspecialchars($styleKey . $colorField, ENT_QUOTES) ?>">Füllung</label>
<select
id="avatarFill<?= htmlspecialchars($styleKey . $colorField, ENT_QUOTES) ?>"
name="avatar_config[<?= htmlspecialchars($styleKey, ENT_QUOTES) ?>][<?= htmlspecialchars((string)$colorField, ENT_QUOTES) ?>Fill]"
class="select"
data-avatar-style="<?= htmlspecialchars($styleKey, ENT_QUOTES) ?>"
data-avatar-color-meta="<?= htmlspecialchars((string)$colorField, ENT_QUOTES) ?>"
data-avatar-color-meta-type="fill"
>
<option value="" <?= (($colorMeta['fill'] ?? '') === '') ? 'selected' : '' ?>>Standard</option>
<option value="solid" <?= (($colorMeta['fill'] ?? '') === 'solid') ? 'selected' : '' ?>>Solid</option>
<option value="linear" <?= (($colorMeta['fill'] ?? '') === 'linear') ? 'selected' : '' ?>>Linear</option>
<option value="radial" <?= (($colorMeta['fill'] ?? '') === 'radial') ? 'selected' : '' ?>>Radial</option>
</select>
</div>
<div class="stack gap-6">
<label class="label" for="avatarStops<?= htmlspecialchars($styleKey . $colorField, ENT_QUOTES) ?>">Farb-Stopps</label>
<input
id="avatarStops<?= htmlspecialchars($styleKey . $colorField, ENT_QUOTES) ?>"
type="number"
min="2"
max="8"
step="1"
name="avatar_config[<?= htmlspecialchars($styleKey, ENT_QUOTES) ?>][<?= htmlspecialchars((string)$colorField, ENT_QUOTES) ?>FillStops]"
class="input"
value="<?= htmlspecialchars((string)($colorMeta['fill_stops'] ?? ''), ENT_QUOTES) ?>"
placeholder="Standard"
data-avatar-style="<?= htmlspecialchars($styleKey, ENT_QUOTES) ?>"
data-avatar-color-meta="<?= htmlspecialchars((string)$colorField, ENT_QUOTES) ?>"
data-avatar-color-meta-type="fillStops"
>
</div>
<div class="stack gap-6">
<label class="label" for="avatarAngle<?= htmlspecialchars($styleKey . $colorField, ENT_QUOTES) ?>">Winkel</label>
<input
id="avatarAngle<?= htmlspecialchars($styleKey . $colorField, ENT_QUOTES) ?>"
type="number"
min="-360"
max="360"
step="1"
name="avatar_config[<?= htmlspecialchars($styleKey, ENT_QUOTES) ?>][<?= htmlspecialchars((string)$colorField, ENT_QUOTES) ?>Angle]"
class="input"
value="<?= htmlspecialchars((string)($colorMeta['angle'] ?? ''), ENT_QUOTES) ?>"
placeholder="Standard"
data-avatar-style="<?= htmlspecialchars($styleKey, ENT_QUOTES) ?>"
data-avatar-color-meta="<?= htmlspecialchars((string)$colorField, ENT_QUOTES) ?>"
data-avatar-color-meta-type="angle"
>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
<?php endif; ?>
</div> </div>
<?php endforeach; ?> <?php endforeach; ?>
</div> </div>

View File

@@ -800,13 +800,6 @@ document.addEventListener('DOMContentLoaded', () => {
return input ? input.value : ''; return input ? input.value : '';
}; };
const readAvatarColorMetaValue = (form, style, field, type) => {
const panel = getAvatarStylePanel(form, style);
if (!panel) return '';
const input = panel.querySelector(`[data-avatar-color-meta="${field}"][data-avatar-color-meta-type="${type}"]`);
return input ? input.value : '';
};
const getAvatarStyleConfig = (style) => { const getAvatarStyleConfig = (style) => {
if (avatarStyleConfigs[style]) return avatarStyleConfigs[style]; if (avatarStyleConfigs[style]) return avatarStyleConfigs[style];
return avatarStyleConfigs.lorelei; return avatarStyleConfigs.lorelei;
@@ -845,18 +838,6 @@ document.addEventListener('DOMContentLoaded', () => {
: readAvatarColorValue(form, style, field); : readAvatarColorValue(form, style, field);
if (value) { if (value) {
params.set(field, value); params.set(field, value);
const fill = Object.prototype.hasOwnProperty.call(overrides, `${field}Fill`)
? overrides[`${field}Fill`]
: readAvatarColorMetaValue(form, style, field, 'fill');
const fillStops = Object.prototype.hasOwnProperty.call(overrides, `${field}FillStops`)
? overrides[`${field}FillStops`]
: readAvatarColorMetaValue(form, style, field, 'fillStops');
const angle = Object.prototype.hasOwnProperty.call(overrides, `${field}Angle`)
? overrides[`${field}Angle`]
: readAvatarColorMetaValue(form, style, field, 'angle');
if (fill) params.set(`${field}Fill`, fill);
if (fillStops) params.set(`${field}FillStops`, fillStops);
if (angle) params.set(`${field}Angle`, angle);
} }
}); });
@@ -937,13 +918,9 @@ document.addEventListener('DOMContentLoaded', () => {
const panel = getAvatarStylePanel(form, style); const panel = getAvatarStylePanel(form, style);
if (!panel || !field) return; if (!panel || !field) return;
const hidden = panel.querySelector(`[data-avatar-color-input="${field}"]`); const hidden = panel.querySelector(`[data-avatar-color-input="${field}"]`);
const label = panel.querySelector(`[data-avatar-color-label="${style}:${field}"]`);
if (hidden) { if (hidden) {
hidden.value = picker.value; hidden.value = picker.value;
} }
if (label) {
label.textContent = picker.value;
}
ensureAvatarBuilderLoaded(form, style); ensureAvatarBuilderLoaded(form, style);
updateAvatarPreview(form); updateAvatarPreview(form);
updateAvatarOptionPreviews(form, style, getActiveAvatarField(form, style)); updateAvatarOptionPreviews(form, style, getActiveAvatarField(form, style));
@@ -961,10 +938,6 @@ document.addEventListener('DOMContentLoaded', () => {
if (!panel || !field) return; if (!panel || !field) return;
const hidden = panel.querySelector(`[data-avatar-color-input="${field}"]`); const hidden = panel.querySelector(`[data-avatar-color-input="${field}"]`);
const picker = panel.querySelector(`[data-avatar-color-picker="${field}"]`); const picker = panel.querySelector(`[data-avatar-color-picker="${field}"]`);
const label = panel.querySelector(`[data-avatar-color-label="${style}:${field}"]`);
const fill = panel.querySelector(`[data-avatar-color-meta="${field}"][data-avatar-color-meta-type="fill"]`);
const fillStops = panel.querySelector(`[data-avatar-color-meta="${field}"][data-avatar-color-meta-type="fillStops"]`);
const angle = panel.querySelector(`[data-avatar-color-meta="${field}"][data-avatar-color-meta-type="angle"]`);
const defaultValue = picker?.getAttribute('data-avatar-color-default') || '#c4a484'; const defaultValue = picker?.getAttribute('data-avatar-color-default') || '#c4a484';
if (hidden) { if (hidden) {
hidden.value = ''; hidden.value = '';
@@ -972,28 +945,27 @@ document.addEventListener('DOMContentLoaded', () => {
if (picker) { if (picker) {
picker.value = defaultValue; picker.value = defaultValue;
} }
if (label) {
label.textContent = 'DiceBear-Standard';
}
if (fill) fill.value = '';
if (fillStops) fillStops.value = '';
if (angle) angle.value = '';
ensureAvatarBuilderLoaded(form, style); ensureAvatarBuilderLoaded(form, style);
updateAvatarPreview(form); updateAvatarPreview(form);
updateAvatarOptionPreviews(form, style, getActiveAvatarField(form, style)); updateAvatarOptionPreviews(form, style, getActiveAvatarField(form, style));
}); });
}); });
form.querySelectorAll('[data-avatar-color-meta]').forEach((input) => { form.querySelectorAll('[data-avatar-color-sample-field]').forEach((button) => {
const syncMeta = () => { button.addEventListener('click', () => {
const style = input.getAttribute('data-avatar-style') || getCurrentAvatarStyle(form); const style = button.getAttribute('data-avatar-style') || getCurrentAvatarStyle(form);
const field = button.getAttribute('data-avatar-color-sample-field') || '';
const value = button.getAttribute('data-avatar-color-sample-value') || '';
const panel = getAvatarStylePanel(form, style);
if (!panel || !field || !value) return;
const hidden = panel.querySelector(`[data-avatar-color-input="${field}"]`);
const picker = panel.querySelector(`[data-avatar-color-picker="${field}"]`);
if (hidden) hidden.value = value;
if (picker) picker.value = value;
ensureAvatarBuilderLoaded(form, style); ensureAvatarBuilderLoaded(form, style);
updateAvatarPreview(form); updateAvatarPreview(form);
updateAvatarOptionPreviews(form, style, getActiveAvatarField(form, style)); updateAvatarOptionPreviews(form, style, getActiveAvatarField(form, style));
}; });
input.addEventListener('input', syncMeta);
input.addEventListener('change', syncMeta);
}); });
const setActiveComponentPanel = (style, field) => { const setActiveComponentPanel = (style, field) => {
@@ -1018,7 +990,7 @@ document.addEventListener('DOMContentLoaded', () => {
panel.querySelectorAll('[data-avatar-field]').forEach((input) => { panel.querySelectorAll('[data-avatar-field]').forEach((input) => {
input.disabled = !active; input.disabled = !active;
}); });
panel.querySelectorAll('[data-avatar-color-input], [data-avatar-color-picker], [data-avatar-color-reset]').forEach((input) => { panel.querySelectorAll('[data-avatar-color-input], [data-avatar-color-picker], [data-avatar-color-reset], [data-avatar-color-sample-field]').forEach((input) => {
input.disabled = !active; input.disabled = !active;
}); });
}); });

View File

@@ -209,6 +209,9 @@ final class AvatarManager
$styleClass = (string)$meta['class']; $styleClass = (string)$meta['class'];
$styleConfig = $currentStyle === $meta['key'] ? $currentConfig : $styleClass::defaultConfig(); $styleConfig = $currentStyle === $meta['key'] ? $currentConfig : $styleClass::defaultConfig();
$components = []; $components = [];
$colorDefinitions = $styleClass::colorDefinitions();
$colorControls = is_array($meta['color_controls'] ?? null) ? $meta['color_controls'] : [];
$colorGroups = is_array($meta['color_groups'] ?? null) ? $meta['color_groups'] : [];
foreach ($styleClass::componentDefinitions() as $field => $definition) { foreach ($styleClass::componentDefinitions() as $field => $definition) {
$options = []; $options = [];
@@ -221,24 +224,28 @@ final class AvatarManager
]; ];
} }
$componentColors = [];
foreach (($colorGroups[$field] ?? []) as $colorField) {
if (!isset($colorDefinitions[$colorField])) {
continue;
}
$componentColors[$colorField] = [
'field' => $colorField,
'label' => (string)($colorControls[$colorField]['label'] ?? $colorDefinitions[$colorField]['label'] ?? $colorField),
'value' => (string)($styleConfig[$colorField] ?? ''),
'default' => (string)($colorControls[$colorField]['default'] ?? $colorDefinitions[$colorField]['default'] ?? '#c4a484'),
'samples' => is_array($colorControls[$colorField]['samples'] ?? null) ? $colorControls[$colorField]['samples'] : [],
'allow_custom' => (bool)($colorControls[$colorField]['allow_custom'] ?? true),
];
}
$components[$field] = [ $components[$field] = [
'field' => $field, 'field' => $field,
'label' => (string)($definition['label'] ?? $field), 'label' => (string)($definition['label'] ?? $field),
'count' => count(array_filter($options, static fn(array $option): bool => $option['value'] !== '')), 'count' => count(array_filter($options, static fn(array $option): bool => $option['value'] !== '')),
'options' => $options, 'options' => $options,
]; 'colors' => $componentColors,
}
$colors = [];
foreach ($styleClass::colorDefinitions() as $field => $definition) {
$colors[$field] = [
'field' => $field,
'label' => (string)($definition['label'] ?? $field),
'value' => (string)($styleConfig[$field] ?? ''),
'default' => (string)($definition['default'] ?? '#c4a484'),
'fill' => (string)($styleConfig[$field . 'Fill'] ?? ''),
'fill_stops' => (string)($styleConfig[$field . 'FillStops'] ?? ''),
'angle' => (string)($styleConfig[$field . 'Angle'] ?? ''),
]; ];
} }
@@ -246,7 +253,6 @@ final class AvatarManager
'key' => $meta['key'], 'key' => $meta['key'],
'label' => $meta['label'], 'label' => $meta['label'],
'components' => $components, 'components' => $components,
'colors' => $colors,
]; ];
} }