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'} +
+ {#each menus[item.href].items as project (project.name)} + + (menuOpen = false)} + >{project.name} + + {/each} +
+ {: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'] +};