feat(recipes): redesign cake-form and baking info with collapsible card pattern
All checks were successful
CI / update (push) Successful in 3m38s

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.
This commit is contained in:
2026-04-12 21:46:47 +02:00
parent 49665c94db
commit 5416110e81
4 changed files with 851 additions and 148 deletions

View File

@@ -1,6 +1,6 @@
{ {
"name": "homepage", "name": "homepage",
"version": "1.28.0", "version": "1.29.0",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {

View File

@@ -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 }>(); let { lang = 'de' as 'de' | 'en', instructions = $bindable(), add_info = $bindable() } = $props<{ lang?: 'de' | 'en', instructions: any, add_info: any }>();
const BAKING_MODES: Record<string, string[]> = {
de: ['Ober-/Unterhitze', 'Umluft', 'Grill', 'Dampf'],
en: ['Conventional', 'Convection', 'Grill', 'Steam'],
};
// svelte-ignore state_referenced_locally
let bakingExpanded = $state<boolean>(
!(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 // Translation strings
const t: Record<string, Record<string, string>> = { const t: Record<string, Record<string, string>> = {
de: { de: {
@@ -658,31 +679,193 @@ ol li::marker{
font-style: italic; font-style: italic;
font-weight: 400; 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; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
align-items: baseline; gap: 0.35rem;
gap: 0.35em; align-items: center;
min-width: 0;
font-size: 0.95rem;
} }
.baking-row > span[contenteditable]{ .baking-summary .chip{
outline: none; display: inline-flex;
padding: 0 0.15em; align-items: center;
border-bottom: 1px dashed transparent; padding: 0.1rem 0.55rem;
transition: border-color 200ms ease; border-radius: var(--radius-pill);
min-width: 2ch; 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-summary .chip.mode{
.baking-row > span[contenteditable]:focus{ background: color-mix(in srgb, var(--color-primary) 14%, transparent);
border-bottom-color: var(--color-border); 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); 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; 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){ @media (max-width: 560px){
.info-card-baking{ .info-card-baking{
grid-column: span 1; grid-column: span 1;
} }
.baking-form{
grid-template-columns: 1fr;
}
.mode-field{
grid-column: span 1;
}
} }
.button_subtle{ .button_subtle{
padding: 0em; padding: 0em;
@@ -815,15 +998,81 @@ h3{
<p class="info-value" contenteditable="plaintext-only" bind:innerText={add_info.fermentation.final} data-placeholder="z.B. 1 h"></p> <p class="info-value" contenteditable="plaintext-only" bind:innerText={add_info.fermentation.final} data-placeholder="z.B. 1 h"></p>
</div> </div>
<div class="info-card info-card-baking"> <div class="info-card info-card-baking" class:is-expanded={bakingExpanded}>
<h3><Flame size={16} />{t[lang].baking}</h3> <button
<div class="info-value baking-row"> type="button"
<span contenteditable="plaintext-only" bind:innerText={add_info.baking.length} data-placeholder="40 min"></span> class="baking-toggle"
<span class="baking-sep">bei</span> onclick={toggleBaking}
<span contenteditable="plaintext-only" bind:innerText={add_info.baking.temperature} data-placeholder="200"></span> aria-expanded={bakingExpanded}
<span class="baking-sep">°C</span> aria-controls="baking-fields-{lang}"
<span contenteditable="plaintext-only" bind:innerText={add_info.baking.mode} data-placeholder="Ober-/Unterhitze"></span> >
</div> <h3><Flame size={16} />{t[lang].baking}</h3>
{#if !bakingExpanded && bakingHasData}
<span class="baking-summary">
{#if add_info.baking.length}<span class="chip">{add_info.baking.length}</span>{/if}
{#if add_info.baking.temperature}<span class="chip">{add_info.baking.temperature} °C</span>{/if}
{#if add_info.baking.mode}<span class="chip mode">{add_info.baking.mode}</span>{/if}
</span>
{:else if !bakingExpanded}
<span class="baking-summary muted">{lang === 'de' ? 'Nicht gesetzt' : 'Not set'}</span>
{/if}
<svg class="chevron" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
<path d="M7 10l5 5 5-5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
{#if bakingExpanded}
<div id="baking-fields-{lang}" class="baking-form">
<div class="baking-field">
<label for="baking-length-{lang}">{lang === 'de' ? 'Dauer' : 'Duration'}</label>
<div class="input-wrap">
<input
id="baking-length-{lang}"
type="text"
bind:value={add_info.baking.length}
placeholder="40"
inputmode="numeric"
autocomplete="off"
/>
<span class="suffix">min</span>
</div>
</div>
<div class="baking-field">
<label for="baking-temp-{lang}">{lang === 'de' ? 'Temperatur' : 'Temperature'}</label>
<div class="input-wrap">
<input
id="baking-temp-{lang}"
type="text"
bind:value={add_info.baking.temperature}
placeholder="200"
inputmode="numeric"
autocomplete="off"
/>
<span class="suffix">°C</span>
</div>
</div>
<div class="baking-field mode-field">
<span class="mode-label">{lang === 'de' ? 'Modus' : 'Mode'}</span>
<div class="mode-chips">
{#each BAKING_MODES[lang] as mode}
<button
type="button"
class="mode-chip"
class:active={add_info.baking.mode === mode}
onclick={() => pickBakingMode(mode)}
>{mode}</button>
{/each}
</div>
<input
type="text"
class="mode-custom"
bind:value={add_info.baking.mode}
placeholder={lang === 'de' ? 'oder eigenen Modus eingeben…' : 'or enter custom mode…'}
autocomplete="off"
/>
</div>
</div>
{/if}
</div> </div>
<div class="info-card"> <div class="info-card">

View File

@@ -128,13 +128,18 @@ const labels = $derived({
portions: isEnglish ? 'Portions:' : 'Portionen:', portions: isEnglish ? 'Portions:' : 'Portionen:',
adjustAmount: isEnglish ? 'Adjust Amount:' : 'Menge anpassen:', adjustAmount: isEnglish ? 'Adjust Amount:' : 'Menge anpassen:',
ingredients: isEnglish ? 'Ingredients' : 'Zutaten', 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', round: isEnglish ? 'Round' : 'Rund',
rectangular: isEnglish ? 'Rectangular' : 'Rechteckig', rectangular: isEnglish ? 'Rectangular' : 'Rechteckig',
gugelhupf: 'Gugelhupf',
diameter: isEnglish ? 'Diameter' : 'Durchmesser', diameter: isEnglish ? 'Diameter' : 'Durchmesser',
outerDiameter: isEnglish ? 'Outer Ø' : 'Aussen-Ø',
innerDiameter: isEnglish ? 'Inner Ø' : 'Innen-Ø',
width: isEnglish ? 'Width' : 'Breite', width: isEnglish ? 'Width' : 'Breite',
length: isEnglish ? 'Length' : 'Länge', length: isEnglish ? 'Length' : 'Länge',
factor: isEnglish ? 'Factor' : 'Faktor', factor: isEnglish ? 'Factor' : 'Faktor',
restoreDefault: isEnglish ? 'Restore default' : 'Standard wiederherstellen',
}); });
// Cake form scaling // Cake form scaling
@@ -173,11 +178,46 @@ const formMultiplier = $derived(
// Track whether multiplier is driven by form or manual buttons // Track whether multiplier is driven by form or manual buttons
let formDriven = $state(false); let formDriven = $state(false);
let cakeFormExpanded = $state(false);
function applyFormMultiplier() { function applyFormMultiplier() {
formDriven = true; 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 // Reactively update multiplier when form dimensions change and form is driving
$effect(() => { $effect(() => {
if (formDriven) { if (formDriven) {
@@ -497,50 +537,216 @@ const nutritionFlatIngredients = $derived.by(() => {
.cake-form { .cake-form {
margin-block: 1rem; 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 { .cake-form-shape {
display: flex; display: flex;
gap: 0.75rem; gap: 0.4rem;
justify-content: center; margin-top: 0.5rem;
margin-bottom: 0.5rem;
} }
.cake-form-shape label { .shape-tile {
cursor: pointer; flex: 1 1 0;
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 {
display: flex; display: flex;
gap: 1rem;
justify-content: center;
align-items: 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 { .shape-tile:hover,
width: 3.5em; .shape-tile:focus-visible {
padding: 0.2em 0.4em; border-color: color-mix(in srgb, var(--color-primary) 50%, var(--color-border));
border: 1px solid var(--color-border); color: var(--color-text-primary);
border-radius: var(--radius-sm); outline: none;
text-align: center;
font-size: inherit;
background: transparent;
color: inherit;
} }
.cake-form-factor { .shape-tile[aria-checked="true"] {
text-align: center; border-color: var(--color-primary);
margin-top: 0.4rem; background: color-mix(in srgb, var(--color-primary) 10%, var(--color-bg-tertiary));
font-weight: bold;
color: var(--color-primary); 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; }
}
</style> </style>
{#if data.ingredients} {#if data.ingredients}
@@ -577,36 +783,122 @@ const nutritionFlatIngredients = $derived.by(() => {
{#if hasDefaultForm} {#if hasDefaultForm}
<div class="cake-form"> <div class="cake-form">
<h3>{labels.cakeForm}</h3> <button
<div class="cake-form-shape"> type="button"
<label class:cake-form-selected={userFormShape === 'round'}> class="cake-form-toggle"
<input type="radio" name="userFormShape" value="round" bind:group={userFormShape} onchange={applyFormMultiplier} /> aria-expanded={cakeFormExpanded}
{labels.round} aria-controls="cake-form-body"
</label> onclick={() => { cakeFormExpanded = !cakeFormExpanded; }}
<label class:cake-form-selected={userFormShape === 'rectangular'}> >
<input type="radio" name="userFormShape" value="rectangular" bind:group={userFormShape} onchange={applyFormMultiplier} /> <span class="cake-form-toggle-label">
{labels.rectangular} <span class="cake-form-title">{labels.adjustForm}</span>
</label> <span class="cake-form-summary">{cakeSummaryText}</span>
{#if data.defaultForm?.shape === 'gugelhupf'} </span>
<label class:cake-form-selected={userFormShape === 'gugelhupf'}> <span class="cake-form-toggle-right">
<input type="radio" name="userFormShape" value="gugelhupf" bind:group={userFormShape} onchange={applyFormMultiplier} /> {#if formDriven && Math.abs(formMultiplier - 1) > 0.005}
Gugelhupf <span class="cake-form-factor-badge">{formMultiplier.toFixed(2)}×</span>
</label> {/if}
<svg class="cake-form-chevron" class:expanded={cakeFormExpanded} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg>
</span>
</button>
{#if cakeFormExpanded}
<div id="cake-form-body" class="cake-form-body">
<div class="cake-form-shape" role="radiogroup" aria-label={labels.cakeForm}>
<button
type="button"
role="radio"
aria-checked={userFormShape === 'round'}
aria-label={labels.round}
title={labels.round}
class="shape-tile"
onclick={() => pickShape('round')}
>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" aria-hidden="true">
<circle cx="12" cy="12" r="8.5"/>
</svg>
</button>
<button
type="button"
role="radio"
aria-checked={userFormShape === 'rectangular'}
aria-label={labels.rectangular}
title={labels.rectangular}
class="shape-tile"
onclick={() => pickShape('rectangular')}
>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" aria-hidden="true">
<rect x="3" y="6" width="18" height="12" rx="1.5"/>
</svg>
</button>
{#if data.defaultForm?.shape === 'gugelhupf'}
<button
type="button"
role="radio"
aria-checked={userFormShape === 'gugelhupf'}
aria-label={labels.gugelhupf}
title={labels.gugelhupf}
class="shape-tile"
onclick={() => pickShape('gugelhupf')}
>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" aria-hidden="true">
<circle cx="12" cy="12" r="8.5"/>
<circle cx="12" cy="12" r="3"/>
</svg>
</button>
{/if}
</div>
<div class="cake-form-inputs">
{#if userFormShape === 'round'}
<label class="input-wrap">
<span class="input-label">{labels.diameter}</span>
<span class="input-box">
<input type="number" min="1" step="1" bind:value={userFormDiameter} oninput={applyFormMultiplier} />
<span class="input-suffix">cm</span>
</span>
</label>
{:else if userFormShape === 'rectangular'}
<label class="input-wrap">
<span class="input-label">{labels.width}</span>
<span class="input-box">
<input type="number" min="1" step="1" bind:value={userFormWidth} oninput={applyFormMultiplier} />
<span class="input-suffix">cm</span>
</span>
</label>
<label class="input-wrap">
<span class="input-label">{labels.length}</span>
<span class="input-box">
<input type="number" min="1" step="1" bind:value={userFormLength} oninput={applyFormMultiplier} />
<span class="input-suffix">cm</span>
</span>
</label>
{:else if userFormShape === 'gugelhupf'}
<label class="input-wrap">
<span class="input-label">{labels.outerDiameter}</span>
<span class="input-box">
<input type="number" min="1" step="1" bind:value={userFormDiameter} oninput={applyFormMultiplier} />
<span class="input-suffix">cm</span>
</span>
</label>
<label class="input-wrap">
<span class="input-label">{labels.innerDiameter}</span>
<span class="input-box">
<input type="number" min="1" step="1" bind:value={userFormInnerDiameter} oninput={applyFormMultiplier} />
<span class="input-suffix">cm</span>
</span>
</label>
{/if}
</div>
{#if !isDefaultForm}
<div class="cake-form-footer">
<button type="button" class="reset-link" onclick={resetCakeForm}>{labels.restoreDefault}</button>
</div>
{/if} {/if}
</div> </div>
<div class="cake-form-inputs">
{#if userFormShape === 'round'}
<label>{labels.diameter}: <input type="number" min="1" step="1" class="cake-form-num" bind:value={userFormDiameter} oninput={applyFormMultiplier} /> cm</label>
{:else if userFormShape === 'rectangular'}
<label>{labels.width}: <input type="number" min="1" step="1" class="cake-form-num" bind:value={userFormWidth} oninput={applyFormMultiplier} /> cm</label>
<label>{labels.length}: <input type="number" min="1" step="1" class="cake-form-num" bind:value={userFormLength} oninput={applyFormMultiplier} /> cm</label>
{:else if userFormShape === 'gugelhupf'}
<label>{isEnglish ? 'Outer Ø' : 'Aussen-Ø'}: <input type="number" min="1" step="1" class="cake-form-num" bind:value={userFormDiameter} oninput={applyFormMultiplier} /> cm</label>
<label>{isEnglish ? 'Inner Ø' : 'Innen-Ø'}: <input type="number" min="1" step="1" class="cake-form-num" bind:value={userFormInnerDiameter} oninput={applyFormMultiplier} /> cm</label>
{/if}
</div>
{#if formDriven}
<div class="cake-form-factor">{labels.factor}: {formMultiplier.toFixed(2)}x</div>
{/if} {/if}
</div> </div>
{/if} {/if}

View File

@@ -571,40 +571,129 @@
.form-size-section { .form-size-section {
max-width: 600px; max-width: 600px;
margin: 2rem auto; margin: 2rem auto;
text-align: center; background: var(--color-surface);
}
.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;
border: 1px solid var(--color-border); 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); 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); 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; 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 { .error-message {
@@ -1127,40 +1216,113 @@
</div> </div>
<div class="form-size-section"> <div class="form-size-section">
<h3>Backform (Standard)</h3> <div class="form-size-head">
<div class="form-size-controls"> <span class="form-size-title">Backform (Standard)</span>
<label>
<input type="radio" name="formShape" value="none" checked={!defaultForm} onchange={() => { defaultForm = null; }} />
Keine
</label>
<label>
<input type="radio" name="formShape" value="round" checked={defaultForm?.shape === 'round'} onchange={() => { defaultForm = { shape: 'round', diameter: defaultForm?.diameter || 26 }; }} />
Rund
</label>
<label>
<input type="radio" name="formShape" value="rectangular" checked={defaultForm?.shape === 'rectangular'} onchange={() => { defaultForm = { shape: 'rectangular', width: defaultForm?.width || 20, length: defaultForm?.length || 30 }; }} />
Rechteckig
</label>
<label>
<input type="radio" name="formShape" value="gugelhupf" checked={defaultForm?.shape === 'gugelhupf'} onchange={() => { defaultForm = { shape: 'gugelhupf', diameter: defaultForm?.diameter || 24, innerDiameter: defaultForm?.innerDiameter || 8 }; }} />
Gugelhupf
</label>
</div> </div>
{#if defaultForm?.shape === 'round'} <div class="form-size-body">
<div class="form-size-inputs"> <div class="form-shape-row" role="radiogroup" aria-label="Backform">
<label>Durchmesser: <input type="number" min="1" step="1" bind:value={defaultForm.diameter} /> cm</label> <button
type="button"
role="radio"
aria-checked={!defaultForm}
aria-label="Keine"
title="Keine"
class="shape-tile"
onclick={() => { defaultForm = null; }}
>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" aria-hidden="true">
<circle cx="12" cy="12" r="8.5"/>
<path d="m6 6 12 12"/>
</svg>
</button>
<button
type="button"
role="radio"
aria-checked={defaultForm?.shape === 'round'}
aria-label="Rund"
title="Rund"
class="shape-tile"
onclick={() => { defaultForm = { shape: 'round', diameter: defaultForm?.diameter || 26 }; }}
>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" aria-hidden="true">
<circle cx="12" cy="12" r="8.5"/>
</svg>
</button>
<button
type="button"
role="radio"
aria-checked={defaultForm?.shape === 'rectangular'}
aria-label="Rechteckig"
title="Rechteckig"
class="shape-tile"
onclick={() => { defaultForm = { shape: 'rectangular', width: defaultForm?.width || 20, length: defaultForm?.length || 30 }; }}
>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" aria-hidden="true">
<rect x="3" y="6" width="18" height="12" rx="1.5"/>
</svg>
</button>
<button
type="button"
role="radio"
aria-checked={defaultForm?.shape === 'gugelhupf'}
aria-label="Gugelhupf"
title="Gugelhupf"
class="shape-tile"
onclick={() => { defaultForm = { shape: 'gugelhupf', diameter: defaultForm?.diameter || 24, innerDiameter: defaultForm?.innerDiameter || 8 }; }}
>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" aria-hidden="true">
<circle cx="12" cy="12" r="8.5"/>
<circle cx="12" cy="12" r="3"/>
</svg>
</button>
</div> </div>
{:else if defaultForm?.shape === 'rectangular'}
<div class="form-size-inputs"> {#if defaultForm?.shape === 'round'}
<label>Breite: <input type="number" min="1" step="1" bind:value={defaultForm.width} /> cm</label> <div class="form-size-inputs">
<label>Länge: <input type="number" min="1" step="1" bind:value={defaultForm.length} /> cm</label> <label class="input-wrap">
</div> <span class="input-label">Durchmesser</span>
{:else if defaultForm?.shape === 'gugelhupf'} <span class="input-box">
<div class="form-size-inputs"> <input type="number" min="1" step="1" bind:value={defaultForm.diameter} />
<label>Aussen-Ø: <input type="number" min="1" step="1" bind:value={defaultForm.diameter} /> cm</label> <span class="input-suffix">cm</span>
<label>Innen-Ø: <input type="number" min="1" step="1" bind:value={defaultForm.innerDiameter} /> cm</label> </span>
</div> </label>
{/if} </div>
{:else if defaultForm?.shape === 'rectangular'}
<div class="form-size-inputs">
<label class="input-wrap">
<span class="input-label">Breite</span>
<span class="input-box">
<input type="number" min="1" step="1" bind:value={defaultForm.width} />
<span class="input-suffix">cm</span>
</span>
</label>
<label class="input-wrap">
<span class="input-label">Länge</span>
<span class="input-box">
<input type="number" min="1" step="1" bind:value={defaultForm.length} />
<span class="input-suffix">cm</span>
</span>
</label>
</div>
{:else if defaultForm?.shape === 'gugelhupf'}
<div class="form-size-inputs">
<label class="input-wrap">
<span class="input-label">Aussen-Ø</span>
<span class="input-box">
<input type="number" min="1" step="1" bind:value={defaultForm.diameter} />
<span class="input-suffix">cm</span>
</span>
</label>
<label class="input-wrap">
<span class="input-label">Innen-Ø</span>
<span class="input-box">
<input type="number" min="1" step="1" bind:value={defaultForm.innerDiameter} />
<span class="input-suffix">cm</span>
</span>
</label>
</div>
{/if}
</div>
</div> </div>
<div class="list_wrapper"> <div class="list_wrapper">