fix(budgets,tags,piggy-banks,dashboard): live buttons that answer in place, lucide icons, mobile wrapping

This commit is contained in:
hh committed 2026-10-16 04:26:07 +00:00
1 parent aba2509399
commit f3a603648e
10 files changed
+102 -71

No files matched your search

+18 -5
View File
@@ -56,6 +56,7 @@
let busy = $state(false);
let status = $state("");
let failure = $state("");
let pressed = $state(false);
const account = createQuery(() => ({
queryFn: () => api<One<Account>>(`/v1/accounts/${id}`),
@@ -179,6 +180,17 @@
const canCorrect = $derived(Boolean(reconciliationId));
const correcting = $derived(correct && canCorrect && difference !== 0);
const ready = $derived(before.isSuccess && after.isSuccess && list.isSuccess);
const hint = $derived.by(() => {
if (!pressed) {
return "";
}
if (!ready) {
return "Balances are still loading";
}
return selected.length === 0 && !correcting
? "Tick the transactions to clear first"
: "";
});
function setDates(nextStart: string, nextEnd: string) {
if (!(nextStart && nextEnd) || nextStart > nextEnd) {
@@ -241,7 +253,8 @@
}
async function store() {
if (busy || !ready || (selected.length === 0 && !correcting)) {
pressed = true;
if (busy || hint) {
return;
}
busy = true;
@@ -525,10 +538,7 @@
</fieldset>
{/if}
<div class="flex flex-wrap items-center gap-2">
<Button
disabled={busy || !ready || (selected.length === 0 && !correcting)}
onclick={store}
type="button"
<Button disabled={busy} onclick={store} type="button"
>Store reconciliation</Button
>
<span class="flex gap-1">
@@ -536,6 +546,9 @@
<span class="kbd">{key}</span>
{/each}
</span>
{#if hint}
<span class="text-[12px] text-expense" role="alert">{hint}</span>
{/if}
</div>
{#if status}
<p class="text-[12px] text-cladd-fg-soft" role="status">{status}</p>
+10 -8
View File
@@ -114,7 +114,7 @@
</div>
{:else}
<div
class="truncate font-semibold text-[18px] text-cladd-fg tracking-[-0.02em] sm:text-[22px]"
class="wrap-anywhere font-semibold text-[18px] text-cladd-fg tracking-[-0.02em] sm:text-[22px]"
>
{fmt(list[0], abs)}
</div>
@@ -142,7 +142,7 @@
{:else}
<ArrowDown class="size-3.5 shrink-0" />
{/if}
<span class="truncate"
<span
><span class="num">{delta.text}</span>
<span class="text-cladd-fg-soft">than {prevName}</span></span
>
@@ -203,18 +203,20 @@
segments={[{ color: "var(--ff-series-3)", value: paidShare.paid }]}
/>
{/if}
<div class="truncate text-[12px] text-cladd-fg-soft">
Paid:
<span class="num"
>{paid.length ? paid.map((e) => fmt(e, true)).join(", ") : "0"}</span
>
<div class="flex flex-wrap gap-x-2 text-[12px] text-cladd-fg-soft">
<span>Paid:</span>
{#each paid as e (e.code)}
<span class="num">{fmt(e, true)}</span>
{:else}
<span class="num">0</span>
{/each}
</div>
{/snippet}
{#snippet leftToSpend()}
{@render values(left)}
{#if left[0]?.sub}
<div class="truncate text-[12px] text-cladd-fg-soft">
<div class="text-[12px] text-cladd-fg-soft">
{leftState === "none" ? "" : "Per day: "}<span class="num"
>{left[0].sub}</span
>
@@ -30,6 +30,7 @@
const action = new Action();
const values = $state<Record<string, string>>({});
const fields: Record<string, HTMLElement> = {};
const links = $derived(
(piggy.attributes.accounts ?? []).map((link) => {
@@ -50,9 +51,10 @@
}
}
function fail(message: string) {
function fail(message: string, id = open[0]?.id ?? "") {
action.failed = true;
action.message = message;
fields[id]?.focus();
}
async function save() {
@@ -64,11 +66,11 @@
}
const amount = evalAmount(raw);
if (amount === null || amount <= 0) {
return fail("Enter an amount above zero");
return fail("Enter an amount above zero", link.id);
}
if (amount > link.max + 1e-9) {
const max = formatMoney(piggyMoney(piggy.attributes, link.max));
return fail(`${link.name} allows at most ${max}`);
return fail(`${link.name} allows at most ${max}`, link.id);
}
deltas[link.id] = mode === "add" ? amount : -amount;
}
@@ -109,6 +111,7 @@
onkeydown={onEscape}
bind:value={values[link.id]}
{@attach (node) => {
fields[link.id] = node;
if (i === 0) {
node.focus();
}
@@ -1,4 +1,5 @@
<script lang="ts">
import Check from "@lucide/svelte/icons/check";
import { formatDate } from "#lib/format.js";
import { STATUS_TONE, type Status, statusText } from "./bills.js";
@@ -9,9 +10,9 @@
<span class="inline-flex flex-wrap gap-x-2">
{#each state.paid as paid (paid.transaction_journal_id)}
<a
class="text-income hover:underline"
class="inline-flex items-center gap-1 text-income hover:underline"
href="/transactions/show/{paid.transaction_group_id}"
>✓ {formatDate(paid.date, "short")}</a
><Check class="size-3.5 shrink-0" />{formatDate(paid.date, "short")}</a
>
{/each}
</span>
@@ -133,7 +133,12 @@
async function create(event: SubmitEvent) {
event.preventDefault();
const name = newName.trim();
if (!name || creating) {
if (creating) {
return;
}
if (!name) {
createError = "Type a name first";
createInput?.focus();
return;
}
creating = true;
@@ -333,12 +338,12 @@
{/if}
{#if !extra && row.lines.length < enabled.length}
<button
class="ml-auto text-[11px] text-cladd-fg-softer hover:text-brand max-sm:hidden"
class="ml-auto inline-flex items-center gap-0.5 text-[11px] text-cladd-fg-softer hover:text-brand max-sm:hidden"
onclick={() => startAdding(row)}
title="Budget an amount in another currency (a)"
type="button"
>
+ currency
<Plus class="size-3" />currency
</button>
{/if}
</div>
@@ -453,23 +458,30 @@
</tr>
{/each}
{/each}
<tr class="border-cladd-outline border-t bg-cladd-surface-next/40">
<td class="px-3 py-1.5 sm:whitespace-nowrap">
<a class="text-brand italic hover:underline" href={noBudgetHref}
>Expenses without a budget</a
>
{#if query.data?.noBudgetCount}
<span class="ml-1.5 text-[12px] text-cladd-fg-soft"
>{query.data.noBudgetCount}
transactions</span
>
{/if}
<tr
class="border-cladd-outline border-t bg-cladd-surface-next/40 max-sm:hidden"
>
<td class="whitespace-nowrap px-3 py-1.5">
{@render noBudgetLabel()}
</td>
<td></td>
<td class="px-3 py-1.5 text-right">
<MoneyList sums={query.data?.noBudget ?? []} />
</td>
<td class="max-sm:hidden" colspan="2"></td>
<td colspan="2"></td>
</tr>
<tr
class="border-cladd-outline border-t bg-cladd-surface-next/40 sm:hidden"
>
<td class="px-3 py-1.5" colspan="3">
<div class="flex items-start justify-between gap-3">
<div>{@render noBudgetLabel()}</div>
<MoneyList
class="flex-col items-end"
sums={query.data?.noBudget ?? []}
/>
</div>
</td>
</tr>
</tbody>
{/snippet}
@@ -481,13 +493,12 @@
<input
aria-label="New budget name"
class="cladd-cut cladd-focusable-input h-7 w-64 min-w-0 flex-1 rounded-cladd-sm px-2 text-[13px] sm:flex-none"
oninput={() => (createError = "")}
placeholder="New budget name (c)"
bind:this={createInput}
bind:value={newName}
>
<Button disabled={creating || !newName.trim()} size="sm" type="submit"
>Create</Button
>
<Button disabled={creating} size="sm" type="submit">Create</Button>
{#if createError}
<span class="text-expense">{createError}</span>
{/if}
@@ -495,6 +506,18 @@
{/snippet}
</Box>
{#snippet noBudgetLabel()}
<a class="text-brand italic hover:underline" href={noBudgetHref}
>Expenses without a budget</a
>
{#if query.data?.noBudgetCount}
<span class="ml-1.5 whitespace-nowrap text-[12px] text-cladd-fg-soft"
>{query.data.noBudgetCount}
transactions</span
>
{/if}
{/snippet}
{#if inactive.length > 0}
<Box class="mt-4" title="Inactive budgets">
<p class="text-[13px]">
+8 -4
View File
@@ -115,7 +115,12 @@
async function create(event: SubmitEvent) {
event.preventDefault();
const name = newName.trim();
if (!name || creating) {
if (creating) {
return;
}
if (!name) {
createError = "Type a name first";
createInput?.focus();
return;
}
creating = true;
@@ -326,13 +331,12 @@
<input
aria-label="New category name"
class="cladd-cut cladd-focusable-input h-7 w-64 rounded-cladd-sm px-2 text-[13px]"
oninput={() => (createError = "")}
placeholder="New category name (c)"
bind:this={createInput}
bind:value={newName}
>
<Button disabled={creating || !newName.trim()} size="sm" type="submit"
>Create</Button
>
<Button disabled={creating} size="sm" type="submit">Create</Button>
{#if createError}
<span class="text-expense">{createError}</span>
{/if}
+2 -22
View File
@@ -19,7 +19,6 @@
import { errorText } from "#lib/components/misc/action.svelte.js";
import MoneyEditor from "#lib/components/piggy-banks/MoneyEditor.svelte";
import {
limit,
type Mode,
piggyMoney,
rooms,
@@ -67,19 +66,6 @@
.map((p) => piggyMoney(p.attributes, p.attributes[field]))
);
const canAdd = (p: Item<PiggyBank>) =>
(p.attributes.accounts ?? []).some(
(a) =>
limit(
p.attributes,
a.account_id ?? "",
"add",
room.get(a.account_id ?? "")
) > 0
);
const canRemove = (p: Item<PiggyBank>) =>
toNumber(p.attributes.current_amount) > 0;
function startMoney(p: Item<PiggyBank> | undefined, mode: Mode): void {
if (p) {
nav.cursor = rows.indexOf(p);
@@ -294,7 +280,6 @@
size: "icon-xs",
variant: "ghost",
})}
disabled={!canRemove(item)}
onclick={() => startMoney(item, "remove")}
title="Remove money (-)"
type="button"
@@ -329,7 +314,6 @@
size: "icon-xs",
variant: "ghost",
})}
disabled={!canAdd(item)}
onclick={() => startMoney(item, "add")}
title="Add money (+)"
type="button"
@@ -391,14 +375,10 @@
onSelect={() => goto(`/piggy-banks/edit/${target.id}`)}
>Edit</ContextMenuItem
>
<ContextMenuItem
disabled={!canAdd(target)}
onSelect={() => startMoney(target, "add")}
<ContextMenuItem onSelect={() => startMoney(target, "add")}
>Add money</ContextMenuItem
>
<ContextMenuItem
disabled={!canRemove(target)}
onSelect={() => startMoney(target, "remove")}
<ContextMenuItem onSelect={() => startMoney(target, "remove")}
>Remove money</ContextMenuItem
>
<ContextMenuSeparator />
@@ -345,7 +345,6 @@
</button>
<button
class={buttonVariants({ size: "sm", variant: "outline" })}
disabled={toNumber(piggy.current_amount) <= 0}
onclick={() => setMode("remove")}
type="button"
>
@@ -118,10 +118,12 @@
</Field>
</div>
{#snippet footer()}
<Button disabled={save.busy || locked} size="sm" type="submit">
{item ? "Update link type" : "Store new link type"}
</Button>
<Status action={save} />
{#if !locked}
<Button disabled={save.busy} size="sm" type="submit">
{item ? "Update link type" : "Store new link type"}
</Button>
<Status action={save} />
{/if}
{#if item && !locked}
<span class="ml-auto flex items-center gap-2">
<Status action={removal} busy="Deleting" />
+8 -4
View File
@@ -130,7 +130,12 @@
async function create(event: SubmitEvent) {
event.preventDefault();
const tag = newTag.trim();
if (!tag || creating) {
if (creating) {
return;
}
if (!tag) {
createError = "Type a tag first";
createInput?.focus();
return;
}
creating = true;
@@ -446,13 +451,12 @@
<input
aria-label="New tag"
class="cladd-cut cladd-focusable-input h-7 w-64 rounded-cladd-sm px-2 text-[13px]"
oninput={() => (createError = "")}
placeholder="New tag, parent/child works (c)"
bind:this={createInput}
bind:value={newTag}
>
<Button disabled={creating || !newTag.trim()} size="sm" type="submit"
>Create</Button
>
<Button disabled={creating} size="sm" type="submit">Create</Button>
{#if createError}
<span class="text-expense">{createError}</span>
{/if}