trackers: colourful tracker icons on /trackers and home tiles, 3/4-column desktop grid with lift hover
All checks were successful
Deploy production / deploy (push) Successful in 1m9s

This commit is contained in:
2026-10-25 11:27:56 +00:00
parent 704880dc98
commit 5226ed7fec
3 changed files with 108 additions and 32 deletions

View File

@@ -1,59 +1,81 @@
<script lang="ts">
// Card grid shared by the home page, /projects and /trackers.
// `variant="tracker"` uses the colourful tracker icons (same source as the header's
// Trackers dropdown: #lib/tracker-icons) and, with `wide`, a 3/4-column desktop grid.
import { resolve } from '$app/paths';
import ArrowRight from 'lucide-svelte/icons/arrow-right';
import ExternalLink from 'lucide-svelte/icons/external-link';
import * as Card from '#lib/components/ui/card/index.js';
import ProjectIcon from '#lib/components/ProjectIcon.svelte';
import TrackerIcon from '#lib/components/TrackerIcon.svelte';
type Item = { name: string; subtitle: string; href: string; external: boolean };
let {
items,
subtitleClass = 'mt-1 block text-sm text-muted-foreground'
}: { items: readonly Item[]; subtitleClass?: string } = $props();
subtitleClass = 'mt-1 block text-sm text-muted-foreground',
variant = 'project',
wide = false
}: {
items: readonly Item[];
subtitleClass?: string;
variant?: 'project' | 'tracker';
wide?: boolean;
} = $props();
const tracker = $derived(variant === 'tracker');
</script>
<ul class="grid gap-3 sm:grid-cols-2">
{#snippet body(project: Item)}
<span class="flex min-w-0 items-start gap-3">
{#if tracker}
<TrackerIcon name={project.name} class="catalog-icon mt-0.5 size-10" />
{:else}
<span
class="mt-0.5 flex size-10 shrink-0 items-center justify-center rounded-md border border-border bg-[var(--site-surface)] text-[var(--site-muted)]"
>
<ProjectIcon name={project.name} class="size-5" />
</span>
{/if}
<span class="min-w-0"
><span class="block font-medium">{project.name}</span><span class={subtitleClass}
>{project.subtitle}</span
></span
>
</span>
{#if project.external}
<ExternalLink
size={18}
aria-hidden="true"
class="catalog-arrow shrink-0 text-[var(--site-muted)]"
/>
{:else}
<ArrowRight
size={18}
aria-hidden="true"
class="catalog-arrow shrink-0 text-[var(--site-muted)]"
/>
{/if}
{/snippet}
<ul class="grid gap-3 sm:grid-cols-2" class:wide>
{#each items as project (project.name)}
<li>
<Card.Root class="h-full py-0 transition-colors hover:bg-muted/50"
><Card.Content class="p-0">
<Card.Root class="catalog-card h-full py-0 transition-colors hover:bg-muted/50"
><Card.Content class="h-full p-0">
{#if project.external}
<a
href={project.href}
rel="external"
class="flex min-h-28 items-start justify-between gap-4 p-5"
class="flex h-full min-h-28 items-start justify-between gap-4 p-5"
>
<span class="flex min-w-0 items-start gap-3">
<span
class="mt-0.5 flex size-10 shrink-0 items-center justify-center rounded-md border border-border bg-[var(--site-surface)] text-[var(--site-muted)]"
>
<ProjectIcon name={project.name} class="size-5" />
</span>
<span class="min-w-0"
><span class="block font-medium">{project.name}</span><span class={subtitleClass}
>{project.subtitle}</span
></span
>
</span><ExternalLink size={18} aria-hidden="true" class="text-[var(--site-muted)]" />
{@render body(project)}
</a>
{:else}
<a
href={resolve(project.href as '/')}
class="flex min-h-28 items-start justify-between gap-4 p-5"
class="flex h-full min-h-28 items-start justify-between gap-4 p-5"
>
<span class="flex min-w-0 items-start gap-3">
<span
class="mt-0.5 flex size-10 shrink-0 items-center justify-center rounded-md border border-border bg-[var(--site-surface)] text-[var(--site-muted)]"
>
<ProjectIcon name={project.name} class="size-5" />
</span>
<span class="min-w-0"
><span class="block font-medium">{project.name}</span><span class={subtitleClass}
>{project.subtitle}</span
></span
>
</span><ArrowRight size={18} aria-hidden="true" class="text-[var(--site-muted)]" />
{@render body(project)}
</a>
{/if}
</Card.Content></Card.Root
@@ -61,3 +83,56 @@
</li>
{/each}
</ul>
<style>
@media (min-width: 768px) {
.wide {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
@media (min-width: 1024px) {
.wide {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
}
ul :global(.catalog-card) {
transition:
transform 160ms cubic-bezier(0.2, 0.8, 0.2, 1),
background-color 160ms ease,
border-color 160ms ease,
box-shadow 160ms ease;
}
ul :global(.catalog-card:hover) {
border-color: color-mix(in oklab, var(--foreground) 22%, transparent);
transform: translateY(-2px);
box-shadow: 0 10px 24px -14px rgb(0 0 0 / 0.5);
}
ul :global(.catalog-card:active) {
transform: translateY(0);
box-shadow: none;
}
ul :global(.catalog-icon),
ul :global(.catalog-arrow) {
transition: transform 160ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
ul :global(.catalog-card:hover .catalog-icon) {
transform: scale(1.06);
}
ul :global(.catalog-card:hover .catalog-arrow) {
transform: translateX(2px);
}
@media (prefers-reduced-motion: reduce) {
ul :global(.catalog-card),
ul :global(.catalog-icon),
ul :global(.catalog-arrow) {
transition:
background-color 160ms ease,
border-color 160ms ease;
}
ul :global(.catalog-card:hover),
ul :global(.catalog-card:hover .catalog-icon),
ul :global(.catalog-card:hover .catalog-arrow) {
transform: none;
}
}
</style>

View File

@@ -139,6 +139,7 @@
</h2>
<CatalogGrid
items={visibleTrackers(data.isOwner)}
variant="tracker"
subtitleClass="mt-1 block text-base text-[var(--site-muted)]"
/>
</section>

View File

@@ -10,5 +10,5 @@
<p class="mb-10 text-[var(--site-muted)]">
Personal data I keep track of: games, money, houses, books, prices, animals and lists.
</p>
<CatalogGrid items={visibleTrackers(data.isOwner)} />
<CatalogGrid items={visibleTrackers(data.isOwner)} variant="tracker" wide />
</div>