Files
learnharness/components/plan-window.tsx
T
amosblomqvist 62cfb8112b confidence slider UI round: global ←/→, fraction positions, color ramp; plan-flank phantom fix
Owner UI round 2026-10-01, three rulings on the slider plus one en-route
bug:

- GLOBAL ←/→, the ↑/↓ pattern: confidence adjusts wherever focus sits
  (window keydown, typing-guarded), no click-in, no focus ring. The
  slider itself takes NO focus — tabIndex -1, mousedown prevented,
  removed from the Tab chain (rows → thinking directly again).
- Position = fraction: the thumb's scale is 0-100 with the rendered
  value the stop's own percent, so 50 sits halfway; onChange snaps to
  the nearest stop (magnet feel while dragged).
- Six-step ramp (owner pick over three bands): 0 red, 30 orange,
  50 yellow (the coin flip), 70 lime, 90 green, 100 emerald — fill AND
  handle wear the stop's color; taller 10px pill bar, custom track/thumb
  (webkit + moz), muted remainder via --muted.
- Plan-flank phantom: gating the marginalia button on a JSX element
  (always truthy) mounted an invisible clickable button in the
  bottom-right whenever a question was pending with no plan.
  planExists() now gates it (pinned in plan-window tests).
2026-10-02 00:44:02 +03:00

66 lines
2.5 KiB
TypeScript

"use client";
import { useId, useMemo } from "react";
import { derivedNodeStatus, derivePlanStatuses } from "@/src/core";
import {
PlanDiagram,
GRAPH_PAD,
latestTree,
TEXT_CAP_SCALE,
type PopupMessage,
} from "@/components/plan-block";
/**
* THE PLAN MARGINALIA: the plan's ambient presence in the session view —
* the successor of the old fixed corner card (the plan window, retired
* with the dock-band placement round). NO container: the bare diagram,
* meet-fitted into the dock band's right flank — the wrapper (DockOverlay)
* positions and fades it — so it reads as marginalia on the lesson's
* page, not a floating widget. Live by construction: statuses derive from
* the transcript on every render (derivePlanStatuses — calls, not
* results), so the glow moves the moment a tagged check streams in.
*
* Visibile only while a question owns the dock band (the reserved tail's
* height is the diagram's canvas — quiz open, tall band, full plan);
* folding the dock fades + blurs the whole thing out of existence. The
* top-bar plan chip carries access at every width; the popup is the study
* view. The viewBox pad clears the current node's blurred yellow halo
* (GLOW_REACH) at the extent edges.
*/
const MARGIN_PAD = GRAPH_PAD + 16;
/** True when the transcript holds a plan tree. Gates the marginalia's
* clickable flank (chat.tsx): PlanMarginalia renders null without a
* tree, but the WRAPPER button must unmount too — a JSX element is
* always truthy, so gating on it alone left a phantom click target in
* the bottom-right whenever a question was pending with no plan
* (owner report, UI round 2026-10-01). */
export function planExists(messages: readonly PopupMessage[]): boolean {
const tree = latestTree(messages);
return !!tree && tree.nodes.length > 0;
}
export function PlanMarginalia({ messages }: { messages: readonly PopupMessage[] }) {
const markerId = useId().replace(/[^a-zA-Z0-9-]/g, "");
const tree = useMemo(() => latestTree(messages), [messages]);
const statuses = useMemo(
() => (tree ? derivePlanStatuses(tree, messages as Parameters<typeof derivePlanStatuses>[1]) : null),
[tree, messages],
);
if (!tree || tree.nodes.length === 0) return null; // no plan yet: nothing in the margin
const statusOf = (id: string) => (statuses ? derivedNodeStatus(statuses, id) : "upcoming");
return (
<PlanDiagram
nodes={tree.nodes}
edges={tree.edges}
statusOf={statusOf}
glowId={statuses?.current ?? null}
markerId={markerId}
pad={MARGIN_PAD}
textCapScale={TEXT_CAP_SCALE}
fill
/>
);
}