From 5416110e81d0ffec9b0dfd5e308fd4926bb00a45 Mon Sep 17 00:00:00 2001 From: Alexander Bocken Date: Sun, 12 Apr 2026 21:46:47 +0200 Subject: [PATCH] feat(recipes): redesign cake-form and baking info with collapsible card pattern Viewer: cake-form adjust now collapses into a summary trigger with live factor badge; shape picker replaced with icon-only tiles that flex to fill the row; numeric inputs gain inline cm suffix; restore-default link appears when user deviates from the default. Editor: default-Backform config mirrors the same card + tile pattern (adds "none" tile), plus inline cm suffixes. Baking info row in instruction editor becomes a click-to-reveal card with summary chips, mode presets, and editable fields behind the chevron. --- package.json | 2 +- .../components/recipes/CreateStepList.svelte | 293 +++++++++++- .../components/recipes/IngredientsPage.svelte | 418 +++++++++++++++--- .../edit/[name]/+page.svelte | 286 +++++++++--- 4 files changed, 851 insertions(+), 148 deletions(-) diff --git a/package.json b/package.json index 2894e7b..c57c9fd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "homepage", - "version": "1.28.0", + "version": "1.29.0", "private": true, "type": "module", "scripts": { diff --git a/src/lib/components/recipes/CreateStepList.svelte b/src/lib/components/recipes/CreateStepList.svelte index 180193b..ec6e710 100644 --- a/src/lib/components/recipes/CreateStepList.svelte +++ b/src/lib/components/recipes/CreateStepList.svelte @@ -14,6 +14,27 @@ import BaseRecipeSelector from '$lib/components/recipes/BaseRecipeSelector.svelt let { lang = 'de' as 'de' | 'en', instructions = $bindable(), add_info = $bindable() } = $props<{ lang?: 'de' | 'en', instructions: any, add_info: any }>(); +const BAKING_MODES: Record = { + de: ['Ober-/Unterhitze', 'Umluft', 'Grill', 'Dampf'], + en: ['Conventional', 'Convection', 'Grill', 'Steam'], +}; + +// svelte-ignore state_referenced_locally +let bakingExpanded = $state( + !(add_info?.baking?.length || add_info?.baking?.temperature || add_info?.baking?.mode) +); +let bakingHasData = $derived( + !!(add_info?.baking?.length || add_info?.baking?.temperature || add_info?.baking?.mode) +); + +function toggleBaking() { + bakingExpanded = !bakingExpanded; +} +function pickBakingMode(mode: string) { + if (!add_info.baking) add_info.baking = { length: '', temperature: '', mode: '' }; + add_info.baking.mode = add_info.baking.mode === mode ? '' : mode; +} + // Translation strings const t: Record> = { de: { @@ -658,31 +679,193 @@ ol li::marker{ font-style: italic; font-weight: 400; } -.baking-row{ +.baking-toggle{ + all: unset; + box-sizing: border-box; + display: flex; + align-items: center; + gap: 0.6rem; + width: 100%; + cursor: pointer; + min-height: 1.5rem; +} +.baking-toggle:focus-visible{ + outline: 2px solid var(--color-primary); + outline-offset: 3px; + border-radius: var(--radius-sm); +} +.baking-toggle h3{ + margin: 0; + flex-shrink: 0; + cursor: pointer; +} +.baking-summary{ + flex: 1; display: flex; flex-wrap: wrap; - align-items: baseline; - gap: 0.35em; + gap: 0.35rem; + align-items: center; + min-width: 0; + font-size: 0.95rem; } -.baking-row > span[contenteditable]{ - outline: none; - padding: 0 0.15em; - border-bottom: 1px dashed transparent; - transition: border-color 200ms ease; - min-width: 2ch; +.baking-summary .chip{ + display: inline-flex; + align-items: center; + padding: 0.1rem 0.55rem; + border-radius: var(--radius-pill); + background: var(--color-bg-tertiary); + color: var(--color-text-primary); + font-weight: 600; + font-size: 0.85rem; + line-height: 1.4; } -.baking-row > span[contenteditable]:hover, -.baking-row > span[contenteditable]:focus{ - border-bottom-color: var(--color-border); +.baking-summary .chip.mode{ + background: color-mix(in srgb, var(--color-primary) 14%, transparent); + color: var(--color-primary); } -.baking-sep{ +.baking-summary.muted{ + color: var(--color-text-tertiary); + font-style: italic; + font-size: 0.85rem; +} +.chevron{ + color: var(--color-text-tertiary); + flex-shrink: 0; + transition: transform 200ms ease; +} +.is-expanded .chevron{ + transform: rotate(180deg); + color: var(--color-primary); +} + +.baking-form{ + display: grid; + grid-template-columns: 1fr 1fr; + gap: 0.75rem 1rem; + margin-top: 0.85rem; + padding-top: 0.85rem; + border-top: 1px solid var(--color-border); + animation: baking-slide-down 180ms ease-out; +} +@keyframes baking-slide-down{ + from { opacity: 0; transform: translateY(-4px); } + to { opacity: 1; transform: translateY(0); } +} +.baking-field{ + display: flex; + flex-direction: column; + gap: 0.3rem; + min-width: 0; +} +.baking-field label, +.baking-field .mode-label{ + font-size: 0.75rem; + font-weight: 700; + letter-spacing: 0.04em; + text-transform: uppercase; color: var(--color-text-secondary); +} +.mode-field{ + grid-column: span 2; +} + +.input-wrap{ + position: relative; + display: flex; + align-items: stretch; +} +.input-wrap input{ + flex: 1; + min-width: 0; + padding: 0.5rem 2.8rem 0.5rem 0.7rem; + font-size: 1rem; + font-weight: 600; + font-family: inherit; + color: var(--color-text-primary); + background: var(--color-bg-tertiary); + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + outline: none; + transition: border-color 150ms ease, box-shadow 150ms ease; +} +.input-wrap input:focus{ + border-color: var(--color-primary); + box-shadow: 0 0 0 2px color-mix(in srgb, var(--color-primary) 25%, transparent); +} +.input-wrap input::placeholder{ + color: var(--color-text-tertiary); font-weight: 400; } +.input-wrap .suffix{ + position: absolute; + right: 0.7rem; + top: 50%; + transform: translateY(-50%); + color: var(--color-text-tertiary); + font-size: 0.85rem; + font-weight: 600; + pointer-events: none; + letter-spacing: 0.02em; +} + +.mode-chips{ + display: flex; + flex-wrap: wrap; + gap: 0.4rem; +} +.mode-chip{ + all: unset; + cursor: pointer; + padding: 0.35rem 0.85rem; + font-size: 0.85rem; + font-weight: 600; + border-radius: var(--radius-pill); + background: var(--color-bg-tertiary); + color: var(--color-text-primary); + border: 1px solid var(--color-border); + transition: var(--transition-fast); +} +.mode-chip:hover{ + border-color: var(--color-primary); + transform: scale(1.03); +} +.mode-chip.active{ + background: var(--color-primary); + color: var(--color-text-on-primary); + border-color: var(--color-primary); + box-shadow: var(--shadow-sm); +} +.mode-custom{ + margin-top: 0.1rem; + padding: 0.45rem 0.7rem; + font-size: 0.9rem; + font-family: inherit; + color: var(--color-text-primary); + background: transparent; + border: none; + border-bottom: 1px dashed var(--color-border); + outline: none; + transition: border-color 150ms ease; +} +.mode-custom:focus{ + border-bottom-color: var(--color-primary); + border-bottom-style: solid; +} +.mode-custom::placeholder{ + color: var(--color-text-tertiary); + font-style: italic; +} + @media (max-width: 560px){ .info-card-baking{ grid-column: span 1; } + .baking-form{ + grid-template-columns: 1fr; + } + .mode-field{ + grid-column: span 1; + } } .button_subtle{ padding: 0em; @@ -815,15 +998,81 @@ h3{

-
-

{t[lang].baking}

-
- - bei - - °C - -
+
+ + + {#if bakingExpanded} +
+
+ +
+ + min +
+
+
+ +
+ + °C +
+
+
+ {lang === 'de' ? 'Modus' : 'Mode'} +
+ {#each BAKING_MODES[lang] as mode} + + {/each} +
+ +
+
+ {/if}
diff --git a/src/lib/components/recipes/IngredientsPage.svelte b/src/lib/components/recipes/IngredientsPage.svelte index deb3588..46417ec 100644 --- a/src/lib/components/recipes/IngredientsPage.svelte +++ b/src/lib/components/recipes/IngredientsPage.svelte @@ -128,13 +128,18 @@ const labels = $derived({ portions: isEnglish ? 'Portions:' : 'Portionen:', adjustAmount: isEnglish ? 'Adjust Amount:' : 'Menge anpassen:', ingredients: isEnglish ? 'Ingredients' : 'Zutaten', - cakeForm: isEnglish ? 'Cake form:' : 'Backform:', + cakeForm: isEnglish ? 'Cake form' : 'Backform', + adjustForm: isEnglish ? 'Adjust cake form' : 'Backform anpassen', round: isEnglish ? 'Round' : 'Rund', rectangular: isEnglish ? 'Rectangular' : 'Rechteckig', + gugelhupf: 'Gugelhupf', diameter: isEnglish ? 'Diameter' : 'Durchmesser', + outerDiameter: isEnglish ? 'Outer Ø' : 'Aussen-Ø', + innerDiameter: isEnglish ? 'Inner Ø' : 'Innen-Ø', width: isEnglish ? 'Width' : 'Breite', length: isEnglish ? 'Length' : 'Länge', factor: isEnglish ? 'Factor' : 'Faktor', + restoreDefault: isEnglish ? 'Restore default' : 'Standard wiederherstellen', }); // Cake form scaling @@ -173,11 +178,46 @@ const formMultiplier = $derived( // Track whether multiplier is driven by form or manual buttons let formDriven = $state(false); +let cakeFormExpanded = $state(false); function applyFormMultiplier() { formDriven = true; } +/** @param {string} shape */ +function pickShape(shape) { + userFormShape = shape; + applyFormMultiplier(); +} + +const isDefaultForm = $derived( + hasDefaultForm + && userFormShape === data.defaultForm.shape + && userFormDiameter === (data.defaultForm.diameter ?? 26) + && userFormWidth === (data.defaultForm.width ?? 20) + && userFormLength === (data.defaultForm.length ?? 30) + && userFormInnerDiameter === (data.defaultForm.innerDiameter ?? 8) +); + +const cakeSummaryText = $derived.by(() => { + if (userFormShape === 'round') return `${userFormDiameter} cm ${isEnglish ? 'round' : 'rund'}`; + if (userFormShape === 'rectangular') return `${userFormWidth}×${userFormLength} cm`; + if (userFormShape === 'gugelhupf') return `${userFormDiameter}/${userFormInnerDiameter} cm Gugelhupf`; + return ''; +}); + +function resetCakeForm() { + if (!data.defaultForm) return; + userFormShape = data.defaultForm.shape || 'round'; + userFormDiameter = data.defaultForm.diameter || 26; + userFormWidth = data.defaultForm.width || 20; + userFormLength = data.defaultForm.length || 30; + userFormInnerDiameter = data.defaultForm.innerDiameter || 8; + formDriven = false; + multiplier = 1; + updateUrl(1); +} + // Reactively update multiplier when form dimensions change and form is driving $effect(() => { if (formDriven) { @@ -497,50 +537,216 @@ const nutritionFlatIngredients = $derived.by(() => { .cake-form { margin-block: 1rem; + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--radius-lg); + overflow: hidden; + transition: border-color 150ms ease, box-shadow 150ms ease; } +.cake-form:has(.cake-form-toggle[aria-expanded="true"]) { + box-shadow: var(--shadow-sm); +} + +.cake-form-toggle { + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + padding: 0.75rem 1rem; + background: transparent; + border: none; + cursor: pointer; + font: inherit; + color: inherit; + text-align: left; + transition: background-color 150ms ease; +} +.cake-form-toggle:hover, +.cake-form-toggle:focus-visible { + background: var(--color-bg-elevated); + outline: none; +} +.cake-form-toggle-label { + display: flex; + flex-direction: column; + gap: 0.1rem; + min-width: 0; +} +.cake-form-title { + font-weight: 600; + font-size: var(--text-sm); + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--color-text-secondary); +} +.cake-form-summary { + font-size: 1rem; + color: var(--color-text-primary); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.cake-form-toggle-right { + display: flex; + align-items: center; + gap: 0.6rem; + flex-shrink: 0; +} +.cake-form-factor-badge { + padding: 0.2rem 0.55rem; + border-radius: var(--radius-pill); + background: color-mix(in srgb, var(--color-primary) 18%, transparent); + color: var(--color-primary); + font-weight: 700; + font-size: 0.85rem; + letter-spacing: 0.02em; +} +.cake-form-chevron { + width: 1rem; + height: 1rem; + color: var(--color-text-tertiary); + transition: transform 200ms ease; +} +.cake-form-chevron.expanded { + transform: rotate(180deg); +} + +.cake-form-body { + padding: 0.25rem 1rem 1rem; + border-top: 1px solid var(--color-border); + display: flex; + flex-direction: column; + gap: 1rem; +} + .cake-form-shape { display: flex; - gap: 0.75rem; - justify-content: center; - margin-bottom: 0.5rem; + gap: 0.4rem; + margin-top: 0.5rem; } -.cake-form-shape label { - cursor: pointer; - padding: 0.25em 0.6em; - border-radius: var(--radius-sm); - transition: var(--transition-fast); -} -.cake-form-shape input[type="radio"] { - display: none; -} -.cake-form-selected { - background-color: var(--color-primary); - color: var(--color-text-on-primary); - font-weight: bold; -} -.cake-form-inputs { +.shape-tile { + flex: 1 1 0; display: flex; - gap: 1rem; - justify-content: center; align-items: center; - flex-wrap: wrap; + justify-content: center; + height: 2.25rem; + padding: 0; + background: var(--color-bg-tertiary); + border: 1.5px solid var(--color-border); + border-radius: var(--radius-md); + cursor: pointer; + color: var(--color-text-secondary); + transition: all 150ms ease; } -.cake-form-num { - width: 3.5em; - padding: 0.2em 0.4em; - border: 1px solid var(--color-border); - border-radius: var(--radius-sm); - text-align: center; - font-size: inherit; - background: transparent; - color: inherit; +.shape-tile:hover, +.shape-tile:focus-visible { + border-color: color-mix(in srgb, var(--color-primary) 50%, var(--color-border)); + color: var(--color-text-primary); + outline: none; } -.cake-form-factor { - text-align: center; - margin-top: 0.4rem; - font-weight: bold; +.shape-tile[aria-checked="true"] { + border-color: var(--color-primary); + background: color-mix(in srgb, var(--color-primary) 10%, var(--color-bg-tertiary)); color: var(--color-primary); } +.shape-tile svg { + width: 1.25rem; + height: 1.25rem; + flex-shrink: 0; +} + +.cake-form-inputs { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr)); + gap: 0.75rem; +} +.input-wrap { + display: flex; + flex-direction: column; + gap: 0.3rem; +} +.input-label { + font-size: 0.75rem; + font-weight: 700; + letter-spacing: 0.04em; + color: var(--color-text-tertiary); + text-transform: uppercase; +} +.input-box { + position: relative; + display: flex; + align-items: center; + background: var(--color-bg-tertiary); + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + transition: border-color 150ms ease, box-shadow 150ms ease; +} +.input-box:focus-within { + border-color: var(--color-primary); + box-shadow: 0 0 0 2px color-mix(in srgb, var(--color-primary) 25%, transparent); +} +.input-box input { + flex: 1; + width: 100%; + padding: 0.55rem 2.25rem 0.55rem 0.75rem; + border: none; + background: transparent; + color: var(--color-text-primary); + font: inherit; + font-size: 1rem; + outline: none; +} +.input-box input::-webkit-outer-spin-button, +.input-box input::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} +.input-box input[type="number"] { + -moz-appearance: textfield; + appearance: textfield; +} +.input-suffix { + position: absolute; + right: 0.75rem; + font-size: 0.8rem; + font-weight: 600; + color: var(--color-text-tertiary); + pointer-events: none; + letter-spacing: 0.02em; +} + +.cake-form-footer { + display: flex; + align-items: center; + justify-content: flex-end; + padding-top: 0.25rem; +} +.reset-link { + background: none; + border: none; + padding: 0.25rem 0.5rem; + font: inherit; + font-size: 0.85rem; + color: var(--color-text-tertiary); + cursor: pointer; + border-bottom: 1px dashed currentColor; + border-radius: 0; + transition: color 150ms ease; +} +.reset-link:hover, +.reset-link:focus-visible { + color: var(--color-primary); + outline: none; +} + +@media (max-width: 560px) { + .cake-form-toggle { padding: 0.65rem 0.75rem; } + .cake-form-body { padding: 0.25rem 0.75rem 0.85rem; } + .shape-tile { height: 2rem; } + .shape-tile svg { width: 1.1rem; height: 1.1rem; } + .cake-form-inputs { grid-template-columns: 1fr 1fr; } +} {#if data.ingredients} @@ -577,36 +783,122 @@ const nutritionFlatIngredients = $derived.by(() => { {#if hasDefaultForm}
-

{labels.cakeForm}

-
- - - {#if data.defaultForm?.shape === 'gugelhupf'} - + + + {#if cakeFormExpanded} +
+
+ + + + + {#if data.defaultForm?.shape === 'gugelhupf'} + + {/if} +
+ +
+ {#if userFormShape === 'round'} + + {:else if userFormShape === 'rectangular'} + + + {:else if userFormShape === 'gugelhupf'} + + + {/if} +
+ + {#if !isDefaultForm} + {/if}
-
- {#if userFormShape === 'round'} - - {:else if userFormShape === 'rectangular'} - - - {:else if userFormShape === 'gugelhupf'} - - - {/if} -
- {#if formDriven} -
→ {labels.factor}: {formMultiplier.toFixed(2)}x
{/if}
{/if} diff --git a/src/routes/[recipeLang=recipeLang]/edit/[name]/+page.svelte b/src/routes/[recipeLang=recipeLang]/edit/[name]/+page.svelte index df2db26..94a7569 100644 --- a/src/routes/[recipeLang=recipeLang]/edit/[name]/+page.svelte +++ b/src/routes/[recipeLang=recipeLang]/edit/[name]/+page.svelte @@ -571,40 +571,129 @@ .form-size-section { max-width: 600px; margin: 2rem auto; - text-align: center; - } - .form-size-section h3 { - margin-top: 0; - } - .form-size-controls { - display: flex; - gap: 1rem 1.25rem; - justify-content: center; - flex-wrap: wrap; - margin-bottom: 0.75rem; - } - .form-size-controls label { - display: inline-flex; - align-items: center; - gap: 0.4em; - color: var(--color-text-primary); - } - .form-size-inputs { - display: flex; - gap: 1rem; - justify-content: center; - align-items: center; - flex-wrap: wrap; - } - .form-size-inputs input[type='number'] { - width: 4em; - padding: 0.3em 0.5em; - margin: 0 0.3em; + background: var(--color-surface); border: 1px solid var(--color-border); + border-radius: var(--radius-lg); + overflow: hidden; + } + .form-size-head { + padding: 0.75rem 1rem; + } + .form-size-title { + font-weight: 600; + font-size: var(--text-sm); + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--color-text-secondary); + } + .form-size-body { + padding: 0.25rem 1rem 1rem; + border-top: 1px solid var(--color-border); + display: flex; + flex-direction: column; + gap: 1rem; + } + + .form-shape-row { + display: flex; + gap: 0.4rem; + margin-top: 0.75rem; + } + .form-shape-row .shape-tile { + flex: 1 1 0; + display: flex; + align-items: center; + justify-content: center; + height: 2.25rem; + padding: 0; background: var(--color-bg-tertiary); + border: 1.5px solid var(--color-border); + border-radius: var(--radius-md); + cursor: pointer; + color: var(--color-text-secondary); + transition: all 150ms ease; + } + .form-shape-row .shape-tile:hover, + .form-shape-row .shape-tile:focus-visible { + border-color: color-mix(in srgb, var(--color-primary) 50%, var(--color-border)); color: var(--color-text-primary); - border-radius: var(--radius-sm); + outline: none; + } + .form-shape-row .shape-tile[aria-checked="true"] { + border-color: var(--color-primary); + background: color-mix(in srgb, var(--color-primary) 10%, var(--color-bg-tertiary)); + color: var(--color-primary); + } + .form-shape-row .shape-tile svg { + width: 1.25rem; + height: 1.25rem; + } + + .form-size-inputs { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr)); + gap: 0.75rem; + } + .form-size-inputs .input-wrap { + display: flex; + flex-direction: column; + gap: 0.3rem; + } + .form-size-inputs .input-label { + font-size: 0.75rem; + font-weight: 700; + letter-spacing: 0.04em; + color: var(--color-text-tertiary); + text-transform: uppercase; + } + .form-size-inputs .input-box { + position: relative; + display: flex; + align-items: center; + background: var(--color-bg-tertiary); + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + transition: border-color 150ms ease, box-shadow 150ms ease; + } + .form-size-inputs .input-box:focus-within { + border-color: var(--color-primary); + box-shadow: 0 0 0 2px color-mix(in srgb, var(--color-primary) 25%, transparent); + } + .form-size-inputs .input-box input { + flex: 1; + width: 100%; + padding: 0.55rem 2.25rem 0.55rem 0.75rem; + border: none; + background: transparent; + color: var(--color-text-primary); + font: inherit; font-size: 1rem; + outline: none; + } + .form-size-inputs .input-box input::-webkit-outer-spin-button, + .form-size-inputs .input-box input::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; + } + .form-size-inputs .input-box input[type="number"] { + -moz-appearance: textfield; + appearance: textfield; + } + .form-size-inputs .input-suffix { + position: absolute; + right: 0.75rem; + font-size: 0.8rem; + font-weight: 600; + color: var(--color-text-tertiary); + pointer-events: none; + letter-spacing: 0.02em; + } + @media (max-width: 560px) { + .form-size-head { padding: 0.65rem 0.75rem; } + .form-size-body { padding: 0.25rem 0.75rem 0.85rem; } + .form-shape-row .shape-tile { height: 2rem; } + .form-shape-row .shape-tile svg { width: 1.1rem; height: 1.1rem; } + .form-size-inputs { grid-template-columns: 1fr 1fr; } } .error-message { @@ -1127,40 +1216,113 @@
-

Backform (Standard)

-
- - - - +
+ Backform (Standard)
- {#if defaultForm?.shape === 'round'} -
- +
+
+ + + +
- {:else if defaultForm?.shape === 'rectangular'} -
- - -
- {:else if defaultForm?.shape === 'gugelhupf'} -
- - -
- {/if} + + {#if defaultForm?.shape === 'round'} +
+ +
+ {:else if defaultForm?.shape === 'rectangular'} +
+ + +
+ {:else if defaultForm?.shape === 'gugelhupf'} +
+ + +
+ {/if} +