add help docs and ?

This commit is contained in:
2026-03-30 23:10:42 -07:00
parent ced43bec4f
commit 63d76bac72
76 changed files with 2184 additions and 1 deletions

View File

@@ -1,7 +1,12 @@
import React, { useContext, useRef, useCallback, useState, useEffect, memo, lazy, Suspense } from 'react';
import ReactDOM from 'react-dom';
import { Handle, NodeResizeControl, Position, useStore } from '@xyflow/react';
import { marked } from 'marked';
import { getNodeDoc } from './api';
import LinePlotOverlay from './LinePlotOverlay';
marked.use({ breaks: true, gfm: true });
const SurfaceView = lazy(() => import('./SurfaceView'));
const CrossSectionOverlay = lazy(() => import('./CrossSectionOverlay'));
const CropBoxOverlay = lazy(() => import('./CropBoxOverlay'));
@@ -973,10 +978,48 @@ function NodeTable({ rows }) {
);
}
// ── Node help panel (portal) ──────────────────────────────────────────
function NodeHelpPanel({ title, content, onClose }) {
useEffect(() => {
const handler = (e) => { if (e.key === 'Escape') onClose(); };
document.addEventListener('keydown', handler);
return () => document.removeEventListener('keydown', handler);
}, [onClose]);
return ReactDOM.createPortal(
<div className="node-help-panel">
<div className="node-help-panel-header">
<span className="node-help-panel-title">{title}</span>
<button className="node-help-panel-close" onClick={onClose} title="Close">×</button>
</div>
<div
className="node-help-panel-body nowheel"
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{ __html: marked.parse(content || '') }}
/>
</div>,
document.body,
);
}
// ── CustomNode component ──────────────────────────────────────────────
function CustomNode({ id, data }) {
const ctx = useContext(NodeContext);
const [helpOpen, setHelpOpen] = useState(false);
const [helpContent, setHelpContent] = useState(null);
const onHelpClick = useCallback(async (e) => {
e.stopPropagation();
if (helpOpen) { setHelpOpen(false); return; }
setHelpOpen(true);
if (helpContent === null) {
const text = await getNodeDoc(data.label);
setHelpContent(text || '*No documentation available for this node.*');
}
}, [helpOpen, helpContent, data.label]);
if (data.className === 'Group') {
return <GroupNode id={id} data={data} />;
}
@@ -1181,7 +1224,10 @@ function CustomNode({ id, data }) {
{/* Title */}
<div className="node-title drag-handle" style={{ background: catColor }}>
<span className="node-title-main">{data.label}</span>
{headerMeta && <span className="node-title-meta" title={headerMeta}>{headerMeta}</span>}
<div className="node-title-right">
{headerMeta && <span className="node-title-meta" title={headerMeta}>{headerMeta}</span>}
<button className="node-help-btn nodrag nopan" title="Documentation" onClick={onHelpClick}>?</button>
</div>
</div>
<div className="node-body">
@@ -1528,6 +1574,13 @@ function CustomNode({ id, data }) {
)}
</div>
</div>
{helpOpen && (
<NodeHelpPanel
title={data.label}
content={helpContent ?? '*Loading…*'}
onClose={() => setHelpOpen(false)}
/>
)}
</>
);
}

View File

@@ -62,6 +62,12 @@ export async function getNodes() {
return r.json();
}
export async function getNodeDoc(displayName) {
const r = await sessionFetch(`/docs?name=${encodeURIComponent(displayName)}`);
if (!r.ok) return null;
return r.text();
}
export async function getFiles() {
const r = await sessionFetch('/files');
if (!r.ok) return [];

View File

@@ -435,6 +435,149 @@ html, body, #root {
text-overflow: ellipsis;
}
.node-title-right {
display: flex;
align-items: center;
gap: 5px;
flex-shrink: 0;
}
.node-help-btn {
width: 15px;
height: 15px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.12);
border: 1px solid rgba(255, 255, 255, 0.25);
color: rgba(255, 255, 255, 0.75);
font-size: 9px;
font-weight: 700;
line-height: 1;
padding: 0;
cursor: pointer;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.15s, border-color 0.15s;
}
.node-help-btn:hover {
background: rgba(255, 255, 255, 0.28);
border-color: rgba(255, 255, 255, 0.5);
}
/* ── Node help panel ─────────────────────────────────────── */
.node-help-panel {
position: fixed;
top: 60px;
right: 20px;
width: 420px;
max-height: calc(100vh - 80px);
background: #1e293b;
border: 1px solid #334155;
border-radius: 8px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.55);
display: flex;
flex-direction: column;
z-index: 9999;
overflow: hidden;
}
.node-help-panel-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 9px 12px;
border-bottom: 1px solid #334155;
background: #0f172a;
flex-shrink: 0;
}
.node-help-panel-title {
font-weight: 600;
font-size: 13px;
color: #f1f5f9;
}
.node-help-panel-close {
background: none;
border: none;
color: #64748b;
font-size: 20px;
line-height: 1;
padding: 0 2px;
cursor: pointer;
transition: color 0.12s;
}
.node-help-panel-close:hover {
color: #f1f5f9;
}
.node-help-panel-body {
padding: 14px 16px;
overflow-y: auto;
flex: 1;
font-size: 12.5px;
color: #cbd5e1;
line-height: 1.65;
}
.node-help-panel-body h1,
.node-help-panel-body h2,
.node-help-panel-body h3 {
color: #f1f5f9;
margin: 14px 0 5px;
font-weight: 600;
}
.node-help-panel-body h1 { font-size: 15px; margin-top: 0; }
.node-help-panel-body h2 { font-size: 13px; }
.node-help-panel-body h3 { font-size: 12px; }
.node-help-panel-body p { margin: 4px 0 8px; }
.node-help-panel-body table {
border-collapse: collapse;
width: 100%;
font-size: 11.5px;
margin: 8px 0 12px;
}
.node-help-panel-body th,
.node-help-panel-body td {
border: 1px solid #334155;
padding: 4px 8px;
text-align: left;
}
.node-help-panel-body th {
background: #0f172a;
color: #94a3b8;
font-weight: 600;
}
.node-help-panel-body code {
background: #0f172a;
padding: 1px 5px;
border-radius: 3px;
font-size: 11px;
color: #7dd3fc;
}
.node-help-panel-body ul,
.node-help-panel-body ol {
padding-left: 18px;
margin: 4px 0 8px;
}
.node-help-panel-body li { margin: 2px 0; }
.node-help-panel-body em { color: #94a3b8; }
.node-help-panel-body strong { color: #e2e8f0; }
.node-body {
padding: 4px 0;
display: flex;