From 3fa4cbbcb2a2e3500126d84f52fb00b098bb46d5 Mon Sep 17 00:00:00 2001 From: Aditya Gupta Date: Sun, 10 May 2026 15:22:42 +0000 Subject: [PATCH 1/6] refactor: update class names for consistency and improve styling - Adjusted width classes in GhostPanel and ProjectSidebar components for better layout. --- components/about-panel.tsx | 24 +++++++++---------- components/ghost-panel.tsx | 2 +- components/graph-area.tsx | 6 ++--- components/graph-detail-panel.tsx | 10 ++++---- components/intro-modal.tsx | 4 ++-- components/kanban-area.tsx | 4 ++-- components/mobile-wall.tsx | 2 +- components/project-sidebar.tsx | 26 ++++++++++----------- components/status-bar.tsx | 2 +- components/tile-card.tsx | 38 +++++++++++++++---------------- components/tile-index.tsx | 2 +- components/tiling-area.tsx | 2 +- components/tiling-minimap.tsx | 10 ++++---- components/vim-input.tsx | 18 +++++++-------- 14 files changed, 75 insertions(+), 75 deletions(-) diff --git a/components/about-panel.tsx b/components/about-panel.tsx index 87db0b8..c15445a 100644 --- a/components/about-panel.tsx +++ b/components/about-panel.tsx @@ -64,7 +64,7 @@ function Section({ title, children }: { title: string; children: React.ReactNode function Step({ n, title, children }: { n: number; title: string; children: React.ReactNode }) { return (
-
+
{n}
@@ -100,12 +100,12 @@ export function AboutPanel({ open, onClose }: AboutPanelProps) { { if (!v) onClose() }}> About nodepad {/* Header */} -
+
@@ -205,7 +205,7 @@ export function AboutPanel({ open, onClose }: AboutPanelProps) { const Icon = config.icon return (
- +

{config.label} @@ -224,21 +224,21 @@ export function AboutPanel({ open, onClose }: AboutPanelProps) {

- +

Tiling {mod}1

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.

- +

Kanban {mod}2

Nodes grouped into columns by content type. Good for reviewing your thinking by category. Tasks always appear first.

- +

Graph {mod}3

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.

@@ -258,7 +258,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 }) => (
- +

{title}

{desc}

@@ -272,21 +272,21 @@ export function AboutPanel({ open, onClose }: AboutPanelProps) {
- +

Export .nodepad

Save your full research space as a .nodepad file. Import it on any device to pick up where you left off.

- +

Export Markdown

Export a richly formatted Markdown document with YAML front matter, a table of contents, grouped sections, confidence tables for claims, and cited sources.

- +

Your data, synced

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.

@@ -320,7 +320,7 @@ export function AboutPanel({ open, onClose }: AboutPanelProps) { "Use multiple projects (sidebar) to keep separate research threads isolated.", ].map((tip, i) => (
  • - + {tip}
  • ))} diff --git a/components/ghost-panel.tsx b/components/ghost-panel.tsx index d7dd107..7fcc8ec 100644 --- a/components/ghost-panel.tsx +++ b/components/ghost-panel.tsx @@ -28,7 +28,7 @@ export function GhostPanel({ ghostNotes, isOpen, onClose, onClaim, onDismiss }: }} className="flex flex-col h-full bg-black/20 backdrop-blur-3xl border-l border-border shrink-0 overflow-hidden relative z-50 transition-all duration-200 ease-in-out" > -
    +
    {/* Header */}
    diff --git a/components/graph-area.tsx b/components/graph-area.tsx index 375ed37..8e73805 100644 --- a/components/graph-area.tsx +++ b/components/graph-area.tsx @@ -399,7 +399,7 @@ export function GraphArea({ > {blocks.length === 0 && (
    -
    +

    force-directed graph view

    @@ -706,14 +706,14 @@ export function GraphArea({ >
    {config?.icon && React.createElement(config.icon, { - className: "h-3 w-3 flex-shrink-0", + className: "h-3 w-3 shrink-0", style: { color: "black", opacity: 0.7 }, })} {node.isSynthesis ? "Synthesis" : config?.label} {node.block?.category && ( - + {node.block.category} )} diff --git a/components/graph-detail-panel.tsx b/components/graph-detail-panel.tsx index 1052114..00a264a 100644 --- a/components/graph-detail-panel.tsx +++ b/components/graph-detail-panel.tsx @@ -182,12 +182,12 @@ export function GraphDetailPanel({ {/* ── Header bar — matches tile-card style ───────────────────────── */}
    {/* Type display — read-only label; shimmer while enriching */} - + {config.label} @@ -196,7 +196,7 @@ export function GraphDetailPanel({ #{block.category || "no-topic"}
    -
    +
    {date} {/* Change-type button — portal dropdown, clear of panel overflow:hidden */} @@ -588,14 +588,14 @@ export function ProjectSidebar({
    )} @@ -431,7 +431,7 @@ export const TileCard = memo(function TileCard({ e.stopPropagation() onTogglePin(block.id) }} - className={`flex h-4 w-4 items-center justify-center rounded-sm transition-all shadow-sm ${block.isPinned ? "bg-black/20 opacity-100 scale-110 !opacity-100" : "opacity-40 hover:opacity-100 hover:bg-black/10"}`} + className={`flex h-4 w-4 items-center justify-center rounded-sm transition-all shadow-sm ${block.isPinned ? "bg-black/20 opacity-100 scale-110" : "opacity-40 hover:opacity-100 hover:bg-black/10"}`} aria-label={block.isPinned ? "Unpin note" : "Pin note"} title={block.isPinned ? "Unpin note" : "Pin note"} > @@ -518,7 +518,7 @@ export const TileCard = memo(function TileCard({ }} className={`flex items-center gap-2 rounded-sm px-2 py-1.5 text-left transition-all hover:bg-secondary/60 ${isActive ? "bg-secondary/80" : ""}`} > - + Move or copy to space

    -
    +
    {workspaces.filter(w => w.id !== activeWorkspaceId).map(w => (
    +
    Confidence {Math.round(block.confidence)}% @@ -744,7 +744,7 @@ export const TileCard = memo(function TileCard({ {/* Footer */}
    # - {block.category || "no-topic"} + {block.category || "no-topic"} {block.influencedBy && block.influencedBy.length > 0 && ( @@ -781,7 +781,7 @@ export const TileCard = memo(function TileCard({
    {/* Hover Tooltip */} -
    +
    Connected nodes
    {block.influencedBy.slice(0, 5).map((id, i) => { @@ -811,12 +811,12 @@ export const TileCard = memo(function TileCard({ onClick={() => setIsFooterExpanded(!isFooterExpanded)} className={`rounded-sm p-1 transition-all ${isFooterExpanded ? 'bg-primary/20 text-primary' : 'text-muted-foreground/40 hover:text-muted-foreground/60'}`} > - {isFooterExpanded ? : } + {isFooterExpanded ? : } )}
    - Node ID: + Node ID: #{block.id.slice(0, 6)}
    @@ -896,7 +896,7 @@ function renderBody( return (
    {linkifyText(text)}

    -
    +
    ) default: diff --git a/components/tile-index.tsx b/components/tile-index.tsx index bee334d..ba912f6 100644 --- a/components/tile-index.tsx +++ b/components/tile-index.tsx @@ -74,7 +74,7 @@ export function TileIndex({ blocks, onHighlight, highlightedId, onClose, isOpen, }} className="flex flex-col h-full bg-black/20 backdrop-blur-3xl border-l border-border shrink-0 overflow-hidden relative z-50 transition-all duration-200 ease-in-out" > -
    +
    {/* Header */}
    {onClose && ( diff --git a/components/tiling-area.tsx b/components/tiling-area.tsx index fb352e5..cda9752 100644 --- a/components/tiling-area.tsx +++ b/components/tiling-area.tsx @@ -304,7 +304,7 @@ export function TilingArea({ {/* Empty state — absolutely positioned so it centers identically across all views */} {pageTrees.length === 0 && !taskBlock && (
    -
    +

    spatial research workspace

    diff --git a/components/tiling-minimap.tsx b/components/tiling-minimap.tsx index 3e135b4..bf1b654 100644 --- a/components/tiling-minimap.tsx +++ b/components/tiling-minimap.tsx @@ -49,7 +49,7 @@ export function TilingMinimap({ pages, activePageIdx, onPageClick }: TilingMinim return (
    @@ -68,20 +68,20 @@ export function TilingMinimap({ pages, activePageIdx, onPageClick }: TilingMinim onClick={() => onPageClick(idx)} onMouseEnter={() => setHoveredIdx(idx)} onMouseLeave={() => setHoveredIdx(null)} - className={`group relative flex flex-col items-center gap-[4px] p-1.5 rounded-md transition-all duration-150 outline-none ${ + className={`group relative flex flex-col items-center gap-1 p-1.5 rounded-md transition-all duration-150 outline-none ${ isActive ? "bg-primary/15 border border-primary/40 shadow-[0_0_0_1px_var(--primary)]" - : "border border-white/10 bg-white/[0.04] hover:bg-white/[0.09] hover:border-white/25" + : "border border-white/10 bg-white/4 hover:bg-white/9 hover:border-white/25" }`} > {/* Dot grid — up to 3 columns, rows as needed */} -
    +
    {page.map(block => { const config = CONTENT_TYPE_CONFIG[block.contentType] return (
    +
    { @@ -272,7 +272,7 @@ export function VimInput({ onSubmit, onCommand, isCommandKOpen, setIsCommandKOpe )}
    -
    +
    {/* ── Views ──────────────────────────────────────────────── */} {viewItems.length > 0 && ( @@ -287,9 +287,9 @@ export function VimInput({ onSubmit, onCommand, isCommandKOpen, setIsCommandKOpe ref={el => { itemRefs.current[i] = el }} onClick={() => handleSelect(item.id)} onMouseEnter={() => setFocusedIdx(i)} - className={`group flex flex-col items-center justify-center gap-2 rounded-sm border py-4 px-2 transition-all duration-100 outline-none ${focused ? "bg-primary/12 border-primary/35 text-primary shadow-[0_0_0_1px_var(--primary),inset_0_1px_0_rgba(255,255,255,0.05)]" : "bg-white/[0.03] border-white/[0.07] text-white/55 hover:bg-white/[0.06] hover:border-white/20 hover:text-white/80"}`} + className={`group flex flex-col items-center justify-center gap-2 rounded-sm border py-4 px-2 transition-all duration-100 outline-none ${focused ? "bg-primary/12 border-primary/35 text-primary shadow-[0_0_0_1px_var(--primary),inset_0_1px_0_rgba(255,255,255,0.05)]" : "bg-white/3 border-white/[0.07] text-white/55 hover:bg-white/6 hover:border-white/20 hover:text-white/80"}`} > - +
    {item.label}
    {item.sub &&
    {item.sub}
    } @@ -315,9 +315,9 @@ export function VimInput({ onSubmit, onCommand, isCommandKOpen, setIsCommandKOpe ref={el => { itemRefs.current[idx] = el }} onClick={() => handleSelect(item.id)} onMouseEnter={() => setFocusedIdx(idx)} - className={`group flex flex-col items-center justify-center gap-2 rounded-sm border py-4 px-2 transition-all duration-100 outline-none ${focused ? "bg-primary/12 border-primary/35 text-primary shadow-[0_0_0_1px_var(--primary),inset_0_1px_0_rgba(255,255,255,0.05)]" : "bg-white/[0.03] border-white/[0.07] text-white/55 hover:bg-white/[0.06] hover:border-white/20 hover:text-white/80"}`} + className={`group flex flex-col items-center justify-center gap-2 rounded-sm border py-4 px-2 transition-all duration-100 outline-none ${focused ? "bg-primary/12 border-primary/35 text-primary shadow-[0_0_0_1px_var(--primary),inset_0_1px_0_rgba(255,255,255,0.05)]" : "bg-white/3 border-white/[0.07] text-white/55 hover:bg-white/6 hover:border-white/20 hover:text-white/80"}`} > - +
    {item.label}
    {item.sub &&
    {item.sub}
    } @@ -343,9 +343,9 @@ export function VimInput({ onSubmit, onCommand, isCommandKOpen, setIsCommandKOpe ref={el => { itemRefs.current[idx] = el }} onClick={() => handleSelect(item.id)} onMouseEnter={() => setFocusedIdx(idx)} - className={`group flex flex-col items-center justify-center gap-2 rounded-sm border py-4 px-2 transition-all duration-100 outline-none ${focused ? "bg-primary/12 border-primary/35 text-primary shadow-[0_0_0_1px_var(--primary),inset_0_1px_0_rgba(255,255,255,0.05)]" : "bg-white/[0.03] border-white/[0.07] text-white/55 hover:bg-white/[0.06] hover:border-white/20 hover:text-white/80"}`} + className={`group flex flex-col items-center justify-center gap-2 rounded-sm border py-4 px-2 transition-all duration-100 outline-none ${focused ? "bg-primary/12 border-primary/35 text-primary shadow-[0_0_0_1px_var(--primary),inset_0_1px_0_rgba(255,255,255,0.05)]" : "bg-white/3 border-white/[0.07] text-white/55 hover:bg-white/6 hover:border-white/20 hover:text-white/80"}`} > - +
    {item.label}
    {item.sub}
    @@ -385,7 +385,7 @@ export function VimInput({ onSubmit, onCommand, isCommandKOpen, setIsCommandKOpe {/* ── Main Input Bar ─────────────────────────────────────────────── */}
    -
    +
    {showTagSuggestions && (
    From 559855dd3581e402a8b122e762621fb23d36ef47 Mon Sep 17 00:00:00 2001 From: Aditya Gupta Date: Sun, 10 May 2026 17:07:17 +0000 Subject: [PATCH 2/6] feat: Make sidebar resizable --- components/project-sidebar.tsx | 71 +++++++++++++++++++++++++++++++++- 1 file changed, 69 insertions(+), 2 deletions(-) diff --git a/components/project-sidebar.tsx b/components/project-sidebar.tsx index 22a821d..984317b 100644 --- a/components/project-sidebar.tsx +++ b/components/project-sidebar.tsx @@ -67,6 +67,9 @@ export function ProjectSidebar({ openToSettings, onSettingsOpened, }: ProjectSidebarProps) { + const SIDEBAR_MIN_WIDTH = 200 + const SIDEBAR_MAX_WIDTH = 420 + const SIDEBAR_WIDTH_STORAGE_KEY = "nodepad.sidebar.width" const [editingId, setEditingId] = useState(null) const [editName, setEditName] = useState("") const [deletingId, setDeletingId] = useState(null) @@ -74,6 +77,10 @@ export function ProjectSidebar({ const [showKey, setShowKey] = useState(false) const [modelOpen, setModelOpen] = useState(false) const [providerOpen, setProviderOpen] = useState(false) + const [sidebarWidth, setSidebarWidth] = useState(240) + const [isResizing, setIsResizing] = useState(false) + const resizeStartXRef = useRef(0) + const resizeStartWidthRef = useRef(240) // local draft for settings (only save on "Save") const [draft, setDraft] = useState(aiSettings) const inputRef = useRef(null) @@ -90,6 +97,52 @@ export function ProjectSidebar({ if (showSettings) setDraft(aiSettings) }, [showSettings]) + // Load saved width (client only) + useEffect(() => { + if (typeof window === "undefined") return + const stored = window.localStorage.getItem(SIDEBAR_WIDTH_STORAGE_KEY) + if (!stored) return + const parsed = Number(stored) + if (Number.isFinite(parsed)) { + const next = Math.min(SIDEBAR_MAX_WIDTH, Math.max(SIDEBAR_MIN_WIDTH, parsed)) + setSidebarWidth(next) + } + }, []) + + // Persist width + useEffect(() => { + if (typeof window === "undefined") return + window.localStorage.setItem(SIDEBAR_WIDTH_STORAGE_KEY, String(sidebarWidth)) + }, [sidebarWidth]) + + useEffect(() => { + if (!isResizing) return + const body = document.body + const previousUserSelect = body.style.userSelect + const previousCursor = body.style.cursor + body.style.userSelect = "none" + body.style.cursor = "col-resize" + const handleMove = (event: MouseEvent) => { + const delta = event.clientX - resizeStartXRef.current + const nextWidth = Math.min( + SIDEBAR_MAX_WIDTH, + Math.max(SIDEBAR_MIN_WIDTH, resizeStartWidthRef.current + delta) + ) + setSidebarWidth(nextWidth) + } + const handleUp = () => { + setIsResizing(false) + } + window.addEventListener("mousemove", handleMove) + window.addEventListener("mouseup", handleUp) + return () => { + body.style.userSelect = previousUserSelect + body.style.cursor = previousCursor + window.removeEventListener("mousemove", handleMove) + window.removeEventListener("mouseup", handleUp) + } + }, [isResizing]) + // Jump straight to settings when requested externally useEffect(() => { if (openToSettings) { @@ -145,13 +198,13 @@ export function ProjectSidebar({ return (
    -
    +
    {/* Header */}
    @@ -611,6 +664,20 @@ export function ProjectSidebar({
    )}
    + +
    { + if (!isOpen) return + event.preventDefault() + setIsResizing(true) + resizeStartXRef.current = event.clientX + resizeStartWidthRef.current = sidebarWidth + }} + className={`absolute right-0 top-0 h-full w-1 cursor-col-resize transition-colors ${isResizing ? "bg-primary/30" : "bg-transparent hover:bg-white/10"}`} + />
    ) From 7114c11580bd99beac926d575014b3913f6d53d3 Mon Sep 17 00:00:00 2001 From: Aditya Gupta Date: Sun, 10 May 2026 17:22:05 +0000 Subject: [PATCH 3/6] refactor: Convert hardcoded colors to brand variables --- app/globals.css | 46 ++++++++++++++++++++++++++++++++++ app/not-found.tsx | 6 ++--- app/opengraph-image.tsx | 23 +++++++++++------ app/page.tsx | 8 +++--- components/intro-modal.tsx | 2 +- components/mobile-wall.tsx | 6 ++--- components/project-sidebar.tsx | 4 +-- components/tile-card.tsx | 10 ++++---- components/tile-index.tsx | 6 ++--- components/tiling-area.tsx | 2 +- 10 files changed, 83 insertions(+), 30 deletions(-) diff --git a/app/globals.css b/app/globals.css index 0e65dd7..ec77825 100644 --- a/app/globals.css +++ b/app/globals.css @@ -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 { @@ -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 { diff --git a/app/not-found.tsx b/app/not-found.tsx index 0b38538..08ef281 100644 --- a/app/not-found.tsx +++ b/app/not-found.tsx @@ -53,9 +53,9 @@ export default function NotFound() { {/* Logo mark */}
    - - - + + +
    nodepad diff --git a/app/opengraph-image.tsx b/app/opengraph-image.tsx index ef87b4c..bc67e9d 100644 --- a/app/opengraph-image.tsx +++ b/app/opengraph-image.tsx @@ -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( ( @@ -16,7 +23,7 @@ export default function OGImage() { flexDirection: "column", alignItems: "flex-start", justifyContent: "flex-end", - background: "#0a0a0a", + background: OG_COLORS.background, padding: "80px 96px", fontFamily: "sans-serif", }} @@ -24,11 +31,11 @@ export default function OGImage() { {/* Logo mark */}
    -
    -
    -
    +
    +
    +
    - + nodepad
    @@ -38,7 +45,7 @@ export default function OGImage() { style={{ fontSize: 72, fontWeight: 700, - color: "#f0f0f0", + color: OG_COLORS.text, lineHeight: 1.05, letterSpacing: "-2px", marginBottom: 32, @@ -46,11 +53,11 @@ export default function OGImage() { > Think spatially.
    - Let AI fill the gaps. + Let AI fill the gaps.
    {/* Subline */} -
    +
    nodepad.space
    diff --git a/app/page.tsx b/app/page.tsx index 948b1fe..65589c7 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1103,12 +1103,12 @@ export default function Page() { /> {isHydrated && !settings.apiKey && ( -
    - ⚡ AI enrichment requires an OpenRouter API key — use a free model (no credits needed) or add credits for GPT-4o, Claude, and more. Configure in the ☰ left panel. +
    + ⚡ AI enrichment requires an OpenRouter API key — use a free model (no credits needed) or add credits for GPT-4o, Claude, and more. Configure in the ☰ left panel.
    @@ -1204,7 +1204,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" >
    {undoToast} diff --git a/components/intro-modal.tsx b/components/intro-modal.tsx index 2ea3aa6..437ec68 100644 --- a/components/intro-modal.tsx +++ b/components/intro-modal.tsx @@ -44,7 +44,7 @@ export function IntroModal({ open, onClose }: IntroModalProps) { animate={{ opacity: 1, scale: 1, y: 0 }} exit={{ opacity: 0, scale: 0.96, y: 8 }} transition={{ duration: 0.25, ease: "easeOut" }} - className="relative w-full max-w-3xl bg-[#0d0d0d] border border-white/10 rounded-sm shadow-2xl overflow-hidden" + className="relative w-full max-w-3xl bg-(--ui-surface-modal) border border-white/10 rounded-sm shadow-2xl overflow-hidden" > {/* Header */}
    diff --git a/components/mobile-wall.tsx b/components/mobile-wall.tsx index 62b448e..18c89b3 100644 --- a/components/mobile-wall.tsx +++ b/components/mobile-wall.tsx @@ -6,9 +6,9 @@ export function MobileWall() { {/* Logo */}
    - - - + + +
    nodepad diff --git a/components/project-sidebar.tsx b/components/project-sidebar.tsx index 984317b..eae3dc7 100644 --- a/components/project-sidebar.tsx +++ b/components/project-sidebar.tsx @@ -374,7 +374,7 @@ export function ProjectSidebar({ animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -4 }} transition={{ duration: 0.1 }} - className="absolute top-full left-0 right-0 z-20 mt-1 overflow-hidden rounded-md border border-white/10 bg-[#0d0d10] shadow-xl" + className="absolute top-full left-0 right-0 z-20 mt-1 overflow-hidden rounded-md border border-white/10 bg-(--ui-surface-panel) shadow-xl" > {AI_PROVIDER_PRESETS.map(preset => (
    ))} diff --git a/components/tile-index.tsx b/components/tile-index.tsx index ba912f6..f5132dc 100644 --- a/components/tile-index.tsx +++ b/components/tile-index.tsx @@ -17,9 +17,9 @@ interface TileIndexProps { export function TileIndex({ blocks, onHighlight, highlightedId, onClose, isOpen, viewMode }: TileIndexProps) { const getIcon = (type: string) => { switch (type) { - case "task": return - case "thesis": return - case "question": return + case "task": return + case "thesis": return + case "question": return default: return } } diff --git a/components/tiling-area.tsx b/components/tiling-area.tsx index cda9752..0b3c152 100644 --- a/components/tiling-area.tsx +++ b/components/tiling-area.tsx @@ -242,7 +242,7 @@ export function TilingArea({ } return ( -
    +
    {/* Task Header stays sticky at top */} {taskBlock && (
    From eea46201b0e453568747438bc390271f4e25652c Mon Sep 17 00:00:00 2001 From: Aditya Gupta Date: Mon, 11 May 2026 15:06:11 +0000 Subject: [PATCH 4/6] refactor: keybind management and add keybind for sidebar --- app/page.tsx | 12 +++++++--- components/about-panel.tsx | 9 +++---- lib/keybinds.tsx | 49 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 63 insertions(+), 7 deletions(-) create mode 100644 lib/keybinds.tsx diff --git a/app/page.tsx b/app/page.tsx index 65589c7..9a76ca0 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -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" @@ -683,11 +684,16 @@ 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") { @@ -695,7 +701,7 @@ export default function Page() { undo() } } - if (e.key === "Escape") { + if (action === "escape") { if (isCommandKOpen) { setIsCommandKOpen(false) } else if (isGhostPanelOpen) { diff --git a/components/about-panel.tsx b/components/about-panel.tsx index c15445a..8e3e05d 100644 --- a/components/about-panel.tsx +++ b/components/about-panel.tsx @@ -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 @@ -96,6 +97,7 @@ const CONTENT_TYPE_HIGHLIGHTS = [ export function AboutPanel({ open, onClose }: AboutPanelProps) { const mod = useModKey() + const shortcuts = getKeyboardShortcuts(mod) return ( { if (!v) onClose() }}>
    - - - - + {shortcuts.map((shortcut) => ( + + ))}
    diff --git a/lib/keybinds.tsx b/lib/keybinds.tsx new file mode 100644 index 0000000..f6eae15 --- /dev/null +++ b/lib/keybinds.tsx @@ -0,0 +1,49 @@ + +export interface KeybindDefinition { + id: string + keys: string[] + label: string +} + +export interface KeybindBinding { + id: string + key: string + requiresMod?: boolean + requiresShift?: boolean +} + +export const getKeyboardShortcuts = (modKey: string): KeybindDefinition[] => [ + { id: "command-menu", keys: [modKey, "K"], label: "Command menu" }, + { id: "toggle-sidebar", keys: [modKey, "B"], label: "Toggle sidebar" }, + { id: "undo", keys: [modKey, "Z"], label: "Undo last action" }, + { id: "submit-node", keys: ["Enter"], label: "Submit a new node" }, + { id: "escape", keys: ["Esc"], label: "Close command menu / deselect" }, +] + +export const GLOBAL_KEYBINDS: KeybindBinding[] = [ + { id: "command-menu", key: "k", requiresMod: true }, + { id: "toggle-sidebar", key: "b", requiresMod: true }, + { id: "undo", key: "z", requiresMod: true, requiresShift: false }, + { id: "escape", key: "Escape" }, +] + +export const matchKeybind = (event: KeyboardEvent, binding: KeybindBinding): boolean => { + const key = event.key.length === 1 ? event.key.toLowerCase() : event.key + const expectedKey = binding.key.length === 1 ? binding.key.toLowerCase() : binding.key + if (key !== expectedKey) return false + + if (binding.requiresMod) { + if (!event.metaKey && !event.ctrlKey) return false + } + + if (typeof binding.requiresShift === "boolean" && event.shiftKey !== binding.requiresShift) { + return false + } + + return true +} + +export const getGlobalKeybindAction = (event: KeyboardEvent): string | null => { + const match = GLOBAL_KEYBINDS.find(binding => matchKeybind(event, binding)) + return match?.id ?? null +} From 4987ada65fde43d98cf0923189b3d5eb75572d1d Mon Sep 17 00:00:00 2001 From: Aditya Gupta Date: Sun, 17 May 2026 02:42:26 +0000 Subject: [PATCH 5/6] feat: Add a retry button for failed enrichments --- components/tile-card.tsx | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/components/tile-card.tsx b/components/tile-card.tsx index cb44454..6c5d596 100644 --- a/components/tile-card.tsx +++ b/components/tile-card.tsx @@ -639,13 +639,20 @@ export const TileCard = memo(function TileCard({
    {block.isError && (
    - + {block.statusText === "no-api-key" ? <>AI enrichment failed — no API key. Open the ☰ sidebar → Settings to add your API key. : block.statusText - ? <>{block.statusText}{" "}Double-click to retry. - : "Enrichment failed. Double-click to retry."} + ? <>{block.statusText} + : "Enrichment failed."} +
    )}
    From fc1370f949dc4518ca66200d878cdd0ac7754f54 Mon Sep 17 00:00:00 2001 From: Aditya Gupta Date: Sun, 17 May 2026 02:51:00 +0000 Subject: [PATCH 6/6] fix: Transparent model selector dropdown --- app/globals.css | 21 +++++++++++++++++++++ components/project-sidebar.tsx | 4 ++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/app/globals.css b/app/globals.css index ec77825..36bd327 100644 --- a/app/globals.css +++ b/app/globals.css @@ -148,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); diff --git a/components/project-sidebar.tsx b/components/project-sidebar.tsx index eae3dc7..175b64f 100644 --- a/components/project-sidebar.tsx +++ b/components/project-sidebar.tsx @@ -374,7 +374,7 @@ export function ProjectSidebar({ animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -4 }} transition={{ duration: 0.1 }} - className="absolute top-full left-0 right-0 z-20 mt-1 overflow-hidden rounded-md border border-white/10 bg-(--ui-surface-panel) shadow-xl" + className="absolute top-full left-0 right-0 z-20 mt-1 overflow-hidden rounded-md border border-white/10 bg-ui-surface-panel shadow-xl" > {AI_PROVIDER_PRESETS.map(preset => (