diff --git a/docs/design/observatory-ledger-design-of-record-2026-07-15.html b/docs/design/observatory-ledger-design-of-record-2026-07-15.html new file mode 100644 index 00000000..e9c4684e --- /dev/null +++ b/docs/design/observatory-ledger-design-of-record-2026-07-15.html @@ -0,0 +1,366 @@ + + +
+
+

“/” Observatory Ledger — round-2 hybrid study

+ 2026-07-15 · TIN-2413 D6 round 2 · base = round-1 Broadsheet × 07-03 Observatory Ledger · register v3 · round 1 · recovered study +
+ +
+

Masthead motion spec (evolves R4)

+

Two states crossfading: A — the real Great Blue Heron header (the existing site identity, asset static/images/header.webp from origin/main, embedded here) with the Trans Scend Survival title; B — the constellation as a night star-field statement. Dwell 14s, crossfade 1.4s. Pauses on hover/focus; explicit pause/play control (WCAG 2.2.2); under prefers-reduced-motion the oscillation never starts — heron state static, the toggle becomes a discrete no-animation switch. The masthead constellation is decorative (aria-hidden nodes); the accessible deterministic interaction (R6) is the disclosure section at the page bottom. Node model already carries kind: post | pulse — a future git kind slots in without redesign (the eventual git-activity-as-pulse “statement”), claimed nowhere in beta copy. Current hero’s scroll-fade behavior is retained.

+
+ +
+

Reductive nav call (current 7 links + wordmark + GitHub → proposed 5 + wordmark + GitHub)

+ + + + + + + + +
EntryTodayProposedWhy
wordmark→ /blogkeep/“/” is now the reader home (D4); wordmark goes home.
Blog→ /blogretireRedundant: the reader IS the front page. /blog remains the paginated archive, linked from register 02 (“full archive →”) and the footer.
Photography · Music · Making · About · CVprimarykeepReal destinations, unchanged.
Signal Boostsprimarydemote → footerGFTB convention: demoted-from-primary items live in footer nav groups.
GitHubexternal linkkeepUnchanged, external.
Orphans (/aag, /consultancy, /pulse*, /THIRD-PARTY-LICENSES…)unlinkedfooter meta / leave/pulse stays out of top-level nav per R5 (not a competing destination); licenses already footer; /aag + /consultancy = operator call, flagged.
+

Mobile drawer keeps the color-mode + theme pickers unchanged.

+
+ +
+

Sidebar reductive call

+ + + + + +
SurfaceTodayProposed
“/” (reader)ProfileSidebar absent on /blog* alreadystays absent — masthead + footer carry identity; the night rail owns the right column.
Section routes (photography/music/making/about/cv)ProfileSidebar, desktop-only, heavy (avatar, equation, bio, badges, TagCloud, co-occurrence SVG, 5 GitHub-activity widgets)slim — keep avatar/name/tagline/equation/bio/badges + TagCloud; reduce GitHub-activity widgets 5 → 2 (stats + streak); full widget set relocates to /about.
Post pagesBlogSidebarunchanged (out of scope this cycle).
+
+ +

▼ The mock — proposed “/” top to bottom

+ + +
+
mock · proposed “/” · fixture data shaped like the live 140-post / 6-pulse snapshot
+ + + +
+
+ + +
+ +
+ Great Blue Heron + +
+

Trans Scend Survival

+

Trans: Latin prefix implying “across” or “beyond” · Scend: originally nautical, the rising motion through a wave · Survival: continuing to live, in spite of.

+

Jess Sullivan — a naturalist-technologist’s log, in one system and two registers

+
+
+ + +
+ +
+
+

01 · Noteworthy 3 entries

+ +
+ + +
+
+

02 · Less-noteworthy 8 of 41 · rest in archive

