proposition pick: persistent lifted selection, live count, honest affordances

Root cause of 'doesn't register': the slice-6 floating-bar edit never
landed (a silent no-match replace) — the Body accepted selectMode but
the bar ignored it, so the picker showed Clear + Delete and nothing
ever confirmed. Found by re-reading; the test suite never pinned the
select-mode bar.

The owner vision instead: the chain table's selection is PERSISTENT
and caller-owned.
- ChainTableOverlay gains controlledSelection/onSelectionChange (every
  check, clear, and delete-prune reports up immediately; uncontrolled
  fallback keeps the delete flow self-contained).
- Select mode's floating bar: Done (closes; the pick already
  registered) — the delete fence never renders while picking.
- GraphView threads the lifted set through, resolving ids → nodes
  against the live snapshot.
- Home: the pick bar above the composer shows 'N propositions
  selected' + Clear in EITHER mode (dashed 'Pick propositions from the
  map' hint only in practice mode with nothing picked); visible hover
  (bg-accent) so the chip reads clickable. Practice sends still seed
  from the pick set.
This commit is contained in:
amosblomqvist
2026-10-04 22:54:17 +03:00
parent 89580d2b93
commit 87f8e1e6f4
4 changed files with 139 additions and 61 deletions
+39 -1
View File
@@ -27,7 +27,7 @@ import React from "react";
import assert from "node:assert/strict";
import { renderToStaticMarkup } from "react-dom/server";
import { describe, it } from "node:test";
import { ChainTableBody } from "../components/chain-table.tsx";
import { ChainTableBody, ChainTableOverlay } from "../components/chain-table.tsx";
import type { ChainSort } from "../components/chain-table.tsx";
import type { GraphChain, GraphNode } from "../src/core/contracts.ts";
@@ -319,3 +319,41 @@ describe("ChainTableBody — the fuzzy search (grouped + auto-expand)", () => {
assert.ok(html.includes("No matching chains."));
});
});
describe("ChainTableOverlay — the lifted practice pick (owner vision, 2026-10-04)", () => {
function overlay(extra: Record<string, unknown> = {}) {
return renderToStaticMarkup(
React.createElement(ChainTableOverlay, {
open: true,
nodes: NODES,
chains: CHAINS,
sessions: { chat1: "Ethics lesson", chat2: "Follow-up", notes1: "Notes" },
fileSources: {},
onClose: () => {},
...extra,
} as never),
);
}
it("CONTROLLED selection renders the caller's set — the bar counts it live", () => {
const html = overlay({ controlledSelection: new Set(["a", "b", "c1"]) });
assert.ok(html.includes("3 nodes → 1 chain"), "the bar reflects the lifted set, not internal state");
});
it("select mode: Done replaces the delete fence — no way to delete while picking", () => {
const html = overlay({
selectMode: true,
controlledSelection: new Set(["a", "b"]),
});
assert.ok(html.includes('data-testid="chain-practice-done"'));
assert.ok(html.includes("Done"));
assert.ok(!html.includes('data-testid="chain-delete"'), "no delete while picking");
assert.ok(!html.includes('data-testid="chain-danger-divider"'));
});
it("browse mode unchanged: the fenced Delete remains", () => {
const html = overlay({ controlledSelection: new Set(["a"]) });
assert.ok(html.includes('data-testid="chain-delete"'));
assert.ok(!html.includes('data-testid="chain-practice-done"'));
});
});