feat(dashboard): TodoView nav-stack in narrow dock; taller scrollable import preview; shared ViewHeader

- TodoView collapses to a single-panel navigation stack (list -> items with Back) via container query when narrow (right dock); wide two-panel layout unchanged.
- Embedded Import Tasks body scrolls vertically so the preview can be much taller (stacked layout gives the preview natural height; wide layout keeps internal scroll).
- Add a shared ViewHeader component (icon + CC-style title + wrapping actions) for consistent main-view headers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-06-22 02:00:12 -07:00
parent cf702c9bbd
commit 125c5f5744
6 changed files with 207 additions and 2 deletions

View File

@@ -997,6 +997,15 @@ Fix (embedded variant only — modal path untouched): turn the embedded root int
container-name: github-import-embedded;
}
/*
FNXC:RightDockEmbedding 2026-06-22-01:00:
Embedded Import Tasks must scroll vertically so a long preview is fully reachable. The view body is the scroll container; in the stacked (narrow) layout the preview takes its natural (content) height and the body scrolls, so the preview can be much taller than the viewport. In the wide two-pane layout the preview keeps its own internal scroll.
*/
.github-import-modal--embedded .github-import-modal__body {
min-height: 0;
overflow-y: auto;
}
/* Default (narrow container): single stacked column. */
.github-import-modal--embedded .github-import-workspace {
flex-direction: column;
@@ -1011,7 +1020,7 @@ Fix (embedded variant only — modal path untouched): turn the embedded root int
}
.github-import-modal--embedded .github-import-preview-pane {
flex: 1 1 auto;
flex: 0 0 auto;
width: 100%;
min-width: 0;
}

View File

