fix: cramped ingredient/step rows on mobile in recipe editor

Both rows packed 4+ inline fields (grip handle, quantity, unit, name,
note for ingredients; step number, textarea, timer, delete for steps)
into a single flex row with no mobile-aware wrap plan — flex-wrap on
ingredients caused the delete button to float disconnected from its
row, and steps had no wrap at all, squeezing the textarea down to a
sliver. Both now split into two grouped rows below sm: breakpoint
(core fields, then secondary field + delete) and collapse back to one
row on larger screens.

Not visually verified in a browser — this sandbox has no DB/dev
server running (Docker unavailable), so this is typecheck+lint
verified only.

v0.46.2
This commit is contained in:
Arnaud
2026-07-17 23:28:34 +02:00
parent 458b5f2b07
commit 21a3622e6c
5 changed files with 83 additions and 63 deletions
+5
View File
@@ -2,6 +2,11 @@
All notable changes to Epicure are documented here. This file is mirrored in-app at `/changelog` (and in the admin dashboard) via `apps/web/lib/changelog.ts` — update both together. All notable changes to Epicure are documented here. This file is mirrored in-app at `/changelog` (and in the admin dashboard) via `apps/web/lib/changelog.ts` — update both together.
## 0.46.2 — 2026-07-17 19:20
### Fixed
- Recipe editor's ingredient and step rows were cramped on mobile — 4 inline fields plus a timer/delete button squeezed into one row with no real wrap plan. Both now stack sanely on narrow screens (name/quantity/unit on one line, note/timer + delete on the next) and stay a single row on larger screens.
## 0.46.1 — 2026-07-17 19:05 ## 0.46.1 — 2026-07-17 19:05
### Fixed ### Fixed
+68 -60
View File
@@ -691,40 +691,44 @@ export function RecipeForm({ recipeId, defaultValues }: RecipeFormProps) {
<Label>{t("ingredients")}</Label> <Label>{t("ingredients")}</Label>
<div className="space-y-2"> <div className="space-y-2">
{ingredients.map((ing, i) => ( {ingredients.map((ing, i) => (
<div key={ing.id} className="flex flex-wrap gap-2 items-start"> <div key={ing.id} className="flex flex-col sm:flex-row gap-2 sm:items-start">
<GripVertical className="h-4 w-4 text-muted-foreground mt-2 cursor-grab shrink-0" /> <div className="flex gap-2 items-start">
<Input <GripVertical className="h-4 w-4 text-muted-foreground mt-2 cursor-grab shrink-0" />
value={ing.quantity} <Input
onChange={(e) => updateIngredient(i, { quantity: e.target.value })} value={ing.quantity}
placeholder={t("amount")} onChange={(e) => updateIngredient(i, { quantity: e.target.value })}
className="w-14 sm:w-16 shrink-0" placeholder={t("amount")}
/> className="w-16 shrink-0"
<Input />
value={ing.unit} <Input
onChange={(e) => updateIngredient(i, { unit: e.target.value })} value={ing.unit}
placeholder={t("unit")} onChange={(e) => updateIngredient(i, { unit: e.target.value })}
className="w-16 sm:w-24 shrink-0" placeholder={t("unit")}
/> className="w-16 sm:w-24 shrink-0"
<Input />
value={ing.rawName} <Input
onChange={(e) => updateIngredient(i, { rawName: e.target.value })} value={ing.rawName}
placeholder={t("ingredient")} onChange={(e) => updateIngredient(i, { rawName: e.target.value })}
className="flex-1 min-w-[100px]" placeholder={t("ingredient")}
/> className="flex-1 min-w-0"
<Input />
value={ing.note} </div>
onChange={(e) => updateIngredient(i, { note: e.target.value })} <div className="flex gap-2 items-start pl-6 sm:pl-0">
placeholder={t("note")} <Input
className="w-full sm:w-48 sm:shrink-0" value={ing.note}
/> onChange={(e) => updateIngredient(i, { note: e.target.value })}
<button placeholder={t("note")}
type="button" className="flex-1 sm:w-48 sm:shrink-0"
onClick={() => removeIngredient(i)} />
disabled={ingredients.length === 1} <button
className="mt-1.5 text-muted-foreground hover:text-destructive transition-colors disabled:opacity-30" type="button"
> onClick={() => removeIngredient(i)}
<Trash2 className="h-4 w-4" /> disabled={ingredients.length === 1}
</button> className="mt-2 text-muted-foreground hover:text-destructive transition-colors disabled:opacity-30 shrink-0"
>
<Trash2 className="h-4 w-4" />
</button>
</div>
</div> </div>
))} ))}
</div> </div>
@@ -835,33 +839,37 @@ export function RecipeForm({ recipeId, defaultValues }: RecipeFormProps) {
<div className="space-y-3"> <div className="space-y-3">
{steps.map((step, i) => ( {steps.map((step, i) => (
<div key={step.id} className="space-y-1.5"> <div key={step.id} className="space-y-1.5">
<div className="flex gap-3 items-start"> <div className="flex flex-col sm:flex-row gap-2 sm:gap-3">
<div className="mt-2.5 flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-muted text-xs font-bold text-muted-foreground"> <div className="flex gap-3 flex-1 min-w-0">
{i + 1} <div className="mt-2.5 flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-muted text-xs font-bold text-muted-foreground">
{i + 1}
</div>
<Textarea
value={step.instruction}
onChange={(e) => updateStep(i, { instruction: e.target.value })}
placeholder={t("stepPlaceholder", { n: i + 1 })}
rows={2}
className="flex-1 min-w-0"
/>
</div>
<div className="flex gap-2 items-start pl-9 sm:pl-0">
<Input
value={step.timerSeconds}
onChange={(e) => updateStep(i, { timerSeconds: e.target.value })}
placeholder={t("timerSeconds")}
type="number"
min={0}
className="w-28 shrink-0"
/>
<button
type="button"
onClick={() => removeStep(i)}
disabled={steps.length === 1}
className="mt-2 text-muted-foreground hover:text-destructive transition-colors disabled:opacity-30 shrink-0"
>
<Trash2 className="h-4 w-4" />
</button>
</div> </div>
<Textarea
value={step.instruction}
onChange={(e) => updateStep(i, { instruction: e.target.value })}
placeholder={t("stepPlaceholder", { n: i + 1 })}
rows={2}
className="flex-1 min-w-0"
/>
<Input
value={step.timerSeconds}
onChange={(e) => updateStep(i, { timerSeconds: e.target.value })}
placeholder={t("timerSeconds")}
type="number"
min={0}
className="w-28 shrink-0"
/>
<button
type="button"
onClick={() => removeStep(i)}
disabled={steps.length === 1}
className="mt-2.5 text-muted-foreground hover:text-destructive transition-colors disabled:opacity-30"
>
<Trash2 className="h-4 w-4" />
</button>
</div> </div>
{isBatchCook && dishes.some((d) => d.name.trim()) && ( {isBatchCook && dishes.some((d) => d.name.trim()) && (
<div className="flex flex-wrap gap-1.5 pl-9"> <div className="flex flex-wrap gap-1.5 pl-9">
+8 -1
View File
@@ -1,5 +1,5 @@
// Mirrors CHANGELOG.md at the repo root — update both together. // Mirrors CHANGELOG.md at the repo root — update both together.
export const APP_VERSION = "0.46.1"; export const APP_VERSION = "0.46.2";
export type ChangelogEntry = { export type ChangelogEntry = {
version: string; version: string;
@@ -11,6 +11,13 @@ export type ChangelogEntry = {
}; };
export const CHANGELOG: ChangelogEntry[] = [ export const CHANGELOG: ChangelogEntry[] = [
{
version: "0.46.2",
date: "2026-07-17 19:20",
fixed: [
"Recipe editor's ingredient and step rows were cramped on mobile — 4 inline fields plus a timer/delete button squeezed into one row with no real wrap plan. Both now stack sanely on narrow screens (name/quantity/unit on one line, note/timer + delete on the next) and stay a single row on larger screens.",
],
},
{ {
version: "0.46.1", version: "0.46.1",
date: "2026-07-17 19:05", date: "2026-07-17 19:05",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@epicure/web", "name": "@epicure/web",
"version": "0.46.1", "version": "0.46.2",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "epicure", "name": "epicure",
"version": "0.46.1", "version": "0.46.2",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "pnpm --filter web dev", "dev": "pnpm --filter web dev",