feat(workbench): extend TruncatedCell to trace edges and selection rails (#750)
Follow-up to #749, which deliberately scoped TruncatedCell to the non-virtualized columnar tables and left trace edges + single-column selection rails as-is. Wire it into those too so every truncated data cell in the workbench has the same full-content reveal (popover + copy + modal-for-long). Component: - Add `align` prop ("start" | "end"). `end` hugs the right edge and right-justifies the display, for right-aligned columns like the trace `to_stage` cell. Defaults to "start" (unchanged). Wiring: - Trace propagation edges: from_stage / to_stage (to_stage align=end). - Trace pipeline stage rail: stage label + summary. - Trace turns rail: prompt + surface excerpts. - Runs rail: session_id. Replay rail: prompt excerpt. Packs rail: pack_id. CORE-Logos pack rail: pack_id. Logos alignment rail: edge (rich source→target display, plain-text value for copy/reveal). - Contemplation run rail: run_id; stage-evidence ids. - Demos rail: title; scenario trace hash. - Proposal detail: replay-equivalence hash, evidence hashes; proposal chain provenance. Two row elements that were real <button>s (trace stage rail, contemplation run row) are converted to <div role="button"> with an Enter/Space onKeyDown handler — the codebase's established selectable-row pattern (ProposalTable, RowShell) — so nesting TruncatedCell's button trigger is valid HTML rather than a button-in-button. Intentionally left as-is (documented): - RunsRoute TurnRefRow is an <a>/<Link> row; nesting an interactive trigger inside an anchor is invalid HTML. The surface excerpt is reachable by following the link. - Digests everywhere keep DigestBadge (already copy + full-value title). - Section headings (e.g. the trace stage-detail <h3>) are not data cells. Evidence: workbench-ui `pnpm build` (tsc -b) green; full vitest suite 535 passed / 60 files, incl. a new align assertion on TruncatedCell. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
99a52746ae
commit
de2d5da4f3
12 changed files with 178 additions and 50 deletions
|
|
@ -8,6 +8,7 @@ import { Panel } from "../../design/components/Panel/Panel";
|
|||
import { SearchInput } from "../../design/components/SearchInput/SearchInput";
|
||||
import { SplitPane } from "../../design/components/SplitPane/SplitPane";
|
||||
import { StableJsonViewer } from "../../design/components/StableJsonViewer";
|
||||
import { TruncatedCell } from "../../design/components/TruncatedCell";
|
||||
import { EmptyState } from "../../design/components/states/EmptyState";
|
||||
import { ErrorState } from "../../design/components/states/ErrorState";
|
||||
import { LoadingState } from "../../design/components/states/LoadingState";
|
||||
|
|
@ -57,10 +58,17 @@ function RunRow({
|
|||
onSelect: () => void;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-current={selected ? "true" : undefined}
|
||||
onClick={onSelect}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter" || event.key === " ") {
|
||||
event.preventDefault();
|
||||
onSelect();
|
||||
}
|
||||
}}
|
||||
className={`grid w-full grid-cols-[minmax(0,1fr)_auto] items-start gap-3 border-b border-[var(--color-border-subtle)] px-3 py-2 text-left transition-colors hover:bg-[var(--color-surface-inset)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-inset focus-visible:outline-[var(--color-focus-ring)] ${
|
||||
selected
|
||||
? "border-l-2 border-l-[var(--color-selected-border)] bg-[var(--color-selected-bg)] pl-[10px]"
|
||||
|
|
@ -68,8 +76,13 @@ function RunRow({
|
|||
}`}
|
||||
>
|
||||
<span className="min-w-0">
|
||||
<span className="block truncate font-mono text-xs text-[var(--color-text-muted)]">
|
||||
{run.run_id}
|
||||
<span className="block min-w-0">
|
||||
<TruncatedCell
|
||||
value={run.run_id}
|
||||
label="run id"
|
||||
mono
|
||||
className="text-xs text-[var(--color-text-muted)]"
|
||||
/>
|
||||
</span>
|
||||
<span className="mt-1 block text-sm text-[var(--color-text-primary)]">
|
||||
{run.prompt ?? "Prompt not recorded"}
|
||||
|
|
@ -82,7 +95,7 @@ function RunRow({
|
|||
<span className="justify-self-end text-xs text-[var(--color-text-secondary)]">
|
||||
{boolLabel(run.learning_arc_closed)}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -108,8 +121,13 @@ function StageEvidence({ scene }: { scene: ContemplationScene }) {
|
|||
{rows.map((row) => (
|
||||
<div key={row.label} className="contents">
|
||||
<dt className="text-[var(--color-text-secondary)]">{row.label}</dt>
|
||||
<dd className="m-0 truncate font-mono text-[var(--color-text-primary)]" title={row.value}>
|
||||
{row.value}
|
||||
<dd className="m-0 min-w-0">
|
||||
<TruncatedCell
|
||||
value={row.value}
|
||||
label={row.label}
|
||||
mono
|
||||
className="text-[var(--color-text-primary)]"
|
||||
/>
|
||||
</dd>
|
||||
</div>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { DigestBadge } from "../../design/components/DigestBadge/DigestBadge";
|
|||
import { Button } from "../../design/components/primitives/Button";
|
||||
import { Panel } from "../../design/components/Panel/Panel";
|
||||
import { SearchInput } from "../../design/components/SearchInput/SearchInput";
|
||||
import { TruncatedCell } from "../../design/components/TruncatedCell";
|
||||
import { VirtualizedList } from "../../design/components/VirtualizedList/VirtualizedList";
|
||||
import { EmptyState } from "../../design/components/states/EmptyState";
|
||||
import { ErrorState } from "../../design/components/states/ErrorState";
|
||||
|
|
@ -63,9 +64,11 @@ function DemoRow({
|
|||
: "border-l-2 border-l-transparent pl-[10px]"
|
||||
}`}
|
||||
>
|
||||
<span className="truncate text-sm font-semibold text-[var(--color-text-primary)]">
|
||||
{demo.title}
|
||||
</span>
|
||||
<TruncatedCell
|
||||
value={demo.title}
|
||||
label="demo title"
|
||||
className="text-sm font-semibold text-[var(--color-text-primary)]"
|
||||
/>
|
||||
<span className="font-mono text-xs text-[var(--color-text-muted)]">
|
||||
{demo.scenario_count} scenarios
|
||||
</span>
|
||||
|
|
@ -225,9 +228,12 @@ function ResultRow({ scenario }: { scenario: DemoScenarioRunResult }) {
|
|||
</p>
|
||||
) : null}
|
||||
{scenario.trace_hash ? (
|
||||
<code className="truncate font-mono text-xs text-[var(--color-text-muted)]">
|
||||
{scenario.trace_hash}
|
||||
</code>
|
||||
<TruncatedCell
|
||||
value={scenario.trace_hash}
|
||||
label="trace hash"
|
||||
mono
|
||||
className="text-xs text-[var(--color-text-muted)]"
|
||||
/>
|
||||
) : null}
|
||||
{scenario.problems.length > 0 ? (
|
||||
<ul className="m-0 grid list-none gap-1 p-0">
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { useMemo } from "react";
|
||||
import { DagViewer } from "../../design/components/Dag/Dag";
|
||||
import type { DagEdgeInput, DagNodeInput } from "../../design/components/Dag/layout";
|
||||
import { TruncatedCell } from "../../design/components/TruncatedCell";
|
||||
import type { LogosAlignmentRow } from "../../types/api";
|
||||
|
||||
// Read-only CORE-Logos alignment tab (LG-4). The trilingual resonance graph
|
||||
|
|
@ -70,9 +71,16 @@ function EdgeCard({
|
|||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2 font-mono text-xs text-[var(--color-text-primary)]">
|
||||
<span className="truncate">
|
||||
{row.source_id} <span aria-hidden className="text-[var(--color-text-muted)]">→</span> {row.target_id}
|
||||
</span>
|
||||
<TruncatedCell
|
||||
value={`${row.source_id} → ${row.target_id}`}
|
||||
display={
|
||||
<>
|
||||
{row.source_id}{" "}
|
||||
<span aria-hidden className="text-[var(--color-text-muted)]">→</span> {row.target_id}
|
||||
</>
|
||||
}
|
||||
label="alignment edge"
|
||||
/>
|
||||
<span className="text-[var(--color-text-secondary)]">{row.weight.toFixed(2)}</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-x-2 gap-y-1 text-[11px] text-[var(--color-text-secondary)]">
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import { Panel } from "../../design/components/Panel/Panel";
|
|||
import { SplitPane } from "../../design/components/SplitPane/SplitPane";
|
||||
import { StableJsonViewer } from "../../design/components/StableJsonViewer";
|
||||
import { TabBar, type Tab } from "../../design/components/TabBar/TabBar";
|
||||
import { TruncatedCell } from "../../design/components/TruncatedCell";
|
||||
import { EmptyState } from "../../design/components/states/EmptyState";
|
||||
import { ErrorState } from "../../design/components/states/ErrorState";
|
||||
import { LoadingState } from "../../design/components/states/LoadingState";
|
||||
|
|
@ -165,8 +166,13 @@ function PackRow({
|
|||
}`}
|
||||
>
|
||||
<span className="min-w-0">
|
||||
<span className="block truncate font-mono text-sm text-[var(--color-text-primary)]">
|
||||
{pack.pack_id}
|
||||
<span className="block min-w-0">
|
||||
<TruncatedCell
|
||||
value={pack.pack_id}
|
||||
label="pack id"
|
||||
mono
|
||||
className="text-sm text-[var(--color-text-primary)]"
|
||||
/>
|
||||
</span>
|
||||
<span className="mt-1 flex flex-wrap items-center gap-2 text-xs text-[var(--color-text-secondary)]">
|
||||
<span>{roleLabel(pack.role)}</span>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { Panel } from "../../design/components/Panel/Panel";
|
|||
import { SearchInput } from "../../design/components/SearchInput/SearchInput";
|
||||
import { SplitPane } from "../../design/components/SplitPane/SplitPane";
|
||||
import { StableJsonViewer } from "../../design/components/StableJsonViewer";
|
||||
import { TruncatedCell } from "../../design/components/TruncatedCell";
|
||||
import { TabBar, type Tab } from "../../design/components/TabBar/TabBar";
|
||||
import { TreeView } from "../../design/components/TreeView/TreeView";
|
||||
import { VirtualizedList } from "../../design/components/VirtualizedList/VirtualizedList";
|
||||
|
|
@ -81,8 +82,13 @@ function PackRow({
|
|||
}`}
|
||||
>
|
||||
<span className="min-w-0">
|
||||
<span className="block truncate text-sm text-[var(--color-text-primary)]">
|
||||
{pack.pack_id}
|
||||
<span className="block min-w-0">
|
||||
<TruncatedCell
|
||||
value={pack.pack_id}
|
||||
label="pack id"
|
||||
mono
|
||||
className="text-sm text-[var(--color-text-primary)]"
|
||||
/>
|
||||
</span>
|
||||
<span className="mt-1 flex flex-wrap items-center gap-2 text-xs text-[var(--color-text-secondary)]">
|
||||
<span>{sourceLabel(pack.source)}</span>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { StableJsonViewer } from "../../design/components/StableJsonViewer";
|
||||
import { TruncatedCell } from "../../design/components/TruncatedCell";
|
||||
import { DagViewer, type DagEdgeInput, type DagNodeInput } from "../../design/components/Dag";
|
||||
import type { ProposalDetail } from "../../types/api";
|
||||
import { chainRecords, isRecord, jsonSource, stringField } from "./proposalView";
|
||||
|
|
@ -68,9 +69,12 @@ export function ProposalChainViewer({ proposal }: { proposal: ProposalDetail })
|
|||
{index + 1}. {nodeLabel(record, index)}
|
||||
</span>
|
||||
{provenance(record) ? (
|
||||
<span className="truncate text-xs text-[var(--color-text-muted)]">
|
||||
{provenance(record)}
|
||||
</span>
|
||||
<TruncatedCell
|
||||
value={provenance(record) as string}
|
||||
label="provenance"
|
||||
align="end"
|
||||
className="text-xs text-[var(--color-text-muted)]"
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
<StableJsonViewer source={jsonSource(record)} />
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import type { ProposalSummary, MathProposalDetail, ProposalState, DownstreamEffe
|
|||
import { Button } from "../../design/components/primitives/Button";
|
||||
import { EmptyState } from "../../design/components/states/EmptyState";
|
||||
import { SearchInput } from "../../design/components/SearchInput/SearchInput";
|
||||
import { TruncatedCell } from "../../design/components/TruncatedCell";
|
||||
import { useListNavigation } from "../../design/hooks/useListNavigation";
|
||||
import { ErrorState } from "../../design/components/states/ErrorState";
|
||||
import { LoadingState } from "../../design/components/states/LoadingState";
|
||||
|
|
@ -396,8 +397,13 @@ function MathProposalDetailView({
|
|||
</div>
|
||||
<div>
|
||||
<dt className="text-[var(--color-text-muted)]">Replay Equivalence Hash</dt>
|
||||
<dd className="m-0 text-[var(--color-text-primary)] font-mono truncate mt-0.5" title={proposal.replay_equivalence_hash}>
|
||||
{proposal.replay_equivalence_hash}
|
||||
<dd className="m-0 mt-0.5 min-w-0">
|
||||
<TruncatedCell
|
||||
value={proposal.replay_equivalence_hash}
|
||||
label="replay equivalence hash"
|
||||
mono
|
||||
className="text-[var(--color-text-primary)]"
|
||||
/>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
|
@ -454,9 +460,14 @@ function MathProposalDetailView({
|
|||
{hashes.map((hash: string, i: number) => (
|
||||
<div key={i} className="flex items-center gap-2">
|
||||
<span className="text-xs text-[var(--color-text-muted)] font-mono">#{i + 1}</span>
|
||||
<code className="bg-[var(--color-surface-inset)] px-2 py-1 rounded font-mono text-xs text-[var(--color-text-primary)] truncate flex-1">
|
||||
{hash}
|
||||
</code>
|
||||
<span className="min-w-0 flex-1 rounded bg-[var(--color-surface-inset)] px-2 py-1">
|
||||
<TruncatedCell
|
||||
value={hash}
|
||||
label={`evidence hash #${i + 1}`}
|
||||
mono
|
||||
className="text-xs text-[var(--color-text-primary)]"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { Panel } from "../../design/components/Panel/Panel";
|
|||
import { SearchInput } from "../../design/components/SearchInput/SearchInput";
|
||||
import { SplitPane } from "../../design/components/SplitPane/SplitPane";
|
||||
import { Timestamp } from "../../design/components/Timestamp/Timestamp";
|
||||
import { TruncatedCell } from "../../design/components/TruncatedCell";
|
||||
import { VirtualizedList } from "../../design/components/VirtualizedList/VirtualizedList";
|
||||
import { EmptyState } from "../../design/components/states/EmptyState";
|
||||
import { ErrorState } from "../../design/components/states/ErrorState";
|
||||
|
|
@ -75,8 +76,13 @@ function TurnRow({
|
|||
<span className="block text-xs text-[var(--color-text-secondary)]">
|
||||
<Timestamp iso={turn.timestamp} format="relative" />
|
||||
</span>
|
||||
<span className="mt-1 block truncate text-sm text-[var(--color-text-primary)]">
|
||||
{turn.prompt_excerpt || `Turn #${turn.turn_id}`}
|
||||
<span className="mt-1 block min-w-0">
|
||||
<TruncatedCell
|
||||
value={turn.prompt_excerpt || `Turn #${turn.turn_id}`}
|
||||
label="prompt"
|
||||
wrap="pre-wrap"
|
||||
className="text-sm text-[var(--color-text-primary)]"
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
<span className="justify-self-end font-mono text-xs text-[var(--color-text-muted)]">
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import { SplitPane } from "../../design/components/SplitPane/SplitPane";
|
|||
import { StableJsonViewer } from "../../design/components/StableJsonViewer";
|
||||
import { TabBar, type Tab } from "../../design/components/TabBar/TabBar";
|
||||
import { Timestamp } from "../../design/components/Timestamp/Timestamp";
|
||||
import { TruncatedCell } from "../../design/components/TruncatedCell";
|
||||
import { VirtualizedList } from "../../design/components/VirtualizedList/VirtualizedList";
|
||||
import { Button } from "../../design/components/primitives/Button";
|
||||
import { EmptyState } from "../../design/components/states/EmptyState";
|
||||
|
|
@ -155,8 +156,13 @@ function RunRow({
|
|||
<span className="block text-sm text-[var(--color-text-primary)]">
|
||||
{sourceLabel(run.source)}
|
||||
</span>
|
||||
<span className="mt-1 block truncate font-mono text-xs text-[var(--color-text-muted)]">
|
||||
{run.session_id}
|
||||
<span className="mt-1 block min-w-0">
|
||||
<TruncatedCell
|
||||
value={run.session_id}
|
||||
label="session id"
|
||||
mono
|
||||
className="text-xs text-[var(--color-text-muted)]"
|
||||
/>
|
||||
</span>
|
||||
<span className="mt-1 flex items-center gap-2 text-xs text-[var(--color-text-secondary)]">
|
||||
<span>{run.turn_count} turns</span>
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import { SplitPane } from "../../design/components/SplitPane/SplitPane";
|
|||
import { StableJsonViewer } from "../../design/components/StableJsonViewer";
|
||||
import { TabBar, type Tab } from "../../design/components/TabBar/TabBar";
|
||||
import { Timestamp } from "../../design/components/Timestamp/Timestamp";
|
||||
import { TruncatedCell } from "../../design/components/TruncatedCell";
|
||||
import { VirtualizedList } from "../../design/components/VirtualizedList/VirtualizedList";
|
||||
import {
|
||||
EpistemicStateBadge,
|
||||
|
|
@ -212,7 +213,9 @@ function PipelineStageRail({
|
|||
{stages.map((stage, index) => {
|
||||
const selected = stage.stage_id === selectedStageId;
|
||||
return (
|
||||
<button
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-pressed={selected}
|
||||
className={`grid min-h-16 grid-cols-[2rem_minmax(0,1fr)] gap-2 rounded border px-2 py-2 text-left focus-visible:outline focus-visible:outline-2 focus-visible:outline-[var(--color-focus-ring)] ${
|
||||
selected
|
||||
|
|
@ -221,23 +224,36 @@ function PipelineStageRail({
|
|||
}`}
|
||||
key={stage.stage_id}
|
||||
onClick={() => onSelect(stage.stage_id)}
|
||||
type="button"
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter" || event.key === " ") {
|
||||
event.preventDefault();
|
||||
onSelect(stage.stage_id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span className="font-mono text-xs text-[var(--color-text-muted)]">
|
||||
{String(index + 1).padStart(2, "0")}
|
||||
</span>
|
||||
<span className="min-w-0">
|
||||
<span className="flex items-center justify-between gap-2">
|
||||
<span className="truncate font-mono text-xs font-semibold text-[var(--color-text-primary)]">
|
||||
{stage.label}
|
||||
</span>
|
||||
<TruncatedCell
|
||||
value={stage.label}
|
||||
label="stage"
|
||||
mono
|
||||
className="text-xs font-semibold text-[var(--color-text-primary)]"
|
||||
/>
|
||||
<PipelineStageStatus status={stage.status} />
|
||||
</span>
|
||||
<span className="mt-1 block truncate text-xs text-[var(--color-text-secondary)]">
|
||||
{stage.summary}
|
||||
<span className="mt-1 block min-w-0">
|
||||
<TruncatedCell
|
||||
value={stage.summary}
|
||||
label="summary"
|
||||
wrap="pre-wrap"
|
||||
className="text-xs text-[var(--color-text-secondary)]"
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
|
|
@ -259,11 +275,20 @@ function PipelineTransitionList({ record }: { record: CognitivePipelineRecord })
|
|||
className="grid grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)] items-center gap-2 rounded border border-[var(--color-border-subtle)] bg-[var(--color-surface-raised)] px-2 py-1 font-mono text-[11px]"
|
||||
key={`${edge.from_stage}:${edge.to_stage}`}
|
||||
>
|
||||
<span className="truncate text-[var(--color-text-secondary)]">{edge.from_stage}</span>
|
||||
<TruncatedCell
|
||||
value={edge.from_stage}
|
||||
label="from stage"
|
||||
mono
|
||||
className="text-[var(--color-text-secondary)]"
|
||||
/>
|
||||
<span className="text-[var(--color-text-muted)]">{edge.label ?? "propagate"}</span>
|
||||
<span className="truncate text-right text-[var(--color-text-primary)]">
|
||||
{edge.to_stage}
|
||||
</span>
|
||||
<TruncatedCell
|
||||
value={edge.to_stage}
|
||||
label="to stage"
|
||||
mono
|
||||
align="end"
|
||||
className="text-[var(--color-text-primary)]"
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
|
|
@ -563,11 +588,21 @@ function TraceRow({
|
|||
<span className="block text-xs text-[var(--color-text-secondary)]">
|
||||
<Timestamp iso={turn.timestamp} format="relative" />
|
||||
</span>
|
||||
<span className="mt-1 block truncate text-sm text-[var(--color-text-primary)]">
|
||||
{firstLine(turn.prompt_excerpt) || `Turn #${turn.turn_id}`}
|
||||
<span className="mt-1 block min-w-0">
|
||||
<TruncatedCell
|
||||
value={firstLine(turn.prompt_excerpt) || `Turn #${turn.turn_id}`}
|
||||
label="prompt"
|
||||
wrap="pre-wrap"
|
||||
className="text-sm text-[var(--color-text-primary)]"
|
||||
/>
|
||||
</span>
|
||||
<span className="mt-1 block truncate text-xs text-[var(--color-text-muted)]">
|
||||
{turn.surface_excerpt}
|
||||
<span className="mt-1 block min-w-0">
|
||||
<TruncatedCell
|
||||
value={turn.surface_excerpt}
|
||||
label="surface"
|
||||
wrap="pre-wrap"
|
||||
className="text-xs text-[var(--color-text-muted)]"
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
<span className="justify-self-end">
|
||||
|
|
|
|||
|
|
@ -45,6 +45,12 @@ describe("TruncatedCell", () => {
|
|||
expect(screen.getByRole("dialog", { name: "source" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("right-justifies content when align is end", () => {
|
||||
render(<TruncatedCell value="to_stage" label="to stage" align="end" />);
|
||||
const display = screen.getByText("to_stage");
|
||||
expect(display).toHaveClass("text-right");
|
||||
});
|
||||
|
||||
it("does not bubble clicks to a surrounding row handler", async () => {
|
||||
const onRowClick = vi.fn();
|
||||
render(
|
||||
|
|
|
|||
|
|
@ -44,6 +44,12 @@ export interface TruncatedCellProps {
|
|||
wrap?: "break-all" | "pre-wrap";
|
||||
/** Class applied to the inline display span. */
|
||||
className?: string;
|
||||
/**
|
||||
* Horizontal alignment of the cell content. `end` hugs the right edge (for
|
||||
* right-aligned table columns) and right-justifies the display text.
|
||||
* Defaults to `start`.
|
||||
*/
|
||||
align?: "start" | "end";
|
||||
}
|
||||
|
||||
function shouldOfferModal(value: string) {
|
||||
|
|
@ -94,6 +100,7 @@ export function TruncatedCell({
|
|||
mono,
|
||||
wrap = "break-all",
|
||||
className,
|
||||
align = "start",
|
||||
}: TruncatedCellProps) {
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const offerModal = shouldOfferModal(value);
|
||||
|
|
@ -101,8 +108,17 @@ export function TruncatedCell({
|
|||
const stop = (event: { stopPropagation: () => void }) => event.stopPropagation();
|
||||
|
||||
return (
|
||||
<span className={cn("group/cell flex min-w-0 items-center gap-1", mono && "font-mono")}>
|
||||
<span className={cn("truncate", className)} title={value}>
|
||||
<span
|
||||
className={cn(
|
||||
"group/cell flex min-w-0 items-center gap-1",
|
||||
align === "end" && "justify-end",
|
||||
mono && "font-mono",
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={cn("truncate", align === "end" && "text-right", className)}
|
||||
title={value}
|
||||
>
|
||||
{display ?? value}
|
||||
</span>
|
||||
<Popover.Root>
|
||||
|
|
|
|||
Loading…
Reference in a new issue