fix(comic): proper input focus + sichtbare Entfern-Affordance

Drei Tweaks an der Create-Seite, ausgelöst von User-Feedback nach
dem ersten Story-Anlegen:

1. **Inputs/Textarea ohne Focus-Ring.** Title-Input und
   Story-Kontext-Textarea hatten nur `focus:border-primary`
   und sahen im aktiven Zustand fast nicht anders aus als
   inaktiv. Wardrobe's GarmentForm nutzt `focus:ring-1
   focus:ring-primary` plus `disabled:opacity-50` — übernommen.
   Textarea zusätzlich `resize-none` (vertikales Resize von Hand
   ist nett, aber kollidiert mit Padding bei kleiner Karten-Breite).

2. **Face-Tile war ein nackter `<img>`.** Kein border-2, kein
   "Pflicht"-Hinweis — User dachte er könnte's entfernen und
   suchte nach dem X. Jetzt: border-2 border-primary/40 wie
   Body-Tile, plus ein "PFLICHT"-Badge mit Gradient-Overlay am
   unteren Rand. Title="Face-Ref ist Pflicht — kann nicht
   entfernt werden". Damit ist klar: Face = locked.

3. **Body und Garments waren entfernbar, aber das war unsichtbar.**
   - Body-Toggle: bisher gar kein Hover-Feedback im aktiven
     Zustand (Plus-Overlay nur bei inactive). Jetzt im aktiven
     Zustand auf Hover ein rotes X-Overlay über dem Bild
     (group-hover-Pattern, error/60-bg, opacity 0→100).
     Title-Tooltips nochmal verschärft: "Klick zum Entfernen"
     vs. "Klick zum Hinzufügen".
   - Garment-Tiles: das X-Button war 5x5 (20px), abgerundet,
     bg-background/80 — verschwand visuell auf manchen
     Garment-Fotos. Jetzt:
     • Die ganze Tile ist klickbar (Touch-friendly), mit
       hover:border-error/60 (visueller "achtung, klicken
       entfernt das")
     • X-Badge h-6 w-6 (24px) mit Border-Ring, weißer bg-Pille,
       wechselt bei group-hover auf error-bg + weiß. Immer
       sichtbar, nicht erst auf Hover.
   - Heading-Subline ergänzt um "klicke ein Bild oder das ✕,
     um es wieder zu entfernen" — explizite Anleitung.

Comic-Files type-checken sauber.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-25 13:51:05 +02:00
parent e0c0791bb5
commit 95bedf4625
2 changed files with 56 additions and 26 deletions

View file

@ -111,19 +111,32 @@
Protagonist
</h3>
<p class="mt-0.5 text-xs text-muted-foreground">
Dein Gesicht ist Pflicht. Body-Ref und bis zu {MAX_GARMENTS} Kostüm-Fotos sind optional.
Dein Gesicht ist Pflicht. Body-Ref und bis zu {MAX_GARMENTS} Kostüm-Fotos sind optional — klicke
ein Bild oder das ✕, um es wieder zu entfernen.
</p>
</div>
<div class="flex flex-wrap items-start gap-2">
<!-- Face ref tile — mandatory -->
<!-- Face ref tile — mandatory, not deselectable. Small "Pflicht"-
badge makes the locked state explicit so the user doesn't
hunt for a remove button that doesn't exist. -->
<div class="flex flex-col items-center gap-1">
{#if face?.publicUrl}
<img
src={face.thumbnailUrl ?? face.publicUrl}
alt="Face-Ref"
class="h-20 w-20 rounded-md border border-primary/30 object-cover"
/>
<div
class="relative h-20 w-20 overflow-hidden rounded-md border-2 border-primary/40"
title="Face-Ref ist Pflicht — kann nicht entfernt werden"
>
<img
src={face.thumbnailUrl ?? face.publicUrl}
alt="Face-Ref"
class="h-full w-full object-cover"
/>
<span
class="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/70 to-transparent px-1 py-0.5 text-center text-[9px] font-semibold uppercase tracking-wider text-white"
>
Pflicht
</span>
</div>
{:else}
<div
class="flex h-20 w-20 flex-col items-center justify-center gap-1 rounded-md border border-dashed border-border bg-muted/50 text-[10px] text-muted-foreground"
@ -135,19 +148,24 @@
<span class="text-[10px] font-medium text-muted-foreground">Face</span>
</div>
<!-- Body ref tile — optional toggle -->
<!-- Body ref tile — optional toggle. Two states need clear visual
differentiation:
- inactive: dimmed image + Plus overlay → "click to add"
- active: primary border + on-hover X overlay → "click to remove"
The X-on-hover when active is the part the user was missing
(previously nothing changed on hover when picked). -->
<div class="flex flex-col items-center gap-1">
{#if body?.publicUrl}
<button
type="button"
{disabled}
onclick={toggleBody}
class="relative h-20 w-20 overflow-hidden rounded-md border transition-all active:translate-y-px
class="group relative h-20 w-20 overflow-hidden rounded-md border-2 transition-all active:translate-y-px
{bodyInValue
? 'border-primary shadow-sm shadow-primary/20'
: 'border-border opacity-60 hover:border-primary/50 hover:opacity-100 hover:shadow-sm'}"
aria-pressed={bodyInValue}
title={bodyInValue ? 'Body-Ref entfernen' : 'Body-Ref hinzufügen'}
title={bodyInValue ? 'Klick zum Entfernen' : 'Klick zum Hinzufügen'}
>
<img
src={body.thumbnailUrl ?? body.publicUrl}
@ -156,9 +174,15 @@
/>
{#if !bodyInValue}
<div
class="absolute inset-0 flex items-center justify-center bg-background/40 text-xs text-foreground"
class="absolute inset-0 flex items-center justify-center bg-background/50 text-foreground"
>
<Plus size={16} />
<Plus size={20} weight="bold" />
</div>
{:else}
<div
class="absolute inset-0 flex items-center justify-center bg-error/0 text-white opacity-0 transition-all group-hover:bg-error/60 group-hover:opacity-100"
>
<X size={20} weight="bold" />
</div>
{/if}
</button>
@ -174,11 +198,21 @@
<span class="text-[10px] font-medium text-muted-foreground">Body</span>
</div>
<!-- Garment tiles (picked) -->
<!-- Garment tiles (picked). Whole tile is also clickable to
remove — easier to hit on touch. Plus a dedicated X badge
in the corner that's bigger + higher contrast than before
so it reads as a control even at a glance. -->
{#each garmentPicks as g (g.id)}
{@const mediaId = g.mediaIds[0]}
<div class="flex flex-col items-center gap-1">
<div class="relative h-20 w-20 overflow-hidden rounded-md border border-primary/30">
<button
type="button"
{disabled}
onclick={() => mediaId && removeGarment(mediaId)}
class="group relative h-20 w-20 overflow-hidden rounded-md border-2 border-primary/40 shadow-sm transition-all active:translate-y-px hover:border-error/60"
aria-label={`${g.name} entfernen`}
title="Klick zum Entfernen"
>
{#if mediaId}
<img
src={garmentPhotoUrl(mediaId, 'thumb')}
@ -186,17 +220,13 @@
class="h-full w-full object-cover"
/>
{/if}
<button
type="button"
{disabled}
onclick={() => mediaId && removeGarment(mediaId)}
class="absolute right-0 top-0 m-0.5 flex h-5 w-5 items-center justify-center rounded-full bg-background/80 text-foreground shadow-sm hover:bg-background"
aria-label={`${g.name} entfernen`}
title="Entfernen"
<!-- Always-visible X badge -->
<span
class="absolute right-1 top-1 flex h-6 w-6 items-center justify-center rounded-full bg-background text-foreground shadow ring-1 ring-border transition-all group-hover:bg-error group-hover:text-white group-hover:ring-error"
>
<X size={10} />
</button>
</div>
<X size={12} weight="bold" />
</span>
</button>
<span class="max-w-20 truncate text-[10px] font-medium text-muted-foreground">
{g.name}
</span>

View file

@ -65,7 +65,7 @@
placeholder="Bug-Hunt-Frust, Urlaubs-Abenteuer, …"
maxlength={120}
autocomplete="off"
class="block w-full rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none"
class="block w-full rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary disabled:opacity-50"
disabled={submitting}
required
/>
@ -102,7 +102,7 @@
rows={3}
maxlength={800}
placeholder="Kurze Zusammenfassung, Ton, Ziel der Geschichte. Wird im AI-Storyboard-Flow (M4) als Briefing genutzt."
class="block w-full rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none"
class="block w-full resize-none rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary disabled:opacity-50"
disabled={submitting}
></textarea>
</div>