feat: align recipe edit page with viewer design

Replace CardAdd with EditTitleImgParallax so /rezepte/edit/[name]
mirrors the hero-parallax layout of /rezepte/[name]. Add Lucide icons
and a width-constrained info-card grid to CreateStepList's additional
info section. Refactor the English translation view to use semantic
theme vars, side-by-side German/English field comparison, and a card
aesthetic matching the rest of the site.

Fix portions binding bug where the shared portions store was being
written by both language ingredient lists. CreateIngredientList now
accepts a bindable portions prop; the English list uses it with
useStore=false to stay isolated from the German value.
This commit is contained in:
2026-04-12 21:22:45 +02:00
parent 69c2e05462
commit f108e9ceaa
7 changed files with 1533 additions and 637 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "homepage",
"version": "1.27.0",
"version": "1.28.0",
"private": true,
"type": "module",
"scripts": {
@@ -13,17 +13,39 @@ import { do_on_key } from '$lib/components/recipes/do_on_key.js'
import { portions } from '$lib/js/portions_store.js'
import BaseRecipeSelector from '$lib/components/recipes/BaseRecipeSelector.svelte'
let portions_local = $state<string | undefined>()
portions.subscribe((p: any) => {
portions_local = p
let {
lang = 'de' as 'de' | 'en',
ingredients = $bindable(),
portions: portionsProp = $bindable<string | undefined>(undefined),
useStore = true,
} = $props<{
lang?: 'de' | 'en',
ingredients: any,
portions?: string,
useStore?: boolean,
}>();
let portions_local = $state<string | undefined>(portionsProp)
if (useStore) {
portions.subscribe((p: any) => {
portions_local = p
});
}
$effect(() => {
if (!useStore) {
portions_local = portionsProp ?? ''
}
});
export function set_portions(){
portions.update((_p: any) => portions_local)
if (useStore) {
portions.update((_p: any) => portions_local)
}
portionsProp = portions_local
}
let { lang = 'de' as 'de' | 'en', ingredients = $bindable() } = $props<{ lang?: 'de' | 'en', ingredients: any }>();
// Translation strings
const t: Record<string, Record<string, string>> = {
de: {
+101 -59
View File
@@ -4,6 +4,7 @@ import Pen from '$lib/assets/icons/Pen.svelte'
import Cross from '$lib/assets/icons/Cross.svelte'
import Plus from '$lib/assets/icons/Plus.svelte'
import Check from '$lib/assets/icons/Check.svelte'
import { Timer, Wheat, Croissant, Flame, CookingPot, UtensilsCrossed } from '@lucide/svelte';
import "$lib/css/action_button.css"
@@ -592,7 +593,7 @@ ol li::marker{
.instructions{
flex-basis: 0;
flex-grow: 2;
background-color: var(--nord5);
background-color: var(--color-bg-secondary);
padding-block: 1rem;
padding-inline: 2rem;
}
@@ -605,24 +606,83 @@ ol li::marker{
}
.additional_info{
display: grid;
grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
gap: 0.75rem;
margin-block: 1rem;
}
.info-card{
padding: 0.75rem 1rem;
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
box-shadow: var(--shadow-sm);
transition: var(--transition-fast);
}
.info-card:focus-within{
border-color: var(--color-primary);
box-shadow: var(--shadow-md);
}
.info-card-baking{
grid-column: span 2;
}
.info-card h3{
display: flex;
align-items: center;
gap: 0.4rem;
margin: 0 0 0.25rem 0;
font-size: var(--text-sm);
color: var(--color-text-secondary);
cursor: default;
user-select: auto;
}
.info-value{
display: block;
margin: 0;
font-size: 1rem;
font-weight: 600;
color: var(--color-text-primary);
outline: none;
min-height: 1.2em;
border-bottom: 1px dashed transparent;
transition: border-color 200ms ease;
}
.info-value:hover,
.info-value:focus{
border-bottom-color: var(--color-border);
}
.info-value:empty::before,
.info-value span:empty::before{
content: attr(data-placeholder);
color: var(--color-text-tertiary);
font-style: italic;
font-weight: 400;
}
.baking-row{
display: flex;
flex-wrap: wrap;
gap: 1em;
align-items: baseline;
gap: 0.35em;
}
.additional_info > *{
flex-grow: 0;
overflow: hidden;
padding: 1em;
background-color: #FAFAFE;
box-shadow: 0.3em 0.3em 1em 0.2em rgba(0,0,0,0.3);
/*max-width: 30%*/
.baking-row > span[contenteditable]{
outline: none;
padding: 0 0.15em;
border-bottom: 1px dashed transparent;
transition: border-color 200ms ease;
min-width: 2ch;
}
.additional_info > div > *:not(h4){
line-height: 2em;
.baking-row > span[contenteditable]:hover,
.baking-row > span[contenteditable]:focus{
border-bottom-color: var(--color-border);
}
h4{
line-height: 1em;
margin-block: 0;
.baking-sep{
color: var(--color-text-secondary);
font-weight: 400;
}
@media (max-width: 560px){
.info-card-baking{
grid-column: span 1;
}
}
.button_subtle{
padding: 0em;
@@ -641,21 +701,6 @@ h3{
cursor: pointer;
user-select: none;
}
.additional_info p[contenteditable]{
display: inline;
padding: 0.25em 1em;
border: 2px solid grey;
border-radius: var(--radius-pill);
}
.additional_info div:has(p[contenteditable]){
transition: var(--transition-normal);
display: inline;
}
.additional_info div:has(p[contenteditable]):hover,
.additional_info div:has(p[contenteditable]):focus-within
{
transform: scale(1.1, 1.1);
}
@media screen and (max-width: 500px){
dialog h2{
margin-top: 2rem;
@@ -664,20 +709,6 @@ h3{
width: 80%;
}
}
@media (prefers-color-scheme: dark){
:global(:root:not([data-theme="light"])) .additional_info div {
background-color: var(--accent-dark);
}
:global(:root:not([data-theme="light"])) .instructions {
background-color: var(--nord6-dark);
}
}
:global(:root[data-theme="dark"]) .additional_info div {
background-color: var(--accent-dark);
}
:global(:root[data-theme="dark"]) .instructions {
background-color: var(--nord6-dark);
}
.button_arrow{
fill: var(--nord1);
}
@@ -768,30 +799,41 @@ h3{
</style>
<div class=instructions>
<div class=additional_info>
<div><h4>{t[lang].preparation}</h4>
<p contenteditable bind:innerText={add_info.preparation}></p>
<div class="additional_info">
<div class="info-card">
<h3><Timer size={16} />{t[lang].preparation}</h3>
<p class="info-value" contenteditable="plaintext-only" bind:innerText={add_info.preparation} data-placeholder="z.B. 30 min"></p>
</div>
<div><h4>{t[lang].bulkFermentation}</h4>
<p contenteditable bind:innerText={add_info.fermentation.bulk}></p>
<div class="info-card">
<h3><Wheat size={16} />{t[lang].bulkFermentation}</h3>
<p class="info-value" contenteditable="plaintext-only" bind:innerText={add_info.fermentation.bulk} data-placeholder="z.B. 4 h"></p>
</div>
<div><h4>{t[lang].finalFermentation}</h4>
<p contenteditable bind:innerText={add_info.fermentation.final}></p>
<div class="info-card">
<h3><Croissant size={16} />{t[lang].finalFermentation}</h3>
<p class="info-value" contenteditable="plaintext-only" bind:innerText={add_info.fermentation.final} data-placeholder="z.B. 1 h"></p>
</div>
<div><h4>{t[lang].baking}</h4>
<div><p bind:innerText={add_info.baking.length} contenteditable placeholder="40 min..."></p></div> bei <div><p bind:innerText={add_info.baking.temperature} contenteditable placeholder=200...></p></div> °C <div><p bind:innerText={add_info.baking.mode} contenteditable placeholder="Ober-/Unterhitze..."></p></div></div>
<div><h4>{t[lang].cooking}</h4>
<p contenteditable bind:innerText={add_info.cooking}></p>
<div class="info-card info-card-baking">
<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>
</div>
</div>
<div><h4>{t[lang].totalTime}</h4>
<p contenteditable bind:innerText={add_info.total_time}></p>
<div class="info-card">
<h3><CookingPot size={16} />{t[lang].cooking}</h3>
<p class="info-value" contenteditable="plaintext-only" bind:innerText={add_info.cooking} data-placeholder="z.B. 20 min"></p>
</div>
<div class="info-card">
<h3><UtensilsCrossed size={16} />{t[lang].totalTime}</h3>
<p class="info-value" contenteditable="plaintext-only" bind:innerText={add_info.total_time} data-placeholder="z.B. 1 h"></p>
</div>
</div>
@@ -0,0 +1,557 @@
<script lang="ts">
import Cross from '$lib/assets/icons/Cross.svelte';
import { toast } from '$lib/js/toast.svelte';
import { onMount, type Snippet } from 'svelte';
type CardData = {
icon?: string;
category?: string;
name?: string;
description?: string;
tags?: string[];
};
type Props = {
card_data: CardData;
image_preview_url: string;
selected_image_file: File | null;
color?: string;
titleExtras?: Snippet;
children?: Snippet;
};
let {
card_data = $bindable(),
image_preview_url = $bindable(''),
selected_image_file = $bindable<File | null>(null),
color = '',
titleExtras,
children
}: Props = $props();
const ALLOWED_MIME = ['image/webp', 'image/jpeg', 'image/jpg', 'image/png'];
const MAX_SIZE = 5 * 1024 * 1024;
let fileInput: HTMLInputElement;
let new_tag = $state('');
if (!card_data.tags) card_data.tags = [];
function handleFileSelect(event: Event) {
const input = event.currentTarget as HTMLInputElement;
const file = input.files?.[0];
if (!file) return;
if (!ALLOWED_MIME.includes(file.type)) {
toast.error('Ungültiger Dateityp. Bitte JPEG, PNG oder WebP hochladen.');
input.value = '';
return;
}
if (file.size > MAX_SIZE) {
toast.error(
`Datei zu gross. Maximum 5 MB, deine Datei ${(file.size / 1024 / 1024).toFixed(2)} MB.`
);
input.value = '';
return;
}
if (image_preview_url?.startsWith('blob:')) URL.revokeObjectURL(image_preview_url);
image_preview_url = URL.createObjectURL(file);
selected_image_file = file;
}
function clearSelectedImage() {
if (image_preview_url?.startsWith('blob:')) URL.revokeObjectURL(image_preview_url);
image_preview_url = '';
selected_image_file = null;
if (fileInput) fileInput.value = '';
}
function triggerFilePicker() {
fileInput?.click();
}
onMount(() => {
if (image_preview_url && !image_preview_url.startsWith('blob:')) {
const img = new Image();
img.onload = () => {
if (img.naturalWidth === 150 && img.naturalHeight === 150) image_preview_url = '';
};
img.onerror = () => {
image_preview_url = '';
};
img.src = image_preview_url;
}
});
function addTag() {
const t = new_tag.trim();
if (t && !card_data.tags!.includes(t)) {
card_data.tags = [...card_data.tags!, t];
}
new_tag = '';
}
function removeTag(tag: string) {
card_data.tags = card_data.tags!.filter((x) => x !== tag);
}
function onTagKey(e: KeyboardEvent) {
if (e.key === 'Enter') {
e.preventDefault();
addTag();
}
}
</script>
<section class="section">
<figure class="image-container">
<button
type="button"
class="image-wrap"
onclick={triggerFilePicker}
style:background-color={color || 'var(--color-bg-elevated)'}
aria-label={image_preview_url ? 'Bild ersetzen' : 'Bild hochladen'}
>
{#if image_preview_url}
<img class="image" src={image_preview_url} alt="" />
{/if}
<div class="upload-overlay" class:empty={!image_preview_url}>
<svg
class="camera"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512"
aria-hidden="true"
>
<path
d="M149.1 64.8L138.7 96H64C28.7 96 0 124.7 0 160V416c0 35.3 28.7 64 64 64H448c35.3 0 64-28.7 64-64V160c0-35.3-28.7-64-64-64H373.3L362.9 64.8C356.4 45.2 338.1 32 317.4 32H194.6c-20.7 0-39 13.2-45.5 32.8zM256 192a96 96 0 1 1 0 192 96 96 0 1 1 0-192z"
/>
</svg>
<span class="upload-label">
{image_preview_url ? 'Bild ersetzen' : 'Bild hochladen'}
</span>
</div>
</button>
{#if selected_image_file}
<button
type="button"
class="clear-img"
onclick={clearSelectedImage}
title="Auswahl verwerfen"
aria-label="Auswahl verwerfen"
>
<Cross fill="white" width="1.25rem" height="1.25rem" />
</button>
{/if}
<input
bind:this={fileInput}
type="file"
accept="image/webp,image/jpeg,image/jpg,image/png"
onchange={handleFileSelect}
class="file-input"
tabindex="-1"
aria-hidden="true"
/>
</figure>
<div class="content">
<div class="title" style="view-transition-name: recipe-title">
<input
class="category g-pill g-btn-dark"
placeholder="Kategorie…"
bind:value={card_data.category}
aria-label="Kategorie"
/>
<input
class="icon g-icon-badge"
placeholder="🥫"
bind:value={card_data.icon}
aria-label="Icon"
maxlength="4"
/>
<input
class="name"
placeholder="Rezeptname…"
bind:value={card_data.name}
aria-label="Rezeptname"
/>
<p
class="description"
contenteditable="plaintext-only"
bind:innerText={card_data.description}
data-placeholder="Kurzbeschreibung…"
aria-label="Kurzbeschreibung"
></p>
<h2 class="section-label">Stichwörter</h2>
<div class="tags center">
{#each card_data.tags ?? [] as tag (tag)}
<button
type="button"
class="g-tag tag-chip"
onclick={() => removeTag(tag)}
aria-label={`Stichwort ${tag} entfernen`}
>
<span>{tag}</span><span class="x" aria-hidden="true">×</span>
</button>
{/each}
<label class="g-tag tag-add">
<span aria-hidden="true">+</span>
<input
type="text"
bind:value={new_tag}
onkeydown={onTagKey}
onblur={addTag}
placeholder="neu…"
size="1"
aria-label="Neues Stichwort"
/>
</label>
</div>
{#if titleExtras}{@render titleExtras()}{/if}
</div>
{#if children}{@render children()}{/if}
</div>
</section>
<style>
.section {
--scale: 0.3;
margin-bottom: -20vh;
margin-top: calc(-3.5rem - 12px - env(safe-area-inset-top, 0px));
transform-origin: center top;
transform: scaleY(calc(1 - var(--scale)));
}
@media (prefers-reduced-motion) {
.section {
--scale: 0;
}
}
.section > :global(*) {
transform-origin: center top;
transform: scaleY(calc(1 / (1 - var(--scale))));
}
.content {
position: relative;
margin: 30vh auto 0;
}
.image-container {
position: sticky;
top: 0;
height: max(55dvh, 540px);
z-index: -10;
margin: 0;
display: flex;
align-items: center;
justify-content: center;
}
.image-wrap {
all: unset;
box-sizing: border-box;
display: block;
position: absolute;
top: 0;
left: 50%;
transform: translateX(-50%);
width: min(calc(1000px + 2rem), 100dvw);
height: max(65dvh, 640px);
overflow: hidden;
cursor: pointer;
}
.image {
display: block;
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
object-position: 50% 20%;
}
.upload-overlay {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.75rem;
color: white;
background: linear-gradient(
180deg,
rgba(0, 0, 0, 0.15) 0%,
rgba(0, 0, 0, 0.45) 100%
);
opacity: 0;
transition: opacity 200ms ease;
font-size: 1rem;
letter-spacing: 0.04em;
text-transform: uppercase;
font-weight: 600;
}
.image-wrap:hover .upload-overlay,
.image-wrap:focus-visible .upload-overlay {
opacity: 1;
}
.upload-overlay.empty {
opacity: 1;
background: color-mix(in srgb, var(--color-primary) 65%, transparent);
}
.camera {
width: 2.5rem;
height: 2.5rem;
fill: white;
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.4));
}
.upload-label {
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
}
.clear-img {
position: absolute;
top: calc(1rem + env(safe-area-inset-top, 0px));
right: 1rem;
background: rgba(0, 0, 0, 0.55);
border: none;
width: 2.5rem;
height: 2.5rem;
border-radius: 50%;
display: grid;
place-items: center;
cursor: pointer;
z-index: 5;
transition:
transform 150ms ease,
background 150ms ease;
backdrop-filter: blur(6px);
}
.clear-img:hover,
.clear-img:focus-visible {
background: var(--red);
transform: scale(1.08);
}
.file-input {
position: absolute;
width: 1px;
height: 1px;
opacity: 0;
pointer-events: none;
}
.title {
position: relative;
width: min(800px, 80vw);
margin-inline: auto;
background-color: var(--color-bg-tertiary);
padding: 1rem 2rem 1.5rem;
translate: 0 1px;
z-index: 1;
}
.category {
--size: 1.75rem;
position: absolute;
top: calc(-1 * var(--size));
left: calc(-1.5 * var(--size));
font-size: var(--size);
padding: calc(var(--size) * 2 / 3);
border: none;
outline: none;
max-width: min(16rem, 70%);
text-align: center;
transition: var(--transition-fast);
}
.category::placeholder {
color: var(--color-text-tertiary);
font-style: italic;
opacity: 0.8;
}
.category:hover,
.category:focus-visible {
scale: 1.04;
}
.icon {
position: absolute;
top: -1em;
right: -0.75em;
padding: 0.5em;
font-size: 1.5rem;
background-color: var(--color-bg-tertiary);
text-align: center;
border: none;
outline: none;
width: 2.6rem;
height: 2.6rem;
box-sizing: border-box;
transition: var(--transition-fast);
}
.icon::placeholder {
opacity: 0.5;
}
.icon:hover,
.icon:focus-visible {
scale: 1.15;
}
.name {
all: unset;
display: block;
width: 100%;
box-sizing: border-box;
text-align: center;
padding-block: 0.5em;
margin: 0;
font-size: 3rem;
font-weight: 600;
line-height: 1.1;
color: var(--color-text-primary);
overflow-wrap: break-word;
text-wrap: balance;
border-bottom: 1px dashed transparent;
transition: border-color 200ms ease;
}
.name::placeholder {
color: var(--color-text-tertiary);
font-style: italic;
font-weight: 400;
}
.name:hover,
.name:focus-visible {
border-bottom-color: var(--color-border);
}
.description {
text-align: center;
margin: -0.25em 0 1.75em;
padding: 0.25em 0.5em;
color: var(--color-text-secondary);
font-size: 1.05rem;
min-height: 1.2em;
outline: none;
border-bottom: 1px dashed transparent;
transition: border-color 200ms ease;
}
.description:hover,
.description:focus {
border-bottom-color: var(--color-border);
}
.description:empty::before {
content: attr(data-placeholder);
color: var(--color-text-tertiary);
font-style: italic;
}
.section-label {
font-size: 1.2rem;
font-weight: 700;
text-align: center;
margin-block: 1.25rem 0.5rem;
color: var(--color-text-primary);
}
.tags {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5em;
margin-block: 0.5rem 1rem;
font-size: 1.05rem;
}
.tags.center {
justify-content: center;
}
.tag-chip {
all: unset;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 0.35em;
padding: 0.25em 0.9em;
border-radius: var(--radius-pill);
background: var(--color-bg-elevated);
color: var(--color-text-primary);
transition: var(--transition-fast);
}
.tag-chip .x {
opacity: 0.55;
font-size: 1.1em;
line-height: 1;
}
.tag-chip:hover,
.tag-chip:focus-visible {
background: var(--red);
color: white;
transform: scale(1.05);
}
.tag-chip:hover .x,
.tag-chip:focus-visible .x {
opacity: 1;
}
.tag-add {
display: inline-flex;
align-items: center;
gap: 0.35em;
background: transparent;
border: 1px dashed var(--color-border);
color: var(--color-text-secondary);
padding: 0.15em 0.7em;
border-radius: var(--radius-pill);
}
.tag-add input {
all: unset;
min-width: 6ch;
color: var(--color-text-primary);
font-size: inherit;
}
.tag-add input::placeholder {
color: var(--color-text-tertiary);
font-style: italic;
}
.tag-add:focus-within {
border-style: solid;
border-color: var(--color-primary);
}
@media screen and (max-width: 800px) {
.title {
width: 100%;
}
.icon {
right: 1rem;
top: -1.75rem;
}
.category {
left: 1rem;
top: calc(var(--size) * -1.5);
}
.name {
font-size: 2.2rem;
}
}
:global(::view-transition-new(recipe-title)) {
animation: slide-up 0.35s ease both;
}
:global(::view-transition-old(recipe-title)) {
animation: slide-down 0.25s ease both;
}
@keyframes slide-up {
from {
transform: translateY(var(--title-slide, 100vh));
}
}
@keyframes slide-down {
to {
transform: translateY(var(--title-slide, 100vh));
}
}
</style>
@@ -358,76 +358,79 @@
oncancelled?.();
}
// Get status badge color
function getStatusColor(status: string): string {
switch (status) {
case 'approved': return 'var(--nord14)';
case 'pending': return 'var(--nord13)';
case 'needs_update': return 'var(--nord12)';
default: return 'var(--nord9)';
}
}
</script>
<style>
.translation-approval {
margin: 2rem 0;
padding: 1.5rem;
border: 2px solid var(--nord9);
border-radius: 8px;
background: var(--nord1);
margin: 3rem auto 2rem;
padding: 2rem clamp(1rem, 3vw, 2.5rem);
max-width: 1200px;
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-card);
box-shadow: var(--shadow-md);
position: relative;
}
@media(prefers-color-scheme: light) {
:global(:root:not([data-theme="dark"])) .translation-approval {
background: var(--nord6);
border-color: var(--nord4);
}
}
:global(:root[data-theme="light"]) .translation-approval {
background: var(--nord6);
border-color: var(--nord4);
.translation-approval::before{
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: linear-gradient(90deg, var(--color-primary), var(--blue), var(--green));
border-radius: var(--radius-card) var(--radius-card) 0 0;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
gap: 1rem;
flex-wrap: wrap;
margin-bottom: 1.75rem;
padding-bottom: 1rem;
border-bottom: 1px solid var(--color-border);
}
.header h3 {
margin: 0;
color: var(--nord6);
font-size: 1.5rem;
font-weight: 700;
color: var(--color-text-primary);
display: flex;
align-items: center;
gap: 0.6rem;
letter-spacing: -0.01em;
}
@media(prefers-color-scheme: light) {
:global(:root:not([data-theme="dark"])) .header h3 {
color: var(--nord0);
}
}
:global(:root[data-theme="light"]) .header h3 {
color: var(--nord0);
}
.status-badge {
padding: 0.25rem 0.75rem;
border-radius: 16px;
font-size: 0.85rem;
font-weight: 600;
color: var(--nord0);
display: inline-flex;
align-items: center;
gap: 0.4rem;
padding: 0.35rem 0.9rem;
border-radius: var(--radius-pill);
font-size: 0.8rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
}
.status-badge::before{
content: '';
width: 0.45rem;
height: 0.45rem;
border-radius: 50%;
background: currentColor;
}
.status-pending {
background: var(--nord13);
background: color-mix(in srgb, var(--orange) 18%, transparent);
color: var(--orange);
}
.status-approved {
background: var(--nord14);
background: color-mix(in srgb, var(--green) 18%, transparent);
color: var(--green);
}
.status-needs_update {
background: var(--nord12);
background: color-mix(in srgb, var(--red) 18%, transparent);
color: var(--red);
}
.translation-preview {
@@ -436,10 +439,9 @@
}
.field-section {
margin-bottom: 1.5rem;
max-width: 800px;
margin-left: auto;
margin-right: auto;
margin-bottom: 1.25rem;
max-width: 900px;
margin-inline: auto;
}
.list-wrapper {
@@ -451,160 +453,251 @@
justify-content: center;
margin-bottom: 2rem;
}
@media screen and (max-width: 700px) {
.list-wrapper {
flex-direction: column;
}
}
/* Fix button icon visibility in dark mode */
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .list-wrapper :global(svg) {
fill: white !important;
}
:global(:root:not([data-theme="light"])) .list-wrapper :global(.button_arrow) {
fill: var(--nord4) !important;
}
}
:global(:root[data-theme="dark"]) .list-wrapper :global(svg) {
fill: white !important;
}
:global(:root[data-theme="dark"]) .list-wrapper :global(.button_arrow) {
fill: var(--nord4) !important;
}
.column-header {
.preview-title{
margin: 0 0 1.5rem;
font-size: 1.15rem;
font-weight: 700;
font-size: 1.1rem;
color: var(--nord8);
margin-bottom: 1rem;
color: var(--color-primary);
display: flex;
align-items: center;
gap: 0.5rem;
padding-bottom: 0.5rem;
border-bottom: 2px solid var(--nord9);
}
.field-group {
margin-bottom: 1.5rem;
border-bottom: 2px solid var(--color-primary);
max-width: 900px;
margin-inline: auto;
}
.actions {
display: flex;
gap: 1rem;
gap: 0.75rem;
justify-content: flex-end;
margin-top: 1.5rem;
margin-top: 2rem;
padding-top: 1.5rem;
border-top: 1px solid var(--color-border);
flex-wrap: wrap;
}
button {
padding: 0.75rem 1.5rem;
padding: 0.65rem 1.25rem;
border: none;
border-radius: 4px;
font-size: 1rem;
border-radius: var(--radius-pill);
font-size: 0.95rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
transition: var(--transition-fast);
font-family: inherit;
}
.btn-primary {
background: var(--nord14);
color: var(--nord0);
button:hover:not(:disabled){
transform: scale(1.03);
}
.btn-primary:hover {
background: var(--nord15);
button:active:not(:disabled){
transform: scale(0.98);
}
.btn-secondary {
background: var(--nord9);
color: var(--nord6);
}
.btn-secondary:hover {
background: var(--nord10);
}
.btn-danger {
background: var(--nord11);
color: var(--nord6);
}
.btn-danger:hover {
background: var(--nord12);
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btn-primary {
background: var(--color-primary);
color: var(--color-text-on-primary);
box-shadow: var(--shadow-sm);
}
.btn-primary:hover:not(:disabled) {
background: var(--color-primary-hover);
}
.btn-secondary {
background: var(--color-bg-elevated);
color: var(--color-text-primary);
border: 1px solid var(--color-border);
}
.btn-secondary:hover:not(:disabled) {
background: var(--color-bg-tertiary);
border-color: var(--color-primary);
}
.btn-danger {
background: transparent;
color: var(--red);
border: 1px solid color-mix(in srgb, var(--red) 40%, transparent);
}
.btn-danger:hover:not(:disabled) {
background: var(--red);
color: white;
border-color: var(--red);
}
.loading-spinner {
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid var(--nord4);
border-top-color: var(--nord14);
width: 18px;
height: 18px;
border: 2px solid var(--color-border);
border-top-color: var(--color-primary);
border-radius: 50%;
animation: spin 1s linear infinite;
margin-right: 0.5rem;
vertical-align: middle;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.error-message {
background: var(--nord11);
color: var(--nord6);
padding: 1rem;
border-radius: 4px;
margin: 1rem 0;
.notice {
padding: 1rem 1.25rem;
border-radius: var(--radius-md);
margin-bottom: 1.25rem;
font-size: 0.95rem;
display: flex;
gap: 0.75rem;
align-items: flex-start;
border: 1px solid;
}
.notice-body{ flex: 1; min-width: 0; }
.notice-body strong{ display: block; margin-bottom: 0.2rem; }
.notice-error {
background: color-mix(in srgb, var(--red) 10%, var(--color-surface));
color: var(--color-text-primary);
border-color: color-mix(in srgb, var(--red) 40%, transparent);
}
.notice-warn {
background: color-mix(in srgb, var(--orange) 10%, var(--color-surface));
color: var(--color-text-primary);
border-color: color-mix(in srgb, var(--orange) 40%, transparent);
}
.notice-info {
background: color-mix(in srgb, var(--blue) 10%, var(--color-surface));
color: var(--color-text-primary);
border-color: color-mix(in srgb, var(--blue) 40%, transparent);
}
.validation-errors {
background: var(--nord12);
color: var(--nord0);
padding: 1rem;
border-radius: 4px;
margin: 1rem 0;
.notice ul {
margin: 0.35rem 0 0;
padding-left: 1.25rem;
}
.validation-errors ul {
margin: 0.5rem 0 0 0;
padding-left: 1.5rem;
.notice ul li{
margin: 0.2rem 0;
}
.changed-fields {
background: var(--nord13);
color: var(--nord0);
padding: 0.75rem;
border-radius: 4px;
margin-bottom: 1rem;
font-size: 0.9rem;
}
.changed-fields strong {
font-weight: 700;
.notice a{
color: var(--color-primary);
text-decoration: underline;
margin-left: 0.4rem;
}
.idle-state {
text-align: center;
padding: 2rem;
color: var(--nord4);
color: var(--color-text-secondary);
}
@media(prefers-color-scheme: light) {
:global(:root:not([data-theme="dark"])) .idle-state {
color: var(--nord2);
}
}
:global(:root[data-theme="light"]) .idle-state {
color: var(--nord2);
}
.idle-state p {
margin-bottom: 1rem;
font-size: 1.05rem;
}
.approved-pill {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
border-radius: var(--radius-pill);
background: color-mix(in srgb, var(--green) 18%, transparent);
color: var(--green);
font-weight: 700;
}
/* Images section */
.images-section {
margin: 2rem auto;
max-width: 900px;
padding: 1.25rem;
background: var(--color-bg-secondary);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
}
.images-section h4{
margin: 0 0 1rem;
font-size: 1rem;
color: var(--color-text-primary);
display: flex;
align-items: center;
gap: 0.5rem;
}
.image-card{
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
padding: 1rem;
margin-bottom: 0.75rem;
}
.image-card:last-child{ margin-bottom: 0; }
.image-card-row{
display: flex;
gap: 1rem;
align-items: flex-start;
}
.image-card-row img{
width: 96px;
height: 96px;
object-fit: cover;
border-radius: var(--radius-sm);
flex-shrink: 0;
}
.image-card-body{ flex: 1; min-width: 0; }
.image-path{
margin: 0 0 0.75rem;
font-size: 0.8rem;
color: var(--color-text-tertiary);
font-family: monospace;
word-break: break-all;
}
.image-grid{
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.75rem;
margin-bottom: 0.75rem;
}
@media (max-width: 560px){
.image-card-row{ flex-direction: column; }
.image-card-row img{ width: 100%; height: 140px; }
.image-grid{ grid-template-columns: 1fr; gap: 0.5rem; }
}
.image-field label{
display: block;
margin-bottom: 0.25rem;
font-size: 0.75rem;
font-weight: 700;
color: var(--color-text-secondary);
text-transform: uppercase;
letter-spacing: 0.04em;
}
.image-field input{
width: 100%;
padding: 0.45rem 0.6rem;
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
background: var(--color-bg-tertiary);
color: var(--color-text-primary);
font-size: 0.9rem;
box-sizing: border-box;
font-family: inherit;
transition: border-color 150ms ease;
}
.image-field input:focus{
outline: none;
border-color: var(--color-primary);
}
.image-field input:disabled{
background: var(--color-bg-secondary);
color: var(--color-text-tertiary);
cursor: default;
}
</style>
<div class="translation-approval">
@@ -620,64 +713,73 @@ button:disabled {
</div>
{#if errorMessage}
<div class="error-message">
<strong>Error:</strong> {errorMessage}
<div class="notice notice-error">
<div class="notice-body">
<strong>Error</strong>
{errorMessage}
</div>
</div>
{/if}
{#if validationErrors.length > 0}
<div class="validation-errors">
<strong>Please fix the following errors:</strong>
<ul>
{#each validationErrors as error}
<li>{error}</li>
{/each}
</ul>
<div class="notice notice-warn">
<div class="notice-body">
<strong>Please fix the following errors:</strong>
<ul>
{#each validationErrors as error}
<li>{error}</li>
{/each}
</ul>
</div>
</div>
{/if}
{#if isEditMode && changedFields.length > 0}
<div class="changed-fields">
<strong>Changed fields:</strong> {changedFields.join(', ')}
<br>
<small>Only these fields will be re-translated if you use auto-translate.</small>
<div class="notice notice-info">
<div class="notice-body">
<strong>Changed fields: {changedFields.join(', ')}</strong>
<small>Only these fields will be re-translated if you use auto-translate.</small>
</div>
</div>
{/if}
{#if checkingBaseRecipes}
<div style="background: var(--nord9); color: var(--nord6); padding: 1rem; border-radius: 4px; margin-bottom: 1.5rem; text-align: center;">
<p>Checking if referenced base recipes are translated...</p>
<div class="notice notice-info">
<div class="notice-body"><span class="loading-spinner"></span>Checking if referenced base recipes are translated</div>
</div>
{/if}
{#if untranslatedBaseRecipes.length > 0}
<div style="background: var(--nord12); color: var(--nord0); padding: 1.5rem; border-radius: 4px; margin-bottom: 1.5rem;">
<h4 style="margin-top: 0;">⚠️ Base Recipes Need Translation</h4>
<p>The following base recipes need to be translated to English before you can translate this recipe:</p>
<ul style="margin: 1rem 0;">
{#each untranslatedBaseRecipes as baseRecipe}
<li>
<strong>{baseRecipe.name}</strong>
<a href="/de/edit/{baseRecipe.shortName}" target="_blank" rel="noopener noreferrer" style="margin-left: 0.5rem; color: var(--nord10);">
Open in new tab →
</a>
</li>
{/each}
</ul>
<p style="margin-bottom: 0;">
<button class="btn-secondary" onclick={syncBaseRecipeReferences}>
Re-check Base Recipes
</button>
</p>
<div class="notice notice-warn">
<div class="notice-body">
<strong>Base recipes need translation</strong>
The following base recipes need to be translated to English before you can translate this recipe:
<ul>
{#each untranslatedBaseRecipes as baseRecipe}
<li>
<strong>{baseRecipe.name}</strong>
<a href="/de/edit/{baseRecipe.shortName}" target="_blank" rel="noopener noreferrer">
Open in new tab →
</a>
</li>
{/each}
</ul>
<div style="margin-top: 0.75rem;">
<button class="btn-secondary" onclick={syncBaseRecipeReferences}>
Re-check base recipes
</button>
</div>
</div>
</div>
{/if}
{#if translationState === 'idle'}
<div style="background: var(--nord13); color: var(--nord0); padding: 1rem; border-radius: 4px; margin-bottom: 1.5rem; text-align: center;">
<strong>Preview (Not yet translated)</strong>
<p style="margin: 0.5rem 0;">The structure below shows what will be translated. Click "Auto-translate" to generate English translation.</p>
<div class="notice notice-info">
<div class="notice-body">
<strong>Preview — not yet translated</strong>
The structure below shows what will be translated. Click “Auto-translate” to generate the English translation.
</div>
</div>
{/if}
{#if translationState === 'translating'}
@@ -691,7 +793,7 @@ button:disabled {
{#if translationState === 'idle' || translationState === 'preview' || translationState === 'approved'}
<div class="translation-preview">
<h3 style="margin-bottom: 1.5rem; color: var(--nord8);">🇬🇧 English Translation</h3>
<h3 class="preview-title">Side-by-side review</h3>
<!-- Basic Fields -->
<div class="field-section">
@@ -780,77 +882,59 @@ button:disabled {
</div>
{/if}
{#if editableEnglish?.portions !== undefined}
<div class="field-section">
<TranslationFieldComparison
label="Portions"
germanValue={germanData.portions || ''}
englishValue={editableEnglish.portions}
fieldName="portions"
readonly={false}
onchange={(value) => handleFieldChange(value, 'portions')}
/>
</div>
{/if}
<!-- Images Section -->
{#if germanData.images && germanData.images.length > 0}
<div class="field-section" style="background-color: var(--nord13); padding: 1rem; border-radius: 5px; margin-top: 1.5rem;">
<h4 style="margin-top: 0; color: var(--nord0);">🖼️ Images - English Alt Texts & Captions</h4>
<section class="images-section">
<h4>Images Alt texts &amp; captions</h4>
{#each germanData.images as germanImage, i}
{#if editableEnglish.images && editableEnglish.images[i]}
<div style="background-color: white; padding: 1rem; margin-bottom: 1rem; border-radius: 5px; border: 2px solid var(--nord9);">
<div style="display: flex; gap: 1rem; align-items: start;">
<div class="image-card">
<div class="image-card-row">
<img
src="https://bocken.org/static/rezepte/thumb/{germanImage.mediapath}"
alt={germanImage.alt || 'Recipe image'}
style="width: 100px; height: 100px; object-fit: cover; border-radius: 5px;"
/>
<div style="flex: 1;">
<p style="margin: 0 0 0.5rem 0; font-size: 0.85rem; color: var(--nord3);"><strong>Image {i + 1}:</strong> {germanImage.mediapath}</p>
<div class="image-card-body">
<p class="image-path"><strong>Image {i + 1}:</strong> {germanImage.mediapath}</p>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin-bottom: 0.75rem;">
<div>
<label for="german-alt-{i}" style="display: block; margin-bottom: 0.25rem; font-weight: bold; font-size: 0.85rem; color: var(--nord0);">🇩🇪 German Alt-Text:</label>
<div class="image-grid">
<div class="image-field">
<label for="german-alt-{i}">German Alt-Text</label>
<input
id="german-alt-{i}"
type="text"
value={germanImage.alt || ''}
disabled
style="width: 100%; padding: 0.4rem; border: 1px solid var(--nord4); border-radius: 3px; background-color: var(--nord5); color: var(--nord2); font-size: 0.85rem;"
/>
</div>
<div>
<label for="english-alt-{i}" style="display: block; margin-bottom: 0.25rem; font-weight: bold; font-size: 0.85rem; color: var(--nord0);">🇬🇧 English Alt-Text:</label>
<div class="image-field">
<label for="english-alt-{i}">English Alt-Text</label>
<input
id="english-alt-{i}"
type="text"
bind:value={editableEnglish.images[i].alt}
placeholder="English image description for screen readers"
style="width: 100%; padding: 0.4rem; border: 1px solid var(--nord8); border-radius: 3px; font-size: 0.85rem;"
/>
</div>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem;">
<div>
<label for="german-caption-{i}" style="display: block; margin-bottom: 0.25rem; font-weight: bold; font-size: 0.85rem; color: var(--nord0);">🇩🇪 German Caption:</label>
<div class="image-grid">
<div class="image-field">
<label for="german-caption-{i}">German Caption</label>
<input
id="german-caption-{i}"
type="text"
value={germanImage.caption || ''}
disabled
style="width: 100%; padding: 0.4rem; border: 1px solid var(--nord4); border-radius: 3px; background-color: var(--nord5); color: var(--nord2); font-size: 0.85rem;"
/>
</div>
<div>
<label for="english-caption-{i}" style="display: block; margin-bottom: 0.25rem; font-weight: bold; font-size: 0.85rem; color: var(--nord0);">🇬🇧 English Caption:</label>
<div class="image-field">
<label for="english-caption-{i}">English Caption</label>
<input
id="english-caption-{i}"
type="text"
bind:value={editableEnglish.images[i].caption}
placeholder="English caption (optional)"
style="width: 100%; padding: 0.4rem; border: 1px solid var(--nord8); border-radius: 3px; font-size: 0.85rem;"
/>
</div>
</div>
@@ -863,7 +947,7 @@ button:disabled {
</div>
{/if}
{/each}
</div>
</section>
{/if}
<!-- Ingredients and Instructions in two-column layout -->
@@ -871,7 +955,12 @@ button:disabled {
<div class="list-wrapper">
<div>
{#if editableEnglish?.ingredients}
<CreateIngredientList bind:ingredients={editableEnglish.ingredients} lang="en" />
<CreateIngredientList
bind:ingredients={editableEnglish.ingredients}
bind:portions={editableEnglish.portions}
useStore={false}
lang="en"
/>
{/if}
</div>
<div>
@@ -926,7 +1015,7 @@ button:disabled {
Approve Translation
</button>
{:else}
<span style="color: var(--nord14); font-weight: 700;">Translation Approved</span>
<span class="approved-pill" aria-live="polite">Translation Approved</span>
{/if}
</div>
{/if}
@@ -31,66 +31,86 @@
}
.field-label {
display: flex;
align-items: center;
gap: 0.5rem;
font-weight: 600;
color: var(--nord4);
margin-bottom: 0.5rem;
font-size: 0.9rem;
color: var(--color-text-secondary);
margin-bottom: 0.4rem;
font-size: var(--text-sm);
text-transform: uppercase;
letter-spacing: 0.5px;
letter-spacing: 0.06em;
}
.field-label::before{
content: '';
width: 0.35rem;
height: 0.35rem;
border-radius: 50%;
background: var(--color-primary);
}
@media(prefers-color-scheme: light) {
:global(:root:not([data-theme="dark"])) .field-label {
color: var(--nord2);
.pair {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.75rem;
align-items: stretch;
}
@media (max-width: 640px) {
.pair {
grid-template-columns: 1fr;
}
}
:global(:root[data-theme="light"]) .field-label {
color: var(--nord2);
}
.lang-column {
display: flex;
flex-direction: column;
gap: 0.35rem;
min-width: 0;
}
.lang-chip {
font-size: 0.75rem;
font-weight: 700;
letter-spacing: 0.04em;
color: var(--color-text-tertiary);
display: flex;
align-items: center;
gap: 0.35rem;
}
.field-value {
padding: 0.75rem;
background: var(--nord0);
border-radius: 4px;
color: var(--nord6);
border: 1px solid var(--nord3);
min-height: 3rem;
}
@media(prefers-color-scheme: light) {
:global(:root:not([data-theme="dark"])) .field-value {
background: var(--nord5);
color: var(--nord0);
border-color: var(--nord3);
}
}
:global(:root[data-theme="light"]) .field-value {
background: var(--nord5);
color: var(--nord0);
border-color: var(--nord3);
padding: 0.6rem 0.75rem;
background: var(--color-bg-tertiary);
border-radius: var(--radius-md);
color: var(--color-text-primary);
border: 1px solid var(--color-border);
min-height: 2.6rem;
font-size: 0.95rem;
box-sizing: border-box;
width: 100%;
font-family: inherit;
transition: border-color 150ms ease, box-shadow 150ms ease;
}
.field-value.readonly {
opacity: 0.8;
background: var(--color-bg-secondary);
color: var(--color-text-secondary);
opacity: 0.95;
}
input.field-value,
textarea.field-value {
width: 100%;
font-family: inherit;
font-size: 1rem;
box-sizing: border-box;
resize: vertical;
}
input.field-value:focus,
textarea.field-value:focus {
outline: 2px solid var(--nord14);
border-color: var(--nord14);
outline: none;
border-color: var(--color-primary);
box-shadow: 0 0 0 2px color-mix(in srgb, var(--color-primary) 25%, transparent);
}
textarea.field-value {
min-height: 6rem;
min-height: 5rem;
}
.readonly-text {
@@ -100,63 +120,57 @@ textarea.field-value {
:global(.readonly-text strong) {
display: block;
margin-top: 1rem;
margin-bottom: 0.5rem;
color: var(--nord8);
margin-top: 0.75rem;
margin-bottom: 0.35rem;
color: var(--color-primary);
}
:global(.readonly-text strong:first-child) {
margin-top: 0;
}
:global(.readonly-text ul),
:global(.readonly-text ol) {
margin: 0.5rem 0;
padding-left: 1.5rem;
margin: 0.4rem 0;
padding-left: 1.25rem;
}
:global(.readonly-text li) {
margin: 0.25rem 0;
color: var(--nord4);
}
@media(prefers-color-scheme: light) {
:global(:root:not([data-theme="dark"]) .readonly-text strong) {
color: var(--nord10);
}
:global(:root:not([data-theme="dark"]) .readonly-text li) {
color: var(--nord2);
}
}
:global(:root[data-theme="light"]) :global(.readonly-text strong) {
color: var(--nord10);
}
:global(:root[data-theme="light"]) :global(.readonly-text li) {
color: var(--nord2);
margin: 0.2rem 0;
color: var(--color-text-secondary);
}
</style>
<div class="field-comparison">
<div class="field-label">{label}</div>
{#if readonly}
<div class="field-value readonly readonly-text">
{germanValue || '(empty)'}
<div class="pair">
<div class="lang-column">
<span class="lang-chip">Deutsch</span>
<div class="field-value readonly readonly-text">
{germanValue || '—'}
</div>
</div>
{:else if multiline}
<textarea
class="field-value"
value={englishValue}
oninput={handleInput}
placeholder="Enter {label.toLowerCase()}..."
></textarea>
{:else}
<input
type="text"
class="field-value"
value={englishValue}
oninput={handleInput}
placeholder="Enter {label.toLowerCase()}..."
/>
{/if}
<div class="lang-column">
<span class="lang-chip">English</span>
{#if readonly}
<div class="field-value readonly readonly-text">
{englishValue || '—'}
</div>
{:else if multiline}
<textarea
class="field-value"
value={englishValue}
oninput={handleInput}
placeholder="Enter {label.toLowerCase()}…"
></textarea>
{:else}
<input
type="text"
class="field-value"
value={englishValue}
oninput={handleInput}
placeholder="Enter {label.toLowerCase()}…"
/>
{/if}
</div>
</div>
</div>
@@ -8,7 +8,7 @@
import TranslationApproval from '$lib/components/recipes/TranslationApproval.svelte';
import GenerateAltTextButton from '$lib/components/recipes/GenerateAltTextButton.svelte';
import EditRecipeNote from '$lib/components/recipes/EditRecipeNote.svelte';
import CardAdd from '$lib/components/recipes/CardAdd.svelte';
import EditTitleImgParallax from '$lib/components/recipes/EditTitleImgParallax.svelte';
import CreateIngredientList from '$lib/components/recipes/CreateIngredientList.svelte';
import CreateStepList from '$lib/components/recipes/CreateStepList.svelte';
import Toggle from '$lib/components/Toggle.svelte';
@@ -419,20 +419,116 @@
</script>
<style>
input {
/* ===== Below-hero content wrapper mirrors viewer's .wrapper_wrapper trick:
a full-width backdrop behind the editor content hides the sticky hero image. */
.below-hero {
--bg-color: var(--color-bg-primary);
position: relative;
max-width: 1000px;
margin: 0 auto;
padding: 2rem 1rem 4rem;
}
.below-hero::before {
content: '';
position: absolute;
inset: 0;
left: 50%;
transform: translateX(-50%);
width: 100vw;
background-color: var(--bg-color);
z-index: -1;
}
h3 {
text-align: center;
font-size: 1.15rem;
letter-spacing: 0.02em;
margin-block: 1.25rem 0.75rem;
color: var(--color-text-primary);
}
/* ===== Meta row under the hero: URL + base-recipe toggle ===== */
.meta-row {
display: flex;
gap: 1.5rem 2rem;
align-items: flex-end;
justify-content: center;
flex-wrap: wrap;
margin-block: 0.5rem 2rem;
padding-bottom: 1.5rem;
border-bottom: 1px solid var(--color-border);
}
.url-field {
display: flex;
flex-direction: column;
gap: 0.35rem;
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--color-text-secondary);
font-weight: 700;
}
.url-field input {
display: block;
border: unset;
margin: 1rem auto;
padding: 0.5em 1em;
border: 1px solid var(--color-border);
margin: 0;
padding: 0.55em 1.1em;
border-radius: var(--radius-pill);
background-color: var(--nord4);
font-size: 1.1rem;
background-color: var(--color-bg-tertiary);
color: var(--color-text-primary);
font-size: 1rem;
font-weight: 400;
letter-spacing: 0;
text-transform: none;
min-width: 16rem;
transition: var(--transition-fast);
}
input:hover,
input:focus-visible {
scale: 1.05 1.05;
.url-field input:hover,
.url-field input:focus-visible {
border-color: var(--color-primary);
outline: none;
}
.toggle-field {
align-self: center;
}
/* ===== Title-card extras (inside hero card) ===== */
.section-label {
font-size: 1.1rem;
font-weight: 700;
text-align: center;
margin-block: 1.25rem 0.5rem;
color: var(--color-text-primary);
}
.season-wrapper {
margin-block: 0.25rem 0.75rem;
}
.preamble {
margin: 0.5rem 0 0.25rem;
padding: 1em 1.25em;
background: var(--color-bg-primary);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
color: var(--color-text-primary);
font-size: 1rem;
min-height: 3em;
outline: none;
transition: border-color 200ms ease;
}
.preamble:focus,
.preamble:hover {
border-color: var(--color-primary);
}
.preamble:empty::before {
content: attr(data-placeholder);
color: var(--color-text-tertiary);
font-style: italic;
}
.note-slot {
margin-top: 1.5rem;
}
/* ===== Ingredients + Instructions two-col ===== */
.list_wrapper {
margin-inline: auto;
display: flex;
@@ -440,79 +536,58 @@
max-width: 1000px;
gap: 2rem;
justify-content: center;
margin-block: 2.5rem;
}
@media screen and (max-width: 700px) {
.list_wrapper {
flex-direction: column;
gap: 1rem;
}
}
h1 {
text-align: center;
margin-bottom: 2rem;
}
.title_container {
max-width: 1000px;
display: flex;
flex-direction: column;
margin-inline: auto;
}
.title {
position: relative;
width: min(800px, 80vw);
margin-block: 2rem;
margin-inline: auto;
background-color: var(--nord6);
padding: 1rem 2rem;
}
.title p {
border: 2px solid var(--nord1);
border-radius: 10000px;
padding: 0.5em 1em;
font-size: 1.1rem;
transition: var(--transition-normal);
}
.title p:hover,
.title p:focus-within {
scale: 1.02 1.02;
}
.addendum {
font-size: 1.1rem;
max-width: 90%;
margin-inline: auto;
border: 2px solid var(--nord1);
border-radius: 45px;
padding: 1em 1em;
transition: var(--transition-fast);
}
.addendum:hover,
.addendum:focus-within {
scale: 1.02 1.02;
}
/* ===== Addendum ===== */
.addendum_wrapper {
max-width: 1000px;
margin: 2.5rem auto;
}
.addendum {
font-size: 1.05rem;
max-width: min(720px, 100%);
margin-inline: auto;
padding: 1em 1.25em;
background: var(--color-bg-primary);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
color: var(--color-text-primary);
outline: none;
transition: border-color 200ms ease;
}
h3 {
text-align: center;
.addendum:hover,
.addendum:focus-visible {
border-color: var(--color-primary);
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .title {
background-color: var(--nord6-dark);
}
}
:global(:root[data-theme="dark"]) .title {
background-color: var(--nord6-dark);
}
/* ===== Form-size / Backform ===== */
.form-size-section {
max-width: 600px;
margin: 1rem auto;
margin: 2rem auto;
text-align: center;
}
.form-size-section h3 {
margin-top: 0;
}
.form-size-controls {
display: flex;
gap: 1.5rem;
gap: 1rem 1.25rem;
justify-content: center;
margin-bottom: 0.5rem;
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;
@@ -521,29 +596,39 @@
align-items: center;
flex-wrap: wrap;
}
.form-size-inputs input[type="number"] {
.form-size-inputs input[type='number'] {
width: 4em;
display: inline;
padding: 0.3em 0.5em;
margin: 0 0.3em;
border: 1px solid var(--color-border);
background: var(--color-bg-tertiary);
color: var(--color-text-primary);
border-radius: var(--radius-sm);
font-size: 1rem;
}
.error-message {
background: var(--nord11);
color: var(--nord6);
background: var(--red);
color: white;
padding: 1rem;
border-radius: 4px;
border-radius: var(--radius-md);
margin: 1rem auto;
max-width: 800px;
text-align: center;
}
/* ===== Nutrition ===== */
.nutrition-section {
max-width: 1000px;
margin: 1.5rem auto;
margin: 2.5rem auto;
}
.nutrition-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 0.75rem;
flex-wrap: wrap;
gap: 0.75rem;
}
.nutrition-header h3 {
margin: 0;
@@ -553,7 +638,7 @@
color: var(--color-text-on-primary);
border: none;
border-radius: var(--radius-pill);
padding: 0.4rem 1rem;
padding: 0.45rem 1.1rem;
font-size: 0.85rem;
cursor: pointer;
transition: opacity var(--transition-fast);
@@ -566,57 +651,74 @@
cursor: not-allowed;
}
.nutrition-table-wrapper {
background: var(--color-bg-secondary);
border-radius: 12px;
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
padding: 1rem;
overflow-x: auto;
}
.nutrition-result-summary {
margin: 0 0 0.75rem;
font-weight: 600;
font-weight: 700;
color: var(--color-text-secondary);
font-size: 0.9rem;
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.06em;
}
.nutrition-result-table {
width: 100%;
border-collapse: collapse;
font-size: 0.85rem;
font-size: 0.9rem;
}
.nutrition-result-table th,
.nutrition-result-table td {
text-align: left;
padding: 0.4rem 0.6rem;
border-bottom: 1px solid var(--color-bg-elevated);
padding: 0.55rem 0.6rem;
border-bottom: 1px solid var(--color-border);
vertical-align: top;
}
.nutrition-result-table th {
color: var(--color-text-secondary);
font-weight: 600;
font-size: 0.8rem;
font-weight: 700;
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.03em;
}
.unmapped-row {
opacity: 0.6;
letter-spacing: 0.06em;
}
.unmapped-row { opacity: 0.55; }
.manual-row { border-left: 3px solid var(--orange); }
.recipe-ref-row { border-left: 3px solid var(--blue); }
.excluded-row { opacity: 0.45; }
.excluded-row .name-td,
.excluded-row .src-td { text-decoration: line-through; }
.search-cell {
position: relative;
}
.search-input {
display: inline !important;
display: block !important;
width: 100%;
padding: 0.3rem 0.5rem !important;
padding: 0.4rem 0.6rem !important;
margin: 0 !important;
border: 1px solid var(--color-bg-elevated) !important;
border-radius: 6px !important;
border: 1px solid var(--color-border) !important;
border-radius: var(--radius-sm) !important;
background: var(--color-bg-primary) !important;
color: var(--color-text-primary) !important;
font-size: 0.85rem !important;
font-size: 0.9rem !important;
scale: 1 !important;
box-sizing: border-box;
}
.search-input:hover,
.search-input:focus-visible {
scale: 1 !important;
border-color: var(--color-primary) !important;
outline: none;
}
.search-input.has-match {
opacity: 0.55;
font-size: 0.8rem !important;
}
.search-input.has-match:focus {
opacity: 1;
font-size: 0.9rem !important;
}
.search-dropdown {
position: absolute;
@@ -628,26 +730,26 @@
margin: 2px 0 0;
padding: 0;
background: var(--color-bg-primary);
border: 1px solid var(--color-bg-elevated);
border-radius: 8px;
max-height: 240px;
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
max-height: 260px;
overflow-y: auto;
min-width: 300px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
min-width: 260px;
box-shadow: var(--shadow-md);
}
.search-dropdown li button {
display: block;
width: 100%;
text-align: left;
padding: 0.4rem 0.6rem;
padding: 0.55rem 0.75rem;
border: none;
background: none;
color: var(--color-text-primary);
font-size: 0.8rem;
font-size: 0.85rem;
cursor: pointer;
}
.search-dropdown li button:hover {
background: var(--color-bg-tertiary);
background: var(--color-bg-elevated);
}
.search-cal {
color: var(--color-text-secondary);
@@ -658,54 +760,28 @@
display: inline-block;
font-size: 0.6rem;
font-weight: 700;
padding: 0.1rem 0.35rem;
border-radius: 4px;
padding: 0.15rem 0.45rem;
border-radius: var(--radius-sm);
margin-right: 0.3rem;
background: var(--nord10);
color: white;
background: var(--color-primary);
color: var(--color-text-on-primary);
vertical-align: middle;
letter-spacing: 0.05em;
}
.source-badge.bls {
background: var(--nord14);
color: var(--nord0);
}
.source-badge.skip {
background: var(--nord11);
color: white;
}
.source-badge.bls { background: var(--green); color: white; }
.source-badge.skip { background: var(--red); color: white; }
.source-badge.recipe-ref { background: var(--blue); color: white; }
.manual-indicator {
display: inline-block;
font-size: 0.55rem;
font-weight: 700;
color: var(--nord13);
color: var(--orange);
margin-left: 0.2rem;
vertical-align: super;
}
.excluded-row {
opacity: 0.4;
}
.excluded-row td {
text-decoration: line-through;
}
.excluded-row td:last-child,
.excluded-row td:nth-last-child(2),
.excluded-row td:nth-last-child(3),
.excluded-row td:nth-last-child(4) {
text-decoration: none;
}
.manual-row {
border-left: 2px solid var(--nord13);
}
.recipe-ref-row {
border-left: 2px solid var(--nord8);
}
.source-badge.recipe-ref {
background: var(--nord8);
color: var(--nord0);
}
.recipe-ref-label {
font-size: 0.85rem;
color: var(--nord8);
font-size: 0.88rem;
color: var(--blue);
font-weight: 600;
}
.ref-multiplier {
@@ -716,13 +792,11 @@
color: var(--color-text-secondary);
margin-left: 0.5rem;
}
.ref-multiplier .gpu-input {
width: 3.5rem;
}
.ref-multiplier .gpu-input { width: 3.5rem; }
.excluded-label {
font-style: italic;
color: var(--nord11);
font-size: 0.8rem;
color: var(--red);
font-size: 0.85rem;
}
.en-name {
color: var(--color-text-secondary);
@@ -730,26 +804,19 @@
}
.current-match {
display: block;
font-size: 0.8rem;
margin-bottom: 0.2rem;
font-size: 0.85rem;
margin-bottom: 0.3rem;
color: var(--color-text-primary);
}
.current-match.manual-match {
color: var(--nord13);
}
.search-input.has-match {
opacity: 0.5;
font-size: 0.75rem !important;
}
.search-input.has-match:focus {
opacity: 1;
font-size: 0.85rem !important;
color: var(--orange);
}
.row-controls {
display: flex;
align-items: center;
gap: 0.5rem;
margin-top: 0.3rem;
gap: 0.75rem;
margin-top: 0.45rem;
flex-wrap: wrap;
}
.row-controls :global(.toggle-wrapper) {
font-size: 0.75rem;
@@ -758,30 +825,30 @@
gap: 0.4rem;
}
.row-controls :global(.toggle-track),
.row-controls :global(input[type="checkbox"]) {
.row-controls :global(input[type='checkbox']) {
width: 32px !important;
height: 18px !important;
}
.row-controls :global(.toggle-track::before),
.row-controls :global(input[type="checkbox"]::before) {
.row-controls :global(input[type='checkbox']::before) {
width: 14px !important;
height: 14px !important;
}
.row-controls :global(.toggle-track.checked::before),
.row-controls :global(input[type="checkbox"]:checked::before) {
.row-controls :global(input[type='checkbox']:checked::before) {
transform: translateX(14px) !important;
}
.revert-btn {
background: none;
border: 1px solid var(--color-bg-elevated);
border-radius: 4px;
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
color: var(--color-text-secondary);
cursor: pointer;
padding: 0.15rem 0.4rem;
font-size: 0.65rem;
padding: 0.2rem 0.55rem;
font-size: 0.7rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.03em;
letter-spacing: 0.05em;
transition: all var(--transition-fast);
}
.revert-btn:hover {
@@ -791,51 +858,148 @@
}
.skip-btn {
background: none;
border: 1px solid var(--color-bg-elevated);
border-radius: 4px;
color: var(--nord11);
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
color: var(--red);
cursor: pointer;
padding: 0.15rem 0.4rem;
font-size: 0.8rem;
padding: 0.3rem 0.55rem;
font-size: 0.9rem;
line-height: 1;
transition: all var(--transition-fast);
}
.skip-btn:hover {
background: var(--nord11);
color: white;
}
.skip-btn:hover,
.skip-btn.active {
background: var(--nord11);
background: var(--red);
color: white;
border-color: var(--red);
}
.skip-btn.active:hover {
background: var(--nord14);
color: var(--nord0);
background: var(--green);
color: white;
border-color: var(--green);
}
.gpu-input {
display: inline !important;
width: 4em !important;
padding: 0.2rem 0.3rem !important;
padding: 0.25rem 0.4rem !important;
margin: 0 !important;
border: 1px solid var(--color-bg-elevated) !important;
border-radius: 4px !important;
border: 1px solid var(--color-border) !important;
border-radius: var(--radius-sm) !important;
background: var(--color-bg-primary) !important;
color: var(--color-text-primary) !important;
font-size: 0.8rem !important;
font-size: 0.85rem !important;
scale: 1 !important;
text-align: right;
}
.gpu-input:hover, .gpu-input:focus-visible {
.gpu-input:hover,
.gpu-input:focus-visible {
scale: 1 !important;
}
/* ===== Mobile nutrition: table → card stack ===== */
@media (max-width: 700px) {
.nutrition-table-wrapper {
padding: 0;
background: transparent;
border: none;
}
.nutrition-result-table,
.nutrition-result-table tbody,
.nutrition-result-table tr,
.nutrition-result-table td {
display: block;
width: 100%;
box-sizing: border-box;
}
.nutrition-result-table thead {
display: none;
}
.nutrition-result-table tr {
position: relative;
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
padding: 0.85rem 1rem 0.75rem;
margin-bottom: 0.85rem;
box-shadow: var(--shadow-sm);
}
.nutrition-result-table td {
padding: 0.45rem 0;
border-bottom: 1px dashed var(--color-border);
display: flex;
justify-content: space-between;
gap: 0.75rem;
align-items: baseline;
}
.nutrition-result-table td:last-child {
border-bottom: none;
}
.nutrition-result-table td::before {
content: attr(data-label);
color: var(--color-text-secondary);
font-size: 0.66rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
flex-shrink: 0;
min-width: 5rem;
align-self: center;
}
.nutrition-result-table td.num-td {
position: absolute;
top: 0.6rem;
right: 0.9rem;
padding: 0;
border: none;
width: auto;
opacity: 0.5;
font-size: 0.7rem;
display: inline-block;
}
.nutrition-result-table td.num-td::before {
display: none;
}
.nutrition-result-table td.name-td {
font-size: 1.05rem;
font-weight: 600;
padding-right: 2rem;
flex-direction: column;
align-items: flex-start;
gap: 0.15rem;
}
.nutrition-result-table td.name-td::before {
font-size: 0.62rem;
opacity: 0.7;
}
.nutrition-result-table td.search-td {
flex-direction: column;
align-items: stretch;
gap: 0.4rem;
}
.nutrition-result-table td.action-td {
justify-content: flex-end;
}
.nutrition-result-table td.action-td::before {
display: none;
}
.search-input {
font-size: 1rem !important;
padding: 0.6rem 0.75rem !important;
}
.search-dropdown {
min-width: 100%;
}
}
.section-actions {
display: flex;
gap: 0.75rem;
justify-content: center;
}
.section-btn {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 4px;
border-radius: var(--radius-pill);
font-size: 1rem;
font-weight: 600;
cursor: pointer;
@@ -852,7 +1016,8 @@
}
.translation-section-trigger {
max-width: 1000px;
margin: 1.5rem auto;
margin: 2.5rem auto;
text-align: center;
}
</style>
@@ -861,8 +1026,6 @@
<meta name="description" content="Bearbeite das Rezept {data.recipe.name}" />
</svelte:head>
<h1>Rezept bearbeiten</h1>
{#if form?.error}
<div class="error-message">
<strong>Fehler:</strong> {form.error}
@@ -908,16 +1071,6 @@
})} />
{/if}
<CardAdd
bind:card_data
bind:image_preview_url
bind:selected_image_file
short_name={short_name}
/>
<h3>Kurzname (für URL):</h3>
<input name="short_name" bind:value={short_name} placeholder="Kurzname" required />
<!-- Hidden inputs for card data -->
<input type="hidden" name="name" value={card_data.name} />
<input type="hidden" name="description" value={card_data.description} />
@@ -926,72 +1079,89 @@
<input type="hidden" name="portions" value={portions_local} />
<input type="hidden" name="isBaseRecipe" value={isBaseRecipe ? "true" : "false"} />
<input type="hidden" name="defaultForm_json" value={defaultForm ? JSON.stringify(defaultForm) : ''} />
<div style="text-align: center; margin: 1rem;">
<Toggle
bind:checked={isBaseRecipe}
label="Als Basisrezept markieren (kann von anderen Rezepten referenziert werden)"
/>
</div>
<!-- Default Form (Cake Pan) -->
<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>
{#if defaultForm?.shape === 'round'}
<div class="form-size-inputs">
<label>Durchmesser: <input type="number" min="1" step="1" bind:value={defaultForm.diameter} /> cm</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>
</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>
</div>
{/if}
</div>
<!-- Recipe Note Component -->
<EditRecipeNote bind:note />
<input type="hidden" name="preamble" value={preamble} />
<input type="hidden" name="note" value={note} />
<div class="title_container">
<div class="title">
<h4>Eine etwas längere Beschreibung:</h4>
<p bind:innerText={preamble} contenteditable></p>
<input type="hidden" name="preamble" value={preamble} />
<div class="tags">
<h4>Saison:</h4>
<EditTitleImgParallax
bind:card_data
bind:image_preview_url
bind:selected_image_file
>
{#snippet titleExtras()}
<h2 class="section-label">Saison</h2>
<div class="season-wrapper">
<SeasonSelect />
</div>
</div>
</div>
<h2 class="section-label">Einleitung</h2>
<p
class="preamble"
contenteditable="plaintext-only"
bind:innerText={preamble}
data-placeholder="Eine etwas längere Einleitung für dieses Rezept…"
aria-label="Einleitung"
></p>
<div class="note-slot">
<EditRecipeNote bind:note />
</div>
{/snippet}
<div class="below-hero">
<div class="meta-row">
<label class="url-field">
<span>URL-Kurzname</span>
<input
name="short_name"
bind:value={short_name}
placeholder="Kurzname"
required
/>
</label>
<div class="toggle-field">
<Toggle
bind:checked={isBaseRecipe}
label="Als Basisrezept markieren"
/>
</div>
</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>
{#if defaultForm?.shape === 'round'}
<div class="form-size-inputs">
<label>Durchmesser: <input type="number" min="1" step="1" bind:value={defaultForm.diameter} /> cm</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>
</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>
</div>
{/if}
</div>
<div class="list_wrapper">
<div>
@@ -1030,14 +1200,14 @@
{#each nutritionMappings as m, i (mappingKey(m))}
{@const key = mappingKey(m)}
<tr class:unmapped-row={m.matchMethod === 'none' && !m.excluded && !m.recipeRef} class:excluded-row={m.excluded && !m.recipeRef} class:manual-row={m.manuallyEdited && !m.excluded} class:recipe-ref-row={!!m.recipeRef}>
<td>{i + 1}</td>
<td>
<td data-label="#" class="num-td">{i + 1}</td>
<td data-label="Zutat" class="name-td">
{m.ingredientNameDe || m.ingredientName}
{#if m.ingredientName && m.ingredientName !== m.ingredientNameDe}
<span class="en-name">({m.ingredientName})</span>
{/if}
</td>
<td>
<td data-label="Quelle" class="src-td">
{#if m.recipeRef}
<span class="source-badge recipe-ref">REF</span>
{:else if m.excluded}
@@ -1049,7 +1219,7 @@
{/if}
</td>
<td>
<td data-label="Treffer / Suche" class="search-td">
<div class="search-cell">
{#if m.recipeRef}
<span class="recipe-ref-label">{m.recipeRef}</span>
@@ -1096,8 +1266,8 @@
{/if}
</div>
</td>
<td>{m.recipeRef ? '—' : (m.matchConfidence ? (m.matchConfidence * 100).toFixed(0) + '%' : '—')}</td>
<td>
<td data-label="Konf." class="conf-td">{m.recipeRef ? '—' : (m.matchConfidence ? (m.matchConfidence * 100).toFixed(0) + '%' : '—')}</td>
<td data-label="g/u" class="unit-td">
{#if m.recipeRef}
{:else if m.manuallyEdited}
@@ -1106,7 +1276,7 @@
{m.gramsPerUnit || '—'}
{/if}
</td>
<td>
<td data-label="" class="action-td">
{#if !m.recipeRef}
<button
type="button"
@@ -1146,6 +1316,8 @@
</div>
</div>
{/if}
</div>
</EditTitleImgParallax>
</form>
{#if translationData || showTranslationWorkflow}