diff --git a/src/lib/components/SiteChrome.svelte b/src/lib/components/SiteChrome.svelte
index 3d5ffbf..7e4d9db 100644
--- a/src/lib/components/SiteChrome.svelte
+++ b/src/lib/components/SiteChrome.svelte
@@ -6,6 +6,7 @@
import { page } from '$app/state';
import CopyButton from '#lib/components/CopyButton.svelte';
import ProjectIcon from '#lib/components/ProjectIcon.svelte';
+ import TrackerIcon from '#lib/components/TrackerIcon.svelte';
import { projects, visibleTrackers } from '#lib/projects.js';
import { Button, buttonVariants } from '#lib/components/ui/button/index.js';
import * as Sheet from '#lib/components/ui/sheet/index.js';
@@ -219,15 +220,21 @@
>
{#if openMenu === item.href}
all {menu.label}
-
+
{#each menu.items as project (project.name)}
@@ -236,11 +243,15 @@
rel={project.external ? 'external' : undefined}
class="flex items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent hover:text-accent-foreground focus-visible:bg-accent"
>
-
-
-
+ {#if item.href === '/trackers'}
+
+ {:else}
+
+
+
+ {/if}
{project.name} (menuOpen = false)}>{item.label}
{#if menus[item.href]}
-
- {#each menus[item.href].items as project (project.name)}
-
- {/each}
-
+ {#if item.href === '/trackers'}
+
+ {:else}
+
+ {#each menus[item.href].items as project (project.name)}
+
+ {/each}
+
+ {/if}
{/if}
{/each}
diff --git a/src/lib/components/TrackerIcon.svelte b/src/lib/components/TrackerIcon.svelte
new file mode 100644
index 0000000..be34a34
--- /dev/null
+++ b/src/lib/components/TrackerIcon.svelte
@@ -0,0 +1,27 @@
+
+
+
+
+
diff --git a/src/lib/tracker-icons.test.ts b/src/lib/tracker-icons.test.ts
new file mode 100644
index 0000000..929bfd8
--- /dev/null
+++ b/src/lib/tracker-icons.test.ts
@@ -0,0 +1,20 @@
+import { describe, expect, it } from 'vitest';
+import { trackerIcons } from './tracker-icons';
+import { visibleTrackers } from './projects';
+
+describe('tracker icons', () => {
+ it('gives every tracker its own colour', () => {
+ const names = visibleTrackers(true).map((t) => t.name);
+ for (const name of names) expect(trackerIcons[name], name).toBeDefined();
+ const colours = names.map((n) => trackerIcons[n].to);
+ expect(new Set(colours).size).toBe(colours.length);
+ });
+
+ it('keeps tasks, money and books first', () => {
+ expect(
+ visibleTrackers(true)
+ .slice(0, 3)
+ .map((t) => t.name)
+ ).toEqual(['tasks', 'money', 'books']);
+ });
+});
diff --git a/src/lib/tracker-icons.ts b/src/lib/tracker-icons.ts
new file mode 100644
index 0000000..8305233
--- /dev/null
+++ b/src/lib/tracker-icons.ts
@@ -0,0 +1,125 @@
+// Colourful tracker tiles for the header menu: a gradient rounded tile per tracker with a
+// small inline SVG icon (24×24, stroke paths). No icon library: each icon is a few path
+// strings, so the whole set is a couple of KB.
+
+/** A circle as a path, so every icon is just a list of `d` strings. */
+const c = (cx: number, cy: number, r: number) =>
+ `M${cx - r} ${cy}a${r} ${r} 0 1 0 ${2 * r} 0a${r} ${r} 0 1 0 ${-2 * r} 0`;
+
+export type TrackerIcon = {
+ /** Gradient start and end (top-left to bottom-right). */
+ from: string;
+ to: string;
+ /** Icon colour; white unless the tile is light (yellow, lime). */
+ ink: string;
+ paths: string[];
+};
+
+const WHITE = '#ffffff';
+
+export const trackerIcons: Record = {
+ tasks: {
+ from: '#60a5fa',
+ to: '#2563eb',
+ ink: WHITE,
+ paths: [c(12, 12, 9), 'm8.5 12 2.5 2.5 4.5-5']
+ },
+ money: {
+ from: '#34d399',
+ to: '#059669',
+ ink: WHITE,
+ paths: [c(12, 12, 9), 'M15.5 8.6a4 4 0 1 0 0 6.8', 'M7 10.8h6M7 13.2h6']
+ },
+ books: {
+ from: '#2dd4bf',
+ to: '#0d9488',
+ ink: WHITE,
+ paths: [
+ 'M12 7v14',
+ 'M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z'
+ ]
+ },
+ cars: {
+ from: '#f87171',
+ to: '#dc2626',
+ ink: WHITE,
+ paths: ['M3 16v-3.5L5.5 7h13l2.5 5.5V16', 'M3 12.5h18', c(7.5, 16.5, 2), c(16.5, 16.5, 2)]
+ },
+ music: {
+ from: '#a78bfa',
+ to: '#7c3aed',
+ ink: WHITE,
+ paths: ['M9 18V5l12-2v13', c(6, 18, 3), c(18, 16, 3)]
+ },
+ health: {
+ from: '#f472b6',
+ to: '#db2777',
+ ink: WHITE,
+ paths: [
+ 'M19 14c1.5-1.5 3-3.2 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.8 0-3 .5-4.5 2-1.5-1.5-2.7-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4 3 5.5l7 7Z',
+ 'M3.2 12h6.3l.5-1 2 4.5 2-7 1.5 3.5h5.3'
+ ]
+ },
+ groceries: {
+ from: '#bef264',
+ to: '#65a30d',
+ ink: '#1a2e05',
+ paths: [
+ c(8, 20.5, 1.2),
+ c(18.5, 20.5, 1.2),
+ 'M2 2.5h2.2l2.6 12a2 2 0 0 0 2 1.5h9.5a2 2 0 0 0 2-1.5L22 7H5.2'
+ ]
+ },
+ houses: {
+ from: '#fb923c',
+ to: '#ea580c',
+ ink: WHITE,
+ paths: ['M3 10.5 12 3l9 7.5', 'M5 9.5V20h14V9.5', 'M10 20v-6h4v6']
+ },
+ energy: {
+ from: '#fde047',
+ to: '#eab308',
+ ink: '#422006',
+ paths: ['M13 2 3 14h9l-1 8 10-12h-9l1-8z']
+ },
+ games: {
+ from: '#818cf8',
+ to: '#4338ca',
+ ink: WHITE,
+ paths: [
+ 'M6 11h4M8 9v4M15 12h.01M18 10h.01',
+ 'M17.3 5H6.7a4 4 0 0 0-4 3.6C2.6 9.4 2 14.5 2 16a3 3 0 0 0 3 3c1 0 1.5-.5 2-1l1.4-1.4A2 2 0 0 1 9.8 16h4.4a2 2 0 0 1 1.4.6L17 18c.5.5 1 1 2 1a3 3 0 0 0 3-3c0-1.5-.6-6.6-.7-7.3A4 4 0 0 0 17.3 5z'
+ ]
+ },
+ skins: {
+ from: '#e879f9',
+ to: '#a21caf',
+ ink: WHITE,
+ paths: [c(12, 12, 8), c(12, 12, 2.5), 'M22 12h-4M6 12H2M12 6V2M12 22v-4']
+ },
+ pets: {
+ from: '#d6a46a',
+ to: '#92602a',
+ ink: WHITE,
+ paths: [
+ c(11, 4, 2),
+ c(18, 8, 2),
+ c(20, 16, 2),
+ 'M9 10a5 5 0 0 1 5 5v3.5a3.5 3.5 0 0 1-6.8 1C6.5 17.5 4.5 17 4.5 17A3.5 3.5 0 0 1 5.5 10Z'
+ ]
+ },
+ lists: {
+ from: '#94a3b8',
+ to: '#475569',
+ ink: WHITE,
+ paths: ['M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01']
+ }
+};
+
+/** Neutral fallback for a tracker without its own tile yet. */
+export const defaultTrackerIcon: TrackerIcon = {
+ from: '#a1a1aa',
+ to: '#52525b',
+ ink: WHITE,
+ paths: ['M4 20V10M10 20V4M16 20v-7M22 20H2']
+};