diff --git a/src/lib/pc/TimeSlider.svelte b/src/lib/pc/TimeSlider.svelte
index 76b9785..3c104d4 100644
--- a/src/lib/pc/TimeSlider.svelte
+++ b/src/lib/pc/TimeSlider.svelte
@@ -9,8 +9,16 @@
min,
max,
events,
+ today = max,
value = $bindable()
- }: { min: string; max: string; events: TimelineEvent[]; value: string } = $props();
+ }: {
+ min: string;
+ /** Last day on the slider; later than `today` when parts are ordered but not here yet. */
+ max: string;
+ today?: string;
+ events: TimelineEvent[];
+ value: string;
+ } = $props();
const DAY = 86_400_000;
const toDay = (d: string) => Math.round(Date.parse(`${d}T00:00:00Z`) / DAY);
@@ -22,7 +30,12 @@
const ticks = $derived(
events
.filter((e) => e.date >= min && e.date <= max)
- .map((e) => ({ ...e, day: toDay(e.date), pct: ((toDay(e.date) - lo) / span) * 100 }))
+ .map((e) => ({
+ ...e,
+ day: toDay(e.date),
+ pct: ((toDay(e.date) - lo) / span) * 100,
+ upcoming: e.date > today
+ }))
);
const years = $derived.by(() => {
const out: { y: number; pct: number }[] = [];
@@ -31,6 +44,8 @@
return out;
});
const current = $derived(toDay(value));
+ /** Where today sits on the track when the slider runs into the future (ordered parts). */
+ const nowPct = $derived(today < max ? ((toDay(today) - lo) / span) * 100 : null);
/** Latest change on or before the chosen day. */
const last = $derived([...ticks].reverse().find((t) => t.day <= current) ?? null);
@@ -66,7 +81,9 @@
+ {#if nowPct !== null}
{/if}
+ {#if nowPct !== null}
{/if}
{#each years.filter((y, i) => width > 520 || i % 2 === 0) as y (y.y)}
{y.y}
{/each}
@@ -76,6 +93,7 @@
class="tick"
class:past={t.day <= current}
class:on={t.day === current}
+ class:upcoming={t.upcoming}
style:left="{t.pct}%"
aria-label="{shortDate(t.date)}: {eventLabel(t)}"
onmouseenter={() => (hover = t)}
@@ -105,13 +123,14 @@
{shortDate(value)}
-
- {#if last}
+ {#if value > today}upcoming ·
+ {/if}{#if last}
{monthYear(last.date)} · {eventLabel(last)}
{:else}
before the first part
@@ -199,6 +218,36 @@
background: var(--site-text);
opacity: 0.9;
}
+ .tick.upcoming {
+ background: var(--site-bg);
+ box-shadow: inset 0 0 0 1.5px #db2777;
+ opacity: 1;
+ }
+ .future {
+ position: absolute;
+ top: 0.55rem;
+ right: 0;
+ height: 0.3rem;
+ border-radius: 999px;
+ background: repeating-linear-gradient(90deg, #db2777 0 3px, transparent 3px 7px);
+ opacity: 0.55;
+ }
+ .now {
+ position: absolute;
+ top: 0.2rem;
+ z-index: 1;
+ width: 2px;
+ height: 1rem;
+ margin-left: -1px;
+ border-radius: 1px;
+ background: var(--site-text);
+ opacity: 0.7;
+ pointer-events: none;
+ }
+ .soon {
+ color: #db2777;
+ font-weight: 600;
+ }
.tick.on,
.tick:hover,
.tick:focus-visible {
diff --git a/src/lib/pc/shared.test.ts b/src/lib/pc/shared.test.ts
index ced2a91..1a2fe98 100644
--- a/src/lib/pc/shared.test.ts
+++ b/src/lib/pc/shared.test.ts
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest';
-import { cmFor, sensFor, specsToText, textToSpecs } from './shared';
+import { cmFor, sensFor, specsToText, textToSpecs, usageBadges } from './shared';
describe('pc tracker helpers', () => {
it('computes CS2 sensitivity from cm/360 and real DPI', () => {
@@ -14,4 +14,16 @@ describe('pc tracker helpers', () => {
expect(textToSpecs(specsToText(specs))).toEqual(specs);
expect(textToSpecs('no colon here\n: empty key\nspeed_mts: 3600')).toEqual({ speed_mts: 3600 });
});
+ it('dates the badges of ordered and outgoing parts', () => {
+ const base = { status: 'in_use', usage: 'current', model: 'X' };
+ const ordered = { ...base, installedAt: '2026-10-28', orderDate: '2026-10-26' };
+ expect(usageBadges(ordered, '2026-10-26')).toEqual(['ordered, arriving Oct 28']);
+ expect(usageBadges(ordered, '2026-10-28')).toEqual([]);
+ expect(usageBadges({ ...base, installedAt: '2026-10-28' }, '2026-10-26')).toEqual([
+ 'from Oct 28'
+ ]);
+ const old = { status: 'retired', usage: 'previous', model: 'Y', removedAt: '2026-10-28' };
+ expect(usageBadges(old, '2026-10-26')).toEqual(['until Oct 28']);
+ expect(usageBadges(old, '2026-10-29')).toEqual(['previous']);
+ });
});
diff --git a/src/lib/pc/shared.ts b/src/lib/pc/shared.ts
index 96bc6c1..0595d0f 100644
--- a/src/lib/pc/shared.ts
+++ b/src/lib/pc/shared.ts
@@ -430,11 +430,43 @@ export type LightboxEntry = {
};
export type LightboxItem = { title: string; images: string[]; entries: LightboxEntry[] };
-export function usageBadges(p: { usage?: string | null; status: string; model: string | null }) {
+/** Today as YYYY-MM-DD (UTC, the same day the page and slider use). */
+export const todayIso = () => new Date().toISOString().slice(0, 10);
+
+/** "2026-10-28" -> "Oct 28". */
+export const monthDay = (d: string) =>
+ new Date(`${d}T12:00:00Z`).toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
+
+/**
+ * Card badges. Date-aware: a part with a future install date is "ordered, arriving " (or
+ * "from " when it is not a purchase, e.g. a relabelled drive), and a part with a future
+ * removal date is still in use, so it shows "until " instead of "previous".
+ */
+export function usageBadges(
+ p: {
+ usage?: string | null;
+ status: string;
+ model: string | null;
+ installedAt?: string | null;
+ removedAt?: string | null;
+ orderDate?: string | null;
+ },
+ today: string = todayIso()
+) {
const out: string[] = [];
- if (p.usage === 'stored' || p.status === 'spare') out.push('stored');
+ const arriving = p.installedAt && p.installedAt > today ? p.installedAt : null;
+ const leaving = p.removedAt && p.removedAt > today ? p.removedAt : null;
+ if (arriving)
+ out.push(
+ p.orderDate ? `ordered, arriving ${monthDay(arriving)}` : `from ${monthDay(arriving)}`
+ );
+ if (leaving) out.push(`until ${monthDay(leaving)}`);
+ if (!leaving && (p.usage === 'stored' || p.status === 'spare')) out.push('stored');
if (p.usage === 'work-issued') out.push('work');
- if (p.usage === 'previous' || p.usage === 'legacy-previous' || p.status === 'retired')
+ if (
+ !leaving &&
+ (p.usage === 'previous' || p.usage === 'legacy-previous' || p.status === 'retired')
+ )
out.push('previous');
if (p.usage === 'legacy') out.push('long ago');
if (p.model && /\bTBD\b/.test(p.model)) out.push('model TBD');
diff --git a/src/routes/trackers/pc/+page.svelte b/src/routes/trackers/pc/+page.svelte
index d48ec62..4eb23c5 100644
--- a/src/routes/trackers/pc/+page.svelte
+++ b/src/routes/trackers/pc/+page.svelte
@@ -2,6 +2,7 @@
import { enhance } from '$app/forms';
import { replaceState } from '$app/navigation';
import { page } from '$app/state';
+ import { untrack } from 'svelte';
import { flip } from 'svelte/animate';
import { fade } from 'svelte/transition';
import Plus from 'lucide-svelte/icons/plus';
@@ -13,6 +14,7 @@
import Sparkles from 'lucide-svelte/icons/sparkles';
import Trash2 from 'lucide-svelte/icons/trash-2';
import EyeOff from 'lucide-svelte/icons/eye-off';
+ import Truck from 'lucide-svelte/icons/truck';
import EditDialogs from '#lib/pc/EditDialogs.svelte';
import Lightbox from '#lib/pc/Lightbox.svelte';
import PcIcon from '#lib/pc/PcIcon.svelte';
@@ -69,9 +71,21 @@
])
);
const minDay = $derived(events.find((e) => e.date <= today)?.date ?? today);
+ // Ordered parts have a future install date: the slider runs on to that day so the new build
+ // can be previewed; "today" stays the default.
+ const maxDay = $derived(events.reduce((m, e) => (e.date > m ? e.date : m), today));
+ const incoming = $derived(
+ parts.filter((p) => p.orderDate && p.installedAt && p.installedAt > today && !isUndatedPast(p))
+ );
+ const incomingDay = $derived(
+ incoming.reduce(
+ (m, p) => (m === null || p.installedAt! < m ? p.installedAt : m),
+ null
+ )
+ );
// Read ?at= during SSR too, so a shared link renders that day without a flash.
const q = page.url.searchParams.get('at');
- let at = $state(isDay(q) && q <= today ? q : today);
+ let at = $state(isDay(q) && q <= untrack(() => maxDay) ? q : today);
let urlTimer: ReturnType | undefined;
let mounted = false;
$effect(() => {
@@ -274,7 +288,22 @@
-
+
+ {#if incomingDay && at < incomingDay}
+
+
+ Ordered, arriving {shortDate(incomingDay)}: {incoming.map(displayName).join(', ')}.
+ (at = incomingDay)}>preview the new build
+
+ {:else if at > today}
+
+
+ Preview of {shortDate(at)}: parts marked "ordered" have not arrived yet.
+ (at = today)}>back to today
+
+ {/if}
@@ -950,4 +979,36 @@
.link:hover {
text-decoration: underline;
}
+ .incoming {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 0.35rem 0.5rem;
+ margin-top: 0.4rem;
+ padding: 0.4rem 0.8rem;
+ border: 1px dashed color-mix(in srgb, #db2777 55%, var(--site-border));
+ border-radius: 0.8rem;
+ background: color-mix(in srgb, var(--site-surface) 92%, transparent);
+ backdrop-filter: blur(10px);
+ font-size: 0.78rem;
+ color: var(--site-muted);
+ }
+ .incoming :global(svg) {
+ color: #db2777;
+ flex-shrink: 0;
+ }
+ .incoming span {
+ flex: 1 1 16rem;
+ min-width: 0;
+ }
+ .incoming button {
+ border: 1px solid var(--site-border);
+ border-radius: 0.6rem;
+ padding: 0.15rem 0.55rem;
+ color: var(--site-text);
+ transition: background 0.15s;
+ }
+ .incoming button:hover {
+ background: var(--site-hover);
+ }
diff --git a/static/hardware/case-o11d-mini-v2/1.t.webp b/static/hardware/case-o11d-mini-v2/1.t.webp
new file mode 100644
index 0000000..c758985
Binary files /dev/null and b/static/hardware/case-o11d-mini-v2/1.t.webp differ
diff --git a/static/hardware/case-o11d-mini-v2/1.webp b/static/hardware/case-o11d-mini-v2/1.webp
new file mode 100644
index 0000000..793153f
Binary files /dev/null and b/static/hardware/case-o11d-mini-v2/1.webp differ
diff --git a/static/hardware/case-o11d-mini-v2/2.t.webp b/static/hardware/case-o11d-mini-v2/2.t.webp
new file mode 100644
index 0000000..309fa2f
Binary files /dev/null and b/static/hardware/case-o11d-mini-v2/2.t.webp differ
diff --git a/static/hardware/case-o11d-mini-v2/2.webp b/static/hardware/case-o11d-mini-v2/2.webp
new file mode 100644
index 0000000..5629aaf
Binary files /dev/null and b/static/hardware/case-o11d-mini-v2/2.webp differ
diff --git a/static/hardware/case-o11d-mini-v2/3.t.webp b/static/hardware/case-o11d-mini-v2/3.t.webp
new file mode 100644
index 0000000..88c992a
Binary files /dev/null and b/static/hardware/case-o11d-mini-v2/3.t.webp differ
diff --git a/static/hardware/case-o11d-mini-v2/3.webp b/static/hardware/case-o11d-mini-v2/3.webp
new file mode 100644
index 0000000..c46b600
Binary files /dev/null and b/static/hardware/case-o11d-mini-v2/3.webp differ
diff --git a/static/hardware/cooler-lf3-360/1.t.webp b/static/hardware/cooler-lf3-360/1.t.webp
new file mode 100644
index 0000000..60f6419
Binary files /dev/null and b/static/hardware/cooler-lf3-360/1.t.webp differ
diff --git a/static/hardware/cooler-lf3-360/1.webp b/static/hardware/cooler-lf3-360/1.webp
new file mode 100644
index 0000000..f8d24ff
Binary files /dev/null and b/static/hardware/cooler-lf3-360/1.webp differ
diff --git a/static/hardware/cooler-lf3-360/2.t.webp b/static/hardware/cooler-lf3-360/2.t.webp
new file mode 100644
index 0000000..474ae22
Binary files /dev/null and b/static/hardware/cooler-lf3-360/2.t.webp differ
diff --git a/static/hardware/cooler-lf3-360/2.webp b/static/hardware/cooler-lf3-360/2.webp
new file mode 100644
index 0000000..4404ff7
Binary files /dev/null and b/static/hardware/cooler-lf3-360/2.webp differ
diff --git a/static/hardware/cooler-lf3-360/3.t.webp b/static/hardware/cooler-lf3-360/3.t.webp
new file mode 100644
index 0000000..09483a3
Binary files /dev/null and b/static/hardware/cooler-lf3-360/3.t.webp differ
diff --git a/static/hardware/cooler-lf3-360/3.webp b/static/hardware/cooler-lf3-360/3.webp
new file mode 100644
index 0000000..84b2544
Binary files /dev/null and b/static/hardware/cooler-lf3-360/3.webp differ
diff --git a/static/hardware/cpu-9950x3d/1.t.webp b/static/hardware/cpu-9950x3d/1.t.webp
new file mode 100644
index 0000000..84fd4c6
Binary files /dev/null and b/static/hardware/cpu-9950x3d/1.t.webp differ
diff --git a/static/hardware/cpu-9950x3d/1.webp b/static/hardware/cpu-9950x3d/1.webp
new file mode 100644
index 0000000..8d8779e
Binary files /dev/null and b/static/hardware/cpu-9950x3d/1.webp differ
diff --git a/static/hardware/mb-x870e-ice/1.t.webp b/static/hardware/mb-x870e-ice/1.t.webp
new file mode 100644
index 0000000..39968a7
Binary files /dev/null and b/static/hardware/mb-x870e-ice/1.t.webp differ
diff --git a/static/hardware/mb-x870e-ice/1.webp b/static/hardware/mb-x870e-ice/1.webp
new file mode 100644
index 0000000..8f4d40b
Binary files /dev/null and b/static/hardware/mb-x870e-ice/1.webp differ
diff --git a/static/hardware/mb-x870e-ice/2.t.webp b/static/hardware/mb-x870e-ice/2.t.webp
new file mode 100644
index 0000000..20d9539
Binary files /dev/null and b/static/hardware/mb-x870e-ice/2.t.webp differ
diff --git a/static/hardware/mb-x870e-ice/2.webp b/static/hardware/mb-x870e-ice/2.webp
new file mode 100644
index 0000000..7c6fa28
Binary files /dev/null and b/static/hardware/mb-x870e-ice/2.webp differ
diff --git a/static/hardware/mb-x870e-ice/3.t.webp b/static/hardware/mb-x870e-ice/3.t.webp
new file mode 100644
index 0000000..1f693dc
Binary files /dev/null and b/static/hardware/mb-x870e-ice/3.t.webp differ
diff --git a/static/hardware/mb-x870e-ice/3.webp b/static/hardware/mb-x870e-ice/3.webp
new file mode 100644
index 0000000..26bb365
Binary files /dev/null and b/static/hardware/mb-x870e-ice/3.webp differ
diff --git a/static/hardware/ram-klevv-fitv/1.t.webp b/static/hardware/ram-klevv-fitv/1.t.webp
new file mode 100644
index 0000000..8f49754
Binary files /dev/null and b/static/hardware/ram-klevv-fitv/1.t.webp differ
diff --git a/static/hardware/ram-klevv-fitv/1.webp b/static/hardware/ram-klevv-fitv/1.webp
new file mode 100644
index 0000000..299483d
Binary files /dev/null and b/static/hardware/ram-klevv-fitv/1.webp differ
diff --git a/static/hardware/ram-klevv-fitv/2.t.webp b/static/hardware/ram-klevv-fitv/2.t.webp
new file mode 100644
index 0000000..487d2a0
Binary files /dev/null and b/static/hardware/ram-klevv-fitv/2.t.webp differ
diff --git a/static/hardware/ram-klevv-fitv/2.webp b/static/hardware/ram-klevv-fitv/2.webp
new file mode 100644
index 0000000..e8afeb5
Binary files /dev/null and b/static/hardware/ram-klevv-fitv/2.webp differ
diff --git a/static/hardware/ssd-renegade-g5/1.t.webp b/static/hardware/ssd-renegade-g5/1.t.webp
new file mode 100644
index 0000000..ec5807b
Binary files /dev/null and b/static/hardware/ssd-renegade-g5/1.t.webp differ
diff --git a/static/hardware/ssd-renegade-g5/1.webp b/static/hardware/ssd-renegade-g5/1.webp
new file mode 100644
index 0000000..be7ceee
Binary files /dev/null and b/static/hardware/ssd-renegade-g5/1.webp differ
diff --git a/static/hardware/ssd-renegade-g5/2.t.webp b/static/hardware/ssd-renegade-g5/2.t.webp
new file mode 100644
index 0000000..a0da2ea
Binary files /dev/null and b/static/hardware/ssd-renegade-g5/2.t.webp differ
diff --git a/static/hardware/ssd-renegade-g5/2.webp b/static/hardware/ssd-renegade-g5/2.webp
new file mode 100644
index 0000000..690918e
Binary files /dev/null and b/static/hardware/ssd-renegade-g5/2.webp differ