@@ -14,6 +14,20 @@ FN-6829 mounts Todos as a flex child of .project-content like GoalsView; grow, z
width: 100%;
overflow: hidden;
padding: var(--space-lg);
/*
FNXC:TodosStyling 2026-06-22-00:00:
TodoView renders both in the wide main area and inside the narrow right dock (no width prop). Make it a query container so the layout switch is driven by the actual rendered width, not a viewport media query or a prop. Below the container breakpoint the two-panel split collapses into a single-panel navigation stack (see `@container todo-view (max-width: 520px)`).
*/
container-type: inline-size;
container-name: todo-view;
}
/*
FNXC:TodosStyling 2026-06-22-00:00:
The narrow-stack Back button is hidden by default (wide two-panel layout shows both panels, so there is nothing to go "back" to). The narrow container query reveals it.
*/
.todo-mobile-back-btn {
display: none;
}
.todo-view-header {
@@ -401,6 +415,93 @@ FN-6829 mounts Todos as a flex child of .project-content like GoalsView; grow, z
}
}
/*
FNXC:TodosStyling 2026-06-22-00:00:
NARROW container (right dock): collapse the side-by-side split into a single-panel navigation stack. Exactly one panel shows at a time, full-width with its own internal scroll and no horizontal overflow. `data-mobile-stack-view` (set by the component from `mobileStackView`) decides which panel is visible: "list" shows the master list-selection panel; "detail" shows the items panel with the Back button revealed. Tap targets are enlarged for touch. 520px is tuned to the content: below it the sidebar's fixed width plus the items pane no longer fit comfortably.
*/
@container todo-view (max-width: 520px) {
.todo-view-layout {
flex-direction: column;
min-height: 0;
overflow: hidden;
gap: 0;
}
/* Single panel at a time: full-width, owns its vertical scroll. */
.todo-view-sidebar,
.todo-view-main {
width: 100%;
min-width: 0;
flex: 1 1 auto;
border-right: none;
padding-right: 0;
overflow-x: hidden;
overflow-y: auto;
}
.todo-view-sidebar {
border-bottom: none;
}
/* On the list panel, hide the items pane; on the detail panel, hide the list pane. */
.todo-view-layout[data-mobile-stack-view="list"] .todo-view-main {
display: none;
}
.todo-view-layout[data-mobile-stack-view="detail"] .todo-view-sidebar {
display: none;
}
/* Reveal the Back affordance only in the narrow stack. */
.todo-mobile-back-btn {
display: inline-flex;
}
/* Comfortable touch targets and full-width add controls in the stack. */
.todo-list-item,
.todo-list-select-btn,
.todo-add-list-btn,
.todo-icon-btn,
.todo-item,
.todo-item-reorder-btn,
.todo-add-item-row .btn {
min-height: calc(var(--space-2xl) + var(--space-xs));
}
.todo-list-item-actions,
.todo-item-actions {
opacity: 1;
}
.todo-item-actions {
margin-left: 0;
}
.todo-add-item-row {
flex-wrap: wrap;
}
.todo-add-item-row .btn {
width: 100%;
}
/* Anchor the agent picker to the full stack width to avoid horizontal overflow. */
.todo-agent-picker-trigger {
position: static;
}
.todo-agent-picker-dropdown {
left: 0;
right: 0;
min-width: 100%;
max-height: calc(var(--space-2xl) * 8);
}
.todo-agent-picker-item {
min-height: calc(var(--space-2xl) + var(--space-xs));
}
}
@media (max-width: 768px) {
.todo-view {
padding: var(--space-md);

View File

@@ -8,6 +8,7 @@ import {
X,
ChevronUp,
ChevronDown,
ChevronLeft,
Loader2,
ListChecks,
Bot,
@@ -80,6 +81,12 @@ export function TodoView({
const agentPickerRef = useRef<HTMLDivElement>(null);
const { confirm } = useConfirm();
/*
FNXC:Todos 2026-06-22-00:00:
TodoView mounts in the narrow right dock (no width prop) where the two side-by-side panels (list selection + items) cannot fit. The layout switch is driven by a CSS container query on `.todo-view` (container-name: todo-view), NOT a prop. In the NARROW container we render a single-panel navigation stack: the master list-selection panel first, and selecting a list navigates forward to its items panel with a Back affordance. `mobileStackView` tracks which panel the narrow stack shows; the WIDE two-panel layout ignores it entirely (both panels always render). Selecting a list pushes to "detail"; Back returns to "list".
*/
const [mobileStackView, setMobileStackView] = useState<"list" | "detail">("list");
const selectedList = useMemo(
() => lists.find((list) => list.id === selectedListId) ?? null,
[lists, selectedListId],
@@ -106,6 +113,13 @@ export function TodoView({
resetListDraftState();
resetItemDraftState();
setSelectedListId(listId);
// FNXC:Todos 2026-06-22-00:00: Narrow stack navigates forward to the items panel on selection; no-op visually in the wide two-panel layout.
setMobileStackView("detail");
}
// FNXC:Todos 2026-06-22-00:00: Narrow-stack Back affordance returns to the master list-selection panel. Inert in the wide layout where both panels are always visible.
function handleMobileBack(): void {
setMobileStackView("list");
}
const loadAgents = useCallback(async () => {
@@ -329,7 +343,7 @@ export function TodoView({
return (
<div className="todo-view" data-testid="todo-view-root">
{header}
<div className="todo-view-layout">
<div className="todo-view-layout" data-mobile-stack-view={mobileStackView}>
<aside className="todo-view-sidebar" aria-label={t("todo.listsLabel", "Todo lists sidebar")}>
<div className="todo-sidebar-header">
<h3 className="todo-sidebar-title">{t("todo.lists", "Lists")}</h3>
@@ -517,6 +531,16 @@ export function TodoView({
) : (
<>
<div className="todo-items-header">
{/* FNXC:Todos 2026-06-22-00:00: Back button is visible only in the narrow container (CSS-gated) to pop the items panel back to the list-selection panel. Hidden in the wide two-panel layout where both panels coexist. */}
<button
type="button"
className="btn btn-sm btn-icon todo-icon-btn todo-mobile-back-btn"
onClick={handleMobileBack}
aria-label={t("todo.backToLists", "Back to lists")}
data-testid="todo-mobile-back-button"
>
<ChevronLeft />
</button>
<h3>{selectedList.title}</h3>
</div>

View File

@@ -0,0 +1,42 @@
/*
FNXC:Navigation 2026-06-22-01:00:
Shared main-content view header, modeled after Command Center (.cc-header / .cc-title). Provides the standard --space-lg side/top padding and --space-md bottom gap, an icon + 1.125rem title, and an optional right-aligned actions cluster that wraps below the title on narrow widths so the two never overlap.
*/
.view-header {
display: flex;
flex-shrink: 0;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: var(--space-sm) var(--space-md);
padding: var(--space-lg) var(--space-lg) var(--space-md);
}
.view-header__title {
display: flex;
align-items: center;
gap: var(--space-sm);
min-width: 0;
margin: 0;
font-size: 1.125rem;
font-weight: 600;
color: var(--text);
}
.view-header__title svg {
flex-shrink: 0;
}
.view-header__title span {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.view-header__actions {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: var(--space-sm);
}

View File

@@ -0,0 +1,28 @@
import "./ViewHeader.css";
import type { ComponentType, ReactNode } from "react";
import type { LucideProps } from "lucide-react";
/*
FNXC:Navigation 2026-06-22-01:00:
Shared header for main-content views so every left-sidebar destination reads consistently, modeled after Command Center (cc-header/cc-title): an icon + 1.125rem title on the left, optional actions on the right, with the standard --space-lg view padding. Views adopting this should NOT add their own outer top/side padding for the header row.
*/
export interface ViewHeaderProps {
icon: ComponentType<LucideProps>;
title: string;
/** Optional right-aligned actions (buttons, filters, status). */
actions?: ReactNode;
/** Optional id for the heading element (for aria-labelledby). */
titleId?: string;
}
export function ViewHeader({ icon: Icon, title, actions, titleId }: ViewHeaderProps) {
return (
<header className="view-header">
<h2 className="view-header__title" id={titleId}>
<Icon size={20} aria-hidden="true" />
<span>{title}</span>
</h2>
{actions ? <div className="view-header__actions">{actions}</div> : null}
</header>
);
}

View File

@@ -29,6 +29,7 @@ vi.mock("lucide-react", () => ({
X: () => <span data-testid="icon-x" />,
ChevronUp: () => <span data-testid="icon-chevron-up" />,
ChevronDown: () => <span data-testid="icon-chevron-down" />,
ChevronLeft: () => <span data-testid="icon-chevron-left" />,
Loader2: () => <span data-testid="icon-loader" />,
ListChecks: () => <span data-testid="icon-list-checks" />,
Bot: () => <span data-testid="icon-bot" />,