fix: constrain mobile recipe layout and wrap long source URLs

This commit is contained in:
Sascha Nesterovic committed 2026-10-24 17:54:26 +02:00
1 parent df5efa24e4
commit bee3b990f9
6 files changed
+56 -11

No files matched your search

+4
View File
@@ -77,6 +77,10 @@ Nach der gestalterischen Überarbeitung wurden TypeScript-Prüfung und Produktio
- Visuell angesehen: neue Desktop-Rezeptkarten für Schritte, Smartphone-Rezeptseite mit Schrittbildern, dunkle Desktop-Schrittkarte und mobiler Kochmodus. Die Rezept-/Kochmodus-Screenshots unter `docs/screenshots/` wurden aktualisiert; zusätzliche Bild-/Listenansichten liegen in den Playwright-Testartefakten. - Visuell angesehen: neue Desktop-Rezeptkarten für Schritte, Smartphone-Rezeptseite mit Schrittbildern, dunkle Desktop-Schrittkarte und mobiler Kochmodus. Die Rezept-/Kochmodus-Screenshots unter `docs/screenshots/` wurden aktualisiert; zusätzliche Bild-/Listenansichten liegen in den Playwright-Testartefakten.
- Die JSON-Vorlage in `docs/CHATGPT-PROMPT.md` wurde mit dem tatsächlichen Importparser geprüft; das öffentliche JSON-Schema und die Beispiele wurden neu erzeugt. - Die JSON-Vorlage in `docs/CHATGPT-PROMPT.md` wurde mit dem tatsächlichen Importparser geprüft; das öffentliche JSON-Schema und die Beispiele wurden neu erzeugt.
### Nachprüfung: mobile Rezeptbreite
Der mobile Überlauf wurde am veröffentlichten Pljeskavica-Rezept reproduziert: lange Bildquellen-URLs in den Tipps vergrößerten die minimale Grid-Spaltenbreite und damit auch die Zutatenliste. Die Spalte verwendet nun `minmax(0, 1fr)`, ihre Kinder dürfen schrumpfen und lange Texte umbrechen. Mit dem echten Rezept und dem korrigierten Stylesheet wurden 320, 360, 390, 430 und 768 Pixel Breite ohne Überlauf geprüft. Produktionsbuild und alle 15 öffentlichen Browserabläufe bestanden. Der Regressionstest enthält eine lange Quellen-URL und vergleicht die Inhaltsbreite mit `document.documentElement.clientWidth`; `innerWidth` kann sich auf mobilen Browsern selbst mit dem Überlauf vergrößern und ist dafür ungeeignet.
## Präzise Grenzen ## Präzise Grenzen
- Kein Test auf physischem Smartphone, Safari oder Firefox; keine tatsächliche iOS-/Android-Installation über das Betriebssystem-Installationsmenü. - Kein Test auf physischem Smartphone, Safari oder Firefox; keine tatsächliche iOS-/Android-Installation über das Betriebssystem-Installationsmenü.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 941 KiB

After

Width:  |  Height:  |  Size: 938 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 MiB

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

After

Width:  |  Height:  |  Size: 1.9 MiB

