chain table round 5: tree threads + the framed table (owner trial)
- TREE THREADS: open folders and chains drop a vertical guide from their expander arrow straight down through their children — at the arrow's own column (37px clusters / 73px chains), subtle border/50. Live-measured pixel-exact: arrow center = thread x (325=325, 361=361). - The framed table: a rule under the header and under the last group join the between-group separators; rows stay lineless.
This commit is contained in:
@@ -272,7 +272,9 @@ export function ChainTableBody({
|
|||||||
statements pl-21 where a chain icon would sit).
|
statements pl-21 where a chain icon would sit).
|
||||||
All three columns sort, small chevron on active. */}
|
All three columns sort, small chevron on active. */}
|
||||||
<div className="min-h-0 flex-1 overflow-y-auto">
|
<div className="min-h-0 flex-1 overflow-y-auto">
|
||||||
<div className="sticky top-0 z-10 flex items-center gap-1 px-3 py-1.5 text-[11px] uppercase tracking-widest text-muted-foreground/70 backdrop-blur">
|
{/* The TOP rule (owner round 5: the table is framed — a line under
|
||||||
|
the header, a line under the last group, lines between groups). */}
|
||||||
|
<div className="sticky top-0 z-10 flex items-center gap-1 border-b border-border/60 bg-background/80 px-3 py-1.5 text-[11px] uppercase tracking-widest text-muted-foreground/70 backdrop-blur">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
data-testid="sort-name"
|
data-testid="sort-name"
|
||||||
@@ -308,17 +310,29 @@ export function ChainTableBody({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{groups.map((group, groupIndex) => {
|
{groups.map((group) => {
|
||||||
const open = searching || expandedClusters.has(group.key);
|
const open = searching || expandedClusters.has(group.key);
|
||||||
const selectedCount = group.nodeIds.filter((id) => selection.has(id)).length;
|
const selectedCount = group.nodeIds.filter((id) => selection.has(id)).length;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={group.key}
|
key={group.key}
|
||||||
data-testid={`cluster-group-${group.key}`}
|
data-testid={`cluster-group-${group.key}`}
|
||||||
// The one line that survives (owner ruling): the separator
|
// The horizontal rules (owner rulings, rounds 2 + 5): between
|
||||||
// BETWEEN cluster groups — never on individual rows.
|
// cluster groups AND under the last one — the table is framed
|
||||||
className={groupIndex < groups.length - 1 ? "border-b border-border/60" : ""}
|
// top (header) and bottom. Never on individual rows.
|
||||||
|
className="relative border-b border-border/60"
|
||||||
>
|
>
|
||||||
|
{/* The TREE THREAD (owner round 5, trial): a vertical guide
|
||||||
|
dropping from the cluster's expander arrow down through
|
||||||
|
its chain rows — the arrow's column (center 37px: pl-3 +
|
||||||
|
checkbox 14 + gap 4 + half the 14px expander), from the
|
||||||
|
arrow's own height to the last row's middle. */}
|
||||||
|
{open && group.chains.length > 0 && (
|
||||||
|
<div
|
||||||
|
data-testid={`cluster-thread-${group.key}`}
|
||||||
|
className="pointer-events-none absolute bottom-3.5 left-[37px] top-4 w-px bg-border/50"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{/* The cluster row: birth-session group header. */}
|
{/* The cluster row: birth-session group header. */}
|
||||||
<div className="flex items-center gap-1 px-3 py-1.5">
|
<div className="flex items-center gap-1 px-3 py-1.5">
|
||||||
<RowCheckbox
|
<RowCheckbox
|
||||||
@@ -353,7 +367,16 @@ export function ChainTableBody({
|
|||||||
const chainOpen = expandedChains.has(chain.id);
|
const chainOpen = expandedChains.has(chain.id);
|
||||||
const selectedInChain = memberIds.filter((id) => selection.has(id)).length;
|
const selectedInChain = memberIds.filter((id) => selection.has(id)).length;
|
||||||
return (
|
return (
|
||||||
<div key={chain.id} data-testid={`chain-block-${chain.id}`}>
|
<div key={chain.id} data-testid={`chain-block-${chain.id}`} className="relative">
|
||||||
|
{/* The chain's own thread: from its expander arrow down
|
||||||
|
through the statement rows (center 73px: pl-12 +
|
||||||
|
checkbox 14 + gap 4 + half the expander). */}
|
||||||
|
{chainOpen && memberIds.length > 0 && (
|
||||||
|
<div
|
||||||
|
data-testid={`chain-thread-${chain.id}`}
|
||||||
|
className="pointer-events-none absolute bottom-3.5 left-[73px] top-3.5 w-px bg-border/50"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<div
|
<div
|
||||||
data-testid={`chain-row-${chain.id}`}
|
data-testid={`chain-row-${chain.id}`}
|
||||||
aria-label={formatChainLine(chain, Object.fromEntries([...byId.entries()].map(([id, n]) => [id, { name: n.name }])))}
|
aria-label={formatChainLine(chain, Object.fromEntries([...byId.entries()].map(([id, n]) => [id, { name: n.name }])))}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -133,16 +133,28 @@ describe("ChainTableBody — THE shared TopBar + the lines law (owner, round 2)"
|
|||||||
assert.ok(html.includes('data-testid="chain-table-search"'), "the search input");
|
assert.ok(html.includes('data-testid="chain-table-search"'), "the search input");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("the ONLY lines are the separators between cluster groups — no per-row or vertical lines", () => {
|
it("the table is FRAMED: header rule + rules between groups + rule under the last group; rows stay lineless (owner round 5)", () => {
|
||||||
const html = render({ expandedClusters: new Set(["chat1"]) });
|
const html = render({ expandedClusters: new Set(["chat1"]) });
|
||||||
// group wrappers: every group but the last carries the separator
|
// every group wrapper carries the rule now — between groups AND under
|
||||||
|
// the last one; the header carries the TOP rule
|
||||||
const sep = html.split("border-b border-border/60").length - 1;
|
const sep = html.split("border-b border-border/60").length - 1;
|
||||||
assert.ok(sep >= 2, "separators between groups");
|
assert.ok(sep >= 4, "header + 3 group rules");
|
||||||
// chain rows themselves carry NO border — their entire class list is
|
// chain rows themselves carry NO border — their entire class list is
|
||||||
// the staircase law (pl-12 = aligned with the cluster's FOLDER icon);
|
// the staircase law (pl-12 = aligned with the cluster's FOLDER icon);
|
||||||
// the chip outlines' border-border/70 is an outline, not a row line
|
// the chip outlines' border-border/70 is an outline, not a row line
|
||||||
assert.ok(html.includes('class="flex items-center gap-1 py-1 pl-12 pr-3"'), "the chain row: staircase indent, no border");
|
assert.ok(html.includes('class="flex items-center gap-1 py-1 pl-12 pr-3"'), "the chain row: staircase indent, no border");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("TREE THREADS: open folders/chains drop a vertical guide from their expander arrow down the children's edge (owner round 5)", () => {
|
||||||
|
const collapsed = render();
|
||||||
|
assert.ok(!collapsed.includes("cluster-thread-"), "no thread while collapsed");
|
||||||
|
const open = render({ expandedClusters: new Set(["chat1"]), expandedChains: new Set(["c1"]) });
|
||||||
|
assert.ok(open.includes('data-testid="cluster-thread-chat1"'), "the cluster thread");
|
||||||
|
assert.ok(open.includes('data-testid="chain-thread-c1"'), "the chain thread");
|
||||||
|
// the columns: the cluster arrow sits at 37px, the chain arrow at 73px
|
||||||
|
assert.ok(open.includes("left-[37px]"), "thread at the cluster arrow's column");
|
||||||
|
assert.ok(open.includes("left-[73px]"), "thread at the chain arrow's column");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("ChainTableBody — chain rows (chips + connectors)", () => {
|
describe("ChainTableBody — chain rows (chips + connectors)", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user