start: trackers section mirroring the header dropdown (same order and icons, spotting chips, f filter)
All checks were successful
Deploy production / deploy (push) Successful in 1m11s
All checks were successful
Deploy production / deploy (push) Successful in 1m11s
This commit is contained in:
@@ -3,6 +3,8 @@
|
|||||||
import { invalidate } from '$app/navigation';
|
import { invalidate } from '$app/navigation';
|
||||||
import { ago, bytes, duration, timerStatus, type Container } from '#lib/home.js';
|
import { ago, bytes, duration, timerStatus, type Container } from '#lib/home.js';
|
||||||
import { linkIcon } from '#lib/link-icons.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();
|
let { data, form } = $props();
|
||||||
|
|
||||||
@@ -26,16 +28,12 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const trackers = [
|
// Same list, order and icons as the header Trackers dropdown (owner-only ones only when
|
||||||
{ name: 'Tasks', href: '/trackers/tasks' },
|
// the header shows them). Spotting is a group tile with one-click chips per sub-tracker.
|
||||||
{ name: 'Money', href: '/trackers/money' },
|
const trackers = $derived(visibleTrackers(data.isOwner));
|
||||||
{ name: 'Health', href: '/trackers/health' },
|
// Search-only shortcuts that aren't trackers of their own.
|
||||||
|
const extraHits = [
|
||||||
{ name: 'Weigh-in', href: '/trackers/health#body' },
|
{ 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' }
|
{ name: 'DB', href: '/lists/db' }
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -65,6 +63,18 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
const filtering = $derived(linkFilter.trim() !== '');
|
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 shownSections = $derived.by(() => {
|
||||||
const q = linkFilter.trim().toLowerCase();
|
const q = linkFilter.trim().toLowerCase();
|
||||||
if (!q) return sections;
|
if (!q) return sections;
|
||||||
@@ -97,10 +107,16 @@
|
|||||||
linkFilter = '';
|
linkFilter = '';
|
||||||
linkFilterEl?.blur();
|
linkFilterEl?.blur();
|
||||||
} else if (e.key === 'Enter') {
|
} else if (e.key === 'Enter') {
|
||||||
const first = shownSections[0]?.[1][0];
|
// First visible item in page order: trackers sit above the links.
|
||||||
if (!first) return;
|
const t = shownTrackers[0];
|
||||||
if (e.metaKey || e.ctrlKey) window.open(first.url, '_blank');
|
const url = t
|
||||||
else location.href = first.url;
|
? 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<Hit[]>([
|
const everything = $derived<Hit[]>([
|
||||||
...data.links.map((l) => ({ name: l.title, href: l.url, kind: l.section })),
|
...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' })),
|
...(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 hits = $derived.by(() => {
|
||||||
const q = query.trim().toLowerCase();
|
const q = query.trim().toLowerCase();
|
||||||
@@ -260,6 +279,45 @@
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
<!-- Trackers: mirrors the header dropdown -->
|
||||||
|
{#if shownTrackers.length}
|
||||||
|
<section class="card trackers" aria-label="Trackers">
|
||||||
|
<h2>
|
||||||
|
<a href="/trackers">Trackers</a>
|
||||||
|
<small
|
||||||
|
>{filtering ? `${shownTrackers.length} of ${trackers.length}` : trackers.length}</small
|
||||||
|
>
|
||||||
|
</h2>
|
||||||
|
<ul class="tgrid">
|
||||||
|
{#each shownTrackers as t (t.name)}
|
||||||
|
<li class={{ group: !!t.children }}>
|
||||||
|
{#if t.children}
|
||||||
|
<!-- Group tile: stretched main link, chips layered above it (no nested <a>). -->
|
||||||
|
<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>
|
||||||
|
</a>
|
||||||
|
<span class="tchips">
|
||||||
|
{#each t.children as c (c.name)}
|
||||||
|
<a class="tchip" href={c.href} title={c.subtitle}
|
||||||
|
><TrackerIcon name={c.name} class="t-icon size-5" />{c.name}</a
|
||||||
|
>
|
||||||
|
{/each}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{: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>
|
||||||
|
</a>
|
||||||
|
{/if}
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<!-- Tasks -->
|
<!-- Tasks -->
|
||||||
@@ -421,12 +479,6 @@
|
|||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</ul>
|
||||||
<h3>Trackers</h3>
|
|
||||||
<ul class="tiles">
|
|
||||||
{#each trackers as t (t.href)}
|
|
||||||
<li><a href={t.href}>{t.name}</a></li>
|
|
||||||
{/each}
|
|
||||||
</ul>
|
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
@@ -440,8 +492,8 @@
|
|||||||
bind:value={linkFilter}
|
bind:value={linkFilter}
|
||||||
onkeydown={onFilterKey}
|
onkeydown={onFilterKey}
|
||||||
type="search"
|
type="search"
|
||||||
placeholder="Filter links (f)"
|
placeholder="Filter links & trackers (f)"
|
||||||
aria-label="Filter links"
|
aria-label="Filter links and trackers"
|
||||||
/>
|
/>
|
||||||
<button class="ghost" onclick={() => ((editing = !editing), (editId = null))}
|
<button class="ghost" onclick={() => ((editing = !editing), (editId = null))}
|
||||||
>{editing ? 'done' : 'edit'}</button
|
>{editing ? 'done' : 'edit'}</button
|
||||||
@@ -979,6 +1031,129 @@
|
|||||||
margin: 0 0 0 auto;
|
margin: 0 0 0 auto;
|
||||||
flex: none;
|
flex: none;
|
||||||
}
|
}
|
||||||
|
/* Trackers: black/white tiles with the colourful header icons; spotting spans three. */
|
||||||
|
.trackers {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.tgrid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(132px, 1fr));
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.tgrid > 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 {
|
.ico {
|
||||||
flex: none;
|
flex: none;
|
||||||
width: 16px;
|
width: 16px;
|
||||||
|
|||||||
Reference in New Issue
Block a user