diff --git a/components/graph-view.tsx b/components/graph-view.tsx index 9ce650e..3a5fe59 100644 --- a/components/graph-view.tsx +++ b/components/graph-view.tsx @@ -194,6 +194,328 @@ function SettingSlider({ ); } +/** + * The rendering-settings gear + its collapsible panel (owner, graph-view + * round 2): its own container at the top right, separate from the search + * box and the import button — clicking the gear expands/collapses the + * panel, which drops below the button, right-aligned with it. + */ +function GraphSettingsControl({ + settings, + changeSettings, +}: { + settings: GraphRenderSettings; + changeSettings: (update: (cur: RenderSettings) => RenderSettings) => void; +}) { + const [open, setOpen] = useState(false); + const regionTuning = settings.regions; + const physicsTuning = settings.physics; + return ( +
+ + {open && ( +
+
+ {/* All sliders: log-scaled 0-100, step 5, so every + DEFAULT sits at 50 (src/core/render-settings.ts). */} + + changeSettings((cur) => ({ ...cur, nodeSize: fromSliderPosition(p, NODE_SIZE_RANGE.min, NODE_SIZE_RANGE.max) }))} + /> + + changeSettings((cur) => ({ ...cur, edgeWidth: fromSliderPosition(p, EDGE_WIDTH_RANGE.min, EDGE_WIDTH_RANGE.max) }))} + /> + + changeSettings((cur) => ({ ...cur, arrowLength: fromSliderPosition(p, ARROW_LENGTH_RANGE.min, ARROW_LENGTH_RANGE.max) }))} + /> + + changeSettings((cur) => ({ ...cur, labelSize: fromSliderPosition(p, LABEL_SIZE_RANGE.min, LABEL_SIZE_RANGE.max) }))} + /> + {/* Displayed units normalize the default (0.5) to "1×". */} + + changeSettings((cur) => ({ ...cur, labelZoom: fromSliderPosition(p, LABEL_ZOOM_MIN, LABEL_ZOOM_MAX) }))} + /> + + {/* Regions: the blurred cluster tints. Sliders + a fixed-set + dropdown for the bitmap resolution (it's a discrete + choice, not a free number). Server-persisted like the + sizes; every range end was sweep-validated on the real + graph. */} +
+ + Regions + + + changeSettings((cur) => ({ + ...cur, + regions: { ...cur.regions, dilation: fromSliderPosition(p, REGION_DILATION_RANGE.min, REGION_DILATION_RANGE.max) }, + }))} + /> + + + changeSettings((cur) => ({ + ...cur, + regions: { ...cur.regions, edgeAlpha: fromSliderPosition(p, REGION_EDGE_ALPHA_RANGE.min, REGION_EDGE_ALPHA_RANGE.max) }, + }))} + /> + +
+ {/* Physics: the live sim's forces. Every value feeds the SAME + engine the server settle runs (createGraphSimulation); + changing one rebuilds the sim from the current positions — + drag mid-settle re-anneals, pins survive. Sliders over + sweep-validated ranges (src/core/render-settings.ts): + range ends and all four corners settle cleanly — no NaN, + no explosion, no non-settle. Repulsion is displayed as + the negative it is; cluster pull's far left = off. */} +
+ + Physics + + + + changeSettings((cur) => ({ + ...cur, + physics: { ...cur.physics, depthStrength: fromSliderPosition(p, DEPTH_STRENGTH_RANGE.min, DEPTH_STRENGTH_RANGE.max) }, + }))} + /> + + changeSettings((cur) => ({ + ...cur, + physics: { ...cur.physics, depthSpacing: fromSliderPosition(p, DEPTH_SPACING_RANGE.min, DEPTH_SPACING_RANGE.max) }, + }))} + /> + + changeSettings((cur) => ({ + ...cur, + physics: { ...cur.physics, depthBaseRadius: fromSliderPosition(p, DEPTH_BASE_RADIUS_RANGE.min, DEPTH_BASE_RADIUS_RANGE.max) }, + }))} + /> + + changeSettings((cur) => ({ + ...cur, + physics: { ...cur.physics, chargeStrength: -fromSliderPosition(p, -CHARGE_RANGE.max, -CHARGE_RANGE.min) }, + }))} + /> + + changeSettings((cur) => ({ + ...cur, + physics: { ...cur.physics, linkStrength: fromSliderPosition(p, LINK_STRENGTH_RANGE.min, LINK_STRENGTH_RANGE.max) }, + }))} + /> + + changeSettings((cur) => ({ + ...cur, + physics: { ...cur.physics, linkDistance: fromSliderPosition(p, LINK_DISTANCE_RANGE.min, LINK_DISTANCE_RANGE.max) }, + }))} + /> + + changeSettings((cur) => ({ + ...cur, + physics: { ...cur.physics, gravity: fromSliderPosition(p, GRAVITY_RANGE.min, GRAVITY_RANGE.max) }, + }))} + /> + + changeSettings((cur) => ({ + ...cur, + physics: { ...cur.physics, clusterPull: fromOffSliderPosition(p, CLUSTER_PULL_RANGE.min, CLUSTER_PULL_RANGE.max) }, + }))} + /> + + changeSettings((cur) => ({ + ...cur, + physics: { ...cur.physics, velocityDecay: fromSliderPosition(p, VELOCITY_DECAY_RANGE.min, VELOCITY_DECAY_RANGE.max) }, + }))} + /> +
+
+
+ )} +
+ ); +} + export function GraphView() { const containerRef = useRef(null); const canvasRef = useRef(null); @@ -207,7 +529,6 @@ export function GraphView() { // Search panel state: the query text + the settings section toggle. const [query, setQuery] = useState(""); - const [settingsOpen, setSettingsOpen] = useState(false); // Render settings: SERVER-persisted (data/render-settings.json via // /api/graph/render-settings) — sizes AND the region/physics sections // (user-facing since 2026-09-13; the dev-session era is over). Loaded @@ -522,319 +843,33 @@ export function GraphView() { )} {/* Legend + filters: search box and per-state toggle rows. */} + {/* Top-right controls — three separate floating containers (owner, + graph-view round 2): the search box, the settings gear (its own + collapsible component), and the import button. */} {!isEmpty && ( -
-
- - setQuery(e.target.value)} - placeholder="Search nodes…" - spellCheck={false} - className="h-8 w-full rounded-lg border bg-background pl-8 pr-8 text-xs outline-none placeholder:text-muted-foreground focus:border-ring" - /> - +
+ {/* Search — its own container (icon + input + no-match note). */} +
+
+ + setQuery(e.target.value)} + placeholder="Search nodes…" + spellCheck={false} + className="h-8 w-full rounded-lg border bg-background pl-8 text-xs outline-none placeholder:text-muted-foreground focus:border-ring" + /> +
+ {snapshot !== null && visibleIds.size === 0 && query.trim() !== "" && ( +
+ No matching nodes. +
+ )}
- {settingsOpen && ( -
- {/* All sliders: log-scaled 0-100, step 5, so every - DEFAULT sits at 50 (src/core/render-settings.ts). */} - - changeSettings((cur) => ({ ...cur, nodeSize: fromSliderPosition(p, NODE_SIZE_RANGE.min, NODE_SIZE_RANGE.max) }))} - /> - - changeSettings((cur) => ({ ...cur, edgeWidth: fromSliderPosition(p, EDGE_WIDTH_RANGE.min, EDGE_WIDTH_RANGE.max) }))} - /> - - changeSettings((cur) => ({ ...cur, arrowLength: fromSliderPosition(p, ARROW_LENGTH_RANGE.min, ARROW_LENGTH_RANGE.max) }))} - /> - - changeSettings((cur) => ({ ...cur, labelSize: fromSliderPosition(p, LABEL_SIZE_RANGE.min, LABEL_SIZE_RANGE.max) }))} - /> - {/* Displayed units normalize the default (0.5) to "1×". */} - - changeSettings((cur) => ({ ...cur, labelZoom: fromSliderPosition(p, LABEL_ZOOM_MIN, LABEL_ZOOM_MAX) }))} - /> - - {/* Regions: the blurred cluster tints. Sliders + a fixed-set - dropdown for the bitmap resolution (it's a discrete - choice, not a free number). Server-persisted like the - sizes; every range end was sweep-validated on the real - graph. */} -
- - Regions - - - changeSettings((cur) => ({ - ...cur, - regions: { ...cur.regions, dilation: fromSliderPosition(p, REGION_DILATION_RANGE.min, REGION_DILATION_RANGE.max) }, - }))} - /> - - - changeSettings((cur) => ({ - ...cur, - regions: { ...cur.regions, edgeAlpha: fromSliderPosition(p, REGION_EDGE_ALPHA_RANGE.min, REGION_EDGE_ALPHA_RANGE.max) }, - }))} - /> - -
- {/* Physics: the live sim's forces. Every value feeds the SAME - engine the server settle runs (createGraphSimulation); - changing one rebuilds the sim from the current positions — - drag mid-settle re-anneals, pins survive. Sliders over - sweep-validated ranges (src/core/render-settings.ts): - range ends and all four corners settle cleanly — no NaN, - no explosion, no non-settle. Repulsion is displayed as - the negative it is; cluster pull's far left = off. */} -
- - Physics - - - - changeSettings((cur) => ({ - ...cur, - physics: { ...cur.physics, depthStrength: fromSliderPosition(p, DEPTH_STRENGTH_RANGE.min, DEPTH_STRENGTH_RANGE.max) }, - }))} - /> - - changeSettings((cur) => ({ - ...cur, - physics: { ...cur.physics, depthSpacing: fromSliderPosition(p, DEPTH_SPACING_RANGE.min, DEPTH_SPACING_RANGE.max) }, - }))} - /> - - changeSettings((cur) => ({ - ...cur, - physics: { ...cur.physics, depthBaseRadius: fromSliderPosition(p, DEPTH_BASE_RADIUS_RANGE.min, DEPTH_BASE_RADIUS_RANGE.max) }, - }))} - /> - - changeSettings((cur) => ({ - ...cur, - physics: { ...cur.physics, chargeStrength: -fromSliderPosition(p, -CHARGE_RANGE.max, -CHARGE_RANGE.min) }, - }))} - /> - - changeSettings((cur) => ({ - ...cur, - physics: { ...cur.physics, linkStrength: fromSliderPosition(p, LINK_STRENGTH_RANGE.min, LINK_STRENGTH_RANGE.max) }, - }))} - /> - - changeSettings((cur) => ({ - ...cur, - physics: { ...cur.physics, linkDistance: fromSliderPosition(p, LINK_DISTANCE_RANGE.min, LINK_DISTANCE_RANGE.max) }, - }))} - /> - - changeSettings((cur) => ({ - ...cur, - physics: { ...cur.physics, gravity: fromSliderPosition(p, GRAVITY_RANGE.min, GRAVITY_RANGE.max) }, - }))} - /> - - changeSettings((cur) => ({ - ...cur, - physics: { ...cur.physics, clusterPull: fromOffSliderPosition(p, CLUSTER_PULL_RANGE.min, CLUSTER_PULL_RANGE.max) }, - }))} - /> - - changeSettings((cur) => ({ - ...cur, - physics: { ...cur.physics, velocityDecay: fromSliderPosition(p, VELOCITY_DECAY_RANGE.min, VELOCITY_DECAY_RANGE.max) }, - }))} - /> -
-
- )} - {snapshot !== null && visibleIds.size === 0 && query.trim() !== "" && ( -
- No matching nodes. -
- )} -
+ {/* Rendering settings — own component; the gear toggles it. */} + + {/* Import notes — its own container. */} +
fileInputRef.current?.click()} - className="flex h-7 w-full items-center justify-center gap-1.5 rounded-lg border text-xs text-muted-foreground transition-colors hover:bg-accent disabled:opacity-50" + className="flex h-8 items-center gap-1.5 rounded-lg px-3 text-xs text-muted-foreground transition-colors hover:bg-accent disabled:opacity-50" > {importing ? "Importing…" : "Import notes…"}