From 8a3ba5bfd92ddbd017cb886292a498f967cc4106 Mon Sep 17 00:00:00 2001 From: Ada Date: Sun, 19 Apr 2026 20:44:52 -0400 Subject: [PATCH 1/6] =?UTF-8?q?=F0=9F=8C=89=20ui:=20rename=20relay=20info?= =?UTF-8?q?=20box=20to=20"Liminal=20Bridge"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cosmetic update to match the terminology pass the rest of the stack already went through (lab-api + RELAY_IMPLEMENTATION.md). The single-use relay URL system is now consistently called the Liminal Bridge everywhere. - "๐Ÿ›๏ธ About Relay URLs" โ†’ "๐ŸŒ‰ About Liminal Bridge" Co-authored-by: Sage --- src/pages/admin/pages/AdminRelaysPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/admin/pages/AdminRelaysPage.tsx b/src/pages/admin/pages/AdminRelaysPage.tsx index 5893e58..31676ca 100644 --- a/src/pages/admin/pages/AdminRelaysPage.tsx +++ b/src/pages/admin/pages/AdminRelaysPage.tsx @@ -322,7 +322,7 @@ export function AdminRelaysPage() { {/* Info Box */}

- ๐Ÿ›๏ธ About Relay URLs + ๐ŸŒ‰ About Liminal Bridge

  • โ€ข Each relay URL is single-use and time-limited
  • From d5972f91ef899382e4f2b64a5ad2e5adc730b1b7 Mon Sep 17 00:00:00 2001 From: Ada Date: Sun, 19 Apr 2026 20:45:44 -0400 Subject: [PATCH 2/6] =?UTF-8?q?=F0=9F=93=9D=20content:=20retire=20/lab-not?= =?UTF-8?q?es,=20redirect=20to=20notebook=20subdomain?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Long-form lab notes now live at notebook.thehumanpatternlab.com. The public marketing site no longer needs a local lab-notes surface โ€” everything redirects to the subdomain and the dead code is removed. Changes: - Router: all four /lab-notes routes (en/ko ร— list/detail) now render a NotebookRedirect component that window.location.replaces to the subdomain - Nav: Header "Lab Notes" pill โ†’ "Notebook" external + ExternalLink icon (same pattern for the existing "Docs" external item so external linking is visually consistent) - Footer "Lab Notes" link โ†’ external "Notebook" with icon - Hero CTA "View Lab Notes" โ†’ external "Read the Notebook" - Home: drop RecentLabNotesPreview section entirely - Delete 14 dead files: LabNotesPage, LabNoteDetailPage, LabNoteCard + skeletons, RecentLabNotesPreview + css + skeleton, labNotes/notesIndex libs, labNotesClient, data/labNotes, data/labNote - Delete now-empty src/components/labnotes/ directory - i18n: drop labNotesPage namespace wiring, delete both pages.labNotes.json locale files, rename nav.lab-notes โ†’ nav.notebook (en + ko), remove sections.labNotes from pages.home (en + ko) - .gitignore: add .claude/ (Claude memory scratch dir) - Tests updated for the new nav + CTA labels Co-authored-by: Sage --- .gitignore | 2 + src/__tests__/Header.test.tsx | 4 +- src/__tests__/Hero.test.tsx | 2 +- src/api/labNotesClient.ts | 70 ----- src/components/home/Hero.tsx | 14 +- .../home/RecentLabNotesPreview.module.css | 199 ------------ src/components/home/RecentLabNotesPreview.tsx | 119 -------- .../home/RecentLabNotesSkeleton.tsx | 46 --- src/components/labnotes/LabNoteCard.tsx | 285 ------------------ .../labnotes/LabNoteCardSkeleton.tsx | 32 -- .../labnotes/LabNotesGridSkeleton.tsx | 25 -- src/components/labnotes/labNoteCard.styles.ts | 134 -------- src/components/layout/Footer.tsx | 11 +- src/components/layout/Header.tsx | 6 +- src/data/labNote.ts | 38 --- src/data/labNotes.ts | 96 ------ src/i18n/index.ts | 5 - src/i18n/locales/en/common.json | 2 +- src/i18n/locales/en/pages.home.json | 4 - src/i18n/locales/en/pages.labNotes.json | 7 - src/i18n/locales/ko/common.json | 2 +- src/i18n/locales/ko/pages.home.json | 4 - src/i18n/locales/ko/pages.labNotes.json | 7 - src/lib/labNotes.ts | 267 ---------------- src/lib/notesIndex.ts | 28 -- src/pages/HomePage.tsx | 2 - src/pages/LabNoteDetailPage.tsx | 236 --------------- src/pages/LabNotesPage.tsx | 91 ------ src/router/routes.tsx | 23 +- 29 files changed, 42 insertions(+), 1719 deletions(-) delete mode 100644 src/api/labNotesClient.ts delete mode 100644 src/components/home/RecentLabNotesPreview.module.css delete mode 100644 src/components/home/RecentLabNotesPreview.tsx delete mode 100644 src/components/home/RecentLabNotesSkeleton.tsx delete mode 100644 src/components/labnotes/LabNoteCard.tsx delete mode 100644 src/components/labnotes/LabNoteCardSkeleton.tsx delete mode 100644 src/components/labnotes/LabNotesGridSkeleton.tsx delete mode 100644 src/components/labnotes/labNoteCard.styles.ts delete mode 100644 src/data/labNote.ts delete mode 100644 src/data/labNotes.ts delete mode 100644 src/i18n/locales/en/pages.labNotes.json delete mode 100644 src/i18n/locales/ko/pages.labNotes.json delete mode 100644 src/lib/labNotes.ts delete mode 100644 src/lib/notesIndex.ts delete mode 100644 src/pages/LabNoteDetailPage.tsx delete mode 100644 src/pages/LabNotesPage.tsx diff --git a/.gitignore b/.gitignore index df1e0d8..826d7a2 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,5 @@ dist-ssr *.sw? /.env.development /.env.production + +.claude/ diff --git a/src/__tests__/Header.test.tsx b/src/__tests__/Header.test.tsx index 7b34e7b..9f24778 100644 --- a/src/__tests__/Header.test.tsx +++ b/src/__tests__/Header.test.tsx @@ -16,7 +16,7 @@ const labels = [ "Home", "About", "Departments", - "Lab Notes", + "Notebook", "Docs", "Videos", "Content Use", @@ -73,7 +73,7 @@ describe("TopNav (Site Header)", () => { const q = within(mobile); // now that it's open, the mobile links should be queryable - expect(q.getByRole("link", { name: "Lab Notes" })).toBeInTheDocument(); + expect(q.getByRole("link", { name: "Notebook" })).toBeInTheDocument(); await user.keyboard("{Escape}"); expect(toggle).toHaveAttribute("aria-expanded", "false"); diff --git a/src/__tests__/Hero.test.tsx b/src/__tests__/Hero.test.tsx index e4dbacf..eeda60a 100644 --- a/src/__tests__/Hero.test.tsx +++ b/src/__tests__/Hero.test.tsx @@ -37,7 +37,7 @@ describe("Hero", () => { ).toBeInTheDocument(); expect( - screen.getByRole("link", { name: /view lab notes/i }) + screen.getByRole("link", { name: /read the notebook/i }) ).toBeInTheDocument(); }); }); diff --git a/src/api/labNotesClient.ts b/src/api/labNotesClient.ts deleted file mode 100644 index 1840ab8..0000000 --- a/src/api/labNotesClient.ts +++ /dev/null @@ -1,70 +0,0 @@ -import {apiBaseUrl} from "@/api/api"; - -export type LabNoteStatus = "draft" | "published" | "archived"; -export type LabNoteType = "labnote" | "paper" | "memo"; -export type AuthorKind = "human" | "ai" | "hybrid"; - -export interface LabNoteAuthor { - kind: AuthorKind; - name?: string; - id?: string; -} - -export interface LabNote { - id: string; - slug: string; // NEW (if API provides it) - title: string; - subtitle?: string; // already present - summary?: string; // NEW (if API provides it) - - // content - contentHtml: string; - - // publishing + metadata - published: string; // "" or "YYYY-MM-DD" - status?: LabNoteStatus; // NEW (can be derived server-side) - type?: LabNoteType; // NEW - dept?: string; // NEW (human readable) - card_style?: string; - department_id: string; - locale?: "en" | "ko"; // NEW - - // flags - shadow_density: number; - safer_landing: boolean; - - // taxonomy - tags: string[]; - readingTime: number; - - // authorship - author?: LabNoteAuthor; // NEW -} - -function unwrap(payload: unknown): T { - // Envelope form: { ok: true, data: ... } - if (payload && typeof payload === "object" && (payload as any).ok === true) { - return (payload as any).data as T; - } - // Raw form: [...] or {...} - return payload as T; -} - -export async function fetchLabNotes(signal?: AbortSignal): Promise { - const res = await fetch(`${apiBaseUrl}/lab-notes`, { signal }); - - if (!res.ok) { - const text = await res.text().catch(() => ""); - throw new Error(`Failed to fetch lab notes (${res.status}): ${text}`); - } - - const payload = await res.json(); - const data = unwrap(payload); - - // Optional safety: reject non-arrays early - if (!Array.isArray(data)) { - throw new Error("Unexpected lab-notes response shape (expected array)."); - } - - return data; -} diff --git a/src/components/home/Hero.tsx b/src/components/home/Hero.tsx index 75f6f65..a11a793 100644 --- a/src/components/home/Hero.tsx +++ b/src/components/home/Hero.tsx @@ -23,6 +23,7 @@ // src/components/home/Hero.tsx import { Link } from "react-router-dom"; +import { ExternalLink } from "lucide-react"; import EmotionalWeatherCard from "@/components/ewu/EmotionalWeatherCard"; export function Hero() { return ( @@ -56,12 +57,15 @@ export function Hero() { > Enter the Lab - - View Lab Notes - + Read the Notebook +
diff --git a/src/components/home/RecentLabNotesPreview.module.css b/src/components/home/RecentLabNotesPreview.module.css deleted file mode 100644 index 7a4828f..0000000 --- a/src/components/home/RecentLabNotesPreview.module.css +++ /dev/null @@ -1,199 +0,0 @@ -/* Wrapper */ -.recentLabNotes { - margin-block: 3rem; -} - -/* Header row */ -.recentLabNotesHeader { - display: flex; - justify-content: space-between; - align-items: baseline; - gap: 1rem; - margin-bottom: 1.5rem; -} - -.recentLabNotesTitle { - font-size: 1.4rem; - font-weight: 650; -} - -.recentLabNotesSubtitle { - font-size: 0.95rem; - opacity: 0.8; -} - -/* "View all notes โ†’" */ -.recentLabNotesLink { - font-size: 0.9rem; - font-weight: 500; - text-decoration: none; - color: var(--ifm-color-primary); - display: inline-flex; - align-items: center; - gap: 0.25rem; - transition: transform 150ms ease, opacity 150ms ease; -} - -.recentLabNotesLink:hover { - transform: translateX(2px); - opacity: 0.9; -} - -/* Card grid */ -.recentLabNotesGrid { - display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 1.25rem; -} - -@media (max-width: 992px) { - .recentLabNotesGrid { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } -} - -@media (max-width: 700px) { - .recentLabNotesGrid { - grid-template-columns: minmax(0, 1fr); - } -} - -/* Individual card */ -.recentLabNotesCard { - position: relative; - padding: 1.4rem 1.5rem 1.2rem; - border-radius: 1.25rem; - background: radial-gradient(circle at top left, rgba(90, 230, 255, 0.08), transparent 55%), - radial-gradient(circle at bottom right, rgba(186, 104, 255, 0.08), transparent 55%), - rgba(4, 10, 25, 0.98); - border: 1px solid rgba(255, 255, 255, 0.06); - box-shadow: - 0 18px 45px rgba(0, 0, 0, 0.75), - 0 0 0 1px rgba(255, 255, 255, 0.02); - backdrop-filter: blur(12px); - transition: - transform 160ms ease-out, - box-shadow 160ms ease-out, - border-color 160ms ease-out, - background 200ms ease-out; - overflow: hidden; -} - -/* Neon edge glow */ -.recentLabNotesCard::before { - content: ""; - position: absolute; - inset: 0; - border-radius: inherit; - background: linear-gradient( - 135deg, - rgba(0, 255, 200, 0.18), - rgba(120, 130, 255, 0.08), - rgba(255, 120, 220, 0.16) - ); - opacity: 0; - pointer-events: none; - mix-blend-mode: screen; - transition: opacity 200ms ease-out; -} - -.recentLabNotesCard:hover { - transform: translateY(-4px); - border-color: rgba(0, 255, 200, 0.45); - box-shadow: - 0 22px 55px rgba(0, 0, 0, 0.85), - 0 0 25px rgba(0, 255, 200, 0.25); -} - -.recentLabNotesCard:hover::before { - opacity: 0.7; -} - -/* Top row: tag + date */ -.recentLabNotesMetaRow { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 0.9rem; -} - -/* Category pill */ -.recentLabNotesTag { - font-size: 0.7rem; - letter-spacing: 0.12em; - text-transform: uppercase; - padding: 0.25rem 0.7rem; - border-radius: 999px; - border: 1px solid rgba(0, 255, 200, 0.4); - background: radial-gradient(circle at top left, rgba(0, 255, 200, 0.24), transparent 60%); - color: rgba(210, 255, 245, 0.94); - display: inline-flex; - align-items: center; - gap: 0.35rem; - white-space: nowrap; -} - -/* You can add small icons someday if you want */ -/* .recentLabNotesTagIcon { font-size: 0.9em; } */ - -.recentLabNotesDate { - font-size: 0.75rem; - opacity: 0.7; -} - -/* Title + summary */ -.recentLabNotesCardTitle { - font-size: 1.05rem; - font-weight: 640; - margin-bottom: 0.4rem; -} - -.recentLabNotesCardExcerpt { - font-size: 0.9rem; - line-height: 1.5; - opacity: 0.85; - margin-bottom: 1rem; -} - -/* Divider */ -.recentLabNotesDivider { - height: 1px; - width: 100%; - margin-bottom: 0.75rem; - background: linear-gradient( - 90deg, - transparent, - rgba(255, 255, 255, 0.18), - transparent - ); -} - -.recentLabNotesFooter { - display: flex; - justify-content: space-between; - align-items: center; - font-size: 0.8rem; - color: rgba(255, 255, 255, 0.7); -} - -.recentLabNotesReadTime { - opacity: 0.8; -} - -/* "Read note โ†’" */ -.recentLabNotesReadLink { - display: inline-flex; - align-items: center; - gap: 0.25rem; - text-decoration: none; - font-weight: 500; - font-size: 0.82rem; - color: var(--ifm-color-primary); - transition: transform 160ms ease, color 160ms ease, opacity 160ms ease; -} - -.recentLabNotesReadLink:hover { - transform: translateX(3px); - color: #5bffe3; /* or keep var(--ifm-color-primary) if you prefer */ - opacity: 0.95; -} diff --git a/src/components/home/RecentLabNotesPreview.tsx b/src/components/home/RecentLabNotesPreview.tsx deleted file mode 100644 index db905db..0000000 --- a/src/components/home/RecentLabNotesPreview.tsx +++ /dev/null @@ -1,119 +0,0 @@ -// src/components/home/RecentLabNotesPreview.tsx -import React, { useEffect, useState } from "react"; -import { Link } from "react-router-dom"; -import type { LabNote } from "@/lib/labNotes"; -import { getNotesIndex } from "@/lib/notesIndex"; -import { RecentLabNotesSkeleton } from "@/components/home/RecentLabNotesSkeleton"; - -export function RecentLabNotesPreview() { - const [recent, setRecent] = useState([]); - const [error, setError] = useState(null); - const [loading, setLoading] = useState(true); - - useEffect(() => { - const controller = new AbortController(); - let alive = true; - - (async () => { - setLoading(true); - setError(null); - - try { - const all = await getNotesIndex("en", controller.signal); - if (!alive) return; - setRecent(all.slice(0, 3)); - } catch (e: any) { - if (!alive) return; - - // Abort shouldn't be treated as an error, but it MUST end loading. - if (e?.name === "AbortError") { - setLoading(false); - return; - } - - setError(e?.message ?? "Failed to load notes."); - setRecent([]); - } finally { - if (alive) setLoading(false); - } - })(); - - return () => { - alive = false; - controller.abort(); - }; - }, []); - - return ( - -
- {loading ? ( - - ) : ( -
- {recent.map((note) => { - const category = note.tags?.[0]; // currently [] in your data - const date = note.published ? new Date(note.published) : null; - const formatted = date - ? date.toLocaleDateString(undefined, { year: "numeric", month: "short", day: "2-digit" }) - : ""; - - const excerpt = (note.summary || note.subtitle || "").trim(); - - return ( -
- {/* subtle corner glow */} -
- -
-
- {category ? ( - - {category} - - ) : ( - - Lab Note - - )} - - {formatted && {formatted}} -
- -

- {note.title} -

- - {excerpt && ( -

- {excerpt} -

- )} -
- -
- {note.readingTime ?? "โ€”"} min read - - - Read note - -
-
- ); - })} - - - {error ? ( -

Preview failed: {error}

- ) : null} -
- )} -
- ); -} diff --git a/src/components/home/RecentLabNotesSkeleton.tsx b/src/components/home/RecentLabNotesSkeleton.tsx deleted file mode 100644 index 0abf410..0000000 --- a/src/components/home/RecentLabNotesSkeleton.tsx +++ /dev/null @@ -1,46 +0,0 @@ -export function RecentLabNotesSkeleton({ count = 3 }: { count?: number }) { - return ( -
- {Array.from({ length: count }).map((_, i) => ( -