recipes: overhaul nutrition editor UI and defer saves to form submission

- Nutrition mappings and global overwrites are now local-only until
  the recipe is saved, preventing premature DB writes on generate/edit
- Generate endpoint supports ?preview=true for non-persisting previews
- Show existing nutrition data immediately instead of requiring generate
- Replace raw checkboxes with Toggle component for global overwrites,
  initialized from existing NutritionOverwrite records
- Fix search dropdown readability (solid backgrounds, proper theming)
- Use fuzzy search (fzf-style) for manual nutrition ingredient lookup
- Swap ingredient display: German primary, English in brackets
- Allow editing g/u on manually mapped ingredients
- Make translation optional: separate save (FAB) and translate buttons
- "Vollständig neu übersetzen" now triggers actual full retranslation
- Show existing translation inline instead of behind a button
- Replace nord0 dark backgrounds with semantic theme variables
This commit is contained in:
2026-04-02 19:46:01 +02:00
parent d2a0411937
commit 8f3a3035f0
6 changed files with 458 additions and 317 deletions
+2 -2
View File
@@ -1,5 +1,5 @@
<script lang="ts">
let { checked = $bindable(false), label = "", accentColor = "var(--color-primary)", href = undefined as string | undefined } = $props<{ checked?: boolean, label?: string, accentColor?: string, href?: string }>();
let { checked = $bindable(false), label = "", accentColor = "var(--color-primary)", href = undefined as string | undefined, onchange = undefined as (() => void) | undefined } = $props<{ checked?: boolean, label?: string, accentColor?: string, href?: string, onchange?: () => void }>();
</script>
<style>
@@ -96,7 +96,7 @@
</a>
{:else}
<label>
<input type="checkbox" bind:checked />
<input type="checkbox" bind:checked onchange={onchange} />
<span>{label}</span>
</label>
{/if}
@@ -14,7 +14,6 @@
onapproved?: (event: CustomEvent) => void;
onskipped?: () => void;
oncancelled?: () => void;
onforceFullRetranslation?: () => void;
}
let {
@@ -26,7 +25,6 @@
onapproved,
onskipped,
oncancelled,
onforceFullRetranslation
}: Props = $props();
type TranslationState = 'idle' | 'translating' | 'preview' | 'approved' | 'error';
@@ -204,7 +202,7 @@
});
// Handle auto-translate button click
async function handleAutoTranslate() {
async function handleAutoTranslate(fullRetranslation = false) {
translationState = 'translating';
errorMessage = '';
validationErrors = [];
@@ -217,7 +215,7 @@
},
body: JSON.stringify({
recipe: germanData,
fields: isEditMode && changedFields.length > 0 ? changedFields : undefined,
fields: isEditMode && !fullRetranslation && changedFields.length > 0 ? changedFields : undefined,
oldRecipe: oldRecipeData, // For granular item-level change detection
existingTranslation: englishData, // To merge with unchanged items
}),
@@ -358,11 +356,6 @@
oncancelled?.();
}
// Handle force full retranslation
function handleForceFullRetranslation() {
onforceFullRetranslation?.();
}
// Get status badge color
function getStatusColor(status: string): string {
switch (status) {
@@ -921,10 +914,10 @@ button:disabled {
<button class="btn-danger" onclick={handleCancel}>
Cancel
</button>
<button class="btn-secondary" onclick={handleForceFullRetranslation}>
<button class="btn-secondary" onclick={() => handleAutoTranslate(true)}>
Vollständig neu übersetzen
</button>
<button class="btn-secondary" onclick={handleAutoTranslate}>
<button class="btn-secondary" onclick={() => handleAutoTranslate()}>
Re-translate
</button>
<button class="btn-primary" onclick={handleApprove}>