refactor(wardrobe): lift upload zone to top, move intro behind help icon

The GridView opened with a big welcome card ("Kleiderschrank ·
Fotografiere Kleidungsstücke …") followed by the category tabs and
the grid, with the upload zone tucked at the very bottom. In the
narrow workbench card this pushed every actionable element below the
fold on first open — the user had to scroll past an empty state to
find "Kleidungsstück hochladen".

Match the pattern profile/ListView and other mature modules use:

- Welcome + category-pick hint move into help-content.ts under the
  `wardrobe` key. registerApp auto-attaches it, so the (?) icon in
  the ModuleShell header now renders an overlay with the description,
  features list, and tips.
- Upload zone moves up to sit directly under the category tabs —
  always visible, reflecting the active category in its label.
- Empty-state text updates to point at the zone above instead of the
  (now-removed) "Hinzufügen" button.
- Active-space hint becomes a small footer line, only rendered in
  non-personal spaces where the per-Space wardrobe split actually
  matters.

No data-layer or store changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-24 00:09:56 +02:00
parent ca2809da89
commit 2b5a7b1a46
2 changed files with 52 additions and 47 deletions

View file

@ -967,6 +967,23 @@ export const MODULE_HELP: Record<string, ModuleHelp> = {
],
tips: ['System-Auto folgt deinem OS-Dark-Mode automatisch zur richtigen Uhrzeit'],
},
wardrobe: {
description:
'Dein digitaler Kleiderschrank — fotografiere Kleidungsstücke und Accessoires, komponiere Outfits, und probiere sie mit KI an dir selbst an. Pro Space ein eigener Schrank: was im Family-Space liegt, taucht im Brand-Space nicht auf.',
features: [
'Kleidung nach Kategorien (Oberteile, Hosen, Kleider, Jacken, Schuhe, Accessoires …)',
'Outfits aus mehreren Stücken komponieren und als Set anprobieren',
'Solo-Try-On pro Einzelstück — Accessoire-Modus (Brille, Schmuck, Hut) rendert nur das Gesicht und spart Credits',
'Referenzbilder aus „Meine Bilder" (Gesicht + optional Ganzkörper) werden automatisch genutzt',
'MCP-Tools: listGarments / listOutfits / createOutfit / tryOn für Agents',
],
tips: [
'Aktive Kategorie oben bestimmt den Typ für neue Uploads — erst die Kategorie wählen, dann die Datei droppen.',
'Die Upload-Zone oben akzeptiert Drag-&-Drop direkt aus dem Finder.',
'Frontal-Fotos mit hellem Hintergrund liefern die besten Try-On-Ergebnisse.',
'Ohne Gesichtsbild kannst du kein Try-On starten — der Banner oben hilft beim Upload in einem Schritt.',
],
},
'research-lab': {
description:
'Web-Research-Anbieter Seite-an-Seite vergleichen: gleiche Query an bis zu fünf Provider parallel, Antworten + Latenz + Kosten nebeneinander. Alle Runs werden serverseitig persistiert für spätere Auswertung.',

View file

@ -1,16 +1,21 @@
<!--
Wardrobe grid view — category tabs + garment grid + upload drop-zone.
Wardrobe grid view — category tabs + upload drop-zone + garment grid.
The active category tab pre-selects the kind for new drops, so "Oberteile"
tab + drop = garments created as kind='top'. On the "Alle" tab drops
default to 'other' (user edits later on detail page).
Layout: tabs → upload zone → grid. Upload is always-visible at the top
of the view so a first-time user doesn't have to scroll past an empty
grid to find it. The welcome blurb that used to sit on top now lives
behind the help (?) icon in the ModuleShell header — wired via
`wardrobe` in app-registry/help-content.ts.
Upload flow: drop file(s) → read dimensions client-side → POST to
`/api/v1/wardrobe/garments/upload` → write a LocalWardrobeGarment
through the store (encryption + sync happen in there). Name defaults
to the filename-sans-extension, as in the picture module's upload.
-->
<script lang="ts">
import { Info, Sparkle } from '@mana/shared-icons';
import MeImageUploadZone from '$lib/modules/profile/components/MeImageUploadZone.svelte';
import { readImageDimensions } from '$lib/modules/profile/api/me-images';
import { useAllGarments } from '../queries';
@ -75,39 +80,23 @@
}
</script>
<div class="space-y-6">
<!-- Intro + active-space hint -->
<section class="rounded-2xl border border-border bg-card p-5">
<div class="mb-2 flex items-center justify-between gap-2 text-foreground">
<div class="flex items-center gap-2">
<Sparkle size={18} weight="fill" class="text-primary" />
<h2 class="text-base font-semibold">Kleiderschrank</h2>
</div>
{#if activeSpace}
<span
class="rounded-full bg-muted px-2.5 py-0.5 text-xs font-medium text-muted-foreground"
title="Der Kleiderschrank ist pro Space — andere Spaces haben ihren eigenen."
>
{activeSpace.type === 'personal' ? 'Persönlich' : activeSpace.name}
</span>
{/if}
</div>
<p class="text-sm text-muted-foreground">
Fotografiere Kleidungsstücke und Accessoires, gruppiere sie in Outfits, und probiere sie mit
KI an dir selbst an. Du kannst sie später im Generator als Referenz nutzen.
</p>
<p class="mt-3 flex items-start gap-2 text-xs text-muted-foreground">
<Info size={14} weight="regular" class="mt-0.5 flex-shrink-0" />
<span>
Aktive Kategorie bestimmt den Typ für neue Uploads — auf "Alle" landen sie als "{CATEGORY_LABELS_SINGULAR.other}"
und können auf der Detailseite umgestellt werden.
</span>
</p>
</section>
<!-- Category tabs -->
<div class="space-y-4">
<!-- Category tabs — active tab drives the default kind for drops. -->
<CategoryTabs active={activeTab} {counts} onChange={(next) => (activeTab = next)} />
<!-- Upload zone lives at the top so it's always reachable without
scrolling past an empty grid. Label reflects the active category
so the user knows what kind the drop will be stamped with. -->
<MeImageUploadZone
variant="compact"
label={activeTab === 'all'
? 'Kleidungsstück hochladen'
: `${CATEGORY_LABELS_SINGULAR[activeTab]} hochladen`}
hint="Foto frontal, heller Hintergrund — bessere Try-On-Ergebnisse"
disabled={uploading}
onFiles={ingestFiles}
/>
{#if uploadError}
<div class="rounded-xl border border-error/30 bg-error/10 p-3 text-sm text-error" role="alert">
{uploadError}
@ -122,15 +111,14 @@
{/each}
</div>
{:else if garments.length === 0}
<div class="rounded-2xl border border-dashed border-border bg-background/50 p-8 text-center">
<div class="rounded-2xl border border-dashed border-border bg-background/50 p-6 text-center">
<p class="text-sm font-medium text-foreground">Noch nichts im Schrank.</p>
<p class="mt-1 text-sm text-muted-foreground">
Lade dein erstes Kleidungsstück hoch — unten auf "Hinzufügen" oder zieh eine Datei direkt in
die Zone.
Zieh ein Foto in die Zone oben — oder klick sie an, um eins auszuwählen.
</p>
</div>
{:else}
<div class="rounded-2xl border border-dashed border-border bg-background/50 p-8 text-center">
<div class="rounded-2xl border border-dashed border-border bg-background/50 p-6 text-center">
<p class="text-sm text-muted-foreground">
Keine Einträge unter <strong class="text-foreground"
>{activeTab === 'all' ? 'Alle' : CATEGORY_LABELS[activeTab]}</strong
@ -139,14 +127,14 @@
</div>
{/if}
<!-- Upload zone -->
<MeImageUploadZone
variant="compact"
label={activeTab === 'all'
? 'Kleidungsstück hochladen'
: `${CATEGORY_LABELS_SINGULAR[activeTab]} hochladen`}
hint="Foto frontal, möglichst heller Hintergrund — bessere Try-On-Ergebnisse"
disabled={uploading}
onFiles={ingestFiles}
/>
<!-- Non-personal-space footer hint: the wardrobe is per-Space, so in
a Brand/Family/Club/Team/Practice space it's worth signalling that
uploads don't leak into personal. Hidden in personal to keep the
view clean. -->
{#if activeSpace && activeSpace.type !== 'personal'}
<p class="text-xs text-muted-foreground">
Dieser Schrank gehört zu <strong class="text-foreground">{activeSpace.name}</strong> — Uploads landen
nur hier, nicht in deinem persönlichen Schrank.
</p>
{/if}
</div>