Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/api/labNotesClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export interface LabNote {
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

Expand Down
371 changes: 202 additions & 169 deletions src/components/labnotes/LabNoteCard.tsx

Large diffs are not rendered by default.

11 changes: 6 additions & 5 deletions src/components/labnotes/LabNotesGridSkeleton.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
// src/components/labnotes/LabNotesGridSkeleton.tsx
import React from "react";
import { LabNoteCardSkeleton } from "@/components/labnotes/LabNoteCardSkeleton";
import {LabNote} from "@/lib/labNotes";
import {LabNoteCard} from "@/components/labnotes/LabNoteCard";
import type { LabNote } from "@/lib/labNotes";
import { LabNoteCard } from "@/components/labnotes/LabNoteCard";

export function LabNotesGridSkeleton({ count = 6 }: { count?: number }) {
return (
Expand All @@ -13,12 +13,13 @@ export function LabNotesGridSkeleton({ count = 6 }: { count?: number }) {
</div>
);
}

export function LabNotesGrid({ notes }: { notes: LabNote[] }) {
return (
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{notes.map((n) => (
<LabNoteCard key={n.id} note={n} index={0} />
{notes.map((n, i) => (
<LabNoteCard key={n.id} note={n} index={i} />
))}
</div>
);
}
}
134 changes: 134 additions & 0 deletions src/components/labnotes/labNoteCard.styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
// src/components/labnotes/labNoteCard.styles.ts

export interface DeptStyle {
border: string;
text: string;
glow: string;
shadow: string;

// Optional: used for hover bars / accents (lets Vesper not “own” everything)
accentBarActive?: string;
accentBarInactive?: string;
accentBarShadow?: string;
}

// Central “design tokens” per department.
// Keep these subtle: designer > neon arcade.
export const DEPT_STYLES: Record<string, DeptStyle> = {
coda: {
border: "hover:!border-coda",
text: "!text-coda",
glow: "from-coda/20",
shadow: "hover:!shadow-[0_0_20px_rgba(255,184,0,0.35)]",
accentBarActive: "bg-coda",
accentBarInactive: "bg-slate-800",
accentBarShadow: "shadow-[0_0_8px_rgba(255,184,0,0.45)]",
},
vesper: {
border: "hover:!border-vesper",
text: "!text-vesper",
glow: "from-vesper/20",
shadow: "hover:!shadow-[0_0_20px_rgba(110,0,255,0.25)]",
accentBarActive: "bg-vesper",
accentBarInactive: "bg-slate-800",
accentBarShadow: "shadow-[0_0_8px_rgba(110,0,255,0.45)]",
},
lyric: {
border: "hover:!border-lyric",
text: "!text-lyric",
glow: "from-lyric/20",
shadow: "hover:!shadow-[0_0_20px_rgba(0,255,133,0.22)]",
accentBarActive: "bg-lyric",
accentBarInactive: "bg-slate-800",
accentBarShadow: "shadow-[0_0_8px_rgba(0,255,133,0.35)]",
},

// Default / SCMS stays quiet and structured
scms: {
border: "hover:!border-slate-500",
text: "!text-slate-300",
glow: "from-slate-500/10",
shadow: "hover:!shadow-none",
accentBarActive: "bg-slate-500",
accentBarInactive: "bg-slate-800",
accentBarShadow: "shadow-none",
},

// 🧠 Sage: calm authority, not loud — emerald/graphite vibe
sage: {
border: "hover:!border-emerald-500/40",
text: "!text-emerald-200",
glow: "from-emerald-400/14",
shadow: "hover:!shadow-[0_0_22px_rgba(52,211,153,0.18)]",
accentBarActive: "bg-emerald-400",
accentBarInactive: "bg-slate-800",
accentBarShadow: "shadow-[0_0_8px_rgba(52,211,153,0.32)]",
},
};

export interface GuestStyle {
badge: string;
rail: string;
railMask?: string;
wash?: string;
text?: string;
}

export const GUEST_STYLES: Record<string, GuestStyle> = {
copilot: {
badge: "bg-slate-900/40 text-slate-300 border-slate-700/70",
rail: "bg-gradient-to-b from-slate-400/35 via-slate-400/15 to-transparent",
railMask: "[mask-image:linear-gradient(to_bottom,black_70%,transparent)]",
wash: "from-slate-400/10 via-transparent to-transparent",
text: "text-slate-400",
},
// future-proof examples
gemini: {
badge: "bg-slate-900/40 text-amber-200 border-amber-300/25",
rail: "bg-gradient-to-b from-amber-300/25 via-amber-300/10 to-transparent",
railMask: "[mask-image:linear-gradient(to_bottom,black_70%,transparent)]",
wash: "from-amber-300/10 via-transparent to-transparent",
text: "text-amber-200/70",
},
grok: {
badge: "bg-slate-900/40 text-fuchsia-200 border-fuchsia-300/25",
rail: "bg-gradient-to-b from-fuchsia-300/25 via-fuchsia-300/10 to-transparent",
railMask: "[mask-image:linear-gradient(to_bottom,black_70%,transparent)]",
wash: "from-fuchsia-300/10 via-transparent to-transparent",
text: "text-fuchsia-200/70",
},
claude: {
badge: "bg-slate-900/40 text-emerald-200 border-emerald-300/25",
rail: "bg-gradient-to-b from-emerald-300/25 via-emerald-300/10 to-transparent",
railMask: "[mask-image:linear-gradient(to_bottom,black_70%,transparent)]",
wash: "from-emerald-300/10 via-transparent to-transparent",
text: "text-emerald-200/70",
},
};

export function getGuestKey(note: any): string | null {
const raw = (note?.guest ?? note?.voice ?? "").toString().trim().toLowerCase();
return raw || null;
}

// 🧬 Explicitly map potential YAML names to our style keys
export function getDeptKey(id?: string): keyof typeof DEPT_STYLES {
const normalized = (id || "scms").toLowerCase();

// Your existing aliases
if (normalized === "alignment" || normalized === "coda") return "coda";
if (normalized === "shadow" || normalized === "vesper") return "vesper";
if (normalized === "structure" || normalized === "lyric") return "lyric";
if (normalized === "systems" || normalized === "scms") return "scms";

// New: Sage aliases
if (normalized === "sage") return "sage";
if (normalized === "analysis" || normalized === "philosophy") return "sage";

return "scms";
}

// Tiny class joiner (avoids importing clsx just for this)
export function cx(...parts: Array<string | false | null | undefined>) {
return parts.filter(Boolean).join(" ");
}
1 change: 1 addition & 0 deletions src/data/labNote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,5 @@ type ApiLabNote = {
safer_landing?: boolean;
created?: string;
updated?: string;
card_style?: string;
};
1 change: 1 addition & 0 deletions src/data/labNotes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export interface LabNote {
publishedAt: string; // ISO date
created?: string;
updated?: string;
cardStyle?: string;
}

export const labNotes: LabNote[] = [
Expand Down
3 changes: 3 additions & 0 deletions src/lib/labNotes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export type LabNoteAttributes = {
status?: "published" | "draft" | "archived";

dept?: string;
card_style?: string;
department_id: string;

shadow_density?: number;
Expand Down Expand Up @@ -73,6 +74,7 @@ type ApiLabNoteIndexItem = {

created_at?: string;
updated_at?: string;
card_style?: string;
};

type ApiLabNoteDetail = ApiLabNoteIndexItem & {
Expand Down Expand Up @@ -157,6 +159,7 @@ function normalizeIndex(apiNotes: ApiLabNoteIndexItem[], requestedLocale: string

created_at: n.created_at,
updated_at: n.updated_at,
card_style: n.card_style,
};
})
.filter((n) => n.status !== "archived")
Expand Down
108 changes: 81 additions & 27 deletions src/pages/admin/pages/AdminNotesPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@ export function AdminNotesPage() {
const [form, setForm] = useState({
id: "",
title: "",
subtitle: "",
slug: "",
locale: "en",
type: "labnote",
status: "draft",

department_id: "SCMS",
dept: "",
card_style: "",

category: "",
excerpt: "",
Expand Down Expand Up @@ -79,13 +81,15 @@ export function AdminNotesPage() {
setForm({
id: "",
title: "",
subtitle: "",
slug: "",
locale: "en",
type: "labnote",
status: "draft",

department_id: "SCMS",
dept: "",
card_style: "",

category: "",
excerpt: "",
Expand All @@ -102,6 +106,15 @@ export function AdminNotesPage() {
setEditingId(null);
};

const CARD_STYLE_OPTIONS = [
{ value: "", label: "Auto (from department)" },
{ value: "scms", label: "SCMS (neutral)" },
{ value: "vesper", label: "Vesper (purple)" },
{ value: "sage", label: "Sage (emerald)" },
{ value: "lyric", label: "Lyric (neon green)" },
{ value: "coda", label: "Coda (amber)" },
];

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();

Expand Down Expand Up @@ -141,13 +154,15 @@ export function AdminNotesPage() {
setForm({
id: full.id ?? "",
title: full.title ?? "",
subtitle: full.subtitle ?? "",
slug: full.slug ?? "",
locale: full.locale ?? "en",
type: full.type ?? "labnote",
status: full.status ?? "draft",

department_id: full.department_id ?? "SCMS",
dept: full.dept ?? "",
card_style: full.card_style ?? "",

category: full.category ?? "",
excerpt: full.excerpt ?? "",
Expand Down Expand Up @@ -310,6 +325,19 @@ export function AdminNotesPage() {
required
/>
</div>
{/* SUBTITLE*/}
<label className="block">
<div className="text-xs uppercase tracking-widest text-zinc-400 mb-2">
Subtitle <span className="text-zinc-600">(card hook)</span>
</div>
<input
value={form.subtitle ?? ""}
onChange={(e) => setForm((p) => ({ ...p, subtitle: e.target.value }))}
className="w-full rounded-lg border border-zinc-800 bg-zinc-950 px-3 py-2 text-zinc-100"
placeholder="One sentence. Shows on cards + hover."
maxLength={180}
/>
</label>
{/* SLUG*/}
<div className="space-y-2">
<label className="text-xs uppercase tracking-widest text-zinc-500">
Expand Down Expand Up @@ -363,20 +391,62 @@ export function AdminNotesPage() {
</div>


{/* CATEGORY */}
<div className="space-y-2">
<label className="text-xs uppercase tracking-widest text-zinc-500">
Category
{/* CATEGORY / READ TIME / STYLE */}
<div className="grid gap-6 md:grid-cols-3">
{/* Category */}
<label className="block">
<div className="mb-2 text-xs uppercase tracking-widest text-zinc-500">
Category
</div>
<input
name="category"
value={form.category}
onChange={handleChange}
className="w-full rounded-lg border border-zinc-800 bg-zinc-950/40 px-3 py-2 text-zinc-100"
placeholder="systems / lore / debug / etc"
/>
</label>

{/* Read Time */}
<label className="block">
<div className="mb-2 text-xs uppercase tracking-widest text-zinc-500">
Read Time (minutes)
</div>
<input
type="number"
min={1}
name="read_time_minutes"
value={form.read_time_minutes}
onChange={handleChange}
className="w-full rounded-lg border border-zinc-800 bg-zinc-950/40 px-3 py-2 text-zinc-100"
/>
</label>

{/* Card Style */}
<label className="block">
<div className="mb-2 text-xs uppercase tracking-widest text-zinc-400">
Card Style <span className="text-zinc-600">(override)</span>
</div>
<select
value={form.card_style ?? ""}
onChange={(e) =>
setForm((p) => ({ ...p, card_style: e.target.value }))
}
className="w-full rounded-lg border border-zinc-800 bg-zinc-950 px-3 py-2 text-zinc-100"
>
{CARD_STYLE_OPTIONS.map((o) => (
<option key={o.value} value={o.value}>
{o.label}
</option>
))}
</select>
<div className="mt-1 text-[11px] text-zinc-500">
Presentation only. Does not change ownership/department.
</div>
</label>
<input
name="category"
value={form.category}
onChange={handleChange}
className="w-full rounded-lg border border-zinc-800 bg-zinc-950/40 px-3 py-2 text-zinc-100"
placeholder="systems / lore / debug / etc"
/>
</div>


<div className="grid grid-cols-2 gap-4">
{/* PUBLISHED DATE */}
<div className="space-y-2">
Expand Down Expand Up @@ -409,22 +479,6 @@ export function AdminNotesPage() {
</select>
</div>
</div>


{/* READ TIME */}
<div className="space-y-2">
<label className="text-xs uppercase tracking-widest text-zinc-500">
Read Time (minutes)
</label>
<input
type="number"
min={1}
name="read_time_minutes"
value={form.read_time_minutes}
onChange={handleChange}
className="w-full rounded-lg border border-zinc-800 bg-zinc-950/40 px-3 py-2 text-zinc-100"
/>
</div>
</div>

{/* EXCERPT */}
Expand Down