+ + + + + + + + + +
06-23Less-noteworthy The tiny SSH trick for letting a USB KVM talk to a locked MacmacOS
06-14Less-noteworthy mdsvex components inside posts without brace-escape whack-a-molesveltekit
06-02Printing paperless: email-to-print on a Rocky 10 lab boxprinting
05-21Less-noteworthy Tag graphs as build artifactsmeta
05-09Field notes: soil probes, cheap ADCs, and lying datasheetsdollhouse-farm
04-28QGIS to OpenSCAD and back: parametric beds from parcel shapefilesgis
04-17Less-noteworthy A defensible fish shell promptshell
04-03Reverse-engineering the Canon G-series service protocolhardware
+ +
+ + +
+ +
+

Constellation · grouped by current category & tag metadata

+

The accessible, deterministic view behind the masthead statement — same nodes, honest label, keyboard-native disclosures. Not an embedding, not t-SNE.

+
chapel / performance · 12 entries
  1. Real-time Chapel on a scavenged Dell 7810
  2. NUMA myths, measured
+
homelab / networking · 18 entries
  1. 802.1X for the backyard
  2. NVMe scratch pools for bazel
+
gis / fieldwork · 9 entries
  1. QGIS to OpenSCAD and back
+
pulse · live · 6 items
  1. Northern cardinal, feeder post
  2. Great blue heron, retention pond
+
+ + +
+ +
+

Footer accuracy notes

+

Kept verbatim (current truths): the public-domain dedication, RSS/JSON feeds, licenses link, and the built-with-SvelteKit-&-Skeleton + source line. No copyright/org line is added — the blog has none today and needs none.

+

Adopted from greatfallstoolbus.org: the built from {short-sha} · GitHub-verified provenance line (rendered only when a real build sha is baked in — never on local/static builds), footer nav groups as the home for demoted-from-primary items, and the NOTICE/credits.ts discipline for asset provenance (the heron header currently has no recorded provenance — flagged, must be recorded before ship, not fabricated).

+

Deliberately NOT imported: tinyland.dev's brand statement, address block, or copyleft line — different site, different truths. And zero federation language anywhere on the page (R2/R11: no "federated" claims until TIN-2416's proof packet exists).

+
+ +

Shared invariants unchanged from round 1: badges only where data provides tier · pulse producer order preserved · constellation honestly labeled · Planetarium deferred · type/density hierarchy, no cards · promotion gated on TIN-2891 + R8 + operator review.

+
+ + diff --git a/docs/design/reader-ia-study-2026-07-03.html b/docs/design/reader-ia-study-2026-07-03.html new file mode 100644 index 00000000..a6f51f47 --- /dev/null +++ b/docs/design/reader-ia-study-2026-07-03.html @@ -0,0 +1,1236 @@ + + +
+
+ + +
+

tinyland.dev · design study · /posts + /pulse

+

A naturalist-technologist's log, in one system and two registers

+

+ The feed has three strata of signal, and they split into two registers. + Printed & permanent — the blog: noteworthy (deep reverse-engineering and + architecture write-ups, promoted to the top) and less-noteworthy (everyday recipes, + release notes, field observations). Live & observedpulse: Cayuga-basin + bird sightings, trail runs, and small life updates, fetched from the broker. Same sample content below, + rendered three ways — so you compare layout and motion, not palette, and pick one. +

+ +
+ Printed & permanent — serif on parchment + live & observed — mono on ink-teal +
+ +
+
+ +

Noteworthy

Deep / reverse-engineering / architecture. Hand-curated to the top.

cardinal · printed
+
+
+ +

Less-noteworthy

Recipes, release notes, casual posts. Published, not promoted.

heron · printed
+
+
+ +

Pulse

Notes, bird sightings, runs. Small, frequent, broker-fed. Motion lives here.

goldfinch · live
+
+
+ +
+
+ + + +
+ motion on · respects reduced-motion +
+
+ + +
+
+ Observatory Ledger + SSR-friendly · recommended beta default + A printed field-guide page beside a live night rail — motion only where it's genuinely live. +
+ +
+
+
+ Noteworthy + +
+
+ +

Less-noteworthy — chronological

