Files
nvcurve/frontend/src/components/CurveEditor/CurveToolbar.tsx
T
ARIA 930e56bd07 Clean up LSP diagnostics across backend and frontend
Backend (nvcurve/):
- hal/fans.py, hal/limits.py, hal/gpu.py: replace conditional pynvml
  imports with the established 'pynvml: Any = _pynvml_import' pattern
  (fixes ~50 'possibly unbound' errors); type the result dicts; guard
  query_interface() results; explicit uuid/pci-bus parsing (int, hex
  convention documented); modernize Optional[T] -> T | None
- cli.py: fix 'curve_state' possibly-unbound and snap_path None handling
  in cmd_setup; wrap unchecked int()/open()/makedirs() calls in
  try/except with clean CLI errors; add module logger for silent
  except-pass blocks; raise ... from exc; fix unused loop vars and
  set-comprehension
- hal/snapshot.py: filepath: str | None; wrap all file ops; sorted
  imports; remove unused CT_POINTS import
- daemon.py: extract 0o666 to _SOCKET_MODE constant (intentional for
  /run sockets) with nosemgrep
- server.py: nosemgrep for Python 3.7-compat false positive (project
  requires >= 3.12); log previously-swallowed exception
- profiles/native.py, profiles/apply.py: wrap file ops and int(k)
  profile-key parsing; sorted imports; modernize typing

Frontend (frontend/src):
- Add .js extensions to all relative imports (standard TS-ESM; Vite
  resolves .js -> .ts)
- React.FormEvent (deprecated in React 19 types) -> React.SubmitEvent
- catch (e: any) -> catch (e: unknown) + instanceof Error narrowing
- React-hooks: move ref writes from render into effects; convert
  viewport reset to render-phase state adjustment; split
  selectPoint(index, multi) into selectPoint + togglePoint (no flag
  argument); remove non-null assertion
- Static inline styles -> Tailwind classes (dynamic positioning/cursor
  styles kept)
- Remove non-standard 'container' option from scrollIntoView (browsers
  ignore unknown options) which had orphaned a @ts-expect-error
- Object.fromEntries for Map -> Record conversion

Tooling:
- .gitignore: ignore .codegraph/ local tool data

Verified: tsc --noEmit, vite production build, python imports, and
full LSP scan (0 errors/warnings in both projects).
2026-09-08 23:57:30 +02:00

