Skip to content
Open
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
67 changes: 67 additions & 0 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,29 @@
--thesis-gradient: linear-gradient(135deg, oklch(0.7 0.15 160), oklch(0.7 0.15 250));
--thesis-foreground: oklch(0.15 0.01 260);
--thesis-accent: oklch(0.7 0.15 200);
--ui-brand: #3ecf6e;
--ui-surface-canvas: #020202;
--ui-surface-modal: #0d0d0d;
--ui-surface-panel: #0d0d10;
--ui-warning-bg: rgba(69, 26, 3, 0.8);
--ui-warning-border: rgba(146, 64, 14, 0.6);
--ui-warning-text: #fde68a;
--ui-warning-button-bg: rgba(180, 83, 9, 0.6);
--ui-warning-button-hover: rgba(217, 119, 6, 0.7);
--ui-warning-button-text: #fef3c7;
--ui-warning-button-border: rgba(217, 119, 6, 0.5);
--ui-danger-bg: rgba(239, 68, 68, 0.1);
--ui-danger-border: rgba(239, 68, 68, 0.2);
--ui-danger-text: rgba(248, 113, 113, 0.8);
--ui-danger-strong: #fca5a5;
--ui-danger-icon: #f87171;
--ui-danger-hover-bg: rgba(239, 68, 68, 0.2);
--ui-icon-task: #818cf8;
--ui-icon-thesis: #facc15;
--ui-icon-question: #60a5fa;
--ui-og-bg: #0a0a0a;
--ui-og-text: #f0f0f0;
--ui-og-muted: #666666;
}

.dark {
Expand Down Expand Up @@ -91,6 +114,29 @@
--sidebar-accent-foreground: oklch(0.85 0 0);
--sidebar-border: oklch(0.28 0.01 260);
--sidebar-ring: oklch(0.72 0.19 155);
--ui-brand: #3ecf6e;
--ui-surface-canvas: #020202;
--ui-surface-modal: #0d0d0d;
--ui-surface-panel: #0d0d10;
--ui-warning-bg: rgba(69, 26, 3, 0.8);
--ui-warning-border: rgba(146, 64, 14, 0.6);
--ui-warning-text: #fde68a;
--ui-warning-button-bg: rgba(180, 83, 9, 0.6);
--ui-warning-button-hover: rgba(217, 119, 6, 0.7);
--ui-warning-button-text: #fef3c7;
--ui-warning-button-border: rgba(217, 119, 6, 0.5);
--ui-danger-bg: rgba(239, 68, 68, 0.1);
--ui-danger-border: rgba(239, 68, 68, 0.2);
--ui-danger-text: rgba(248, 113, 113, 0.8);
--ui-danger-strong: #fca5a5;
--ui-danger-icon: #f87171;
--ui-danger-hover-bg: rgba(239, 68, 68, 0.2);
--ui-icon-task: #818cf8;
--ui-icon-thesis: #facc15;
--ui-icon-question: #60a5fa;
--ui-og-bg: #0a0a0a;
--ui-og-text: #f0f0f0;
--ui-og-muted: #666666;
}