+11 -1
View File
@@ -869,6 +869,10 @@ small,
grid-template-columns: 350px minmax(0, 1fr); grid-template-columns: 350px minmax(0, 1fr);
gap: 65px; gap: 65px;
align-items: start; align-items: start;
overflow-wrap: anywhere;
}
.recipe-columns > * {
min-width: 0;
} }
.ingredients-panel { .ingredients-panel {
background: var(--surface); background: var(--surface);
@@ -997,6 +1001,7 @@ small,
.ingredient-name { .ingredient-name {
font-size: 12px; font-size: 12px;
flex: 1; flex: 1;
min-width: 0;
} }
.ingredient-name small { .ingredient-name small {
display: block; display: block;
@@ -1138,6 +1143,11 @@ small,
} }
.tip-card svg { .tip-card svg {
color: var(--green); color: var(--green);
flex-shrink: 0;
}
.tip-card > div {
min-width: 0;
flex: 1;
} }
.tip-card h3 { .tip-card h3 {
font-size: 23px; font-size: 23px;
@@ -2304,7 +2314,7 @@ input[type='checkbox'] {
flex: 1; flex: 1;
} }
.recipe-columns { .recipe-columns {
grid-template-columns: 1fr; grid-template-columns: minmax(0, 1fr);
gap: 35px; gap: 35px;
} }
.ingredients-panel { .ingredients-panel {
+41 -10
View File
@@ -21,6 +21,7 @@ test('external images and structured steps survive import, editing, scaling and
title: `Bilderküche ${info.project.name}`, title: `Bilderküche ${info.project.name}`,
image: cover, image: cover,
images: ['https://images.example.org/gallery.jpg'], images: ['https://images.example.org/gallery.jpg'],
tips: 'Bildquelle: https://commons.wikimedia.org/wiki/File:Pljeskavica_cheese_chili_with_a_very_long_unbroken_filename.jpg\n\nLizenz: https://creativecommons.org/licenses/by-sa/4.0/',
steps: pasta.steps.map((s, n) => steps: pasta.steps.map((s, n) =>
n === 0 n === 0
? { ? {
@@ -86,9 +87,21 @@ test('external images and structured steps survive import, editing, scaling and
await page.getByRole('spinbutton', { name: 'Portionen', exact: true }).fill('4'); await page.getByRole('spinbutton', { name: 'Portionen', exact: true }).fill('4');
await expect(first.locator('.instruction-ingredient').first()).toHaveText('80 g Basilikum'); await expect(first.locator('.instruction-ingredient').first()).toHaveText('80 g Basilikum');
await expect(first.locator('.step-supplies')).toContainText('80 g Basilikum'); await expect(first.locator('.step-supplies')).toContainText('80 g Basilikum');
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe( expect(
true, await page
); .locator('.recipe-columns')
.evaluate((el) =>
[...el.querySelectorAll('*')].every(
(child) =>
child.getBoundingClientRect().right <= document.documentElement.clientWidth + 1,
),
),
).toBe(true);
expect(
await page.evaluate(
() => document.documentElement.scrollWidth <= document.documentElement.clientWidth,
),
).toBe(true);
await page.screenshot({ path: info.outputPath('structured-steps.png'), fullPage: true }); await page.screenshot({ path: info.outputPath('structured-steps.png'), fullPage: true });
await page.getByRole('button', { name: 'Theme wechseln', exact: true }).click(); await page.getByRole('button', { name: 'Theme wechseln', exact: true }).click();
await first.screenshot({ path: info.outputPath('structured-step-dark.png') }); await first.screenshot({ path: info.outputPath('structured-step-dark.png') });
@@ -98,9 +111,11 @@ test('external images and structured steps survive import, editing, scaling and
); );
await expect(page.locator('.cooking-step .step-images img')).toHaveCount(2); await expect(page.locator('.cooking-step .step-images img')).toHaveCount(2);
await expect(page.locator('.cooking-step')).toHaveCSS('opacity', '1'); await expect(page.locator('.cooking-step')).toHaveCSS('opacity', '1');
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe( expect(
true, await page.evaluate(
); () => document.documentElement.scrollWidth <= document.documentElement.clientWidth,
),
).toBe(true);
await page.screenshot({ path: info.outputPath('structured-cooking.png'), fullPage: true }); await page.screenshot({ path: info.outputPath('structured-cooking.png'), fullPage: true });
expect(errors).toEqual([]); expect(errors).toEqual([]);
} finally { } finally {
@@ -147,7 +162,11 @@ test('editor remains usable on the current viewport in dark mode', async ({ page
await expect(page.getByRole('textbox', { name: 'Zutat 1', exact: true })).toHaveValue('Karotten'); await expect(page.getByRole('textbox', { name: 'Zutat 1', exact: true })).toHaveValue('Karotten');
await expect(page.locator('.ingredient-entry').nth(0)).toContainText('optional'); await expect(page.locator('.ingredient-entry').nth(0)).toContainText('optional');
await expect(page.locator('.ingredient-entry').nth(1)).toContainText('geschält und gewürfelt'); await expect(page.locator('.ingredient-entry').nth(1)).toContainText('geschält und gewürfelt');
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true); expect(
await page.evaluate(
() => document.documentElement.scrollWidth <= document.documentElement.clientWidth,
),
).toBe(true);
await page.screenshot({ await page.screenshot({
path: `docs/screenshots/editor-dark-${info.project.name}.png`, path: `docs/screenshots/editor-dark-${info.project.name}.png`,
fullPage: true, fullPage: true,
@@ -166,7 +185,11 @@ test('editor remains usable on the current viewport in dark mode', async ({ page
); );
} }
await page.getByRole('button', { name: 'Live-Vorschau', exact: true }).click(); await page.getByRole('button', { name: 'Live-Vorschau', exact: true }).click();
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true); expect(
await page.evaluate(
() => document.documentElement.scrollWidth <= document.documentElement.clientWidth,
),
).toBe(true);
}); });
test('public library, shareable search, favorites, themes and responsive layouts', async ({ test('public library, shareable search, favorites, themes and responsive layouts', async ({
page, page,
@@ -186,7 +209,11 @@ test('public library, shareable search, favorites, themes and responsive layouts
fullPage: true, fullPage: true,
animations: 'disabled', animations: 'disabled',
}); });
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true); expect(
await page.evaluate(
() => document.documentElement.scrollWidth <= document.documentElement.clientWidth,
),
).toBe(true);
await page.getByRole('textbox', { name: 'Rezepte suchen' }).fill('Basilikum'); await page.getByRole('textbox', { name: 'Rezepte suchen' }).fill('Basilikum');
await expect(page.locator('.recipe-card')).toHaveCount(1); await expect(page.locator('.recipe-card')).toHaveCount(1);
await expect(page).toHaveURL(/q=Basilikum/); await expect(page).toHaveURL(/q=Basilikum/);
@@ -230,7 +257,11 @@ test('portion calculator, ingredient anchor, shopping list and print content', a
path: `docs/screenshots/recipe-${info.project.name}.png`, path: `docs/screenshots/recipe-${info.project.name}.png`,
fullPage: true, fullPage: true,
}); });
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true); expect(
await page.evaluate(
() => document.documentElement.scrollWidth <= document.documentElement.clientWidth,
),
).toBe(true);
await page.getByRole('button', { name: 'Zur Einkaufsliste hinzufügen' }).click(); await page.getByRole('button', { name: 'Zur Einkaufsliste hinzufügen' }).click();
await page.goto('/einkaufsliste'); await page.goto('/einkaufsliste');
await expect(page.locator('.shopping-list')).toContainText('400 g'); await expect(page.locator('.shopping-list')).toContainText('400 g');