From 85da5afdfeaeb209d6d4d555904f88fc2fe877ba Mon Sep 17 00:00:00 2001 From: Stan Runge Date: Sat, 24 Oct 2026 14:09:07 +0000 Subject: [PATCH] =?UTF-8?q?money:=20wishlist=20tier=20totals=20fit=20on=20?= =?UTF-8?q?one=20line=20(compact=20=E2=82=AC,=20scales=20with=20card,=20ex?= =?UTF-8?q?act=20amount=20in=20tooltip)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/money-tracker/wishlist.test.ts | 17 +++++++++++- src/lib/money-tracker/wishlist.ts | 24 +++++++++++++++++ src/routes/trackers/money/+page.svelte | 5 ++-- src/routes/trackers/money/money-tracker.css | 29 ++++++++++++++++----- 4 files changed, 66 insertions(+), 9 deletions(-) diff --git a/src/lib/money-tracker/wishlist.test.ts b/src/lib/money-tracker/wishlist.test.ts index 72d2bb8..1b894ef 100644 --- a/src/lib/money-tracker/wishlist.test.ts +++ b/src/lib/money-tracker/wishlist.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { sortWishlist, tierClass } from './wishlist'; +import { compactAmount, compactEuro, sortWishlist, tierClass } from './wishlist'; describe('wish list order', () => { it('sorts by tier descending, then price descending, unpriced and untiered last', () => { @@ -23,4 +23,19 @@ describe('wish list order', () => { expect(tierClass(null)).toBe('tier-none'); expect(tierClass(9)).toBe('tier-none'); }); + + it('formats tier totals compactly', () => { + expect(compactEuro(0)).toBe('€\u00a00'); + expect(compactEuro(500_00)).toBe('€\u00a0500'); + expect(compactEuro(8_865_00)).toBe('€\u00a08.865'); + expect(compactEuro(9_365_49)).toBe('€\u00a09.365'); + expect(compactEuro(9_999_60)).toBe('€\u00a010k'); + expect(compactEuro(17_580_00)).toBe('€\u00a017,6k'); + expect(compactEuro(28_080_00)).toBe('€\u00a028,1k'); + expect(compactEuro(999_960_00)).toBe('€\u00a01M'); + expect(compactEuro(1_234_567_00)).toBe('€\u00a01,2M'); + expect(compactEuro(-1_500_00)).toBe('−€\u00a01.500'); + expect(compactAmount(28_080_00)).toBe('28,1k'); + expect(compactAmount(-40)).toBe('0'); + }); }); diff --git a/src/lib/money-tracker/wishlist.ts b/src/lib/money-tracker/wishlist.ts index 0cd96b9..6ccbf73 100644 --- a/src/lib/money-tracker/wishlist.ts +++ b/src/lib/money-tracker/wishlist.ts @@ -23,3 +23,27 @@ export function sortWishlist(items: readonly T[]): T[] { export function tierClass(priority: number | null): string { return priority != null && priority >= 1 && priority <= 5 ? `tier-${priority}` : 'tier-none'; } + +const oneDecimal = (value: number) => { + const rounded = Math.round(value * 10) / 10; + return (Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(1)).replace('.', ','); +}; + +/** + * Short amount for the wish list tier cards, so a total fits on one line: + * whole euros below €10k ("8.865"), then thousands ("28,1k") and millions ("1,2M"). + * Uses the same en-NL separators as the rest of the tracker. No currency symbol. + */ +export function compactAmount(cents: number): string { + const euros = Math.round(Math.abs(cents) / 100); + const sign = cents < 0 && euros > 0 ? '−' : ''; + if (euros >= 999_950) return `${sign}${oneDecimal(euros / 1_000_000)}M`; + if (euros >= 10_000) return `${sign}${oneDecimal(euros / 1_000)}k`; + return sign + String(euros).replace(/\B(?=(\d{3})+(?!\d))/g, '.'); +} + +/** compactAmount with the euro sign: "€ 8.865", "€ 28,1k", "−€ 1.500". */ +export function compactEuro(cents: number): string { + const body = compactAmount(cents); + return body.startsWith('−') ? `−€\u00a0${body.slice(1)}` : `€\u00a0${body}`; +} diff --git a/src/routes/trackers/money/+page.svelte b/src/routes/trackers/money/+page.svelte index 606e332..e5f049b 100644 --- a/src/routes/trackers/money/+page.svelte +++ b/src/routes/trackers/money/+page.svelte @@ -17,7 +17,7 @@ import Download from 'lucide-svelte/icons/download'; import MoneyChart from '#lib/money-tracker/MoneyChart.svelte'; import QuickAdd from '#lib/money-tracker/QuickAdd.svelte'; - import { sortWishlist, tierClass } from '#lib/money-tracker/wishlist.js'; + import { compactAmount, sortWishlist, tierClass } from '#lib/money-tracker/wishlist.js'; import { Badge } from '#lib/components/ui/badge/index.js'; import { resolve } from '$app/paths'; import { page } from '$app/state'; @@ -706,7 +706,8 @@
{#each wishTiers as row (row.tier)}
{row.tier === 5 ? 'Tier 5' : `5–${row.tier}`}{amount(row.value)}€ {compactAmount(row.value)}{row.count} {row.count === 1 ? 'item' : 'items'}
{/each}
diff --git a/src/routes/trackers/money/money-tracker.css b/src/routes/trackers/money/money-tracker.css index d8b9999..621434f 100644 --- a/src/routes/trackers/money/money-tracker.css +++ b/src/routes/trackers/money/money-tracker.css @@ -92,25 +92,42 @@ } .money-app .wish-tier-grid { display: grid; - grid-template-columns: repeat(auto-fit, minmax(64px, 1fr)); - gap: 6px; + grid-template-columns: repeat(5, minmax(0, 1fr)); + gap: clamp(4px, 1.2vw, 6px); } .money-app .wish-tier-grid > div { display: grid; gap: 3px; - padding: 9px 11px; + padding: 8px clamp(4px, 1.2vw, 11px); min-width: 0; border: 1px solid var(--site-border); border-radius: 7px; + container-type: inline-size; } +.money-app .wish-tier-grid > div > * { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +/* Totals are compact ("€ 8.865", "€ 28,1k") and scale with the card so they stay on one line. */ .money-app .wish-tier-grid strong { - font-size: 14px; + font-size: clamp(10px, 23cqi, 15px); font-weight: 600; - overflow-wrap: anywhere; + font-variant-numeric: tabular-nums; + letter-spacing: -0.01em; +} +.money-app .wish-tier-grid small, +.money-app .wish-tier-grid .eyebrow { + font-size: clamp(10px, 19cqi, 12px); } .money-app .wish-tier-grid small { color: var(--site-muted); - font-size: 11px; +} +/* On very narrow phones the tier cards drop the euro sign; the exact amount stays in the tooltip. */ +@container (max-width: 42px) { + .money-app .wish-tier-grid .currency { + display: none; + } } .money-app .capitalize { text-transform: capitalize;