@theme inline {
Expand All @@ -102,6 +148,27 @@
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-ui-surface-canvas: var(--ui-surface-canvas);
--color-ui-surface-modal: var(--ui-surface-modal);
--color-ui-surface-panel: var(--ui-surface-panel);
--color-ui-brand: var(--ui-brand);
--color-ui-warning-bg: var(--ui-warning-bg);
--color-ui-warning-border: var(--ui-warning-border);
--color-ui-warning-text: var(--ui-warning-text);
--color-ui-warning-button-bg: var(--ui-warning-button-bg);
--color-ui-warning-button-hover: var(--ui-warning-button-hover);
--color-ui-warning-button-text: var(--ui-warning-button-text);
--color-ui-warning-button-border: var(--ui-warning-button-border);
--color-ui-danger-bg: var(--ui-danger-bg);
--color-ui-danger-border: var(--ui-danger-border);
--color-ui-danger-text: var(--ui-danger-text);
--color-ui-danger-strong: var(--ui-danger-strong);
--color-ui-danger-icon: var(--ui-danger-icon);
--color-ui-danger-hover-bg: var(--ui-danger-hover-bg);
--color-ui-icon-task: var(--ui-icon-task);
--color-ui-icon-thesis: var(--ui-icon-thesis);
--color-ui-icon-question: var(--ui-icon-question);
--color-sidebar: var(--sidebar);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
Expand Down
6 changes: 3 additions & 3 deletions app/not-found.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,9 @@ export default function NotFound() {
{/* Logo mark */}
<div className="mb-2 flex items-center gap-2">
<div className="flex items-center gap-1">
<span className="h-2.5 w-2.5 rounded-[3px] bg-[var(--type-quote)]" />
<span className="h-2.5 w-2.5 rounded-[3px] bg-[var(--type-quote)] opacity-60" />
<span className="h-2.5 w-2.5 rounded-[3px] bg-[var(--type-quote)] opacity-30" />
<span className="h-2.5 w-2.5 rounded-[3px] bg-type-quote" />
<span className="h-2.5 w-2.5 rounded-[3px] bg-type-quote opacity-60" />
<span className="h-2.5 w-2.5 rounded-[3px] bg-type-quote opacity-30" />
</div>
<span className="font-mono text-xs font-semibold tracking-tight text-foreground/60">
nodepad
Expand Down
23 changes: 15 additions & 8 deletions app/opengraph-image.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ export const alt = "nodepad — spatial AI research tool"
export const size = { width: 1200, height: 630 }
export const contentType = "image/png"

const OG_COLORS = {
background: "#0a0a0a",
text: "#f0f0f0",
muted: "#666666",
brand: "#3ecf6e",
}

export default function OGImage() {
return new ImageResponse(
(
Expand All @@ -16,19 +23,19 @@ export default function OGImage() {
flexDirection: "column",
alignItems: "flex-start",
justifyContent: "flex-end",
background: "#0a0a0a",
background: OG_COLORS.background,
padding: "80px 96px",
fontFamily: "sans-serif",
}}
>
{/* Logo mark */}
<div style={{ display: "flex", alignItems: "center", gap: "12px", marginBottom: "48px" }}>
<div style={{ display: "flex", gap: "6px" }}>
<div style={{ width: 28, height: 28, borderRadius: 5, background: "#3ecf6e" }} />
<div style={{ width: 28, height: 28, borderRadius: 5, background: "#3ecf6e", opacity: 0.6 }} />
<div style={{ width: 28, height: 28, borderRadius: 5, background: "#3ecf6e", opacity: 0.3 }} />
<div style={{ width: 28, height: 28, borderRadius: 5, background: OG_COLORS.brand }} />
<div style={{ width: 28, height: 28, borderRadius: 5, background: OG_COLORS.brand, opacity: 0.6 }} />
<div style={{ width: 28, height: 28, borderRadius: 5, background: OG_COLORS.brand, opacity: 0.3 }} />
</div>
<span style={{ fontSize: 28, fontWeight: 600, color: "#f0f0f0", letterSpacing: "-0.5px" }}>
<span style={{ fontSize: 28, fontWeight: 600, color: OG_COLORS.text, letterSpacing: "-0.5px" }}>
nodepad
</span>
</div>
Expand All @@ -38,19 +45,19 @@ export default function OGImage() {
style={{
fontSize: 72,
fontWeight: 700,
color: "#f0f0f0",
color: OG_COLORS.text,
lineHeight: 1.05,
letterSpacing: "-2px",
marginBottom: 32,
}}
>
Think spatially.
<br />
<span style={{ color: "#3ecf6e" }}>Let AI fill the gaps.</span>
<span style={{ color: OG_COLORS.brand }}>Let AI fill the gaps.</span>
</div>

{/* Subline */}
<div style={{ fontSize: 24, color: "#666", fontWeight: 400, letterSpacing: "-0.3px" }}>
<div style={{ fontSize: 24, color: OG_COLORS.muted, fontWeight: 400, letterSpacing: "-0.3px" }}>
nodepad.space
</div>
</div>
Expand Down
20 changes: 13 additions & 7 deletions app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { downloadNodepadFile, parseNodepadFile, NodepadParseError } from "@/lib/
import { detectContentType } from "@/lib/detect-content-type"
import { clearSession, getSessionUser, type SessionUser } from "@/lib/auth"
import { fetchUserState, saveUserState } from "@/lib/user-state"
import { getGlobalKeybindAction } from "@/lib/keybinds"

const SKIP_LOGIN_KEY = "nodepad-skip-login"
const GUEST_PROJECTS_KEY = "nodepad-guest-projects"
Expand Down Expand Up @@ -683,19 +684,24 @@ export default function Page() {

useEffect(() => {
const handleKeys = (e: KeyboardEvent) => {
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
const action = getGlobalKeybindAction(e)
if (action === "command-menu") {
e.preventDefault()
setIsCommandKOpen(prev => !prev)
}
if (e.key === "z" && (e.metaKey || e.ctrlKey) && !e.shiftKey) {
if (action === "toggle-sidebar") {
e.preventDefault()
setIsSidebarOpen(prev => !prev)
}
if (action === "undo") {
// Don't intercept while typing in an input/textarea
const tag = (e.target as HTMLElement).tagName
if (tag !== "INPUT" && tag !== "TEXTAREA") {
e.preventDefault()
undo()
}
}
if (e.key === "Escape") {
if (action === "escape") {
if (isCommandKOpen) {
setIsCommandKOpen(false)
} else if (isGhostPanelOpen) {
Expand Down Expand Up @@ -1103,12 +1109,12 @@ export default function Page() {
/>

{isHydrated && !settings.apiKey && (
<div className="flex items-center justify-center gap-3 px-4 py-2 bg-amber-950/80 border-b border-amber-800/60 text-amber-200 text-xs shrink-0">
<span className="opacity-80">⚡ AI enrichment requires an <strong className="text-amber-200">OpenRouter API key</strong> — use a free model (no credits needed) or add credits for GPT-4o, Claude, and more. Configure in the <strong className="text-amber-200">☰ left panel</strong>.</span>
<div className="flex items-center justify-center gap-3 px-4 py-2 bg-(--ui-warning-bg) border-b border-(--ui-warning-border) text-(--ui-warning-text) text-xs shrink-0">
<span className="opacity-80">⚡ AI enrichment requires an <strong className="text-(--ui-warning-text)">OpenRouter API key</strong> — use a free model (no credits needed) or add credits for GPT-4o, Claude, and more. Configure in the <strong className="text-(--ui-warning-text)">☰ left panel</strong>.</span>
<div className="flex items-center gap-2 shrink-0">
<button
onClick={() => { setIsSidebarOpen(true); setJumpToSettings(true) }}
className="px-2.5 py-1 rounded bg-amber-700/60 hover:bg-amber-600/70 text-amber-100 font-medium transition-colors cursor-pointer border border-amber-600/50"
className="px-2.5 py-1 rounded bg-(--ui-warning-button-bg) hover:bg-(--ui-warning-button-hover) text-(--ui-warning-button-text) font-medium transition-colors cursor-pointer border border-(--ui-warning-button-border)"
>
Add API key →
</button>
Expand Down Expand Up @@ -1204,7 +1210,7 @@ export default function Page() {
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 4 }}
transition={{ duration: 0.15, ease: "easeOut" }}
className="absolute bottom-[72px] left-1/2 -translate-x-1/2 z-[130] pointer-events-none"
className="absolute bottom-18 left-1/2 -translate-x-1/2 z-130 pointer-events-none"
>
<div className="px-3 py-1.5 rounded-sm bg-black/90 border border-white/15 backdrop-blur-md shadow-xl">
<span className="font-mono text-[10px] text-white/70 tracking-tight whitespace-nowrap">{undoToast}</span>
Expand Down
33 changes: 17 additions & 16 deletions components/about-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
FolderInput, Download, Brain, Zap, Globe, Search, Check, Mail
} from "lucide-react"
import { useModKey } from "@/lib/utils"
import { getKeyboardShortcuts } from "@/lib/keybinds"

interface AboutPanelProps {
open: boolean
Expand Down Expand Up @@ -64,7 +65,7 @@ function Section({ title, children }: { title: string; children: React.ReactNode
function Step({ n, title, children }: { n: number; title: string; children: React.ReactNode }) {
return (
<div className="flex gap-4">
<div className="flex-shrink-0 flex h-6 w-6 items-center justify-center rounded-sm bg-primary/10 border border-primary/20 font-mono text-[10px] font-black text-primary">
<div className="shrink-0 flex h-6 w-6 items-center justify-center rounded-sm bg-primary/10 border border-primary/20 font-mono text-[10px] font-black text-primary">
{n}
</div>
<div className="space-y-1 pt-0.5">
Expand Down Expand Up @@ -96,16 +97,17 @@ const CONTENT_TYPE_HIGHLIGHTS = [

export function AboutPanel({ open, onClose }: AboutPanelProps) {
const mod = useModKey()
const shortcuts = getKeyboardShortcuts(mod)
return (
<Sheet open={open} onOpenChange={(v) => { if (!v) onClose() }}>
<SheetContent
side="right"
className="w-full sm:max-w-2xl flex flex-col gap-0 p-0 bg-card border-l border-border z-[200] overflow-hidden"
className="w-full sm:max-w-2xl flex flex-col gap-0 p-0 bg-card border-l border-border z-200 overflow-hidden"
>
<SheetTitle className="sr-only">About nodepad</SheetTitle>

{/* Header */}
<div className="flex-shrink-0 px-8 pt-8 pb-6 border-b border-border">
<div className="shrink-0 px-8 pt-8 pb-6 border-b border-border">
<div className="flex items-center gap-3 mb-3">
<div className="flex items-center gap-0.5">
<span className="inline-block h-3 w-3 rounded-sm bg-primary" />
Expand Down Expand Up @@ -205,7 +207,7 @@ export function AboutPanel({ open, onClose }: AboutPanelProps) {
const Icon = config.icon
return (
<div key={type} className="flex items-center gap-2.5 px-3 py-2 rounded-sm bg-secondary/50 border border-border/50">
<Icon className="h-3.5 w-3.5 flex-shrink-0" style={{ color: config.accentVar }} />
<Icon className="h-3.5 w-3.5 shrink-0" style={{ color: config.accentVar }} />
<div>
<p className="font-mono text-[10px] font-bold uppercase tracking-wider" style={{ color: config.accentVar }}>
{config.label}
Expand All @@ -224,21 +226,21 @@ export function AboutPanel({ open, onClose }: AboutPanelProps) {
<Section title="Views">
<div className="space-y-3">
<div className="flex gap-3 p-3 rounded-sm bg-secondary/30 border border-border/50">
<Layers className="h-4 w-4 flex-shrink-0 text-primary mt-0.5" />
<Layers className="h-4 w-4 shrink-0 text-primary mt-0.5" />
<div>
<p className="text-sm font-semibold text-foreground mb-0.5">Tiling <span className="font-mono text-[10px] text-muted-foreground/50 ml-1">{mod}1</span></p>
<p className="text-sm text-muted-foreground">Default. Nodes are laid out in a Binary Space Partition grid — each new node splits the available space. Navigate pages horizontally. A minimap in the bottom-right shows your spatial position.</p>
</div>
</div>
<div className="flex gap-3 p-3 rounded-sm bg-secondary/30 border border-border/50">
<Kanban className="h-4 w-4 flex-shrink-0 text-primary mt-0.5" />
<Kanban className="h-4 w-4 shrink-0 text-primary mt-0.5" />
<div>
<p className="text-sm font-semibold text-foreground mb-0.5">Kanban <span className="font-mono text-[10px] text-muted-foreground/50 ml-1">{mod}2</span></p>
<p className="text-sm text-muted-foreground">Nodes grouped into columns by content type. Good for reviewing your thinking by category. Tasks always appear first.</p>
</div>
</div>
<div className="flex gap-3 p-3 rounded-sm bg-secondary/30 border border-border/50">
<GitFork className="h-4 w-4 flex-shrink-0 text-primary mt-0.5" />
<GitFork className="h-4 w-4 shrink-0 text-primary mt-0.5" />
<div>
<p className="text-sm font-semibold text-foreground mb-0.5">Graph <span className="font-mono text-[10px] text-muted-foreground/50 ml-1">{mod}3</span></p>
<p className="text-sm text-muted-foreground">An interactive force-directed graph of all your nodes. Connections between them become the focus — highly-connected nodes drift toward the centre, isolated ones settle at the periphery. Click any node to open its full detail panel. Hover to dim unrelated nodes.</p>
Expand All @@ -258,7 +260,7 @@ export function AboutPanel({ open, onClose }: AboutPanelProps) {
{ icon: Sparkles, title: "Synthesis", desc: "After ≥3 nodes, nodepad quietly generates an emergent thesis — a 15–25 word synthesis of what you're actually thinking about. Solidify it to keep it, or dismiss." },
].map(({ icon: Icon, title, desc }) => (
<div key={title} className="flex gap-3">
<Icon className="h-4 w-4 flex-shrink-0 text-primary/70 mt-0.5" />
<Icon className="h-4 w-4 shrink-0 text-primary/70 mt-0.5" />
<div>
<p className="text-sm font-semibold text-foreground mb-0.5">{title}</p>
<p className="text-sm text-muted-foreground">{desc}</p>
Expand All @@ -272,21 +274,21 @@ export function AboutPanel({ open, onClose }: AboutPanelProps) {
<Section title="Export & your data">
<div className="space-y-3">
<div className="flex gap-3">
<FolderDown className="h-4 w-4 flex-shrink-0 text-primary/70 mt-0.5" />
<FolderDown className="h-4 w-4 shrink-0 text-primary/70 mt-0.5" />
<div>
<p className="text-sm font-semibold text-foreground mb-0.5">Export .nodepad</p>
<p className="text-sm text-muted-foreground">Save your full research space as a <code className="px-1 rounded bg-secondary font-mono text-xs">.nodepad</code> file. Import it on any device to pick up where you left off.</p>
</div>
</div>
<div className="flex gap-3">
<Download className="h-4 w-4 flex-shrink-0 text-primary/70 mt-0.5" />
<Download className="h-4 w-4 shrink-0 text-primary/70 mt-0.5" />
<div>
<p className="text-sm font-semibold text-foreground mb-0.5">Export Markdown</p>
<p className="text-sm text-muted-foreground">Export a richly formatted Markdown document with YAML front matter, a table of contents, grouped sections, confidence tables for claims, and cited sources.</p>
</div>
</div>
<div className="flex gap-3">
<FolderInput className="h-4 w-4 flex-shrink-0 text-primary/70 mt-0.5" />
<FolderInput className="h-4 w-4 shrink-0 text-primary/70 mt-0.5" />
<div>
<p className="text-sm font-semibold text-foreground mb-0.5">Your data, synced</p>
<p className="text-sm text-muted-foreground">Your projects and notes are stored in your account on the server (PostgreSQL) and synced across sessions. Notes are still sent to the AI provider of your choice (OpenRouter, OpenAI, or Z.ai) using your own API key.</p>
Expand All @@ -299,10 +301,9 @@ export function AboutPanel({ open, onClose }: AboutPanelProps) {
<Section title="Keyboard shortcuts">
<div className="rounded-sm border border-border overflow-hidden">
<div className="px-3 divide-y divide-border/40">
<Shortcut keys={[mod, "K"]} label="Command menu" />
<Shortcut keys={[mod, "Z"]} label="Undo last action" />
<Shortcut keys={["Enter"]} label="Submit a new node" />
<Shortcut keys={["Esc"]} label="Close command menu / deselect" />
{shortcuts.map((shortcut) => (
<Shortcut key={`${shortcut.label}-${shortcut.keys.join("-")}`} keys={shortcut.keys} label={shortcut.label} />
))}
</div>
</div>
</Section>
Expand All @@ -320,7 +321,7 @@ export function AboutPanel({ open, onClose }: AboutPanelProps) {
"Use multiple projects (sidebar) to keep separate research threads isolated.",
].map((tip, i) => (
<li key={i} className="flex gap-2.5 text-sm text-muted-foreground">
<span className="flex-shrink-0 font-mono text-[10px] text-primary/50 mt-0.5 pt-px">→</span>
<span className="shrink-0 font-mono text-[10px] text-primary/50 mt-0.5 pt-px">→</span>
{tip}
</li>
))}
Expand Down
Loading
Loading