adsad
All checks were successful
Deploy / deploy (push) Successful in 1m41s

This commit is contained in:
2026-07-27 02:08:28 +02:00
parent 75e3cab14b
commit da942a76aa
3 changed files with 26 additions and 75 deletions

View File

@@ -297,12 +297,16 @@ $sectionLinks = [
</div>
<form method="post" class="stack gap-12 avatar-builder" data-avatar-builder style="margin-top:12px;">
<input type="hidden" name="action" value="avatar_update">
<input type="hidden" name="avatar_preset" value="<?= htmlspecialchars((string)$profile['avatar_preset'], ENT_QUOTES) ?>" data-avatar-field="avatar_preset">
<div class="avatar-builder__layout">
<div class="avatar-builder__preview">
<div data-avatar-preview>
<?= \App\Avatar\Lorelei::render($profile, $profile['display_name'] ?: 'Papa', 'xxl') ?>
</div>
<p class="muted small">Wähle eine Lorelei-Variante oder lade dir direkt neue Vorschläge. Die Vorschau aktualisiert sich sofort.</p>
<p class="muted small">Wähle eine Komponente und passe dein neues Profilbild Schritt für Schritt an. Gespeichert wird erst nach Klick auf den Button unten.</p>
<div class="flex gap-12" style="flex-wrap:wrap; justify-content:center;">
<button class="btn ghost" type="button" data-avatar-reroll>Neue Grundvariante laden</button>
</div>
<div class="avatar-component-nav" role="tablist" aria-label="Avatar-Komponenten">
<?php foreach (\App\Avatar\Lorelei::componentDefinitions() as $field => $meta): ?>
<button
@@ -317,10 +321,10 @@ $sectionLinks = [
</div>
<div class="avatar-component-panels">
<?php foreach (\App\Avatar\Lorelei::componentDefinitions() as $field => $meta): ?>
<section class="avatar-component-panel <?= $field === 'avatar_lorelei_eyes_variant' ? 'is-active' : '' ?>" data-avatar-component-panel="<?= htmlspecialchars((string)$field, ENT_QUOTES) ?>">
<section class="avatar-component-panel avatar-component-panel--<?= htmlspecialchars(str_replace(['avatar_lorelei_', '_variant'], '', (string)$field), ENT_QUOTES) ?> <?= $field === 'avatar_lorelei_eyes_variant' ? 'is-active' : '' ?>" data-avatar-component-panel="<?= htmlspecialchars((string)$field, ENT_QUOTES) ?>">
<div class="avatar-component-panel__head">
<strong><?= htmlspecialchars((string)$meta['label'], ENT_QUOTES) ?></strong>
<span class="muted small">Direkt mit Vorschau auswählbar</span>
<span class="muted small">Nur die jeweilige Komponente in groß</span>
</div>
<div class="avatar-component-grid">
<?php foreach (\App\Avatar\Lorelei::previewChoices($profile, $field) as $choice): ?>
@@ -349,38 +353,6 @@ $sectionLinks = [
<?php endforeach; ?>
</div>
</div>
<div class="avatar-builder__controls">
<div class="flex gap-12" style="flex-wrap:wrap;">
<button class="btn ghost" type="button" data-avatar-reroll>Neue Varianten laden</button>
</div>
<div class="stack gap-6">
<span class="label">Variante wählen</span>
<div class="avatar-preset-grid" data-avatar-grid>
<?php foreach ($avatarPresetOptions as $index => $meta): ?>
<label class="avatar-preset-card">
<input
type="radio"
name="avatar_preset"
value="<?= htmlspecialchars((string)$meta['seed'], ENT_QUOTES) ?>"
data-avatar-field="avatar_preset"
data-avatar-url="<?= htmlspecialchars((string)$meta['image'], ENT_QUOTES) ?>"
<?= $profile['avatar_preset'] === $meta['seed'] || ($profile['avatar_preset'] === '' && $index === 0) ? 'checked' : '' ?>
>
<span class="avatar-preset-card__thumb">
<img
src="<?= htmlspecialchars((string)$meta['image'], ENT_QUOTES) ?>"
alt="<?= htmlspecialchars((string)$meta['label'], ENT_QUOTES) ?>"
data-avatar-option-thumb
data-avatar-option-field="avatar_preset"
data-avatar-option-value="<?= htmlspecialchars((string)$meta['seed'], ENT_QUOTES) ?>"
>
</span>
<span class="avatar-preset-card__label"><?= htmlspecialchars((string)$meta['label'], ENT_QUOTES) ?></span>
</label>
<?php endforeach; ?>
</div>
</div>
</div>
</div>
<div class="flex gap-12">
<button class="btn ghost" type="button" data-modal-close>Abbrechen</button>

View File

@@ -247,9 +247,8 @@ body {
.profile-avatar-panel__preview { display: flex; justify-content: center; }
.profile-avatar-panel__content h3 { margin: 0 0 8px; }
.profile-avatar-panel__content p { margin: 0 0 14px; }
.avatar-builder__layout { display: grid; grid-template-columns: 240px minmax(0, 1fr); gap: 24px; align-items: start; }
.avatar-builder__preview { position: sticky; top: 0; display: grid; justify-items: center; gap: 12px; padding: 18px; background: linear-gradient(180deg, #fffdfa, #f8f4ec); border: 1px solid var(--color-border); border-radius: 18px; }
.avatar-builder__controls { display: grid; gap: 18px; }
.avatar-builder__layout { display: grid; grid-template-columns: 1fr; gap: 20px; align-items: start; }
.avatar-builder__preview { display: grid; justify-items: center; gap: 14px; padding: 18px; background: linear-gradient(180deg, #fffdfa, #f8f4ec); border: 1px solid var(--color-border); border-radius: 18px; }
.avatar-component-nav { display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; }
.avatar-component-nav__button { border: 1px solid var(--color-border); background: #fff; color: var(--color-text); border-radius: 999px; padding: 8px 12px; font: inherit; font-size: 13px; font-weight: 700; cursor: pointer; }
.avatar-component-nav__button.is-active { background: var(--color-primary); color: var(--color-primary-contrast); border-color: var(--color-primary); }
@@ -257,22 +256,21 @@ body {
.avatar-component-panel { display: none; width: 100%; gap: 10px; }
.avatar-component-panel.is-active { display: grid; }
.avatar-component-panel__head { display: flex; justify-content: space-between; gap: 10px; align-items: baseline; }
.avatar-component-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; max-height: 360px; overflow: auto; padding-right: 4px; }
.avatar-component-card { position: relative; display: grid; gap: 6px; padding: 8px; background: #fff; border: 1px solid var(--color-border); border-radius: 14px; cursor: pointer; }
.avatar-component-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 14px; max-height: 460px; overflow: auto; padding-right: 4px; }
.avatar-component-card { position: relative; display: grid; gap: 8px; padding: 10px; background: #fff; border: 1px solid var(--color-border); border-radius: 16px; cursor: pointer; }
.avatar-component-card input { position: absolute; opacity: 0; pointer-events: none; }
.avatar-component-card__thumb { display: block; aspect-ratio: 1; border-radius: 12px; overflow: hidden; background: linear-gradient(180deg, #f7efe6, #f0e4d7); border: 1px solid rgba(74, 52, 38, 0.08); }
.avatar-component-card__thumb img { width: 100%; height: 100%; object-fit: cover; object-position: center top; display: block; }
.avatar-component-card__thumb { display: block; aspect-ratio: 1; border-radius: 14px; overflow: hidden; background: linear-gradient(180deg, #f7efe6, #f0e4d7); border: 1px solid rgba(74, 52, 38, 0.08); }
.avatar-component-card__thumb img { width: 100%; height: 100%; object-fit: cover; object-position: center top; display: block; transform-origin: center; }
.avatar-component-card__label { font-size: 12px; font-weight: 700; color: var(--color-text); text-align: center; }
.avatar-component-card:has(input:checked) { border-color: var(--color-primary); box-shadow: 0 0 0 3px rgba(52,72,90,0.14); }
.avatar-preset-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(132px, 1fr)); gap: 12px; }
.avatar-preset-card { position: relative; display: grid; gap: 8px; align-content: start; padding: 10px; border: 1px solid var(--color-border); border-radius: 16px; background: #fff; cursor: pointer; transition: border-color .15s ease, box-shadow .15s ease, transform .15s ease; }
.avatar-preset-card:hover { border-color: var(--color-primary); box-shadow: 0 10px 20px rgba(41, 31, 22, 0.08); transform: translateY(-1px); }
.avatar-preset-card input { position: absolute; opacity: 0; pointer-events: none; }
.avatar-preset-card__thumb { display: block; aspect-ratio: 1; border-radius: 14px; overflow: hidden; background: linear-gradient(180deg, #f7efe6, #f0e4d7); border: 1px solid rgba(74, 52, 38, 0.08); }
.avatar-preset-card__thumb img { width: 100%; height: 100%; object-fit: cover; object-position: center top; display: block; }
.avatar-preset-card__label { font-size: 13px; font-weight: 700; color: var(--color-text); }
.avatar-preset-card:has(input:checked) { border-color: var(--color-primary); box-shadow: 0 0 0 3px rgba(52,72,90,0.14); }
@media (max-width: 860px){ .avatar-builder__layout { grid-template-columns: 1fr; } .avatar-builder__preview { position: static; } .avatar-component-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.avatar-component-panel--eyes .avatar-component-card__thumb img,
.avatar-component-panel--eyebrows .avatar-component-card__thumb img { transform: scale(2.5) translateY(-14%); }
.avatar-component-panel--mouth .avatar-component-card__thumb img { transform: scale(2.45) translateY(14%); }
.avatar-component-panel--glasses .avatar-component-card__thumb img { transform: scale(2.25) translateY(-6%); }
.avatar-component-panel--hair .avatar-component-card__thumb img { transform: scale(2.2) translateY(-26%); }
.avatar-component-panel--beard .avatar-component-card__thumb img { transform: scale(2.3) translateY(8%); }
.avatar-component-panel--earrings .avatar-component-card__thumb img { transform: scale(2.15) translateY(-2%); }
@media (max-width: 860px){ .avatar-component-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.avatar-swatch-group { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 10px; }
.avatar-swatch { display: grid; grid-template-columns: 20px 18px minmax(0, 1fr); gap: 10px; align-items: center; padding: 10px 12px; border: 1px solid var(--color-border); border-radius: 14px; background: #fff; cursor: pointer; }
.avatar-swatch__color { width: 18px; height: 18px; border-radius: 999px; background: var(--swatch-color); border: 1px solid rgba(0,0,0,0.12); }

View File

@@ -763,8 +763,7 @@ document.addEventListener('DOMContentLoaded', () => {
};
const updateAvatarOptionPreviews = (form) => {
const checked = form.querySelector('[data-avatar-field="avatar_preset"]:checked');
const baseSeed = checked?.value || '';
const baseSeed = readValue('avatar_preset');
if (!baseSeed) return;
const currentValues = {};
@@ -794,10 +793,7 @@ document.addEventListener('DOMContentLoaded', () => {
form.querySelectorAll('[data-avatar-option-thumb]').forEach((image) => {
const field = image.getAttribute('data-avatar-option-field') || '';
const value = image.getAttribute('data-avatar-option-value') || '';
const seed = field === 'avatar_preset' ? value : baseSeed;
if (!seed) return;
const params = new URLSearchParams({ seed });
const params = new URLSearchParams({ seed: baseSeed });
Object.entries(currentValues).forEach(([currentField, currentValue]) => {
const param = fieldMap[currentField];
if (!param) return;
@@ -879,24 +875,9 @@ document.addEventListener('DOMContentLoaded', () => {
};
form.querySelector('[data-avatar-reroll]')?.addEventListener('click', () => {
const radios = Array.from(form.querySelectorAll('[data-avatar-field="avatar_preset"]'));
const used = new Set();
radios.forEach((radio, index) => {
let seed = buildSeed();
while (used.has(seed)) {
seed = buildSeed();
}
used.add(seed);
const url = dicebearUrl(seed);
radio.value = seed;
radio.setAttribute('data-avatar-url', url);
radio.checked = index === 0;
const card = radio.closest('.avatar-preset-card');
card?.querySelector('img')?.setAttribute('src', url);
card?.querySelector('img')?.setAttribute('alt', `Variante ${index + 1}`);
const label = card?.querySelector('.avatar-preset-card__label');
if (label) label.textContent = index === 0 ? 'Neue Auswahl' : `Variante ${index + 1}`;
});
const seedField = form.querySelector('[data-avatar-field="avatar_preset"]');
if (!(seedField instanceof HTMLInputElement)) return;
seedField.value = buildSeed();
updateAvatarPreview(form);
updateAvatarOptionPreviews(form);
});