graph: import + cost ride the chains home's TOP BAR (release polish 2.1)
Owner refinement on the chains home: the floating top-right chrome row sat ON TOP of the propositions table — covering content reads badly. The two survivors (Import notes — always, the entry-point law; the cost pill when any run exists) now ride the shared TopBar's right cluster, left of the search input, carved out of the drag band with lh-no-drag — consistent with the graph view, where they already live in the top band. ChainTableBody gains topBarExtra?: React.ReactNode (absent on the overlay — nothing changes there); ChainsHome's floating chrome row is deleted; the empty state keeps its own centered import button. Parked for the next design pass (owner, this round): (1) the pick flow reads messy — checkboxes show by default, so the pill's select mode is near-invisible and after Done the Clear/Delete bar + the practice pill coexist with two Clear buttons; the original vision was checkboxes appear ONLY when picking. (2) the composer's clearance band has no outline — the docked area needs a visual edge. 1415 tests (new pin: topBarExtra renders in the bar with the drag carve; overlay variant injects nothing); tsc, lint, build green.
This commit is contained in:
@@ -114,6 +114,7 @@ export function ChainTableBody({
|
|||||||
onClear,
|
onClear,
|
||||||
onDelete,
|
onDelete,
|
||||||
onClose,
|
onClose,
|
||||||
|
topBarExtra,
|
||||||
selectMode,
|
selectMode,
|
||||||
onSelect,
|
onSelect,
|
||||||
}: {
|
}: {
|
||||||
@@ -140,6 +141,11 @@ export function ChainTableBody({
|
|||||||
* (graph rendering off, owner 2026-10-06): the table is the page,
|
* (graph rendering off, owner 2026-10-06): the table is the page,
|
||||||
* there is nothing to close, and the X does not render. */
|
* there is nothing to close, and the X does not render. */
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
|
/** HOME VARIANT (owner, 2026-10-06 polish): extra controls injected
|
||||||
|
* into the shared TopBar, right cluster, LEFT of the search input —
|
||||||
|
* the chains home's Import-notes + cost pill live here instead of
|
||||||
|
* floating over the list (covering table content read badly). */
|
||||||
|
topBarExtra?: React.ReactNode;
|
||||||
/** PRACTICE SELECT (2026-10-04): the floating bar's confirm becomes
|
/** PRACTICE SELECT (2026-10-04): the floating bar's confirm becomes
|
||||||
* "Practice these" — no delete fence; the caller owns what the
|
* "Practice these" — no delete fence; the caller owns what the
|
||||||
* selection becomes. */
|
* selection becomes. */
|
||||||
@@ -245,12 +251,17 @@ export function ChainTableBody({
|
|||||||
chat surface. NO TITLE (owner round 11, 2026-10-05): the serif
|
chat surface. NO TITLE (owner round 11, 2026-10-05): the serif
|
||||||
title register is RESERVED — it always names a session; the
|
title register is RESERVED — it always names a session; the
|
||||||
chain table names nothing, so its bar carries counts + search +
|
chain table names nothing, so its bar carries counts + search +
|
||||||
close only. Search + close carve out with lh-no-drag (the mask
|
close only — plus, in the home variant, the injected
|
||||||
law: later in the DOM). */}
|
topBarExtra (import + cost, owner 2026-10-06). Search, close,
|
||||||
|
and the extra carve out with lh-no-drag (the mask law: later
|
||||||
|
in the DOM). */}
|
||||||
<TopBar>
|
<TopBar>
|
||||||
<span className="min-w-0 flex-1 truncate text-xs text-muted-foreground">
|
<span className="min-w-0 flex-1 truncate text-xs text-muted-foreground">
|
||||||
{chains.length} {chains.length === 1 ? "chain" : "chains"} · {nodes.length} {nodes.length === 1 ? "node" : "nodes"}
|
{chains.length} {chains.length === 1 ? "chain" : "chains"} · {nodes.length} {nodes.length === 1 ? "node" : "nodes"}
|
||||||
</span>
|
</span>
|
||||||
|
{topBarExtra && (
|
||||||
|
<div className="lh-no-drag flex shrink-0 items-center gap-2">{topBarExtra}</div>
|
||||||
|
)}
|
||||||
<div className="relative shrink-0">
|
<div className="relative shrink-0">
|
||||||
<SearchIcon className="absolute left-2.5 top-1/2 size-3.5 -translate-y-1/2 text-muted-foreground" />
|
<SearchIcon className="absolute left-2.5 top-1/2 size-3.5 -translate-y-1/2 text-muted-foreground" />
|
||||||
<input
|
<input
|
||||||
|
|||||||
+29
-26
@@ -16,8 +16,9 @@
|
|||||||
* (owner-approved wording: "bottom clearance so content scrolls clear").
|
* (owner-approved wording: "bottom clearance so content scrolls clear").
|
||||||
*
|
*
|
||||||
* The graph chrome's survivors: Import notes (ALWAYS — the entry point,
|
* The graph chrome's survivors: Import notes (ALWAYS — the entry point,
|
||||||
* the empty-graph law) and the cost pill, floating top-right below the
|
* the empty-graph law) and the cost pill, riding THE TOP BAR's right
|
||||||
* table's top bar. Node search is canvas-scoped and the render-settings
|
* cluster (owner refinement, 2026-10-06 — floating over the list covered
|
||||||
|
* table content). Node search is canvas-scoped and the render-settings
|
||||||
* gear is canvas rendering — neither applies; the table's own search row
|
* gear is canvas rendering — neither applies; the table's own search row
|
||||||
* covers query.
|
* covers query.
|
||||||
*
|
*
|
||||||
@@ -193,33 +194,35 @@ export function ChainsHome({
|
|||||||
// onClose ABSENT — the home variant: no top-bar X.
|
// onClose ABSENT — the home variant: no top-bar X.
|
||||||
selectMode={chainSelectOpen}
|
selectMode={chainSelectOpen}
|
||||||
onSelect={onChainSelectClose}
|
onSelect={onChainSelectClose}
|
||||||
|
// The chrome survivors ride THE TOP BAR (owner refinement,
|
||||||
|
// 2026-10-06): floating over the list covered table content —
|
||||||
|
// the bar's right cluster (left of search) is their home now,
|
||||||
|
// consistent with the graph view where they sit in the top
|
||||||
|
// band. Import ALWAYS (the entry-point law); the cost pill
|
||||||
|
// when any run exists. Node search + the render gear stay
|
||||||
|
// canvas things.
|
||||||
|
topBarExtra={
|
||||||
|
<>
|
||||||
|
{costs && costs.runs.length > 0 && <GraphCostControl costs={costs} />}
|
||||||
|
<div className="shrink-0 rounded-xl border bg-card/85 px-1 py-[3px] shadow-lg backdrop-blur">
|
||||||
|
<Hint label="Import notes into the graph" side="bottom">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={importing}
|
||||||
|
onClick={() => fileInputRef.current?.click()}
|
||||||
|
data-testid="import-notes"
|
||||||
|
className="flex h-7 items-center gap-1.5 rounded-lg px-2.5 text-xs text-muted-foreground transition-colors hover:bg-accent disabled:opacity-50"
|
||||||
|
>
|
||||||
|
<UploadIcon className="size-3" />
|
||||||
|
{importing ? "Importing…" : "Import notes…"}
|
||||||
|
</button>
|
||||||
|
</Hint>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* The chrome survivors, floating top-right BELOW the table's top
|
|
||||||
bar (h-15 = 60px + breathing room — owner, 2026-10-06): Import
|
|
||||||
ALWAYS — the entry point; the cost pill when any run exists. Node
|
|
||||||
search + the render gear are canvas things. */}
|
|
||||||
{!isEmpty && (
|
|
||||||
<div className="absolute right-3 top-[4.5rem] z-20 flex items-center gap-2">
|
|
||||||
{costs && costs.runs.length > 0 && <GraphCostControl costs={costs} />}
|
|
||||||
<div className="shrink-0 rounded-xl border bg-card/85 px-1 py-[3px] shadow-lg backdrop-blur">
|
|
||||||
<Hint label="Import notes into the graph" side="bottom">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
disabled={importing}
|
|
||||||
onClick={() => fileInputRef.current?.click()}
|
|
||||||
data-testid="import-notes"
|
|
||||||
className="flex h-7 items-center gap-1.5 rounded-lg px-2.5 text-xs text-muted-foreground transition-colors hover:bg-accent disabled:opacity-50"
|
|
||||||
>
|
|
||||||
<UploadIcon className="size-3" />
|
|
||||||
{importing ? "Importing…" : "Import notes…"}
|
|
||||||
</button>
|
|
||||||
</Hint>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{deleteOpen && (
|
{deleteOpen && (
|
||||||
<DeleteNodeDialog
|
<DeleteNodeDialog
|
||||||
nodes={selectedNodes.map((n) => ({ id: n.id, name: n.name }))}
|
nodes={selectedNodes.map((n) => ({ id: n.id, name: n.name }))}
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ const BASE = {
|
|||||||
onGenerate: () => {},
|
onGenerate: () => {},
|
||||||
onDelete: () => {},
|
onDelete: () => {},
|
||||||
onClose: () => {},
|
onClose: () => {},
|
||||||
|
topBarExtra: undefined as React.ReactNode,
|
||||||
};
|
};
|
||||||
|
|
||||||
function render(props: Partial<typeof BASE> = {}) {
|
function render(props: Partial<typeof BASE> = {}) {
|
||||||
@@ -96,6 +97,14 @@ describe("ChainTableBody — the HOME variant (graph rendering off, 2026-10-06)"
|
|||||||
assert.ok(html.includes("chain"), "the counts stay");
|
assert.ok(html.includes("chain"), "the counts stay");
|
||||||
assert.ok(html.includes("lh-drag"), "the window-drag band stays (same bar as every surface)");
|
assert.ok(html.includes("lh-drag"), "the window-drag band stays (same bar as every surface)");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("topBarExtra injects into the bar's right cluster — import + cost ride the TOP BAR (owner, 2026-10-06)", () => {
|
||||||
|
const html = render({ onClose: undefined, topBarExtra: React.createElement("b", { "data-testid": "tbx" }, "X") });
|
||||||
|
assert.ok(html.includes('data-testid="tbx"'), "the extra renders inside the bar");
|
||||||
|
assert.ok(html.includes("lh-no-drag"), "the cluster carves out of the drag band");
|
||||||
|
const overlay = render();
|
||||||
|
assert.ok(!overlay.includes('data-testid="tbx"'), "absent by default — the overlay variant injects nothing");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("ChainTableBody — clusters (the outermost layer)", () => {
|
describe("ChainTableBody — clusters (the outermost layer)", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user