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:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
42
packages/dashboard/app/components/ViewHeader.css
Normal file
42
packages/dashboard/app/components/ViewHeader.css
Normal 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);
|
||||
}
|
||||
28
packages/dashboard/app/components/ViewHeader.tsx
Normal file
28
packages/dashboard/app/components/ViewHeader.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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" />,
|
||||
|
||||
Reference in New Issue
Block a user