diff --git a/src/lib/components/ui/CommandPalette.svelte b/src/lib/components/ui/CommandPalette.svelte
index c43d0bf..3b3b109 100644
--- a/src/lib/components/ui/CommandPalette.svelte
+++ b/src/lib/components/ui/CommandPalette.svelte
@@ -1,111 +1,159 @@
{#if open}
-
-
+
+
-
-
-
-
-
+
+
+
+
+
-
-
- {#each filtered as cmd, i (cmd.id)}
- -
-
-
- {/each}
+
+
+ {#each filtered as cmd, i (cmd.id)}
+ -
+
+
+ {/each}
- {#if filtered.length === 0}
- - No commands found
- {/if}
-
+ {#if filtered.length === 0}
+ -
+ No commands found
+
+ {/if}
+
-
-
- ↑↓ navigate
- ↵ select
-
-
-
+
+
+ ↑↓ navigate
+ ↵ select
+
+
+
{/if}
diff --git a/src/lib/components/ui/Sheet.svelte b/src/lib/components/ui/Sheet.svelte
index d6a240f..5053ad8 100644
--- a/src/lib/components/ui/Sheet.svelte
+++ b/src/lib/components/ui/Sheet.svelte
@@ -10,6 +10,25 @@
let { open, onclose, title, children }: Props = $props();
+ let bottomOffset = $state(0);
+
+ onMount(() => {
+ const vv = window.visualViewport;
+ if (!vv) return;
+
+ function update() {
+ bottomOffset = Math.max(0, window.innerHeight - vv!.offsetTop - vv!.height);
+ }
+
+ vv.addEventListener('resize', update);
+ vv.addEventListener('scroll', update);
+
+ return () => {
+ vv.removeEventListener('resize', update);
+ vv.removeEventListener('scroll', update);
+ };
+ });
+
function handleBackdrop(e: MouseEvent) {
if (e.target === e.currentTarget) onclose();
}
@@ -32,8 +51,9 @@
diff --git a/src/routes/(app)/diary/[date]/add-entry/+page.svelte b/src/routes/(app)/diary/[date]/add-entry/+page.svelte
index e8530ee..367edf8 100644
--- a/src/routes/(app)/diary/[date]/add-entry/+page.svelte
+++ b/src/routes/(app)/diary/[date]/add-entry/+page.svelte
@@ -13,7 +13,9 @@
import { kcal, g } from "$lib/utils/format";
import { today } from "$lib/utils/date";
- const date = $derived(page.params.date === 'today' ? today() : page.params.date!);
+ const date = $derived(
+ page.params.date === "today" ? today() : page.params.date!,
+ );
const mealId = $derived(Number(page.url.searchParams.get("meal_id")));
const queryClient = useQueryClient();
@@ -31,16 +33,8 @@
let scanError = $state
(null);
let searchInput = $state(null);
- $effect(() => {
- if (searchInput) setTimeout(() => searchInput?.focus(), 50);
- });
let gramsInput = $state(null);
- $effect(() => {
- if (selectedProduct && gramsInput) {
- setTimeout(() => gramsInput?.focus(), 50);
- }
- });
function handleSearch(value: string) {
q = value;
@@ -147,6 +141,7 @@
e.preventDefault()}
onclick={() => {
grams = Math.max(1, grams - 10);
- gramsInput?.focus();
}}
class="w-11 h-11 rounded-xl bg-zinc-800 hover:bg-zinc-700 transition-colors text-lg font-medium flex items-center justify-center"
>− e.currentTarget.select()}
onkeydown={(e) => {
if (e.key === "Enter") handleAddEntry();
@@ -322,7 +317,6 @@
onpointerdown={(e) => e.preventDefault()}
onclick={() => {
grams = grams + 10;
- gramsInput?.focus();
}}
class="w-11 h-11 rounded-xl bg-zinc-800 hover:bg-zinc-700 transition-colors text-lg font-medium flex items-center justify-center"
>+
- import { page } from '$app/state';
- import { goto } from '$app/navigation';
- import { useQueryClient } from '@tanstack/svelte-query';
- import { deleteEntry } from '$lib/api/entries';
- import { offlineEditEntry } from '$lib/offline/mutations';
- import { network } from '$lib/offline/network.svelte';
- import TopBar from '$lib/components/ui/TopBar.svelte';
- import { today } from '$lib/utils/date';
+ import { page } from "$app/state";
+ import { goto } from "$app/navigation";
+ import { useQueryClient } from "@tanstack/svelte-query";
+ import { deleteEntry } from "$lib/api/entries";
+ import { offlineEditEntry } from "$lib/offline/mutations";
+ import { network } from "$lib/offline/network.svelte";
+ import TopBar from "$lib/components/ui/TopBar.svelte";
+ import { today } from "$lib/utils/date";
- const date = $derived(page.params.date === 'today' ? today() : page.params.date!);
- const entryId = $derived(Number(page.params.entry_id));
- const queryClient = useQueryClient();
+ const date = $derived(
+ page.params.date === "today" ? today() : page.params.date!,
+ );
+ const entryId = $derived(Number(page.params.entry_id));
+ const queryClient = useQueryClient();
- // Read entry from cache — diary was already fetched on the diary page
- const cachedDiary = $derived(
- queryClient.getQueryData(['diary', date])
- );
- const entry = $derived(
- cachedDiary?.meals.flatMap(m => m.entries).find(e => e.id === entryId)
- );
+ // Read entry from cache — diary was already fetched on the diary page
+ const cachedDiary = $derived(
+ queryClient.getQueryData(["diary", date]),
+ );
+ const entry = $derived(
+ cachedDiary?.meals.flatMap((m) => m.entries).find((e) => e.id === entryId),
+ );
- let grams = $state(entry?.grams ?? 100);
- $effect(() => { if (entry) grams = entry.grams; });
+ let grams = $state(entry?.grams ?? 100);
+ $effect(() => {
+ if (entry) grams = entry.grams;
+ });
- let saving = $state(false);
- let deleting = $state(false);
+ let saving = $state(false);
+ let deleting = $state(false);
- let gramsInput = $state(null);
- $effect(() => {
- if (entry && gramsInput) gramsInput.focus();
- });
+ let gramsInput = $state(null);
- const preview = $derived(entry ? {
- calories: Math.round(entry.product.calories * grams / 100),
- protein: Math.round(entry.product.protein * grams / 100 * 10) / 10,
- carb: Math.round(entry.product.carb * grams / 100 * 10) / 10,
- fat: Math.round(entry.product.fat * grams / 100 * 10) / 10,
- } : null);
+ const preview = $derived(
+ entry
+ ? {
+ calories: Math.round((entry.product.calories * grams) / 100),
+ protein:
+ Math.round(((entry.product.protein * grams) / 100) * 10) / 10,
+ carb: Math.round(((entry.product.carb * grams) / 100) * 10) / 10,
+ fat: Math.round(((entry.product.fat * grams) / 100) * 10) / 10,
+ }
+ : null,
+ );
- async function handleSave() {
- if (!entry) return;
- saving = true;
- try {
- await offlineEditEntry(queryClient, date, entryId, grams);
- goto(`/diary/${date}`);
- } catch {
- goto(`/diary/${date}`);
- } finally {
- saving = false;
- }
- }
+ async function handleSave() {
+ if (!entry) return;
+ saving = true;
+ try {
+ await offlineEditEntry(queryClient, date, entryId, grams);
+ goto(`/diary/${date}`);
+ } catch {
+ goto(`/diary/${date}`);
+ } finally {
+ saving = false;
+ }
+ }
- async function handleDelete() {
- if (!confirm('Remove this entry?')) return;
- deleting = true;
- try {
- await deleteEntry(date, entry!.meal_id, entryId);
- await queryClient.invalidateQueries({ queryKey: ['diary', date] });
- goto(`/diary/${date}`);
- } catch {
- goto(`/diary/${date}`);
- } finally {
- deleting = false;
- }
- }
+ async function handleDelete() {
+ if (!confirm("Remove this entry?")) return;
+ deleting = true;
+ try {
+ await deleteEntry(date, entry!.meal_id, entryId);
+ await queryClient.invalidateQueries({ queryKey: ["diary", date] });
+ goto(`/diary/${date}`);
+ } catch {
+ goto(`/diary/${date}`);
+ } finally {
+ deleting = false;
+ }
+ }
-
- {#snippet action()}
-
- {/snippet}
-
+
+ {#snippet action()}
+
+ {/snippet}
+
- {#if !entry}
-
- Entry not found
-
- {:else}
-
-
-
-
{entry.product.name}
-
- {entry.product.calories} kcal · P {entry.product.protein}g · C {entry.product.carb}g · F {entry.product.fat}g
- per 100g
-
-
+ {#if !entry}
+
+ Entry not found
+
+ {:else}
+
+
+
+
{entry.product.name}
+
+ {entry.product.calories} kcal · P {entry.product.protein}g · C {entry
+ .product.carb}g · F {entry.product.fat}g
+ per 100g
+
+
-
- {#if preview}
-
- {#each [
- { label: 'kcal', value: preview.calories },
- { label: 'protein', value: preview.protein + 'g' },
- { label: 'carbs', value: preview.carb + 'g' },
- { label: 'fat', value: preview.fat + 'g' },
- ] as m}
-
-
{m.value}
-
{m.label}
-
- {/each}
-
- {/if}
+
+ {#if preview}
+
+ {#each [{ label: "kcal", value: preview.calories }, { label: "protein", value: preview.protein + "g" }, { label: "carbs", value: preview.carb + "g" }, { label: "fat", value: preview.fat + "g" }] as m}
+
+
{m.value}
+
{m.label}
+
+ {/each}
+
+ {/if}
-
-
-
-
-
- e.currentTarget.select()}
- onkeydown={(e) => { if (e.key === 'Enter') handleSave(); }}
- class="flex-1 bg-zinc-900 border border-zinc-700 rounded-xl px-4 py-3 text-center text-2xl font-semibold focus:outline-none focus:border-green-500 transition-colors"
- />
-
-
-
-
+
+
+
+
+
+ e.currentTarget.select()}
+ onkeydown={(e) => {
+ if (e.key === "Enter") handleSave();
+ }}
+ class="flex-1 bg-zinc-900 border border-zinc-700 rounded-xl px-4 py-3 text-center text-2xl font-semibold focus:outline-none focus:border-green-500 transition-colors"
+ />
+
+
+
+
-
-
-
- {/if}
+
+
+
+ {/if}
diff --git a/src/routes/(app)/presets/+page.svelte b/src/routes/(app)/presets/+page.svelte
index e369bb5..75b47ce 100644
--- a/src/routes/(app)/presets/+page.svelte
+++ b/src/routes/(app)/presets/+page.svelte
@@ -547,7 +547,6 @@
onpointerdown={(e) => e.preventDefault()}
onclick={() => {
editGrams = Math.max(1, editGrams - 10);
- editGramsInput?.focus();
}}
class="w-11 h-11 rounded-xl bg-zinc-800 hover:bg-zinc-700 transition-colors text-lg font-medium flex items-center justify-center"
>− e.currentTarget.select()}
class="flex-1 bg-zinc-800 border border-zinc-700 rounded-xl px-4 py-2.5 text-center text-xl font-semibold focus:outline-none focus:border-green-500 transition-colors"
@@ -566,7 +566,6 @@
onpointerdown={(e) => e.preventDefault()}
onclick={() => {
editGrams = editGrams + 10;
- editGramsInput?.focus();
}}
class="w-11 h-11 rounded-xl bg-zinc-800 hover:bg-zinc-700 transition-colors text-lg font-medium flex items-center justify-center"
>+ e.preventDefault()}
onclick={() => {
addGrams = Math.max(1, addGrams - 10);
- addGramsInput?.focus();
}}
class="w-11 h-11 rounded-xl bg-zinc-800 hover:bg-zinc-700 transition-colors text-lg font-medium flex items-center justify-center"
>− e.preventDefault()}
onclick={() => {
addGrams = addGrams + 10;
- addGramsInput?.focus();
}}
class="w-11 h-11 rounded-xl bg-zinc-800 hover:bg-zinc-700 transition-colors text-lg font-medium flex items-center justify-center"
>+