feat(recipes): redesign cake-form and baking info with collapsible card pattern
All checks were successful
CI / update (push) Successful in 3m38s
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:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "homepage",
|
||||
"version": "1.28.0",
|
||||
"version": "1.29.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -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<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
|
||||
const t: Record<string, Record<string, string>> = {
|
||||
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,16 +998,82 @@ h3{
|
||||
<p class="info-value" contenteditable="plaintext-only" bind:innerText={add_info.fermentation.final} data-placeholder="z.B. 1 h"></p>
|
||||
</div>
|
||||
|
||||
<div class="info-card info-card-baking">
|
||||
<div class="info-card info-card-baking" class:is-expanded={bakingExpanded}>
|
||||
<button
|
||||
type="button"
|
||||
class="baking-toggle"
|
||||
onclick={toggleBaking}
|
||||
aria-expanded={bakingExpanded}
|
||||
aria-controls="baking-fields-{lang}"
|
||||
>
|
||||
<h3><Flame size={16} />{t[lang].baking}</h3>
|
||||
<div class="info-value baking-row">
|
||||
<span contenteditable="plaintext-only" bind:innerText={add_info.baking.length} data-placeholder="40 min"></span>
|
||||
<span class="baking-sep">bei</span>
|
||||
<span contenteditable="plaintext-only" bind:innerText={add_info.baking.temperature} data-placeholder="200"></span>
|
||||
<span class="baking-sep">°C</span>
|
||||
<span contenteditable="plaintext-only" bind:innerText={add_info.baking.mode} data-placeholder="Ober-/Unterhitze"></span>
|
||||
{#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 class="info-card">
|
||||
<h3><CookingPot size={16} />{t[lang].cooking}</h3>
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
</style>
|
||||
{#if data.ingredients}
|
||||
@@ -577,36 +783,122 @@ const nutritionFlatIngredients = $derived.by(() => {
|
||||
|
||||
{#if hasDefaultForm}
|
||||
<div class="cake-form">
|
||||
<h3>{labels.cakeForm}</h3>
|
||||
<div class="cake-form-shape">
|
||||
<label class:cake-form-selected={userFormShape === 'round'}>
|
||||
<input type="radio" name="userFormShape" value="round" bind:group={userFormShape} onchange={applyFormMultiplier} />
|
||||
{labels.round}
|
||||
</label>
|
||||
<label class:cake-form-selected={userFormShape === 'rectangular'}>
|
||||
<input type="radio" name="userFormShape" value="rectangular" bind:group={userFormShape} onchange={applyFormMultiplier} />
|
||||
{labels.rectangular}
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
class="cake-form-toggle"
|
||||
aria-expanded={cakeFormExpanded}
|
||||
aria-controls="cake-form-body"
|
||||
onclick={() => { cakeFormExpanded = !cakeFormExpanded; }}
|
||||
>
|
||||
<span class="cake-form-toggle-label">
|
||||
<span class="cake-form-title">{labels.adjustForm}</span>
|
||||
<span class="cake-form-summary">{cakeSummaryText}</span>
|
||||
</span>
|
||||
<span class="cake-form-toggle-right">
|
||||
{#if formDriven && Math.abs(formMultiplier - 1) > 0.005}
|
||||
<span class="cake-form-factor-badge">{formMultiplier.toFixed(2)}×</span>
|
||||
{/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'}
|
||||
<label class:cake-form-selected={userFormShape === 'gugelhupf'}>
|
||||
<input type="radio" name="userFormShape" value="gugelhupf" bind:group={userFormShape} onchange={applyFormMultiplier} />
|
||||
Gugelhupf
|
||||
</label>
|
||||
<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>{labels.diameter}: <input type="number" min="1" step="1" class="cake-form-num" bind:value={userFormDiameter} oninput={applyFormMultiplier} /> cm</label>
|
||||
<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>{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>
|
||||
<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>{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>
|
||||
<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}
|
||||
</div>
|
||||
{#if formDriven}
|
||||
<div class="cake-form-factor">→ {labels.factor}: {formMultiplier.toFixed(2)}x</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -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,41 +1216,114 @@
|
||||
</div>
|
||||
|
||||
<div class="form-size-section">
|
||||
<h3>Backform (Standard)</h3>
|
||||
<div class="form-size-controls">
|
||||
<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 class="form-size-head">
|
||||
<span class="form-size-title">Backform (Standard)</span>
|
||||
</div>
|
||||
<div class="form-size-body">
|
||||
<div class="form-shape-row" role="radiogroup" aria-label="Backform">
|
||||
<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>
|
||||
|
||||
{#if defaultForm?.shape === 'round'}
|
||||
<div class="form-size-inputs">
|
||||
<label>Durchmesser: <input type="number" min="1" step="1" bind:value={defaultForm.diameter} /> cm</label>
|
||||
<label class="input-wrap">
|
||||
<span class="input-label">Durchmesser</span>
|
||||
<span class="input-box">
|
||||
<input type="number" min="1" step="1" bind:value={defaultForm.diameter} />
|
||||
<span class="input-suffix">cm</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
{:else if defaultForm?.shape === 'rectangular'}
|
||||
<div class="form-size-inputs">
|
||||
<label>Breite: <input type="number" min="1" step="1" bind:value={defaultForm.width} /> cm</label>
|
||||
<label>Länge: <input type="number" min="1" step="1" bind:value={defaultForm.length} /> cm</label>
|
||||
<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>Aussen-Ø: <input type="number" min="1" step="1" bind:value={defaultForm.diameter} /> cm</label>
|
||||
<label>Innen-Ø: <input type="number" min="1" step="1" bind:value={defaultForm.innerDiameter} /> cm</label>
|
||||
<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 class="list_wrapper">
|
||||
<div>
|
||||
|
||||
Reference in New Issue
Block a user