fix(workspace): move lane list to a vertical column beside detail panel
The detail panel (LaneCard/PipelineMap/proof gallery/console) could grow tall enough to visually crowd out the lane-strip carousel above it once a run was attached in split view. Move the lane list into its own scrolling vertical column beside the detail panel instead of stacking it above, so neither can cover the other; also collapse the info block by default in 2/4-pane split view (toggle to expand) and move the pane-count control into the detail header.
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
/**
|
||||
* @file The compact lane tile used in the Workspace carousel. It carries only
|
||||
* what you need to pick a lane — which lane, is it alive, what stage, how far —
|
||||
* because the full card, its controls and its working-copy facts live in the
|
||||
* detail panel below. Keeping the tile small is what lets a dozen lanes stay
|
||||
* scannable in one horizontal row.
|
||||
* @file The compact lane tile used in the Workspace's vertical lane list. It
|
||||
* carries only what you need to pick a lane — which lane, is it alive, what
|
||||
* stage, how far — because the full card, its controls and its working-copy
|
||||
* facts live in the detail panel beside it. Keeping the tile small and full
|
||||
* width is what lets many lanes stay scannable in one scrolling column.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
@@ -42,7 +42,7 @@ export default function LaneStripCard({
|
||||
aria-pressed={selected}
|
||||
onClick={onSelect}
|
||||
title={lane.cwd}
|
||||
className={`w-56 shrink-0 snap-start rounded-lg border p-3 text-left shadow-sm transition-colors ${
|
||||
className={`w-full shrink-0 rounded-lg border p-3 text-left shadow-sm transition-colors ${
|
||||
selected
|
||||
? "border-accent bg-accent/10"
|
||||
: "border-border bg-surface-2 hover:border-border-light hover:bg-surface-3"
|
||||
|
||||
@@ -90,6 +90,8 @@
|
||||
"git.uncommitted": "{{dirty}} modified · {{untracked}} untracked",
|
||||
"kind.adopted": "adopted",
|
||||
"kind.managed": "managed",
|
||||
"laneDetail.hide": "Hide details",
|
||||
"laneDetail.show": "Lane details",
|
||||
"laneHeader": "Lane {{id}} · {{title}} · {{pipeline}}",
|
||||
"locks.held_one": "{{count}} lock held",
|
||||
"locks.held_other": "{{count}} locks held",
|
||||
|
||||
@@ -90,6 +90,8 @@
|
||||
"git.uncommitted": "{{dirty}} đã sửa · {{untracked}} chưa theo dõi",
|
||||
"kind.adopted": "đã nhận",
|
||||
"kind.managed": "được quản lý",
|
||||
"laneDetail.hide": "Ẩn chi tiết",
|
||||
"laneDetail.show": "Chi tiết lane",
|
||||
"laneHeader": "Làn đường {{id}} · {{title}} · {{pipeline}}",
|
||||
"locks.held_one": "Đang giữ {{count}} khóa",
|
||||
"locks.held_other": "Đang giữ {{count}} khóa",
|
||||
|
||||
@@ -54,39 +54,16 @@ import type { SplitViewState, SplitLayout } from "../lib/splitViewStorage";
|
||||
|
||||
// ── Page ──────────────────────────────────────────────────────────────
|
||||
|
||||
function ConsoleArea({
|
||||
lanes,
|
||||
selectedLaneId,
|
||||
function SplitLayoutToggle({
|
||||
splitView,
|
||||
setLayout,
|
||||
setPaneLaneId,
|
||||
binaryStatus,
|
||||
cwdSuggestions,
|
||||
activeRuns,
|
||||
wsConnected,
|
||||
defaultCwd,
|
||||
onHasActiveRunChange,
|
||||
onLaneCreated,
|
||||
onLaneIdChange,
|
||||
}: {
|
||||
lanes: Lane[];
|
||||
selectedLaneId: number | null;
|
||||
splitView: SplitViewState;
|
||||
setLayout: (layout: SplitLayout) => void;
|
||||
setPaneLaneId: (index: number, id: number) => void;
|
||||
binaryStatus: { found: boolean; path: string | null } | null;
|
||||
cwdSuggestions: CwdSuggestion[];
|
||||
activeRuns: RunListResponse | null;
|
||||
wsConnected: boolean;
|
||||
defaultCwd: string;
|
||||
onHasActiveRunChange: (val: boolean) => void;
|
||||
onLaneCreated: (lane: Lane) => void;
|
||||
onLaneIdChange: (id: number | null) => void;
|
||||
}) {
|
||||
const { t: tLanes } = useTranslation("lanes");
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center gap-1.5">
|
||||
{([1, 2, 4] as const).map((n) => (
|
||||
<button
|
||||
@@ -104,6 +81,43 @@ function ConsoleArea({
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ConsoleArea({
|
||||
lanes,
|
||||
selectedLaneId,
|
||||
splitView,
|
||||
setLayout,
|
||||
setPaneLaneId,
|
||||
binaryStatus,
|
||||
cwdSuggestions,
|
||||
activeRuns,
|
||||
wsConnected,
|
||||
defaultCwd,
|
||||
onHasActiveRunChange,
|
||||
onLaneCreated,
|
||||
onLaneIdChange,
|
||||
showToggle = true,
|
||||
}: {
|
||||
lanes: Lane[];
|
||||
selectedLaneId: number | null;
|
||||
splitView: SplitViewState;
|
||||
setLayout: (layout: SplitLayout) => void;
|
||||
setPaneLaneId: (index: number, id: number) => void;
|
||||
binaryStatus: { found: boolean; path: string | null } | null;
|
||||
cwdSuggestions: CwdSuggestion[];
|
||||
activeRuns: RunListResponse | null;
|
||||
wsConnected: boolean;
|
||||
defaultCwd: string;
|
||||
onHasActiveRunChange: (val: boolean) => void;
|
||||
onLaneCreated: (lane: Lane) => void;
|
||||
onLaneIdChange: (id: number | null) => void;
|
||||
showToggle?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
{showToggle && <SplitLayoutToggle splitView={splitView} setLayout={setLayout} />}
|
||||
{splitView.layout === 1 ? (
|
||||
<LaneConsolePane
|
||||
lanes={lanes}
|
||||
@@ -175,6 +189,11 @@ export function Workspace() {
|
||||
|
||||
// Split view state
|
||||
const [splitView, setSplitView] = useState<SplitViewState>(() => readSplitViewState());
|
||||
// Info block (pickers, LaneCard, PipelineMap, proof gallery) is always shown
|
||||
// in single-pane view; in split view it starts collapsed so tall content
|
||||
// never crowds the pane grid, and the user expands it on demand.
|
||||
const [infoExpanded, setInfoExpanded] = useState(false);
|
||||
const showInfo = splitView.layout === 1 || infoExpanded;
|
||||
|
||||
const setLayout = useCallback((layout: SplitLayout) => {
|
||||
setSplitView((prev) => {
|
||||
@@ -488,11 +507,14 @@ export function Workspace() {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Lane carousel: pick a lane here, read it below. Horizontal scroll with
|
||||
snap so a dozen lanes stay in one row instead of a wall of cards. */}
|
||||
{/* Lane list + detail live side by side so neither can cover the other:
|
||||
the list is a scrolling column (holds any number of lanes), the
|
||||
detail/console area to its right never has to compete with it for
|
||||
vertical space. */}
|
||||
<div className="flex min-h-0 flex-1 gap-4">
|
||||
<div
|
||||
data-testid="lane-strip"
|
||||
className="flex snap-x snap-mandatory gap-2 overflow-x-auto pb-1"
|
||||
className="flex w-60 shrink-0 flex-col gap-2 overflow-y-auto pr-1"
|
||||
>
|
||||
{lanes.map((l) => (
|
||||
<LaneStripCard
|
||||
@@ -509,17 +531,39 @@ export function Workspace() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* The selected lane's pipeline, full width — the thing you actually
|
||||
come to this page to read. */}
|
||||
{/* The selected lane's pipeline — the thing you actually come to this
|
||||
page to read, beside the list rather than stacked under it. */}
|
||||
{currentLane && (
|
||||
<section data-testid="lane-detail" className="card p-4">
|
||||
<section data-testid="lane-detail" className="flex min-h-0 flex-1 flex-col card p-4">
|
||||
<div className="mb-3 flex flex-wrap items-center justify-between gap-2">
|
||||
<span className="truncate text-sm font-semibold text-fg-primary">
|
||||
{tLanes("laneHeader", {
|
||||
id: currentLane.id,
|
||||
title: currentLane.title || currentLane.cwd,
|
||||
pipeline: currentLane.pipeline_name,
|
||||
})}
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
{splitView.layout !== 1 && (
|
||||
<button
|
||||
type="button"
|
||||
data-testid="lane-detail-toggle"
|
||||
aria-expanded={infoExpanded}
|
||||
onClick={() => setInfoExpanded((v) => !v)}
|
||||
className="rounded border border-border-light px-2 py-1 text-xs text-fg-secondary transition-colors hover:text-fg-primary"
|
||||
>
|
||||
{infoExpanded ? tLanes("laneDetail.hide") : tLanes("laneDetail.show")}
|
||||
</button>
|
||||
)}
|
||||
<SplitLayoutToggle splitView={splitView} setLayout={setLayout} />
|
||||
</div>
|
||||
</div>
|
||||
{showInfo && (
|
||||
<>
|
||||
<div className="mb-3 flex flex-wrap items-baseline gap-2">
|
||||
<span className="text-[11px] font-semibold uppercase tracking-widest text-fg-muted">
|
||||
{tLanes("cardId", { id: currentLane.id })}
|
||||
</span>
|
||||
<span className="truncate text-sm font-semibold text-fg-primary">
|
||||
{currentLane.title || currentLane.cwd}
|
||||
</span>
|
||||
<select
|
||||
data-testid="pipeline-picker"
|
||||
aria-label={tLanes("pipelinePicker.label")}
|
||||
@@ -592,7 +636,9 @@ export function Workspace() {
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<PipelineMap
|
||||
nodes={viewedFeature ? viewedFeature.pipeline_nodes : currentLane.pipeline_nodes}
|
||||
nodes={
|
||||
viewedFeature ? viewedFeature.pipeline_nodes : currentLane.pipeline_nodes
|
||||
}
|
||||
detectedSignal={viewedFeature ? undefined : currentLane.detected_signal}
|
||||
/>
|
||||
{viewedFeature && (
|
||||
@@ -642,7 +688,9 @@ export function Workspace() {
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex min-h-0 flex-col gap-2 border-t border-border pt-3">
|
||||
</>
|
||||
)}
|
||||
<div className="flex min-h-0 flex-1 flex-col gap-2 border-t border-border pt-3">
|
||||
<ConsoleArea
|
||||
lanes={lanes}
|
||||
selectedLaneId={selectedLaneId}
|
||||
@@ -659,13 +707,14 @@ export function Workspace() {
|
||||
setLanes((prev) => (prev.some((l) => l.id === lane.id) ? prev : [...prev, lane]))
|
||||
}
|
||||
onLaneIdChange={setSelectedLaneId}
|
||||
showToggle={false}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{!currentLane && (
|
||||
<div className="flex min-h-0 flex-col gap-2">
|
||||
<div className="flex min-h-0 flex-1 flex-col gap-2">
|
||||
<ConsoleArea
|
||||
lanes={lanes}
|
||||
selectedLaneId={selectedLaneId}
|
||||
@@ -685,6 +734,7 @@ export function Workspace() {
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<AddLaneModal
|
||||
open={addLaneOpen}
|
||||
|
||||
@@ -5762,7 +5762,10 @@ exports[`screen snapshots > Run 1`] = `
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="flex snap-x snap-mandatory gap-2 overflow-x-auto pb-1"
|
||||
class="flex min-h-0 flex-1 gap-4"
|
||||
>
|
||||
<div
|
||||
class="flex w-60 shrink-0 flex-col gap-2 overflow-y-auto pr-1"
|
||||
data-testid="lane-strip"
|
||||
>
|
||||
<p
|
||||
@@ -5776,7 +5779,7 @@ exports[`screen snapshots > Run 1`] = `
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="flex min-h-0 flex-col gap-2"
|
||||
class="flex min-h-0 flex-1 flex-col gap-2"
|
||||
>
|
||||
<div
|
||||
class="flex items-center gap-1.5"
|
||||
@@ -6136,6 +6139,7 @@ exports[`screen snapshots > Run 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user