feat(frontend): responsive design for mobile (RWD)

- Layout: mobile hamburger + drawer nav (backdrop button + sibling nav),
  desktop sidebar hidden on small screens, p-4 md:p-8 main padding
- Products: card list view on mobile, flex-wrap filters
- Lab results: card list view on mobile
- ProductForm: responsive grids (grid-cols-1 sm:grid-cols-2), skin
  profile checkboxes 2→3 cols, active ingredient row restructured
  (name+✕ in flex row, percent/strength/irritation in 3-col grid),
  section headers stack on mobile
- Skin snapshots: date+icons on one row, badges on separate row below
- Product [id] header: back link stacked above title, redundant badge removed
- Routines header: flex-col on mobile, sm:flex-row

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Piotr Oleszczyk 2026-03-02 13:35:25 +01:00
parent c85ca355df
commit 679e4e81f4
8 changed files with 193 additions and 60 deletions

View file

@ -451,7 +451,7 @@
<Card>
<CardHeader><CardTitle>{m["productForm_basicInfo"]()}</CardTitle></CardHeader>
<CardContent class="space-y-4">
<div class="grid grid-cols-2 gap-4">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="space-y-2">
<Label for="name">{m["productForm_name"]()}</Label>
<Input id="name" name="name" required placeholder={m["productForm_namePlaceholder"]()} bind:value={name} />
@ -461,7 +461,7 @@
<Input id="brand" name="brand" required placeholder={m["productForm_brandPlaceholder"]()} bind:value={brand} />
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="space-y-2">
<Label for="line_name">{m["productForm_lineName"]()}</Label>
<Input id="line_name" name="line_name" placeholder={m["productForm_lineNamePlaceholder"]()} bind:value={lineName} />
@ -471,7 +471,7 @@
<Input id="url" name="url" type="url" placeholder="https://…" bind:value={url} />
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="space-y-2">
<Label for="sku">{m["productForm_sku"]()}</Label>
<Input id="sku" name="sku" placeholder={m["productForm_skuPlaceholder"]()} bind:value={sku} />
@ -488,7 +488,7 @@
<Card>
<CardHeader><CardTitle>{m["productForm_classification"]()}</CardTitle></CardHeader>
<CardContent>
<div class="grid grid-cols-2 gap-4">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="col-span-2 space-y-2">
<Label>{m["productForm_category"]()}</Label>
<input type="hidden" name="category" value={category} />
@ -564,7 +564,7 @@
<CardContent class="space-y-4">
<div class="space-y-2">
<Label>{m["productForm_recommendedFor"]()}</Label>
<div class="grid grid-cols-3 gap-2">
<div class="grid grid-cols-2 gap-2 sm:grid-cols-3">
{#each skinTypes as st}
<label class="flex cursor-pointer items-center gap-2 text-sm">
<input
@ -588,7 +588,7 @@
<div class="space-y-2">
<Label>{m["productForm_targetConcerns"]()}</Label>
<div class="grid grid-cols-3 gap-2">
<div class="grid grid-cols-2 gap-2 sm:grid-cols-3">
{#each skinConcerns as sc}
<label class="flex cursor-pointer items-center gap-2 text-sm">
<input
@ -641,7 +641,7 @@
</div>
<div class="space-y-3">
<div class="flex items-center justify-between">
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<Label>{m["productForm_activeIngredients"]()}</Label>
<Button type="button" variant="outline" size="sm" onclick={addActive}>{m["productForm_addActive"]()}</Button>
</div>
@ -650,14 +650,23 @@
{#each actives as active, i}
<div class="rounded-md border border-border p-3 space-y-3">
<div class="grid grid-cols-[1fr_100px_120px_120px_auto] gap-2 items-end">
<div class="space-y-1">
<div class="flex items-end gap-2">
<div class="min-w-0 flex-1 space-y-1">
<Label class="text-xs">{m["productForm_activeName"]()}</Label>
<Input
placeholder="e.g. Niacinamide"
bind:value={active.name}
/>
</div>
<Button
type="button"
variant="ghost"
size="sm"
onclick={() => removeActive(i)}
class="h-7 w-7 shrink-0 p-0 text-destructive hover:text-destructive"
>✕</Button>
</div>
<div class="grid grid-cols-3 gap-2">
<div class="space-y-1">
<Label class="text-xs">{m["productForm_activePercent"]()}</Label>
<Input
@ -687,18 +696,11 @@
<option value="3">{m["productForm_strengthHigh"]()}</option>
</select>
</div>
<Button
type="button"
variant="ghost"
size="sm"
onclick={() => removeActive(i)}
class="text-destructive hover:text-destructive"
>✕</Button>
</div>
<div class="space-y-1">
<Label class="text-xs text-muted-foreground">{m["productForm_activeFunctions"]()}</Label>
<div class="grid grid-cols-4 gap-1">
<div class="grid grid-cols-2 gap-1 sm:grid-cols-4">
{#each ingFunctions as fn}
<label class="flex cursor-pointer items-center gap-1.5 text-xs">
<input
@ -764,7 +766,7 @@
</div>
<div class="space-y-3">
<div class="flex items-center justify-between">
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<Label>{m["productForm_incompatibleWith"]()}</Label>
<Button type="button" variant="outline" size="sm" onclick={addIncompatible}>
{m["productForm_addIncompatibility"]()}
@ -774,7 +776,7 @@
<input type="hidden" name="incompatible_with_json" value={incompatibleJson} />
{#each incompatibleWith as row, i}
<div class="grid grid-cols-[1fr_140px_1fr_auto] gap-2 items-end">
<div class="grid grid-cols-2 gap-2 items-end sm:grid-cols-[1fr_140px_1fr_auto]">
<div class="space-y-1">
<Label class="text-xs">{m["productForm_incompTarget"]()}</Label>
<Input placeholder="e.g. Vitamin C" bind:value={row.target} />
@ -813,7 +815,7 @@
<Card>
<CardHeader><CardTitle>{m["productForm_contextRules"]()}</CardTitle></CardHeader>
<CardContent>
<div class="grid grid-cols-2 gap-4">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="space-y-2">
<Label>{m["productForm_ctxAfterShaving"]()}</Label>
<input type="hidden" name="ctx_safe_after_shaving" value={ctxAfterShaving} />
@ -884,7 +886,7 @@
<Card>
<CardHeader><CardTitle>{m["productForm_productDetails"]()}</CardTitle></CardHeader>
<CardContent class="space-y-4">
<div class="grid grid-cols-3 gap-4">
<div class="grid grid-cols-2 gap-4 sm:grid-cols-3">
<div class="space-y-2">
<Label>{m["productForm_priceTier"]()}</Label>
<input type="hidden" name="price_tier" value={priceTier} />
@ -919,7 +921,7 @@
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="space-y-2">
<Label for="ph_min">{m["productForm_phMin"]()}</Label>
<Input id="ph_min" name="ph_min" type="number" min="0" max="14" step="0.1" placeholder="e.g. 3.5" bind:value={phMin} />
@ -948,7 +950,7 @@
<Card>
<CardHeader><CardTitle>{m["productForm_safetyFlags"]()}</CardTitle></CardHeader>
<CardContent>
<div class="grid grid-cols-2 gap-4">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="space-y-2">
<Label>{m["productForm_fragranceFree"]()}</Label>
<input type="hidden" name="fragrance_free" value={fragranceFree} />
@ -1008,7 +1010,7 @@
<Card>
<CardHeader><CardTitle>{m["productForm_usageConstraints"]()}</CardTitle></CardHeader>
<CardContent class="space-y-4">
<div class="grid grid-cols-2 gap-4">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="space-y-2">
<Label for="min_interval_hours">{m["productForm_minIntervalHours"]()}</Label>
<Input id="min_interval_hours" name="min_interval_hours" type="number" min="0" placeholder="e.g. 24" bind:value={minIntervalHours} />