Archilyzer · Source

archilyzer

Archilyzer
git clone https://archilyzer.pages.dev/source/archilyzer.git
Log | Files | Refs | README | LICENSE

commit b89deb812b1d5c70923747581afbbf079c8a7f59
parent 8cd04b1b9e220effdd4d13542ecd298dd7906224
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Sun, 28 Jun 2026 22:42:06 -0400

feat(homepage): Source fonts, minimal chrome, day/2-week Counts default

Refine the cross-site landing per design feedback.

Typography: switch to the Adobe Source superfamily — Source Serif 4
(wordmark + headline numerals), Source Sans 3 (body), Source Code Pro
(tracked micro-labels). Drop Geist + Fraunces.

Minimalism: strip the header descriptor, the hero eyebrow + subtitle (the
masthead is now just the wordmark), the numbered section eyebrows, the chart
hint, and both section headings. Chart and site-grid zones read as distinct
sections via spacing alone.

Chart: add a Day bucket (default) and a 2-week range (default), and default
Display to Counts. The recent 2-week window has comparable per-site
magnitudes, so stacked bars in real counts read clearly and represent the
bursty (campaign-style) activity honestly; Share stays as the long-range
fallback. Day series is capped to the last ~182 days to bound the embedded
summary; HOMEPAGE_SUMMARY_VERSION bumped to 3.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Diffstat:
Mcommon/components/charts/CrossSiteChart.tsx | 22+++++++++++++---------
Mcommon/lib/homepageChart.ts | 4++--
Mcommon/lib/homepageSummary.ts | 44+++++++++++++++++++++++++++++++++++++-------
Mhomepage/CHANGELOG.md | 54+++++++++++++++++++++++++++++-------------------------
Mhomepage/app/components/Header.tsx | 5+----
Mhomepage/app/components/HomeLanding.tsx | 47++++++++---------------------------------------
Mhomepage/app/globals.css | 8++++----
Mhomepage/app/layout.tsx | 23+++++++++++------------
Mhomepage/app/page.tsx | 22+++++++---------------
9 files changed, 112 insertions(+), 117 deletions(-)

