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
All checks were successful
Deploy production / deploy (push) Successful in 1m7s
All checks were successful
Deploy production / deploy (push) Successful in 1m7s
This commit is contained in:
@@ -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 @@
|
||||
</span>
|
||||
{/if}
|
||||
<span class="min-w-0"
|
||||
><span class="block font-medium">{project.name}</span><span class={subtitleClass}
|
||||
>{project.subtitle}</span
|
||||
></span
|
||||
><span class="block font-medium"
|
||||
>{project.name}{#if project.locked}<PrivateLock />{/if}</span
|
||||
><span class={subtitleClass}>{project.subtitle}</span></span
|
||||
>
|
||||
</span>
|
||||
{#if project.external}
|
||||
|
||||
@@ -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}
|
||||
<span class="min-w-0 flex-1">
|
||||
<span class="block font-medium">{s.name}</span>
|
||||
<span class="block font-medium"
|
||||
>{s.name}{#if s.locked}<PrivateLock />{/if}</span
|
||||
>
|
||||
<span class="block truncate text-sm text-muted-foreground">{s.subtitle}</span>
|
||||
</span>
|
||||
<ArrowRight
|
||||
|
||||
29
src/lib/components/PrivateLock.svelte
Normal file
29
src/lib/components/PrivateLock.svelte
Normal file
@@ -0,0 +1,29 @@
|
||||
<script lang="ts">
|
||||
// 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();
|
||||
</script>
|
||||
|
||||
<span class={['private-lock', { tight }, className]} role="img" aria-label="private" title="private"
|
||||
><Lock {size} strokeWidth={2.25} aria-hidden="true" /></span
|
||||
>
|
||||
|
||||
<style>
|
||||
.private-lock {
|
||||
display: inline-flex;
|
||||
flex-shrink: 0;
|
||||
margin-left: 0.3em;
|
||||
vertical-align: -0.08em;
|
||||
color: var(--site-muted, currentColor);
|
||||
opacity: 0.75;
|
||||
}
|
||||
.private-lock.tight {
|
||||
margin-left: -0.2em;
|
||||
}
|
||||
</style>
|
||||
@@ -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}
|
||||
><TrackerIcon name={project.name} class="tile-icon size-9" /><span
|
||||
class="min-w-0"
|
||||
><span class="block font-medium">{project.name}</span><span
|
||||
class="block truncate text-xs text-muted-foreground"
|
||||
><span class="block font-medium"
|
||||
>{project.name}{#if 'locked' in project && project.locked}<PrivateLock
|
||||
/>{/if}</span
|
||||
><span class="block truncate text-xs text-muted-foreground"
|
||||
>{project.subtitle}</span
|
||||
></span
|
||||
><ChevronRight
|
||||
@@ -410,8 +413,10 @@
|
||||
</span>
|
||||
{/if}
|
||||
<span class="min-w-0"
|
||||
><span class="block font-medium">{project.name}</span><span
|
||||
class="block truncate text-xs text-muted-foreground"
|
||||
><span class="block font-medium"
|
||||
>{project.name}{#if 'locked' in project && project.locked}<PrivateLock
|
||||
/>{/if}</span
|
||||
><span class="block truncate text-xs text-muted-foreground"
|
||||
>{project.subtitle}</span
|
||||
></span
|
||||
>
|
||||
@@ -529,7 +534,7 @@
|
||||
onclick={() => (menuOpen = false)}
|
||||
><TrackerIcon name={project.name} class="tile-icon size-7" /><span
|
||||
class="truncate">{project.name}</span
|
||||
></a
|
||||
>{#if 'locked' in project && project.locked}<PrivateLock tight />{/if}</a
|
||||
>
|
||||
<!-- eslint-enable svelte/no-navigation-without-resolve -->
|
||||
{/each}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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 @@
|
||||
<div class="ttile">
|
||||
<a class="stretched" href={t.href} title={t.subtitle}>
|
||||
<TrackerIcon name={t.name} class="t-icon size-8" />
|
||||
<span class="tname">{t.name}</span>
|
||||
<span class="tname">{t.name}</span>{#if t.locked}<PrivateLock tight />{/if}
|
||||
</a>
|
||||
<span class="tchips">
|
||||
{#each tileChildren(t) as c (c.name)}
|
||||
@@ -330,7 +331,7 @@
|
||||
{:else}
|
||||
<a class="ttile" href={t.href} title={t.subtitle}>
|
||||
<TrackerIcon name={t.name} class="t-icon size-8" />
|
||||
<span class="tname">{t.name}</span>
|
||||
<span class="tname">{t.name}</span>{#if t.locked}<PrivateLock tight />{/if}
|
||||
</a>
|
||||
{/if}
|
||||
</li>
|
||||
|
||||
Reference in New Issue
Block a user