+
+
+ + +
+ + + +
+ + +
+
+ Ticker + River + animated · attention-forward + Auto-advancing noteworthy carousel, a live pulse marquee, then a plain river of everyday posts. +
+ +

Noteworthy — carousel

+ + +
+
Pulse
+
+
+ +

Less-noteworthy — river

+
+
+ + +
+
+ Planetarium + wow · heavier + A living constellation. Time = radial distance; plumage-hue + brightness encode the tier. +
+ +
+ +
+
+
Noteworthy — bright cardinal stars
+
Less-noteworthy — dim heron stars
+
Pulse — goldfinch comets
+
+
+ +
hover a star for its title · click to zoom
tag lines = shared topics · center = now, edge = older
+
+
+
+ +

+ Static prototype · all data is sample content · pulse "liveness" is simulated (no broker attached). + Self-contained single file — system serif + mono, inline SVG/unicode only, no external requests. + Reduced-motion: carousel & ticker freeze, the pulse rail pre-fills without animation, the planetarium renders a static star map. +

+
+
+ + diff --git a/e2e/homepage.spec.ts b/e2e/homepage.spec.ts index 849707a0..532256b9 100644 --- a/e2e/homepage.spec.ts +++ b/e2e/homepage.spec.ts @@ -1,14 +1,23 @@ import { test, expect } from '@playwright/test'; -test.describe('Homepage redirect', () => { - test('/ redirects to /blog', async ({ page }) => { +// TIN-2903 ratified contract: `/` IS the canonical reader homepage (the old +// meta-refresh redirect to /blog is retired); /blog remains the archive. +test.describe('Reader homepage', () => { + test('/ serves the reader without redirecting', async ({ page }) => { await page.goto('/'); - await expect(page).toHaveURL(/\/blog/); + await expect(page).toHaveURL(/\/$/); + await expect(page.locator('section.observatory-masthead')).toBeVisible(); }); - test('/ has canonical pointing to /blog', async ({ page }) => { + test('/ has canonical pointing to the apex root', async ({ page }) => { await page.goto('/'); const canonical = page.locator('link[rel="canonical"]'); - await expect(canonical).toHaveAttribute('href', /\/blog/); + await expect(canonical).toHaveAttribute('href', 'https://transscendsurvival.org/'); + }); + + test('/blog still serves the archive listing', async ({ page }) => { + await page.goto('/blog'); + await expect(page).toHaveURL(/\/blog/); + await expect(page.getByRole('heading', { name: 'Blog' })).toBeVisible(); }); }); diff --git a/e2e/visual-qa.spec.ts b/e2e/visual-qa.spec.ts index b2a43574..9d26edb8 100644 --- a/e2e/visual-qa.spec.ts +++ b/e2e/visual-qa.spec.ts @@ -44,10 +44,13 @@ test.describe('Homepage — visual QA across viewports', () => { test('navigation is accessible', async ({ page }) => { if (vp.width >= 768) { - // Desktop nav: visible inline links (md: breakpoint = 768px) + // Desktop nav: visible inline links (md: breakpoint = 768px). + // TIN-2903 nav reduction: the Blog link is retired (wordmark → /), + // so assert a surviving primary link and the Blog link's absence. const desktopNav = page.locator('nav.hidden.md\\:flex'); await expect(desktopNav).toBeVisible(); - await expect(desktopNav.getByRole('link', { name: 'Blog' })).toBeVisible(); + await expect(desktopNav.getByRole('link', { name: 'Photography' })).toBeVisible(); + await expect(desktopNav.getByRole('link', { name: 'Blog' })).toHaveCount(0); } else { // Mobile: hamburger button present const hamburger = page.getByRole('button', { name: 'Open navigation' }); diff --git a/package-lock.json b/package-lock.json index 0c0ca786..497463b3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "packages/*" ], "dependencies": { + "@floating-ui/dom": "^1.8.0", "@fontsource/crimson-pro": "^5.2.8", "@fontsource/fira-code": "^5.2.7", "@fontsource/inter": "^5.2.8", @@ -1074,22 +1075,22 @@ } }, "node_modules/@floating-ui/core": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", - "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz", + "integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==", "license": "MIT", "dependencies": { - "@floating-ui/utils": "^0.2.11" + "@floating-ui/utils": "^0.2.12" } }, "node_modules/@floating-ui/dom": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", - "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.8.0.tgz", + "integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==", "license": "MIT", "dependencies": { - "@floating-ui/core": "^1.7.5", - "@floating-ui/utils": "^0.2.11" + "@floating-ui/core": "^1.8.0", + "@floating-ui/utils": "^0.2.12" } }, "node_modules/@floating-ui/react": { @@ -1123,9 +1124,9 @@ } }, "node_modules/@floating-ui/utils": { - "version": "0.2.11", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", - "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz", + "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==", "license": "MIT" }, "node_modules/@fontsource/crimson-pro": { diff --git a/package.json b/package.json index c79de2f3..d53332d4 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,7 @@ "vitest": "^4.1.4" }, "dependencies": { + "@floating-ui/dom": "^1.8.0", "@fontsource/crimson-pro": "^5.2.8", "@fontsource/fira-code": "^5.2.7", "@fontsource/inter": "^5.2.8", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7946803c..07bbfdda 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@floating-ui/dom': + specifier: ^1.8.0 + version: 1.8.0 '@fontsource/crimson-pro': specifier: ^5.2.8 version: 5.2.8 @@ -586,11 +589,11 @@ packages: resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@floating-ui/core@1.7.5': - resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} + '@floating-ui/core@1.8.0': + resolution: {integrity: sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==} - '@floating-ui/dom@1.7.6': - resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==} + '@floating-ui/dom@1.8.0': + resolution: {integrity: sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==} '@floating-ui/react-dom@2.1.8': resolution: {integrity: sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==} @@ -613,6 +616,9 @@ packages: '@floating-ui/utils@0.2.11': resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} + '@floating-ui/utils@0.2.12': + resolution: {integrity: sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==} + '@fontsource/crimson-pro@5.2.8': resolution: {integrity: sha512-t+cAoAswUKEReDPqDNWWSyAp7baZkKrtF6Ry0KrDZQHJCldS9pIbRH0n2hm1u4+SbIUI7c+nUS9yfK6rEhycTw==} @@ -5944,18 +5950,18 @@ snapshots: '@eslint/core': 0.17.0 levn: 0.4.1 - '@floating-ui/core@1.7.5': + '@floating-ui/core@1.8.0': dependencies: - '@floating-ui/utils': 0.2.11 + '@floating-ui/utils': 0.2.12 - '@floating-ui/dom@1.7.6': + '@floating-ui/dom@1.8.0': dependencies: - '@floating-ui/core': 1.7.5 - '@floating-ui/utils': 0.2.11 + '@floating-ui/core': 1.8.0 + '@floating-ui/utils': 0.2.12 '@floating-ui/react-dom@2.1.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@floating-ui/dom': 1.7.6 + '@floating-ui/dom': 1.8.0 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) @@ -5977,6 +5983,8 @@ snapshots: '@floating-ui/utils@0.2.11': {} + '@floating-ui/utils@0.2.12': {} + '@fontsource/crimson-pro@5.2.8': {} '@fontsource/fira-code@5.2.7': {} @@ -8263,7 +8271,7 @@ snapshots: '@zag-js/popper@1.39.1': dependencies: - '@floating-ui/dom': 1.7.6 + '@floating-ui/dom': 1.8.0 '@zag-js/dom-query': 1.39.1 '@zag-js/utils': 1.39.1 diff --git a/src/app.d.ts b/src/app.d.ts index da08e6da..136ae16d 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -1,6 +1,10 @@ // See https://svelte.dev/docs/kit/types#app.d.ts // for information about these interfaces declare global { + // Public build SHA injected by Vite `define` (see vite.config.ts). Empty + // string when no real build sha is present. + const __BUILD_SHA__: string; + namespace App { // interface Error {} // interface Locals {} diff --git a/src/lib/components/BlogCard.svelte b/src/lib/components/BlogCard.svelte index 62e6f89f..ea4f7e44 100644 --- a/src/lib/components/BlogCard.svelte +++ b/src/lib/components/BlogCard.svelte @@ -1,5 +1,6 @@ + +{#if tier} + + {#if tier === 'noteworthy'} + + {/if} + {TIER_LABELS[tier]} + +{/if} diff --git a/src/lib/components/TierBadge.test.ts b/src/lib/components/TierBadge.test.ts new file mode 100644 index 00000000..2c9260aa --- /dev/null +++ b/src/lib/components/TierBadge.test.ts @@ -0,0 +1,74 @@ +import { describe, expect, it } from 'vitest'; +import { render } from 'svelte/server'; +import type { Post } from '$lib/types'; +import { POST_EDITORIAL_TIERS } from '$lib/types'; +import TierBadge from './TierBadge.svelte'; +import BlogCard from './BlogCard.svelte'; + +describe('TierBadge', () => { + it('renders a labelled badge for the noteworthy tier', () => { + const { html } = render(TierBadge, { props: { tier: 'noteworthy' } }); + + expect(html).toContain('data-testid="tier-badge"'); + expect(html).toContain('data-tier="noteworthy"'); + expect(html).toContain('Noteworthy'); + expect(html).toContain('aria-label="Editorial tier: Noteworthy"'); + }); + + it('renders a labelled badge for the less-noteworthy tier', () => { + const { html } = render(TierBadge, { props: { tier: 'less-noteworthy' } }); + + expect(html).toContain('data-testid="tier-badge"'); + expect(html).toContain('data-tier="less-noteworthy"'); + expect(html).toContain('Less noteworthy'); + expect(html).toContain('aria-label="Editorial tier: Less noteworthy"'); + }); + + it('renders a badge with an aria-label for every declared tier', () => { + for (const tier of POST_EDITORIAL_TIERS) { + const { html } = render(TierBadge, { props: { tier } }); + expect(html).toContain('aria-label="Editorial tier:'); + expect(html).toContain(`data-tier="${tier}"`); + } + }); + + it('renders nothing when the tier is undefined', () => { + const { html } = render(TierBadge, { props: { tier: undefined } }); + + expect(html).not.toContain('tier-badge'); + expect(html).not.toContain('aria-label'); + expect(html.replace(//g, '').trim()).toBe(''); + }); +}); + +function makePost(overrides: Partial = {}): Post { + return { + title: 'A Test Post', + slug: 'a-test-post', + date: '2026-03-04', + description: 'A description for the card.', + tags: ['test'], + published: true, + ...overrides, + }; +} + +describe('BlogCard editorial tier surface', () => { + it('shows the tier badge when the post carries a tier', () => { + const { html } = render(BlogCard, { + props: { post: makePost({ editorial_tier: 'noteworthy' }) }, + }); + + expect(html).toContain('data-testid="tier-badge"'); + expect(html).toContain('aria-label="Editorial tier: Noteworthy"'); + }); + + it('omits the tier badge entirely when the post has no tier', () => { + const { html } = render(BlogCard, { + props: { post: makePost() }, + }); + + expect(html).toContain('A Test Post'); + expect(html).not.toContain('tier-badge'); + }); +}); diff --git a/src/lib/components/pulse/PulseBirdCard.svelte b/src/lib/components/pulse/PulseBirdCard.svelte index a455a119..7cd57c58 100644 --- a/src/lib/components/pulse/PulseBirdCard.svelte +++ b/src/lib/components/pulse/PulseBirdCard.svelte @@ -1,5 +1,6 @@
-
- 🐦 bird sighting +
+ + 🐦 bird sighting + + +
{#if sighting} diff --git a/src/lib/components/pulse/PulseCards.test.ts b/src/lib/components/pulse/PulseCards.test.ts index 5d5b8a4e..2578a2a6 100644 --- a/src/lib/components/pulse/PulseCards.test.ts +++ b/src/lib/components/pulse/PulseCards.test.ts @@ -78,6 +78,30 @@ describe('Pulse cards', () => { }); }); +describe('Pulse card editorial salience surface', () => { + it('shows a TierBadge on a note card only when salience is present', () => { + const withSalience = render(PulseNoteCard, { + props: { item: { ...noteItem, salience: 'noteworthy' } }, + }); + expect(withSalience.html).toContain('data-testid="tier-badge"'); + expect(withSalience.html).toContain('aria-label="Editorial tier: Noteworthy"'); + + const withoutSalience = render(PulseNoteCard, { props: { item: noteItem } }); + expect(withoutSalience.html).not.toContain('tier-badge'); + }); + + it('shows a TierBadge on a bird card only when salience is present', () => { + const withSalience = render(PulseBirdCard, { + props: { item: { ...birdItem, salience: 'less-noteworthy' } }, + }); + expect(withSalience.html).toContain('data-testid="tier-badge"'); + expect(withSalience.html).toContain('data-tier="less-noteworthy"'); + + const withoutSalience = render(PulseBirdCard, { props: { item: birdItem } }); + expect(withoutSalience.html).not.toContain('tier-badge'); + }); +}); + describe('Pulse lab policy result rendering', () => { it('renders allowed projection decisions', () => { const decision: PolicyDecision = { allowed: true, item: noteItem }; diff --git a/src/lib/components/pulse/PulseNoteCard.svelte b/src/lib/components/pulse/PulseNoteCard.svelte index c516fbc2..93f8c5ef 100644 --- a/src/lib/components/pulse/PulseNoteCard.svelte +++ b/src/lib/components/pulse/PulseNoteCard.svelte @@ -1,5 +1,6 @@
-
- 💬 note +
+ + 💬 note + + +

{item.content}

diff --git a/src/lib/components/reader/ConstellationSection.svelte b/src/lib/components/reader/ConstellationSection.svelte new file mode 100644 index 00000000..de39251e --- /dev/null +++ b/src/lib/components/reader/ConstellationSection.svelte @@ -0,0 +1,67 @@ + + +{#if groups.length > 0} +
+

+ Constellation · grouped by current category & tag metadata +

+

+ The accessible, deterministic view behind the masthead — the same nodes, an honest label, keyboard-native + disclosures. Not an embedding, not t-SNE. +

+ + {#each groups as group (group.key)} +
+ + {group.label} + · {group.nodes.length} {group.nodes.length === 1 ? 'entry' : 'entries'} + +
    + {#each group.nodes as node (node.id)} +
  1. + {node.label} + + {#if node.date} + + {/if} +
  2. + {/each} +
+
+ {/each} +
+{/if} + + diff --git a/src/lib/components/reader/ConstellationSection.test.ts b/src/lib/components/reader/ConstellationSection.test.ts new file mode 100644 index 00000000..c89855ae --- /dev/null +++ b/src/lib/components/reader/ConstellationSection.test.ts @@ -0,0 +1,38 @@ +import { describe, expect, it } from 'vitest'; +import { render } from 'svelte/server'; +import type { Post } from '$lib/types'; +import { buildConstellationGroups } from '$lib/reader/ledger'; +import ConstellationSection from './ConstellationSection.svelte'; + +function makePost(i: number, tier?: Post['editorial_tier']): Post { + return { + slug: `post-${i}`, + title: `Entry number ${i}`, + date: `2026-06-${String((i % 27) + 1).padStart(2, '0')}`, + tags: ['hardware'], + category: 'hardware', + content: '', + reading_time: 4, + ...(tier ? { editorial_tier: tier } : {}), + } as Post; +} + +describe('ConstellationSection (accessible counterpart axes)', () => { + it('carries all three canvas axes: topic (grouping), tier (badge), time (