diff --git a/common/components/charts/CrossSiteChart.tsx b/common/components/charts/CrossSiteChart.tsx @@ -33,12 +33,15 @@ import { const TOP_CHANNELS = 8; +type Gran = "day" | "week" | "month"; + // How many trailing buckets each range shows, per granularity. -function windowSize(range: Range, weekly: boolean): number | null { - if (range === "all") return null; - if (range === "12mo") return weekly ? 52 : 12; - return weekly ? 13 : 3; // 90d -} +const WINDOW: Record<Range, Record<Gran, number | null>> = { + "2w": { day: 14, week: 2, month: 1 }, + "90d": { day: 90, week: 13, month: 3 }, + "12mo": { day: 365, week: 52, month: 12 }, + all: { day: null, week: null, month: null }, +}; function runningSum(v: number[]): number[] { let acc = 0; @@ -66,12 +69,13 @@ export function CrossSiteChart({ }) { const { rows, series, labels } = useMemo(() => { const ms = summary.series[metric]; - const weekly = bucket !== "month"; // week + cumulative bin weekly - const src: BucketSeries = weekly ? ms.week : ms.month; const cumulative = bucket === "cumulative"; - const labelOf = weekly ? weekLabel : monthLabel; + // Day + cumulative bin on the day series; week/month on their own. + const gran: Gran = bucket === "week" ? "week" : bucket === "month" ? "month" : "day"; + const src: BucketSeries = ms[gran]; + const labelOf = gran === "month" ? monthLabel : weekLabel; - const win = windowSize(range, weekly); + const win = WINDOW[range][gran]; const start = win == null ? 0 : Math.max(0, src.buckets.length - win); const len = src.buckets.length; const pad = (vals?: number[]) => diff --git a/common/lib/homepageChart.ts b/common/lib/homepageChart.ts @@ -7,8 +7,8 @@ import type { MetricKey } from "./homepageSummary"; // Chart control axes (each a small segmented button group on the landing). export type Breakdown = "site" | "channel"; -export type Bucket = "week" | "month" | "cumulative"; -export type Range = "90d" | "12mo" | "all"; +export type Bucket = "day" | "week" | "month" | "cumulative"; +export type Range = "2w" | "90d" | "12mo" | "all"; export type Metric = MetricKey; // "transcribed" | "downloaded" export type Display = "share" | "counts"; diff --git a/common/lib/homepageSummary.ts b/common/lib/homepageSummary.ts @@ -16,7 +16,11 @@ import type { Site } from "./site"; // Two metrics are pre-binned at two granularities; Cumulative and Share (100%) // are derived client-side from these, so no extra precompute is needed. -export const HOMEPAGE_SUMMARY_VERSION = 2; +export const HOMEPAGE_SUMMARY_VERSION = 3; + +// Day buckets are capped to this many trailing days so the embedded summary stays +// small regardless of archive age (daily detail is only useful recently). +const DAY_WINDOW = 182; // channel slug -> ids of the content sites that expose it (same map // compose-homepage builds for the cross-site charts). @@ -25,8 +29,9 @@ export type ChannelSitesMap = Record<string, string[]>; export type MetricKey = "transcribed" | "downloaded"; // Per-granularity counts. `buckets` are ascending and continuous (gaps are 0): -// week buckets are Monday-anchored UTC dates ("YYYY-MM-DD"); month buckets are -// "YYYY-MM". `total`/`bySite[id]`/`byChannel[slug]` are all aligned to `buckets`. +// day buckets are UTC dates ("YYYY-MM-DD"), capped to the last DAY_WINDOW days; +// week buckets are Monday-anchored UTC dates; month buckets are "YYYY-MM". +// `total`/`bySite[id]`/`byChannel[slug]` are all aligned to `buckets`. export type BucketSeries = { buckets: string[]; total: number[]; @@ -34,7 +39,11 @@ export type BucketSeries = { byChannel: Record<string, number[]>; }; -export type MetricSeries = { week: BucketSeries; month: BucketSeries }; +export type MetricSeries = { + day: BucketSeries; + week: BucketSeries; + month: BucketSeries; +}; export type SiteMetricStat = { total: number; @@ -139,6 +148,23 @@ function weekRange(start: string, end: string): string[] { return out; } +// "YYYYMMDD" -> the calendar UTC day "YYYY-MM-DD" (or null). +function dayOf(date: string | null): string | null { + if (!date || date.length < 8) return null; + return `${date.slice(0, 4)}-${date.slice(4, 6)}-${date.slice(6, 8)}`; +} + +function dayRange(start: string, end: string): string[] { + const out: string[] = []; + const dt = new Date(`${start}T00:00:00Z`); + const endDt = new Date(`${end}T00:00:00Z`); + while (dt.getTime() <= endDt.getTime()) { + out.push(isoDate(dt)); + dt.setUTCDate(dt.getUTCDate() + 1); + } + return out; +} + // ─── helpers ─── type Attributed = { date: string; channelSlug: string; siteId: string }; @@ -149,12 +175,15 @@ function bucketize( keyOf: (date: string) => string | null, rangeOf: (min: string, max: string) => string[], nowKey: string, + floor?: string, ): BucketSeries { let min: string | null = null; for (const it of items) { const k = keyOf(it.date); if (k && (min === null || k < min)) min = k; } + // Clamp the start to `floor` (used to cap the day series to a recent window). + if (floor && (min === null || min < floor)) min = floor; const buckets = min ? rangeOf(min < nowKey ? min : nowKey, nowKey) : []; const index = new Map(buckets.map((b, i) => [b, i])); const total = new Array(buckets.length).fill(0); @@ -192,9 +221,9 @@ export function buildHomepageSummary( now: Date, ): HomepageSummary { const nowMonth = `${now.getUTCFullYear()}-${String(now.getUTCMonth() + 1).padStart(2, "0")}`; - const nowWeek = weekOf( - `${now.getUTCFullYear()}${String(now.getUTCMonth() + 1).padStart(2, "0")}${String(now.getUTCDate()).padStart(2, "0")}`, - )!; + const nowDay = isoDate(now); + const nowWeek = weekOf(nowDay.replace(/-/g, ""))!; + const dayFloor = isoDate(new Date(now.getTime() - (DAY_WINDOW - 1) * 86400000)); // Public sites only (need a link target + a stable place on the chart). const publicSites = sites.filter((s): s is Site & { siteUrl: string } => !!s.siteUrl); @@ -246,6 +275,7 @@ export function buildHomepageSummary( } const metricSeries = (items: Attributed[]): MetricSeries => ({ + day: bucketize(items, dayOf, dayRange, nowDay, dayFloor), week: bucketize(items, weekOf, weekRange, nowWeek), month: bucketize(items, monthOf, monthRange, nowMonth), }); diff --git a/homepage/CHANGELOG.md b/homepage/CHANGELOG.md @@ -7,31 +7,35 @@ from a small summary pre-computed at build (`compose-homepage` writes `public/homepage-summary.json`, embedded into the static HTML — no multi-MB client fetch). It leads with headline KPIs (transcripts, downloads, sites, - channels, hours archived), then one prominent activity chart driven by five - small segmented controls: **Metric** (Transcribed / Downloaded), **Breakdown** - (By site / By channel), **Bucket** (Week / Month / Cumulative), **Range** (90d / - 12mo / All), and **Display** (Share / Counts). It's always a stacked chart — Week - /Month are stacked bars, Cumulative a stacked area — so per-series composition - *and* the combined total read at once; **Share** (100% stacked, the default) - keeps small sites legible next to a dominant one, while the KPIs carry the - absolute totals. By-channel groups to the top-8 channels + an "Other" band. - Below the chart a responsive **site grid** links every *public* content site - (cards follow the active metric — total, "+N this month", inline-SVG sparkline — - sorted by this-month, with a **"#1 this month"** badge); in By-site the cards - double as the chart legend with Show/Hide toggles. The data universe is public - sites only (URL-less sites are hidden), each video attributed to one primary - site so combined totals stay honest. (A recent-additions feed is still built - into the summary but not rendered — kept for easy re-enable.) The `/stats` route - and the trailing "Stats" header link are gone. -- **Distinctive "archive room" visual identity.** The landing moves off the - generic zinc/white look to a committed warm-ink dark theme: an atmospheric - background (overhead brass glow + film grain), a **Fraunces** serif for the - wordmark / headline numerals / section heads paired with Geist Mono instrument - labels, a single brass/gold signature accent (eyebrows, active controls, the - "#1 this month" badge), glass panels with hairline borders, enriched card - sparklines (area fill + endpoint dot), and one orchestrated staggered page-load - reveal. The hub is dark-committed (a class-based `dark` variant, not OS-driven). - Chart entry animation is disabled so the stacked bars/areas paint immediately. + channels, hours archived), then one activity chart driven by five small + segmented controls: **Metric** (Transcribed / Downloaded), **Breakdown** (By + site / By channel), **Bucket** (Day / Week / Month / Cumulative), **Range** (2w / + 90d / 12mo / All), and **Display** (Counts / Share). Day/Week/Month are stacked + bars, Cumulative a stacked area, so per-series composition *and* the combined + total read at once. The default is **Day · 2-week · Counts** — a short recent + window where the per-site magnitudes are comparable, so real counts read clearly + (the data is bursty, which bars represent honestly); **Share** (100% stacked) + stays as the fallback for the long ranges where the historical backlog skews one + site huge. By-channel groups to the top-8 channels + an "Other" band. A + responsive **site grid** links every *public* content site (cards follow the + active metric — total, "+N this month", inline-SVG sparkline — sorted by + this-month, with a **"#1 this month"** badge); in By-site the cards double as + the chart legend with Show/Hide toggles. The data universe is public sites only + (URL-less sites hidden), each video attributed to one primary site so combined + totals stay honest. (A recent-additions feed is still built into the summary but + not rendered — kept for easy re-enable.) The `/stats` route and the trailing + "Stats" header link are gone. +- **Distinctive, minimal "archive" visual identity.** The landing commits to a + warm-ink dark theme (class-based `dark`, not OS-driven) with an atmospheric + background (overhead brass glow + film grain), the **Adobe Source superfamily** + — Source Serif 4 for the wordmark + headline numerals, Source Sans 3 for body, + Source Code Pro for the tracked uppercase micro-labels — a single brass/gold + signature accent (active controls, the "#1 this month" badge), glass panels with + hairline borders, enriched card sparklines (area fill + endpoint dot), and one + orchestrated staggered page-load reveal. Deliberately sparse "less is more" + chrome: the masthead is just the wordmark (no eyebrow/subtitle) and the chart + and site-grid zones carry no section labels — separation is by space alone. + Chart entry animation is disabled so bars/areas paint immediately. - **Hub Markdown pages dropped.** The `[slug]` doc routes, the `index`-page home body, the `PageBody` renderer, and the doc nav are removed — the hub is now a single page (re-addable later if needed). See `app/page.tsx`, the new hub diff --git a/homepage/app/components/Header.tsx b/homepage/app/components/Header.tsx @@ -7,16 +7,13 @@ export default function Header() { const config = currentHomepage(); return ( <header className="sticky top-0 z-20 border-b border-[var(--border)] bg-[rgba(12,10,8,0.72)] backdrop-blur-md"> - <div className="max-w-6xl mx-auto px-5 sm:px-6 h-14 flex items-center justify-between gap-3"> + <div className="max-w-6xl mx-auto px-5 sm:px-6 h-14 flex items-center gap-3"> <Link href="/" className="group flex items-center gap-2.5"> <span className="h-2.5 w-2.5 rotate-45 rounded-[2px] bg-[var(--accent)] shadow-[0_0_14px_var(--accent)] transition group-hover:bg-[var(--accent-strong)]" /> <span className="font-display text-[1.35rem] leading-none font-semibold tracking-tight text-[var(--foreground)]"> {config.headerTitle} </span> </Link> - <span className="hidden sm:block font-mono text-[0.6875rem] uppercase tracking-[0.22em] text-[var(--faint)]"> - Cross-site transcript archive - </span> </div> </header> ); diff --git a/homepage/app/components/HomeLanding.tsx b/homepage/app/components/HomeLanding.tsx @@ -55,25 +55,12 @@ function Segmented<T extends string>({ ); } -function Eyebrow({ index, children }: { index: string; children: string }) { - return ( - <div className="flex items-center gap-3"> - <span className="font-mono text-[0.6875rem] tracking-[0.28em] text-[var(--accent)]"> - {index} - </span> - <span className="font-mono text-[0.6875rem] uppercase tracking-[0.28em] text-[var(--faint)]"> - {children} - </span> - </div> - ); -} - export function HomeLanding({ summary }: { summary: HomepageSummary }) { const [breakdown, setBreakdown] = useState<Breakdown>("site"); - const [bucket, setBucket] = useState<Bucket>("week"); - const [range, setRange] = useState<Range>("90d"); + const [bucket, setBucket] = useState<Bucket>("day"); + const [range, setRange] = useState<Range>("2w"); const [metric, setMetric] = useState<Metric>("transcribed"); - const [display, setDisplay] = useState<Display>("share"); + const [display, setDisplay] = useState<Display>("counts"); const [hidden, setHidden] = useState<ReadonlySet<string>>(new Set()); const toggleSite = useCallback((siteId: string) => { @@ -85,10 +72,8 @@ export function HomeLanding({ summary }: { summary: HomepageSummary }) { }); }, []); - const metricNoun = metric === "downloaded" ? "Downloads" : "Transcriptions"; - return ( - <div className="flex flex-col gap-12 sm:gap-16"> + <div className="flex flex-col gap-14 sm:gap-20"> <div className="reveal" style={{ "--d": "120ms" } as React.CSSProperties}> <KpiHeader summary={summary} /> </div> @@ -97,13 +82,6 @@ export function HomeLanding({ summary }: { summary: HomepageSummary }) { className="reveal flex flex-col gap-5" style={{ "--d": "200ms" } as React.CSSProperties} > - <div className="flex flex-col gap-2"> - <Eyebrow index="01">Activity</Eyebrow> - <h2 className="font-display text-3xl sm:text-4xl font-semibold tracking-[-0.01em] text-[var(--foreground)]"> - {metricNoun} over time - </h2> - </div> - <div className="flex flex-wrap items-center gap-2.5"> <Segmented label="Metric" @@ -128,6 +106,7 @@ export function HomeLanding({ summary }: { summary: HomepageSummary }) { value={bucket} onChange={setBucket} options={[ + { value: "day", label: "Day" }, { value: "week", label: "Week" }, { value: "month", label: "Month" }, { value: "cumulative", label: "Cumulative" }, @@ -138,6 +117,7 @@ export function HomeLanding({ summary }: { summary: HomepageSummary }) { value={range} onChange={setRange} options={[ + { value: "2w", label: "2w" }, { value: "90d", label: "90d" }, { value: "12mo", label: "12mo" }, { value: "all", label: "All" }, @@ -148,8 +128,8 @@ export function HomeLanding({ summary }: { summary: HomepageSummary }) { value={display} onChange={setDisplay} options={[ - { value: "share", label: "Share" }, { value: "counts", label: "Counts" }, + { value: "share", label: "Share" }, ]} /> </div> @@ -166,23 +146,12 @@ export function HomeLanding({ summary }: { summary: HomepageSummary }) { hidden={hidden} /> </div> - {breakdown === "site" && summary.sites.length > 0 && ( - <p className="font-mono text-[0.6875rem] uppercase tracking-[0.16em] text-[var(--faint)]"> - Toggle a site card below to focus the chart - </p> - )} </section> <section - className="reveal flex flex-col gap-5" + className="reveal" style={{ "--d": "280ms" } as React.CSSProperties} > - <div className="flex flex-col gap-2"> - <Eyebrow index="02">Directory</Eyebrow> - <h2 className="font-display text-3xl sm:text-4xl font-semibold tracking-[-0.01em] text-[var(--foreground)]"> - Sites - </h2> - </div> <SiteGrid sites={summary.sites} metric={metric} diff --git a/homepage/app/globals.css b/homepage/app/globals.css @@ -44,9 +44,9 @@ --color-chart-3: var(--chart-3); --color-chart-4: var(--chart-4); --color-chart-5: var(--chart-5); - --font-sans: var(--font-geist-sans); - --font-mono: var(--font-geist-mono); - --font-display: var(--font-fraunces); + --font-sans: var(--font-source-sans); + --font-mono: var(--font-source-code); + --font-display: var(--font-source-serif); } html { @@ -55,7 +55,7 @@ html { body { color: var(--foreground); - font-family: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif; + font-family: var(--font-source-sans), ui-sans-serif, system-ui, sans-serif; -webkit-font-smoothing: antialiased; } diff --git a/homepage/app/layout.tsx b/homepage/app/layout.tsx @@ -1,27 +1,26 @@ import type { Metadata } from "next"; -import { Geist, Geist_Mono, Fraunces } from "next/font/google"; +import { Source_Serif_4, Source_Sans_3, Source_Code_Pro } from "next/font/google"; import { currentHomepage } from "./lib/homepage"; import Header from "./components/Header"; import Footer from "./components/Footer"; import "./globals.css"; -const geistSans = Geist({ - variable: "--font-geist-sans", +// Adobe Source superfamily — one coherent voice across three roles. +// Serif: the wordmark + headline numerals. Sans: body/UI. Code: tracked labels. +const sourceSerif = Source_Serif_4({ + variable: "--font-source-serif", subsets: ["latin"], + style: ["normal", "italic"], }); -const geistMono = Geist_Mono({ - variable: "--font-geist-mono", +const sourceSans = Source_Sans_3({ + variable: "--font-source-sans", subsets: ["latin"], }); -// Distinctive high-contrast serif for the wordmark, headline numerals and -// section heads — the "editorial archive" voice. -const fraunces = Fraunces({ - variable: "--font-fraunces", +const sourceCode = Source_Code_Pro({ + variable: "--font-source-code", subsets: ["latin"], - axes: ["opsz", "SOFT"], - style: ["normal", "italic"], }); export function generateMetadata(): Metadata { @@ -43,7 +42,7 @@ export default function RootLayout({ return ( <html lang="en" - className={`dark ${geistSans.variable} ${geistMono.variable} ${fraunces.variable} h-full antialiased`} + className={`dark ${sourceSerif.variable} ${sourceSans.variable} ${sourceCode.variable} h-full antialiased`} > <body className="min-h-full flex flex-col bg-[var(--background)] text-[var(--foreground)] font-sans selection:bg-[var(--accent-soft)] selection:text-[var(--accent-strong)]"> <Header /> diff --git a/homepage/app/page.tsx b/homepage/app/page.tsx @@ -7,15 +7,15 @@ import { import { currentHomepage } from "./lib/homepage"; import { HomeLanding } from "./components/HomeLanding"; -// The hub home route IS the cross-site landing: a branding line, headline KPIs, -// one big "transcriptions over time" chart with a minimal control strip, the -// site grid, and a recent-additions feed. All of it renders from a small +// The hub home route IS the cross-site landing: the wordmark, headline KPIs, one +// stacked activity chart with a minimal control strip, and the site grid — no +// section labels, kept deliberately sparse. All of it renders from a small // summary pre-computed at build (compose-homepage writes public/homepage- // summary.json), so the page is static HTML with no multi-MB client fetch. function emptySummary(): HomepageSummary { const emptyBucket = { buckets: [], total: [], bySite: {}, byChannel: {} }; - const emptyMetric = { week: emptyBucket, month: emptyBucket }; + const emptyMetric = { day: emptyBucket, week: emptyBucket, month: emptyBucket }; return { version: HOMEPAGE_SUMMARY_VERSION, generatedAt: "", @@ -50,20 +50,12 @@ export default function Home() { const config = currentHomepage(); const summary = loadSummary(); return ( - <div className="flex flex-col gap-10 sm:gap-14"> - <section className="reveal relative flex flex-col gap-4 pt-2"> - <div className="flex items-center gap-3"> - <span className="h-px w-8 rule-accent" /> - <span className="font-mono text-[0.6875rem] uppercase tracking-[0.28em] text-[var(--accent)]"> - The archive room - </span> - </div> + <div className="flex flex-col gap-12 sm:gap-16"> + <section className="reveal flex items-center gap-3.5 pt-2"> + <span className="h-3.5 w-3.5 rotate-45 rounded-[3px] bg-[var(--accent)] shadow-[0_0_18px_var(--accent)]" /> <h1 className="font-display text-5xl sm:text-7xl font-semibold leading-[0.95] tracking-[-0.02em] text-[var(--foreground)]"> {config.siteTitle} </h1> - <p className="max-w-2xl font-display text-xl sm:text-2xl italic leading-snug text-[var(--muted-foreground)]"> - {config.homeTagline || config.siteDescription} - </p> </section> <HomeLanding summary={summary} /> </div>