From 513b09798a18a1b11eef121c5f0c3e8721b6adac Mon Sep 17 00:00:00 2001 From: Emilio Balda Date: Mon, 7 Sep 2026 12:10:24 +0300 Subject: [PATCH] Make concept detail panel resizable and collapsible - Add a header toggle button (PanelRight icon) to show/hide the right-hand concept detail panel, mirroring the existing left sidebar toggle. - Add a drag handle on the detail panel's left edge to resize it horizontally (clamped between 260px and 640px), driven by a --detail-width CSS variable on the workspace grid. - Detail.jsx is unchanged; the panel's size/visibility is purely a layout concern handled in App.jsx and styles.css. Co-authored-by: Cursor --- src/App.jsx | 65 ++++++++++++++++++++++++++++++++++++++++++++++---- src/styles.css | 45 +++++++++++++++++++++++++++++++--- 2 files changed, 102 insertions(+), 8 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index 9769276..e05dc03 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -13,7 +13,7 @@ // limitations under the License. import { lazy, Suspense, useCallback, useEffect, useMemo, useState } from "react"; -import { Moon, PanelLeft, Sun } from "lucide-react"; +import { Moon, PanelLeft, PanelRight, Sun } from "lucide-react"; import { filterConceptIds } from "./navigation.js"; import { Sidebar } from "./Sidebar.jsx"; @@ -22,6 +22,10 @@ import { preferredTheme, readStoredTheme, saveTheme } from "./theme.js"; const Detail = lazy(() => import("./Detail.jsx").then((module) => ({ default: module.Detail }))); const Graph = lazy(() => import("./Graph.jsx").then((module) => ({ default: module.Graph }))); +const MIN_DETAIL_WIDTH = 260; +const MAX_DETAIL_WIDTH = 640; +const DEFAULT_DETAIL_WIDTH = 340; + export function App({ bundle }) { const [search, setSearch] = useState(""); const [typeFilter, setTypeFilter] = useState(""); @@ -30,6 +34,8 @@ export function App({ bundle }) { const [sidebarOpen, setSidebarOpen] = useState( () => typeof window === "undefined" || !window.matchMedia("(max-width: 860px)").matches, ); + const [detailOpen, setDetailOpen] = useState(true); + const [detailWidth, setDetailWidth] = useState(DEFAULT_DETAIL_WIDTH); const [theme, setTheme] = useState(() => readStoredTheme() || preferredTheme()); const [manualTheme, setManualTheme] = useState(() => Boolean(readStoredTheme())); const conceptIds = useMemo(() => new Set(bundle.nodes.map((node) => node.data.id)), [bundle.nodes]); @@ -76,6 +82,29 @@ export function App({ bundle }) { saveTheme(nextTheme); } + function toggleDetail() { + setDetailOpen((open) => !open); + } + + function handleDetailResizeStart(event) { + event.preventDefault(); + const startX = event.clientX; + const startWidth = detailWidth; + + function handleMove(moveEvent) { + const nextWidth = startWidth + (startX - moveEvent.clientX); + setDetailWidth(Math.min(MAX_DETAIL_WIDTH, Math.max(MIN_DETAIL_WIDTH, nextWidth))); + } + + function handleUp() { + document.removeEventListener("pointermove", handleMove); + document.removeEventListener("pointerup", handleUp); + } + + document.addEventListener("pointermove", handleMove); + document.addEventListener("pointerup", handleUp); + } + return (
@@ -126,6 +155,16 @@ export function App({ bundle }) {
@@ -145,7 +184,12 @@ export function App({ bundle }) { ) : null} -
+
{sidebarOpen ? (
); diff --git a/src/styles.css b/src/styles.css index 2869444..5328163 100644 --- a/src/styles.css +++ b/src/styles.css @@ -255,7 +255,7 @@ a:focus-visible { .workspace { display: grid; - grid-template-columns: minmax(0, 3fr) minmax(260px, 2fr); + grid-template-columns: minmax(0, 3fr) var(--detail-width, minmax(260px, 2fr)); flex: 1; min-width: 0; min-height: 0; @@ -263,7 +263,15 @@ a:focus-visible { } .workspace[data-sidebar-open="true"] { - grid-template-columns: 232px minmax(0, 3fr) minmax(260px, 2fr); + grid-template-columns: 232px minmax(0, 3fr) var(--detail-width, minmax(260px, 2fr)); +} + +.workspace[data-detail-open="false"] { + grid-template-columns: minmax(0, 1fr); +} + +.workspace[data-sidebar-open="true"][data-detail-open="false"] { + grid-template-columns: 232px minmax(0, 1fr); } .sidebar-pane { @@ -484,7 +492,31 @@ a:focus-visible { font-weight: 650; } +.detail-pane { + position: relative; + min-width: 0; + min-height: 0; + overflow: hidden; +} + +.detail-resize-handle { + position: absolute; + top: 0; + left: -3px; + z-index: 1; + width: 6px; + height: 100%; + cursor: col-resize; + touch-action: none; +} + +.detail-resize-handle:hover, +.detail-resize-handle:active { + background: var(--accent-soft); +} + .detail { + height: 100%; min-width: 0; min-height: 0; padding: var(--space-20) var(--space-24) var(--space-24); @@ -818,10 +850,17 @@ hr { border-bottom: 1px solid var(--border); } - .detail { + .detail-pane { flex: 0 0 auto; + } + + .detail { overflow-y: visible; } + + .detail-resize-handle { + display: none; + } } @media (max-width: 520px) {