From 676ac2ad648bb97ce4a888b690d45773347fb8e7 Mon Sep 17 00:00:00 2001 From: Stan Runge Date: Sun, 25 Oct 2026 16:22:25 +0000 Subject: [PATCH] trackers: lock icon after owner-only tracker names (header dropdown, mobile menu, home, /trackers, /start, error suggestions) from one `locked` flag in #lib/projects; drop the private: subtitle prefix --- src/lib/components/CatalogGrid.svelte | 8 ++++--- src/lib/components/ErrorView.svelte | 5 +++- src/lib/components/PrivateLock.svelte | 29 +++++++++++++++++++++++ src/lib/components/SiteChrome.svelte | 15 ++++++++---- src/lib/errors.ts | 9 ++++++- src/lib/projects.test.ts | 10 ++++++++ src/lib/projects.ts | 34 ++++++++++++++++++++------- src/routes/start/+page.svelte | 5 ++-- 8 files changed, 95 insertions(+), 20 deletions(-) create mode 100644 src/lib/components/PrivateLock.svelte diff --git a/src/lib/components/CatalogGrid.svelte b/src/lib/components/CatalogGrid.svelte index d67e4cf..7932a4d 100644 --- a/src/lib/components/CatalogGrid.svelte +++ b/src/lib/components/CatalogGrid.svelte @@ -8,6 +8,7 @@ import * as Card from '#lib/components/ui/card/index.js'; import ProjectIcon from '#lib/components/ProjectIcon.svelte'; import TrackerIcon from '#lib/components/TrackerIcon.svelte'; + import PrivateLock from '#lib/components/PrivateLock.svelte'; type Item = { name: string; @@ -17,6 +18,7 @@ children?: readonly Item[]; icon?: string; featured?: boolean; + locked?: boolean; }; let { items, @@ -57,9 +59,9 @@ {/if} {project.name}{project.subtitle}{project.name}{#if project.locked}{/if}{project.subtitle} {#if project.external} diff --git a/src/lib/components/ErrorView.svelte b/src/lib/components/ErrorView.svelte index e644268..37ff101 100644 --- a/src/lib/components/ErrorView.svelte +++ b/src/lib/components/ErrorView.svelte @@ -16,6 +16,7 @@ import ChartColumn from 'lucide-svelte/icons/chart-column'; import ProjectIcon from '#lib/components/ProjectIcon.svelte'; import TrackerIcon from '#lib/components/TrackerIcon.svelte'; + import PrivateLock from '#lib/components/PrivateLock.svelte'; import { errorCopy, suggestPages } from '#lib/errors.js'; let { @@ -140,7 +141,9 @@ > {/if} - {s.name} + {s.name}{#if s.locked}{/if} {s.subtitle} + // Small monochrome lock after the name of an owner-only tracker (driven by `locked` in + // #lib/projects). A status marker, so it inherits the muted text colour, not an accent. + import Lock from 'lucide-svelte/icons/lock'; + // `tight`: inside a flex row whose gap already spaces it from the name. + let { + size = 12, + tight = false, + class: className = '' + }: { size?: number; tight?: boolean; class?: string } = $props(); + + + + + diff --git a/src/lib/components/SiteChrome.svelte b/src/lib/components/SiteChrome.svelte index 73e333e..1e8f875 100644 --- a/src/lib/components/SiteChrome.svelte +++ b/src/lib/components/SiteChrome.svelte @@ -7,6 +7,7 @@ import CopyButton from '#lib/components/CopyButton.svelte'; import ProjectIcon from '#lib/components/ProjectIcon.svelte'; import TrackerIcon from '#lib/components/TrackerIcon.svelte'; + import PrivateLock from '#lib/components/PrivateLock.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'; @@ -342,8 +343,10 @@ aria-expanded={flyout === project.name} >{project.name}{project.name}{#if 'locked' in project && project.locked}{/if}{project.subtitle} {/if} {project.name}{project.name}{#if 'locked' in project && project.locked}{/if}{project.subtitle} @@ -529,7 +534,7 @@ onclick={() => (menuOpen = false)} >{project.name}{#if 'locked' in project && project.locked}{/if} {/each} diff --git a/src/lib/errors.ts b/src/lib/errors.ts index 22361f3..e19b1dc 100644 --- a/src/lib/errors.ts +++ b/src/lib/errors.ts @@ -61,6 +61,7 @@ export type Suggestion = { href: string; kind: 'tracker' | 'project' | 'page'; subtitle: string; + locked?: boolean; }; const pages: Suggestion[] = [ @@ -77,7 +78,13 @@ export function destinations(isOwner: boolean): Suggestion[] { if (!p.external) out.push({ name: p.name, href: p.href, kind: 'project', subtitle: p.subtitle }); for (const t of visibleTrackers(isOwner)) { - out.push({ name: t.name, href: t.href, kind: 'tracker', subtitle: t.subtitle }); + out.push({ + name: t.name, + href: t.href, + kind: 'tracker', + subtitle: t.subtitle, + ...(t.locked ? { locked: true } : {}) + }); for (const c of t.children ?? []) out.push({ name: c.name, href: c.href, kind: 'tracker', subtitle: c.subtitle }); } diff --git a/src/lib/projects.test.ts b/src/lib/projects.test.ts index 1159b57..dfb0f10 100644 --- a/src/lib/projects.test.ts +++ b/src/lib/projects.test.ts @@ -18,4 +18,14 @@ describe('trackers', () => { it('uses short single-word names', () => { for (const t of visibleTrackers(true)) expect(t.name).toMatch(/^[a-z]+$/); }); + it('locks every owner-only tracker, drops the private: prefix', () => { + const owner = visibleTrackers(true); + const pub = new Set(visibleTrackers(false).map((t) => t.name)); + for (const t of owner) { + if (!pub.has(t.name)) expect(t.locked).toBe(true); + expect(t.subtitle).not.toMatch(/^private:/); + } + expect(owner.find((t) => t.name === 'money')?.locked).toBe(true); + expect(owner.find((t) => t.name === 'reading')?.locked).toBeUndefined(); + }); }); diff --git a/src/lib/projects.ts b/src/lib/projects.ts index 47ea734..2f28584 100644 --- a/src/lib/projects.ts +++ b/src/lib/projects.ts @@ -43,6 +43,8 @@ type Tracker = { icon?: string; /** Shown on group tiles before the "more" chip (main and actively played games). */ featured?: boolean; + /** Owner-only (needs login): every listing shows a small lock after the name. */ + locked?: boolean; }; // The spotting trackers: pokedexes of things I can spot in the wild. Grouped under one @@ -110,11 +112,12 @@ export const gameTrackers: readonly Tracker[] = [ // Trackers, in display order. Names are short single nouns. Most keep their existing // /projects/... URLs (no moves, so no redirects needed). `private` trackers are shown in the // header, home page and /trackers only to the logged-in owner (layout data `isOwner`), never -// in the sitemap; their routes 404 for everyone else. +// in the sitemap; their routes 404 for everyone else. Private trackers, and listed ones that +// still need a login (`locked`, e.g. money), get a lock icon next to their name everywhere. const allTrackers: readonly (Tracker & { private: boolean })[] = [ { name: 'tasks', - subtitle: 'private: to-dos, goals and my TickTick history', + subtitle: 'to-dos, goals and my TickTick history', href: '/trackers/tasks', external: false, private: true @@ -124,6 +127,7 @@ const allTrackers: readonly (Tracker & { private: boolean })[] = [ subtitle: 'a manual ledger for accounts, bills and utilities', href: '/trackers/money', external: false, + locked: true, private: false }, { @@ -157,28 +161,28 @@ const allTrackers: readonly (Tracker & { private: boolean })[] = [ }, { name: 'health', - subtitle: 'private: body, workouts, sleep, meds and vaccinations', + subtitle: 'body, workouts, sleep, meds and vaccinations', href: '/trackers/health', external: false, private: true }, { name: 'groceries', - subtitle: 'private: supermarket products, favorites and prices per store', + subtitle: 'supermarket products, favorites and prices per store', href: '/trackers/groceries', external: false, private: true }, { name: 'houses', - subtitle: 'private: rentals around Gouda, applications and alerts', + subtitle: 'rentals around Gouda, applications and alerts', href: '/trackers/houses', external: false, private: true }, { name: 'holders', - subtitle: 'private: who has my address, email, phone and bank details, and a moving checklist', + subtitle: 'who has my address, email, phone and bank details, and a moving checklist', href: '/trackers/data-holders', external: false, private: true @@ -221,8 +225,22 @@ const allTrackers: readonly (Tracker & { private: boolean })[] = [ } ]; -const strip = ({ name, subtitle, href, external, children }: Tracker): Tracker => - children ? { name, subtitle, href, external, children } : { name, subtitle, href, external }; +const strip = ({ + name, + subtitle, + href, + external, + children, + locked, + private: hidden +}: Tracker & { private: boolean }): Tracker => ({ + name, + subtitle, + href, + external, + ...(children ? { children } : {}), + ...(locked || hidden ? { locked: true } : {}) +}); /** Public trackers (sitemap, logged-out navigation). */ export const trackers: readonly Tracker[] = allTrackers.filter((t) => !t.private).map(strip); /** Owner-only trackers. */ diff --git a/src/routes/start/+page.svelte b/src/routes/start/+page.svelte index 4151de3..dd9181b 100644 --- a/src/routes/start/+page.svelte +++ b/src/routes/start/+page.svelte @@ -4,6 +4,7 @@ import { ago, bytes, duration, timerStatus, type Container } from '#lib/home.js'; import { linkIcon } from '#lib/link-icons.js'; import TrackerIcon from '#lib/components/TrackerIcon.svelte'; + import PrivateLock from '#lib/components/PrivateLock.svelte'; import { visibleTrackers } from '#lib/projects.js'; let { data, form } = $props(); @@ -307,7 +308,7 @@
- {t.name} + {t.name}{#if t.locked}{/if} {#each tileChildren(t) as c (c.name)} @@ -330,7 +331,7 @@ {:else} - {t.name} + {t.name}{#if t.locked}{/if} {/if}