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
65 changes: 60 additions & 5 deletions src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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("");
Expand All @@ -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]);
Expand Down Expand Up @@ -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);
}
Comment on lines +89 to +106

return (
<div className="app-shell">
<header className="app-header">
Expand Down Expand Up @@ -126,6 +155,16 @@ export function App({ bundle }) {
<Moon size={16} strokeWidth={1.5} aria-hidden="true" />
)}
</button>
<button
className="icon-button detail-toggle"
type="button"
aria-controls="concept-detail"
aria-expanded={detailOpen}
aria-label={detailOpen ? "Hide concept detail" : "Show concept detail"}
onClick={toggleDetail}
>
<PanelRight size={16} strokeWidth={1.25} aria-hidden="true" />
</button>
</div>
</header>

Expand All @@ -145,7 +184,12 @@ export function App({ bundle }) {
</aside>
) : null}

<main className="workspace" data-sidebar-open={sidebarOpen || undefined}>
<main
className="workspace"
data-sidebar-open={sidebarOpen || undefined}
data-detail-open={detailOpen ? undefined : "false"}
style={detailOpen ? { "--detail-width": `${detailWidth}px` } : undefined}
>
{sidebarOpen ? (
<aside className="sidebar-pane" id="bundle-sidebar">
<Sidebar
Expand All @@ -170,9 +214,20 @@ export function App({ bundle }) {
onSelect={selectConcept}
/>
</Suspense>
<Suspense fallback={<section className="detail loading-panel muted">Loading concept…</section>}>
<Detail bundle={bundle} selectedId={selectedId} onSelect={selectConcept} />
</Suspense>
{detailOpen ? (
<div className="detail-pane" id="concept-detail">
<div
className="detail-resize-handle"
role="separator"
aria-orientation="vertical"
aria-label="Resize concept detail panel"
onPointerDown={handleDetailResizeStart}
/>
Comment on lines +219 to +225
<Suspense fallback={<section className="detail loading-panel muted">Loading concept…</section>}>
<Detail bundle={bundle} selectedId={selectedId} onSelect={selectConcept} />
</Suspense>
</div>
) : null}
</main>
</div>
);
Expand Down
45 changes: 42 additions & 3 deletions src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -255,15 +255,23 @@ 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;
overflow: hidden;
}

.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 {
Expand Down Expand Up @@ -484,7 +492,31 @@ a:focus-visible {
font-weight: 650;
}

.detail-pane {
position: relative;
min-width: 0;
min-height: 0;
overflow: hidden;
}
Comment on lines +495 to +500

.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);
Expand Down Expand Up @@ -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) {
Expand Down