diff --git a/.projektstructure.txt b/.projektstructure.txt index fde45d7..c6ea3af 100755 --- a/.projektstructure.txt +++ b/.projektstructure.txt @@ -8,7 +8,7 @@ Aktueller Projektstand - 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. - 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-.svg` abgelegt und bei Änderungen überschrieben. - 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. diff --git a/PROJECT_CONTEXT.md b/PROJECT_CONTEXT.md index 83661ac..b8a06fb 100644 --- a/PROJECT_CONTEXT.md +++ b/PROJECT_CONTEXT.md @@ -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. - 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-.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 - Eingeloggte Nutzer sehen rechts ein Profil-Menü statt einzelner `Dashboard`-/`Logout`-Buttons. diff --git a/README.md b/README.md index c98ab5f..bb47b62 100644 --- a/README.md +++ b/README.md @@ -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 - 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 -- 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-.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 - Navigation aktuell mit `Home`, `Suche`, `Community` - eingeloggte Nutzer sehen rechts ein Profil-Menü mit Direktlinks zu `Profil`, `Kinder`, `Termine`, `Community`, `Einstellungen` diff --git a/config/avatar.php b/config/avatar.php index d4aa51f..2357645 100644 --- a/config/avatar.php +++ b/config/avatar.php @@ -23,6 +23,15 @@ return [ 'label' => 'Lorelei', 'class' => \App\Avatar\Lorelei::class, 'fixed_colors' => [], + 'color_groups' => [ + 'eyesVariant' => ['eyesColor', 'skinColor', 'backgroundColor'], + 'eyebrowsVariant' => ['eyebrowsColor'], + 'mouthVariant' => ['mouthColor', 'noseColor', 'frecklesColor'], + 'glassesVariant' => ['glassesColor'], + 'hairVariant' => ['hairColor', 'hairAccessoriesColor'], + 'beardVariant' => [], + 'earringsVariant' => ['earringsColor'], + ], 'component_controls' => [ 'eyesVariant', 'eyebrowsVariant', @@ -38,99 +47,66 @@ return [ 'default' => '#d6b36b', 'samples' => ['#d6b36b', '#b08d57', '#d9d9d9', '#8c6c4a'], 'allow_custom' => true, - 'allow_fill' => true, - 'allow_fill_stops' => true, - 'allow_angle' => true, ], 'eyebrowsColor' => [ 'label' => 'Augenbrauen', 'default' => '#4a3227', 'samples' => ['#4a3227', '#6a4938', '#2c221d', '#8a6651'], 'allow_custom' => true, - 'allow_fill' => true, - 'allow_fill_stops' => true, - 'allow_angle' => true, ], 'eyesColor' => [ 'label' => 'Augen', 'default' => '#2a2522', 'samples' => ['#2a2522', '#3f4f66', '#6b4d35', '#1f3a5f'], 'allow_custom' => true, - 'allow_fill' => true, - 'allow_fill_stops' => true, - 'allow_angle' => true, ], 'frecklesColor' => [ 'label' => 'Sommersprossen', 'default' => '#b67852', 'samples' => ['#b67852', '#a16645', '#c48a66', '#8f5a3e'], 'allow_custom' => true, - 'allow_fill' => true, - 'allow_fill_stops' => true, - 'allow_angle' => true, ], 'glassesColor' => [ 'label' => 'Brille', 'default' => '#26313f', 'samples' => ['#26313f', '#4d5f73', '#1b1f27', '#7a5d42'], 'allow_custom' => true, - 'allow_fill' => true, - 'allow_fill_stops' => true, - 'allow_angle' => true, ], 'hairColor' => [ 'label' => 'Haare', 'default' => '#4b3128', 'samples' => ['#4b3128', '#6f4b38', '#221916', '#b38a52'], 'allow_custom' => true, - 'allow_fill' => true, - 'allow_fill_stops' => true, - 'allow_angle' => true, ], 'hairAccessoriesColor' => [ 'label' => 'Haar-Accessoires', 'default' => '#b96f54', 'samples' => ['#b96f54', '#c98664', '#8d5a9e', '#5e8aa8'], 'allow_custom' => true, - 'allow_fill' => true, - 'allow_fill_stops' => true, - 'allow_angle' => true, ], 'mouthColor' => [ 'label' => 'Mund', 'default' => '#9f5a56', 'samples' => ['#9f5a56', '#b66b66', '#804444', '#c98a7a'], 'allow_custom' => true, - 'allow_fill' => true, - 'allow_fill_stops' => true, - 'allow_angle' => true, ], 'noseColor' => [ 'label' => 'Nase', 'default' => '#a56f58', 'samples' => ['#a56f58', '#bf8569', '#8d5f49', '#d3a17b'], 'allow_custom' => true, - 'allow_fill' => true, - 'allow_fill_stops' => true, - 'allow_angle' => true, ], 'skinColor' => [ 'label' => 'Haut', 'default' => '#e7ba9a', 'samples' => ['#f4d2ba', '#e7ba9a', '#c98f69', '#8a5638'], 'allow_custom' => true, - 'allow_fill' => true, - 'allow_fill_stops' => true, - 'allow_angle' => true, ], 'backgroundColor' => [ 'label' => 'Hintergrund', 'default' => '#f5efe5', 'samples' => ['#f5efe5', '#e9dfd1', '#d7e6ef', '#efe2d2'], 'allow_custom' => true, - 'allow_fill' => true, - 'allow_fill_stops' => true, - 'allow_angle' => true, ], ], 'example_profile' => [ @@ -156,6 +132,15 @@ return [ 'label' => 'Croodles', 'class' => \App\Avatar\Croodles::class, 'fixed_colors' => [], + 'color_groups' => [ + 'headVariant' => ['baseColor', 'backgroundColor'], + 'eyesVariant' => ['eyepatchColor', 'glassesColor'], + 'noseVariant' => [], + 'mouthVariant' => [], + 'topVariant' => ['topColor'], + 'beardVariant' => [], + 'mustacheVariant' => [], + ], 'component_controls' => [ 'headVariant', 'eyesVariant', @@ -171,45 +156,30 @@ return [ 'default' => '#e7ba9a', 'samples' => ['#f4d2ba', '#e7ba9a', '#c98f69', '#8a5638'], 'allow_custom' => true, - 'allow_fill' => true, - 'allow_fill_stops' => true, - 'allow_angle' => true, ], 'eyepatchColor' => [ 'label' => 'Augenklappe', 'default' => '#283341', 'samples' => ['#283341', '#1a222d', '#4a5a6f', '#77583f'], 'allow_custom' => true, - 'allow_fill' => true, - 'allow_fill_stops' => true, - 'allow_angle' => true, ], 'glassesColor' => [ 'label' => 'Brille', 'default' => '#283341', 'samples' => ['#283341', '#4a5a6f', '#1a222d', '#74614a'], 'allow_custom' => true, - 'allow_fill' => true, - 'allow_fill_stops' => true, - 'allow_angle' => true, ], 'topColor' => [ 'label' => 'Top-Farbe', 'default' => '#4f6ea8', 'samples' => ['#4f6ea8', '#6d8fc2', '#36507a', '#6f8c55'], 'allow_custom' => true, - 'allow_fill' => true, - 'allow_fill_stops' => true, - 'allow_angle' => true, ], 'backgroundColor' => [ 'label' => 'Hintergrund', 'default' => '#f2eadf', 'samples' => ['#f2eadf', '#e6dccd', '#d9e8ef', '#efe0d0'], 'allow_custom' => true, - 'allow_fill' => true, - 'allow_fill_stops' => true, - 'allow_angle' => true, ], ], 'example_profile' => [ diff --git a/partials/landing/account/dashboard.php b/partials/landing/account/dashboard.php index a486432..d3929c5 100755 --- a/partials/landing/account/dashboard.php +++ b/partials/landing/account/dashboard.php @@ -378,110 +378,64 @@ $sectionLinks = [ + + +
+ $colorMeta): ?> + +
+ + +
+ + +
+ +
+ + + +
+ +
+ +
+ - - - - diff --git a/public/assets/js/app.js b/public/assets/js/app.js index b47270b..1c51b33 100755 --- a/public/assets/js/app.js +++ b/public/assets/js/app.js @@ -800,13 +800,6 @@ document.addEventListener('DOMContentLoaded', () => { 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) => { if (avatarStyleConfigs[style]) return avatarStyleConfigs[style]; return avatarStyleConfigs.lorelei; @@ -845,18 +838,6 @@ document.addEventListener('DOMContentLoaded', () => { : readAvatarColorValue(form, style, field); if (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); if (!panel || !field) return; const hidden = panel.querySelector(`[data-avatar-color-input="${field}"]`); - const label = panel.querySelector(`[data-avatar-color-label="${style}:${field}"]`); if (hidden) { hidden.value = picker.value; } - if (label) { - label.textContent = picker.value; - } ensureAvatarBuilderLoaded(form, style); updateAvatarPreview(form); updateAvatarOptionPreviews(form, style, getActiveAvatarField(form, style)); @@ -961,10 +938,6 @@ document.addEventListener('DOMContentLoaded', () => { if (!panel || !field) return; const hidden = panel.querySelector(`[data-avatar-color-input="${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'; if (hidden) { hidden.value = ''; @@ -972,28 +945,27 @@ document.addEventListener('DOMContentLoaded', () => { if (picker) { picker.value = defaultValue; } - if (label) { - label.textContent = 'DiceBear-Standard'; - } - if (fill) fill.value = ''; - if (fillStops) fillStops.value = ''; - if (angle) angle.value = ''; ensureAvatarBuilderLoaded(form, style); updateAvatarPreview(form); updateAvatarOptionPreviews(form, style, getActiveAvatarField(form, style)); }); }); - form.querySelectorAll('[data-avatar-color-meta]').forEach((input) => { - const syncMeta = () => { - const style = input.getAttribute('data-avatar-style') || getCurrentAvatarStyle(form); + form.querySelectorAll('[data-avatar-color-sample-field]').forEach((button) => { + button.addEventListener('click', () => { + 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); updateAvatarPreview(form); updateAvatarOptionPreviews(form, style, getActiveAvatarField(form, style)); - }; - - input.addEventListener('input', syncMeta); - input.addEventListener('change', syncMeta); + }); }); const setActiveComponentPanel = (style, field) => { @@ -1018,7 +990,7 @@ document.addEventListener('DOMContentLoaded', () => { panel.querySelectorAll('[data-avatar-field]').forEach((input) => { 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; }); }); diff --git a/src/App/Avatar/AvatarManager.php b/src/App/Avatar/AvatarManager.php index 8a4734e..8d9ce5a 100644 --- a/src/App/Avatar/AvatarManager.php +++ b/src/App/Avatar/AvatarManager.php @@ -209,6 +209,9 @@ final class AvatarManager $styleClass = (string)$meta['class']; $styleConfig = $currentStyle === $meta['key'] ? $currentConfig : $styleClass::defaultConfig(); $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) { $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] = [ 'field' => $field, 'label' => (string)($definition['label'] ?? $field), 'count' => count(array_filter($options, static fn(array $option): bool => $option['value'] !== '')), 'options' => $options, - ]; - } - - $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'] ?? ''), + 'colors' => $componentColors, ]; } @@ -246,7 +253,6 @@ final class AvatarManager 'key' => $meta['key'], 'label' => $meta['label'], 'components' => $components, - 'colors' => $colors, ]; }