176 lines
5.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useMemo } from "react";
import { ZoomIn, RotateCcw, Minus } from "lucide-react";
import { useCurveStore } from "../../store/curveStore.js";
import type { VFPoint } from "../../types.js";
interface Props {
/** All curve points — used by global offset slider */
activePts: VFPoint[];
/** Called to reset the x-axis zoom to default */
onResetZoom: () => void;
/** True if the chart is currently zoomed in */
isZoomed: boolean;
/** True when viewing a read-only domain (memory) */
readOnly?: boolean;
/** Current zoom factor (1 = no zoom) */
zoomFactor: number;
/** Called when user changes zoom via slider */
onZoomChange: (factor: number) => void;
}
export function CurveToolbar({
activePts,
onResetZoom,
isZoomed,
readOnly,
zoomFactor,
onZoomChange,
}: Props) {
const {
pendingDeltas,
selectedPoints,
anchorPoint,
curve,
stageRangeEdit,
flattenToAnchor,
} = useCurveStore();
const [offsetMhz, setOffsetMhz] = useState(0);
const uniformDeltaMhz = useMemo(() => {
if (activePts.length === 0) return 0;
const firstD =
pendingDeltas.get(activePts[0].index) ?? activePts[0].delta_khz;
const uniform = activePts.every(
(p) => (pendingDeltas.get(p.index) ?? p.delta_khz) === firstD,
);
return uniform ? firstD / 1000 : null;
}, [activePts, pendingDeltas]);
// Sync the slider to the uniform delta when it changes (adjust state during
// render instead of an effect; undefined sentinel so the first render syncs).
const [lastUniformDelta, setLastUniformDelta] = useState<
number | null | undefined
>();
if (uniformDeltaMhz !== null && lastUniformDelta !== uniformDeltaMhz) {
setLastUniformDelta(uniformDeltaMhz);
setOffsetMhz(uniformDeltaMhz);
}
function handleOffsetChange(mhz: number) {
setOffsetMhz(mhz);
stageRangeEdit(activePts, Math.round(mhz * 1000));
}
return (
<div className="flex flex-wrap items-center gap-2 px-1 pb-2">
{/* Zoom control */}
<div
className="flex items-center gap-1.5 px-2 py-1 rounded bg-zinc-800/60 border border-zinc-700/40"
title="Zoom x-axis (Alt+scroll also works)"
>
<ZoomIn size={11} className="text-zinc-500 shrink-0" />
<input
type="range"
min={1}
max={10}
step={0.1}
value={zoomFactor}
onChange={(e) => onZoomChange(Number(e.target.value))}
className="w-20 h-1 cursor-pointer accent-cyan-400"
/>
<span
className={`text-xs font-mono w-8 tabular-nums ${isZoomed ? "text-cyan-400" : "text-zinc-600"}`}
>
{zoomFactor.toFixed(1)}×
</span>
{isZoomed && (
<button
onClick={onResetZoom}
title="Reset zoom"
className="text-zinc-500 hover:text-zinc-300 transition-colors ml-0.5"
>
<RotateCcw size={11} />
</button>
)}
</div>
{/* Divider */}
<span className="w-px h-6 bg-zinc-800 mx-1" />
{/* Global offset slider — GPU only */}
{!readOnly && uniformDeltaMhz !== null && (
<div className="flex items-center gap-1.5 min-w-[260px]">
<span className="text-zinc-500 text-xs whitespace-nowrap">
Global Offset
</span>
<input
type="range"
min={-1000}
max={1000}
step={5}
value={offsetMhz}
onChange={(e) => handleOffsetChange(Number(e.target.value))}
className="w-32 accent-cyan-400"
title={`${offsetMhz > 0 ? "+" : ""}${offsetMhz} MHz`}
/>
<span
className={[
"text-xs font-mono w-16",
offsetMhz > 0
? "text-cyan-400"
: offsetMhz < 0
? "text-orange-400"
: "text-zinc-500",
].join(" ")}
>
{offsetMhz > 0 ? "+" : ""}
{offsetMhz} MHz
</span>
</div>
)}
{/* Flatten — visible when 2+ points are selected */}
{!readOnly &&
selectedPoints.size >= 2 &&
(() => {
const anchor =
anchorPoint !== null && selectedPoints.has(anchorPoint)
? anchorPoint
: Math.min(...selectedPoints);
const anchorDelta =
pendingDeltas.get(anchor) ??
curve?.points.find((p) => p.index === anchor)?.delta_khz ??
0;
const label = `·${anchor} ${anchorDelta >= 0 ? "+" : ""}${anchorDelta / 1000} MHz`;
return (
<button
onClick={flattenToAnchor}
className="flex items-center gap-1.5 px-2 py-1 rounded text-xs font-medium text-amber-400 hover:text-amber-300 hover:bg-zinc-800 border border-zinc-700/40 transition"
title={`Flatten all selected points to anchor point ${anchor} (${anchorDelta >= 0 ? "+" : ""}${anchorDelta / 1000} MHz)`}
>
<Minus size={11} />
Flatten to {label}
</button>
);
})()}
{/* Legend — right-aligned */}
<div className="flex items-center gap-3 text-xs text-zinc-500 ml-auto">
<span className="flex items-center gap-1">
<span className="inline-block w-3 h-0.5 bg-emerald-400 rounded" />{" "}
effective
</span>
<span className="flex items-center gap-1">
<span className="inline-block w-3 h-px border-t-2 border-dashed border-cyan-400" />{" "}
pending
</span>
<span className="flex items-center gap-1">
<span className="inline-block w-2 h-2 rounded-full bg-yellow-400" />{" "}
current
</span>
</div>
</div>
);
}