diff --git a/src/routes/start/+page.svelte b/src/routes/start/+page.svelte index a1357e2..7361eaf 100644 --- a/src/routes/start/+page.svelte +++ b/src/routes/start/+page.svelte @@ -3,6 +3,8 @@ import { invalidate } from '$app/navigation'; 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 { visibleTrackers } from '#lib/projects.js'; let { data, form } = $props(); @@ -26,16 +28,12 @@ } }); - const trackers = [ - { name: 'Tasks', href: '/trackers/tasks' }, - { name: 'Money', href: '/trackers/money' }, - { name: 'Health', href: '/trackers/health' }, + // Same list, order and icons as the header Trackers dropdown (owner-only ones only when + // the header shows them). Spotting is a group tile with one-click chips per sub-tracker. + const trackers = $derived(visibleTrackers(data.isOwner)); + // Search-only shortcuts that aren't trackers of their own. + const extraHits = [ { name: 'Weigh-in', href: '/trackers/health#body' }, - { name: 'Books', href: '/trackers/books' }, - { name: 'Houses', href: '/trackers/houses' }, - { name: 'Groceries', href: '/trackers/groceries' }, - { name: 'Energy', href: '/projects/energy' }, - { name: 'Lists', href: '/lists' }, { name: 'DB', href: '/lists/db' } ]; @@ -65,6 +63,18 @@ }); const filtering = $derived(linkFilter.trim() !== ''); + // The links filter (f) also narrows the trackers; a group stays if it or a chip matches. + const shownTrackers = $derived.by(() => { + const q = linkFilter.trim().toLowerCase(); + const hit = (t: { name: string; subtitle: string; href: string }) => + t.name.includes(q) || t.subtitle.toLowerCase().includes(q) || t.href.includes(q); + if (!q || 'trackers'.startsWith(q)) return trackers; + return trackers.flatMap((t) => { + if (hit(t)) return [t]; + const children = t.children?.filter(hit) ?? []; + return children.length ? [{ ...t, children }] : []; + }); + }); const shownSections = $derived.by(() => { const q = linkFilter.trim().toLowerCase(); if (!q) return sections; @@ -97,10 +107,16 @@ linkFilter = ''; linkFilterEl?.blur(); } else if (e.key === 'Enter') { - const first = shownSections[0]?.[1][0]; - if (!first) return; - if (e.metaKey || e.ctrlKey) window.open(first.url, '_blank'); - else location.href = first.url; + // First visible item in page order: trackers sit above the links. + const t = shownTrackers[0]; + const url = t + ? t.children?.length === 1 + ? t.children[0].href + : t.href + : shownSections[0]?.[1][0]?.url; + if (!url) return; + if (e.metaKey || e.ctrlKey) window.open(url, '_blank'); + else location.href = url; } } @@ -108,7 +124,10 @@ const everything = $derived([ ...data.links.map((l) => ({ name: l.title, href: l.url, kind: l.section })), ...(host?.services ?? []).map((s) => ({ name: s.name, href: s.url, kind: 'service' })), - ...trackers.map((t) => ({ name: t.name, href: t.href, kind: 'tracker' })) + ...trackers + .flatMap((t) => [t, ...(t.children ?? [])]) + .map((t) => ({ name: t.name, href: t.href, kind: 'tracker' })), + ...extraHits.map((t) => ({ ...t, kind: 'tracker' })) ]); const hits = $derived.by(() => { const q = query.trim().toLowerCase(); @@ -260,6 +279,45 @@ + + {#if shownTrackers.length} +
+

+ Trackers + {filtering ? `${shownTrackers.length} of ${trackers.length}` : trackers.length} +

+ +
+ {/if} +
@@ -421,12 +479,6 @@ {/each} -

Trackers

-
    - {#each trackers as t (t.href)} -
  • {t.name}
  • - {/each} -
@@ -440,8 +492,8 @@ bind:value={linkFilter} onkeydown={onFilterKey} type="search" - placeholder="Filter links (f)" - aria-label="Filter links" + placeholder="Filter links & trackers (f)" + aria-label="Filter links and trackers" /> li.group { + grid-column: span 3; + } + .ttile { + position: relative; + display: flex; + align-items: center; + gap: 9px; + height: 100%; + padding: 6px 8px; + border: 1px solid var(--line); + border-radius: 8px; + background: color-mix(in oklab, var(--site-text) 2%, transparent); + transition: + transform 160ms cubic-bezier(0.2, 0.8, 0.2, 1), + background-color 160ms ease, + border-color 160ms ease, + box-shadow 160ms ease; + } + a.ttile:hover, + div.ttile:has(.stretched:hover) { + background: var(--site-hover); + border-color: var(--site-muted); + transform: translateY(-1px); + box-shadow: 0 6px 16px -10px rgb(0 0 0 / 0.45); + text-decoration: none; + } + .ttile :global(.t-icon) { + transition: transform 160ms cubic-bezier(0.2, 0.8, 0.2, 1); + } + a.ttile:hover :global(.t-icon), + .stretched:hover :global(.t-icon), + .tchip:hover :global(.t-icon) { + transform: scale(1.06); + } + .tname { + font-weight: 500; + font-size: 13px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + .stretched { + display: flex; + align-items: center; + gap: 9px; + flex: none; + } + .stretched::after { + content: ''; + position: absolute; + inset: 0; + border-radius: 8px; + } + .stretched:hover { + text-decoration: none; + } + .tchips { + position: relative; + z-index: 1; + display: flex; + flex-wrap: wrap; + gap: 4px; + margin-left: auto; + } + .tchip { + display: inline-flex; + align-items: center; + gap: 5px; + padding: 2px 9px 2px 2px; + border: 1px solid var(--line); + border-radius: 999px; + background: var(--site-surface); + font-size: 12px; + font-weight: 500; + transition: + transform 160ms cubic-bezier(0.2, 0.8, 0.2, 1), + border-color 160ms ease, + background-color 160ms ease; + } + .tchip :global(.t-icon) { + border-radius: 999px; + } + .tchip:hover { + background: var(--site-hover); + border-color: var(--site-muted); + transform: translateY(-1px); + text-decoration: none; + } + .ttile:focus-visible, + .stretched:focus-visible, + .tchip:focus-visible { + outline: 2px solid var(--site-text); + outline-offset: 2px; + } + @media (prefers-reduced-motion: reduce) { + .ttile, + .tchip, + .ttile :global(.t-icon) { + transition: none; + transform: none !important; + } + } + @media (max-width: 760px) { + .tgrid > li.group { + grid-column: 1 / -1; + } + div.ttile { + flex-wrap: wrap; + } + .tchips { + margin-left: 0; + } + } .ico { flex: none; width: 16px;