mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 19:41:09 +02:00
polish(wardrobe): unify hover vocabulary + lift the Try-On CTA
The garment detail page used three different hover dialects — the model picker reacted with a primary-tinted bg + border (feels like a button), the secondary action buttons had a plain muted-grey hover, the edit pencil was invisible until hover, and the hero photo was entirely static. Result: the model picker was the only place that telegraphed "click me". Everything else felt flat. Align on one vocabulary across the page: primary-tinted border + primary/5 bg on hover for anything interactive. - Hero photo is now a `<button>` that opens the existing ImageLightbox with the garment's full-res mana-media URL (synthesised as a minimal picture.Image — prompt=name, no model/dims/date noise). Hover adds the primary-tinted border + a subtle shadow-md + a 1% scale on the `<img>` for depth. - Edit pencil becomes a labelled button "Bearbeiten" with the same primary hover. No more hover-to-discover — editing reads as a first-class action. - "Heute getragen", "Archivieren" drop the plain muted hover for the primary-tinted one. "Löschen" keeps its destructive-red tint but adds border-error/50 on hover so it feels as interactive as the others. Try-On CTA now reads as the most important action: - rounded-lg + px-5 py-3.5 + text-base + font-semibold (was rounded-md + px-4 py-2 + text-sm + font-medium). - shadow-md shadow-primary/20 at rest → shadow-lg shadow-primary/30 on hover, combined with -translate-y-0.5 for a subtle lift. - active:translate-y-0 + shadow-sm makes the press feel tactile. - Sparkle icon bumped 16 → 18, spinner likewise. Applied to both GarmentTryOnButton (solo) and TryOnButton (outfit) so the two surfaces share CTA weight. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
201a085872
commit
0ee3b145f0
3 changed files with 72 additions and 23 deletions
|
|
@ -163,25 +163,29 @@
|
|||
disabled={running}
|
||||
/>
|
||||
|
||||
<!-- Primary CTA: lifted + shadowed so it reads as the most
|
||||
important action on the page. Hover raises the button
|
||||
subtly (translate + stronger shadow); active-press sinks
|
||||
it back flat for tactile feedback. -->
|
||||
<button
|
||||
type="button"
|
||||
onclick={handleClick}
|
||||
disabled={running || !canTryOn}
|
||||
class="flex w-full flex-col items-center justify-center gap-0.5 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
class="flex w-full flex-col items-center justify-center gap-0.5 rounded-lg bg-primary px-5 py-3.5 text-base font-semibold text-primary-foreground shadow-md shadow-primary/20 transition-all hover:-translate-y-0.5 hover:bg-primary/95 hover:shadow-lg hover:shadow-primary/30 active:translate-y-0 active:shadow-sm disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:translate-y-0 disabled:hover:shadow-md"
|
||||
>
|
||||
{#if running}
|
||||
<span class="flex items-center gap-2">
|
||||
<span
|
||||
class="h-4 w-4 animate-spin rounded-full border-2 border-current border-r-transparent"
|
||||
class="h-5 w-5 animate-spin rounded-full border-2 border-current border-r-transparent"
|
||||
></span>
|
||||
Rendere…
|
||||
</span>
|
||||
{:else}
|
||||
<span class="flex items-center gap-2">
|
||||
<Sparkle size={16} weight="fill" />
|
||||
<Sparkle size={18} weight="fill" />
|
||||
An mir anprobieren
|
||||
</span>
|
||||
<span class="text-xs font-normal opacity-75">{estimatedCredits} Credits</span>
|
||||
<span class="text-xs font-normal opacity-80">{estimatedCredits} Credits</span>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
|
|
|
|||
|
|
@ -160,25 +160,28 @@
|
|||
disabled={running}
|
||||
/>
|
||||
|
||||
<!-- Primary CTA: matches GarmentTryOnButton's lift + shadow
|
||||
treatment so both surfaces use the same visual weight for
|
||||
"produce the generation". -->
|
||||
<button
|
||||
type="button"
|
||||
onclick={handleClick}
|
||||
disabled={running || !canTryOn}
|
||||
class="flex w-full flex-col items-center justify-center gap-0.5 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
class="flex w-full flex-col items-center justify-center gap-0.5 rounded-lg bg-primary px-5 py-3.5 text-base font-semibold text-primary-foreground shadow-md shadow-primary/20 transition-all hover:-translate-y-0.5 hover:bg-primary/95 hover:shadow-lg hover:shadow-primary/30 active:translate-y-0 active:shadow-sm disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:translate-y-0 disabled:hover:shadow-md"
|
||||
>
|
||||
{#if running}
|
||||
<span class="flex items-center gap-2">
|
||||
<span
|
||||
class="h-4 w-4 animate-spin rounded-full border-2 border-current border-r-transparent"
|
||||
class="h-5 w-5 animate-spin rounded-full border-2 border-current border-r-transparent"
|
||||
></span>
|
||||
Rendere…
|
||||
</span>
|
||||
{:else}
|
||||
<span class="flex items-center gap-2">
|
||||
<Sparkle size={16} weight="fill" />
|
||||
<Sparkle size={18} weight="fill" />
|
||||
Anprobieren
|
||||
</span>
|
||||
<span class="text-xs font-normal opacity-75">{estimatedCredits} Credits</span>
|
||||
<span class="text-xs font-normal opacity-80">{estimatedCredits} Credits</span>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
|
|
|
|||
|
|
@ -42,9 +42,32 @@
|
|||
let saving = $state(false);
|
||||
let markingWorn = $state(false);
|
||||
|
||||
// Lightbox state for the Anproben-Strip. Null = closed, Image = open.
|
||||
// Lightbox state — shared between the Anproben-Strip (try-on thumbs)
|
||||
// and the hero-photo. Null = closed, Image = open.
|
||||
let lightboxImage = $state<Image | null>(null);
|
||||
|
||||
// The hero-photo is a garment row, not a picture.Image — synthesise
|
||||
// the shape the lightbox expects so clicking the photo opens the
|
||||
// full-resolution mana-media URL with the garment's name as
|
||||
// prompt-caption. No model / dims / date are rendered (all optional
|
||||
// in the lightbox), keeping the modal clean for a plain clothing
|
||||
// photo.
|
||||
function openPhotoLightbox() {
|
||||
if (!garment || !garment.mediaIds[0]) return;
|
||||
lightboxImage = {
|
||||
id: garment.id,
|
||||
prompt: garment.name,
|
||||
storagePath: garment.mediaIds[0],
|
||||
filename: garment.name,
|
||||
publicUrl: garmentPhotoUrl(garment.mediaIds[0], 'large'),
|
||||
visibility: 'private',
|
||||
isFavorite: false,
|
||||
downloadCount: 0,
|
||||
createdAt: garment.createdAt,
|
||||
updatedAt: garment.updatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
async function handleMarkWorn() {
|
||||
if (!garment) return;
|
||||
markingWorn = true;
|
||||
|
|
@ -98,16 +121,27 @@
|
|||
{/if}
|
||||
{:else}
|
||||
<div class="grid gap-5 md:grid-cols-[minmax(0,1fr)_minmax(0,1.2fr)]">
|
||||
<!-- Photo -->
|
||||
<div class="overflow-hidden rounded-2xl border border-border bg-muted">
|
||||
{#if garment.mediaIds[0]}
|
||||
<!-- Photo — clickable: opens the lightbox with the full-res
|
||||
image so the user can inspect detail at the original
|
||||
resolution. Hover state mirrors the Try-On thumbnail
|
||||
strip + model picker (primary-tinted border) so the
|
||||
whole page uses one interaction vocabulary. -->
|
||||
{#if garment.mediaIds[0]}
|
||||
<button
|
||||
type="button"
|
||||
onclick={openPhotoLightbox}
|
||||
aria-label="Foto vergrößern"
|
||||
class="group block overflow-hidden rounded-2xl border border-border bg-muted transition-all hover:border-primary/50 hover:shadow-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40"
|
||||
>
|
||||
<img
|
||||
src={garmentPhotoUrl(garment.mediaIds[0], 'large')}
|
||||
alt={garment.name}
|
||||
class="h-full w-full object-cover"
|
||||
class="h-full w-full object-cover transition-transform group-hover:scale-[1.01]"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</button>
|
||||
{:else}
|
||||
<div class="rounded-2xl border border-border bg-muted"></div>
|
||||
{/if}
|
||||
|
||||
<!-- Metadata / Edit -->
|
||||
<div class="space-y-4">
|
||||
|
|
@ -120,14 +154,19 @@
|
|||
<h1 class="text-lg font-semibold text-foreground">{garment.name}</h1>
|
||||
<p class="text-sm text-muted-foreground">{CATEGORY_LABELS[garment.category]}</p>
|
||||
</div>
|
||||
<!-- Edit affordance uses the same primary-tinted hover as
|
||||
the Try-On thumbs / model picker so interactive elements
|
||||
on the page share one hover vocabulary. Label is
|
||||
always visible (not hover-to-discover) so editing
|
||||
reads as a first-class action. -->
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => (editing = true)}
|
||||
aria-label="Bearbeiten"
|
||||
title="Bearbeiten"
|
||||
class="flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground hover:bg-muted hover:text-foreground"
|
||||
class="flex items-center gap-1.5 rounded-md border border-border bg-background px-2.5 py-1.5 text-xs font-medium text-muted-foreground transition-colors hover:border-primary/50 hover:bg-primary/5 hover:text-foreground"
|
||||
>
|
||||
<PencilSimple size={16} />
|
||||
<PencilSimple size={14} />
|
||||
Bearbeiten
|
||||
</button>
|
||||
</header>
|
||||
|
||||
|
|
@ -195,23 +234,26 @@
|
|||
<!-- Try-on — "wie sähe das an mir aus" -->
|
||||
<GarmentTryOnButton {garment} />
|
||||
|
||||
<!-- Wear-tracking -->
|
||||
<!-- Wear-tracking — same primary-tinted hover as edit /
|
||||
model picker / try-on thumbs. -->
|
||||
<button
|
||||
type="button"
|
||||
onclick={handleMarkWorn}
|
||||
disabled={markingWorn}
|
||||
class="flex w-full items-center justify-center gap-2 rounded-md border border-border bg-background px-4 py-2 text-sm text-foreground transition-colors hover:bg-muted disabled:opacity-50"
|
||||
class="flex w-full items-center justify-center gap-2 rounded-md border border-border bg-background px-4 py-2 text-sm text-foreground transition-colors hover:border-primary/50 hover:bg-primary/5 disabled:opacity-50 disabled:hover:border-border disabled:hover:bg-background"
|
||||
>
|
||||
<CheckCircle size={14} />
|
||||
{markingWorn ? 'Gespeichert…' : 'Heute getragen'}
|
||||
</button>
|
||||
|
||||
<!-- Secondary actions -->
|
||||
<!-- Secondary actions. Archive keeps the primary-tint hover;
|
||||
Löschen stays destructive-red so the action reads as
|
||||
dangerous even at a glance. -->
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onclick={handleArchive}
|
||||
class="flex flex-1 items-center justify-center gap-2 rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground transition-colors hover:bg-muted"
|
||||
class="flex flex-1 items-center justify-center gap-2 rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground transition-colors hover:border-primary/50 hover:bg-primary/5"
|
||||
>
|
||||
<Archive size={14} />
|
||||
{garment.isArchived ? 'Wieder aktiv' : 'Archivieren'}
|
||||
|
|
@ -219,7 +261,7 @@
|
|||
<button
|
||||
type="button"
|
||||
onclick={handleDelete}
|
||||
class="flex flex-1 items-center justify-center gap-2 rounded-md border border-border bg-background px-3 py-2 text-sm text-error transition-colors hover:bg-error/10"
|
||||
class="flex flex-1 items-center justify-center gap-2 rounded-md border border-border bg-background px-3 py-2 text-sm text-error transition-colors hover:border-error/50 hover:bg-error/10"
|
||||
>
|
||||
<Trash size={14} />
|
||||
Löschen
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue