From 2d038e7ffa10a86e1520487f8d6e6f9174d0e77d Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Mon, 22 Jun 2026 23:58:38 -0700 Subject: [PATCH] feat(FN-6950): polish dashboard responsive chrome --- .../app/__tests__/agent-css-classes.test.ts | 6 + .../left-sidebar-active-accent.css.test.ts | 14 ++- .../app/components/AgentLogViewer.css | 32 +++++ .../app/components/DockFilesView.tsx | 2 +- .../app/components/ExecutorStatusBar.css | 10 +- .../dashboard/app/components/FileBrowser.css | 21 ++++ .../app/components/FileBrowserModal.tsx | 16 ++- .../app/components/FloatingWindow.css | 7 +- .../dashboard/app/components/InsightsView.tsx | 7 +- .../app/components/LeftSidebarNav.css | 14 ++- .../dashboard/app/components/ListView.css | 85 +++++-------- .../dashboard/app/components/ListView.tsx | 76 ++++-------- .../app/components/NewAgentDialog.css | 9 +- .../app/components/NewAgentDialog.tsx | 27 ++-- .../dashboard/app/components/NewTaskModal.css | 8 +- .../dashboard/app/components/RightDock.css | 7 +- .../dashboard/app/components/ScriptsModal.css | 11 ++ .../app/components/TerminalModal.css | 11 +- .../__tests__/AgentLogViewer.test.tsx | 14 +++ .../__tests__/ExecutorStatusBar.test.tsx | 19 +++ .../__tests__/FileBrowserModal.test.tsx | 115 ++++++++++++++++++ .../__tests__/FloatingWindow.test.tsx | 8 ++ .../__tests__/GitManagerModal.test.tsx | 11 ++ .../__tests__/InsightsView.test.tsx | 6 +- .../__tests__/LeftSidebarNav.test.tsx | 2 +- .../components/__tests__/ListView.test.tsx | 26 ++-- .../__tests__/NewTaskModal.test.tsx | 13 ++ .../components/__tests__/RightDock.test.tsx | 56 +++++++++ .../__tests__/TerminalModal.test.tsx | 13 ++ .../app/components/useRightDockController.tsx | 19 ++- 30 files changed, 517 insertions(+), 148 deletions(-) diff --git a/packages/dashboard/app/__tests__/agent-css-classes.test.ts b/packages/dashboard/app/__tests__/agent-css-classes.test.ts index e296401faf..63ae408b5e 100644 --- a/packages/dashboard/app/__tests__/agent-css-classes.test.ts +++ b/packages/dashboard/app/__tests__/agent-css-classes.test.ts @@ -165,6 +165,12 @@ describe("Agent CSS classes", () => { expect(roleFocusBlock).toContain("box-shadow: var(--focus-ring-strong)"); }); + it("should use provider icons instead of decorative role glyphs in the create-agent role picker", () => { + expect(newAgentDialogContent).toContain(""); + expect(newAgentDialogContent).not.toMatch(/icon:\s*"[⊕▶⊙⊞◷⎔✦]"/); + expect(newAgentDialogContent).not.toContain("selectedRole?.icon"); + }); + it("should keep the create-agent empty-state action copy", () => { expect(agentEmptyStateContent).toContain("Create Agent"); }); diff --git a/packages/dashboard/app/__tests__/left-sidebar-active-accent.css.test.ts b/packages/dashboard/app/__tests__/left-sidebar-active-accent.css.test.ts index 5b50794f9c..3caf7e8995 100644 --- a/packages/dashboard/app/__tests__/left-sidebar-active-accent.css.test.ts +++ b/packages/dashboard/app/__tests__/left-sidebar-active-accent.css.test.ts @@ -16,6 +16,18 @@ function extractRuleBody(source: string, selector: string): string { return match?.[1] ?? ""; } +function extractGroupedRuleBody(source: string, selector: string): string { + const sourceWithoutComments = source.replace(/\/\*[\s\S]*?\*\//g, ""); + const match = [...sourceWithoutComments.matchAll(/(^|})\s*([^{}]+)\s*\{([\s\S]*?)\}/g)].find(([, , selectors]) => + selectors + .split(",") + .map((part) => part.trim()) + .includes(selector), + ); + expect(match, `${selector} grouped rule should exist in LeftSidebarNav.css`).not.toBeNull(); + return match?.[3] ?? ""; +} + describe("left sidebar active accent CSS", () => { /** * FNXC:DashboardStyling 2026-06-21-11:16: @@ -23,7 +35,7 @@ describe("left sidebar active accent CSS", () => { */ it("uses the theme accent token for active item and resize handle styling", () => { const source = readLeftSidebarCss(); - const activeItemBody = extractRuleBody(source, ".left-sidebar-nav__item--active"); + const activeItemBody = extractGroupedRuleBody(source, ".left-sidebar-nav__item--active"); expect(activeItemBody).toContain("var(--accent)"); expect(activeItemBody).not.toContain("var(--todo)"); diff --git a/packages/dashboard/app/components/AgentLogViewer.css b/packages/dashboard/app/components/AgentLogViewer.css index df6c7df981..6aab83400b 100644 --- a/packages/dashboard/app/components/AgentLogViewer.css +++ b/packages/dashboard/app/components/AgentLogViewer.css @@ -133,12 +133,34 @@ margin-left: auto; } +/* +FNXC:TaskDetailChat 2026-06-23-23:55: +Task-detail chat output blocks can be long enough that the executor/reviewer label scrolls out of view. +Keep each block full-width and float the role/timestamp badge as a sticky overlay on the left so the visible content always has role context without reserving a permanent label column. +*/ .agent-log-badge-row { + position: sticky; + top: var(--space-xs); + left: var(--space-xs); + z-index: 2; display: inline-flex; align-items: center; + width: max-content; + max-width: calc(100% - var(--space-md)); + margin: 0 0 var(--space-xs) var(--space-xs); + padding: 2px var(--space-xs); + border: 1px solid color-mix(in srgb, var(--border) 70%, transparent); + border-radius: var(--radius-pill); + background: color-mix(in srgb, var(--surface) 88%, transparent); + box-shadow: 0 1px 4px color-mix(in srgb, var(--shadow-color, #000) 12%, transparent); + pointer-events: none; + white-space: nowrap; } .agent-log-tool { + position: relative; + width: 100%; + box-sizing: border-box; color: var(--accent); margin: var(--space-xs) 0; padding: var(--space-xs) var(--space-sm); @@ -147,6 +169,9 @@ } .agent-log-tool-result { + position: relative; + width: 100%; + box-sizing: border-box; color: var(--color-success); margin: calc(var(--space-xs) / 2) 0; padding: var(--space-xs) var(--space-sm); @@ -156,6 +181,9 @@ } .agent-log-tool-error { + position: relative; + width: 100%; + box-sizing: border-box; color: var(--color-error); margin: calc(var(--space-xs) / 2) 0; padding: var(--space-xs) var(--space-sm); @@ -228,11 +256,15 @@ .agent-log-text { display: block; + position: relative; + width: 100%; color: var(--text); } .agent-log-thinking { display: block; + position: relative; + width: 100%; font-style: italic; color: var(--text); } diff --git a/packages/dashboard/app/components/DockFilesView.tsx b/packages/dashboard/app/components/DockFilesView.tsx index 88ee5ff4e9..cd57e368c1 100644 --- a/packages/dashboard/app/components/DockFilesView.tsx +++ b/packages/dashboard/app/components/DockFilesView.tsx @@ -26,7 +26,7 @@ FNXC:RightDockFiles 2026-06-22-23:30: The compact dock Files view and the popped-out (expand) Files view are SEPARATE component instances (one renders in the dock body, the other inside RightDockExpandModal). The currently-viewed file lived in each instance's local `selectedFile` state, so popping out always opened with no file selected. Share the current-file path through scoped localStorage (`kb-dashboard-dock-files-current`, keyed per project via projectStorage). Selecting/clearing a file writes the key; on mount each instance reads it so the expand opens the SAME file the dock was showing. A `storage` listener keeps both instances live-synced when the other tab/instance changes selection. */ -const DOCK_FILES_CURRENT_KEY = "kb-dashboard-dock-files-current"; +export const DOCK_FILES_CURRENT_KEY = "kb-dashboard-dock-files-current"; /* FNXC:RightDockFiles 2026-06-22-00:00: diff --git a/packages/dashboard/app/components/ExecutorStatusBar.css b/packages/dashboard/app/components/ExecutorStatusBar.css index ab8788870c..a55cbd9f85 100644 --- a/packages/dashboard/app/components/ExecutorStatusBar.css +++ b/packages/dashboard/app/components/ExecutorStatusBar.css @@ -71,8 +71,14 @@ FN-6887 makes the footer status bar the canonical desktop/tablet terminal launch padding: 0 var(--space-xs); border: none; background: transparent; - color: var(--text-muted); - font: inherit; + /* + * FNXC:FooterChrome 2026-06-23-00:20: + * Quick Chat and Terminal are peer footer launchers. Pin both to the footer's compact UI font and color token so switching the launcher location does not make one control read heavier or dimmer than the other. + */ + color: inherit; + font-family: var(--font-primary); + font-size: inherit; + font-weight: 500; line-height: 1; white-space: nowrap; cursor: pointer; diff --git a/packages/dashboard/app/components/FileBrowser.css b/packages/dashboard/app/components/FileBrowser.css index a367756102..4ca535d7a3 100644 --- a/packages/dashboard/app/components/FileBrowser.css +++ b/packages/dashboard/app/components/FileBrowser.css @@ -32,8 +32,10 @@ align-items: center; justify-content: space-between; gap: var(--space-lg); + min-height: 48px; cursor: grab; user-select: none; + touch-action: none; } .file-browser-modal-header:active { @@ -585,9 +587,28 @@ Narrow Files windows use the same single-pane list/editor behavior as mobile eve } .file-browser-modal-header { + position: relative; flex-wrap: wrap; align-items: flex-start; gap: var(--space-sm); + min-height: 56px; + padding-block: calc(var(--space-md) + var(--space-xs)) var(--space-md); + } + + /* + FNXC:FileBrowser 2026-06-23-23:25: + On phones the full-screen Files modal still uses the header as its drag handle, but the title row can wrap and the action controls consume much of the top bar. Preserve a large touch-safe grab area with touch-action:none and add a subtle handle marker so dragging is discoverable without adding a second toolbar. + */ + .file-browser-modal-header::before { + content: ""; + position: absolute; + top: var(--space-xs); + left: 50%; + width: calc(var(--space-xl) + var(--space-sm)); + height: calc(var(--space-xs) * 0.75); + transform: translateX(-50%); + border-radius: var(--radius-pill); + background: color-mix(in srgb, var(--text-muted) 44%, transparent); } .file-browser-header-title { diff --git a/packages/dashboard/app/components/FileBrowserModal.tsx b/packages/dashboard/app/components/FileBrowserModal.tsx index eec2b8b8cc..211cd0a1ca 100644 --- a/packages/dashboard/app/components/FileBrowserModal.tsx +++ b/packages/dashboard/app/components/FileBrowserModal.tsx @@ -83,7 +83,7 @@ export function FileBrowserModal({ const [selectedFile, setSelectedFile] = useState(null); const modalRef = useRef(null); const [viewportMobile, setViewportMobile] = useState(false); - const [modalNarrow, setModalNarrow] = useState(false); + const [modalWidth, setModalWidth] = useState(null); const [mobileView, setMobileView] = useState<"list" | "editor">("list"); const [sidebarWidth, setSidebarWidth] = useState(SIDEBAR_DEFAULT_WIDTH); const [showLineNumbers, setShowLineNumbers] = useState(false); @@ -128,25 +128,33 @@ export function FileBrowserModal({ /* FNXC:FileBrowser 2026-06-22-17:25: The Files floating window can be resized narrower than the desktop two-pane layout while the browser viewport is still desktop-sized. Mirror Chat's ResizeObserver-driven responsive mode: once the modal itself is at mobile width, switch to the list/editor single-pane flow and hide the sidebar after a file opens. + + FNXC:FileBrowser 2026-06-23-23:45: + The Files modal layout should be responsive to its own floating-window width: wide modals show the two-pane browser/editor split, narrow modals show the mobile list/editor flow. Viewport width is only a pre-measurement fallback so a widened modal can always return to the split view. */ useLayoutEffect(() => { const element = modalRef.current; - if (!element || typeof ResizeObserver === "undefined") { + if (!element) { return; } const update = () => { const measuredWidth = element.getBoundingClientRect().width || element.clientWidth || window.innerWidth; - setModalNarrow(measuredWidth <= MOBILE_BREAKPOINT); + setModalWidth(measuredWidth); }; update(); + if (typeof ResizeObserver === "undefined") { + window.addEventListener("resize", update); + return () => window.removeEventListener("resize", update); + } + const observer = new ResizeObserver(update); observer.observe(element); return () => observer.disconnect(); }, []); - const isMobile = viewportMobile || modalNarrow; + const isMobile = modalWidth === null ? viewportMobile : modalWidth <= MOBILE_BREAKPOINT; useEffect(() => { if (!selectedFile) { diff --git a/packages/dashboard/app/components/FloatingWindow.css b/packages/dashboard/app/components/FloatingWindow.css index 92816ba5d7..a1e7566c1a 100644 --- a/packages/dashboard/app/components/FloatingWindow.css +++ b/packages/dashboard/app/components/FloatingWindow.css @@ -15,6 +15,7 @@ FNXC:FloatingWindow 2026-06-22-20:45: Floating panel positioned by state-driven inline `left/top/width/height` and stacked by inline `z-index`. min/max keep the panel usable and on-screen. `resize: none` because resizing is handled by the corner/edge handles. `pointer-events: auto` re-enables interaction on the panel only. */ .floating-window { + --floating-window-shadow: var(--shadow-lg); position: fixed; display: flex; flex-direction: column; @@ -26,7 +27,11 @@ Floating panel positioned by state-driven inline `left/top/width/height` and sta background: var(--surface); border: thin solid var(--border); border-radius: var(--radius-lg); - box-shadow: var(--shadow-xl); + /* + FNXC:FloatingWindow 2026-06-23-23:25: + Floating modals need a gentle, theme-controlled drop shadow. Use a local token with the app's existing shadow fallback instead of the undefined --shadow-xl so themes can soften, strengthen, or remove modal elevation intentionally. + */ + box-shadow: var(--floating-window-shadow, var(--shadow-lg)); color: var(--text); resize: none; pointer-events: auto; diff --git a/packages/dashboard/app/components/InsightsView.tsx b/packages/dashboard/app/components/InsightsView.tsx index 45030cff40..c287358ae1 100644 --- a/packages/dashboard/app/components/InsightsView.tsx +++ b/packages/dashboard/app/components/InsightsView.tsx @@ -487,6 +487,9 @@ export function InsightsView({ projectId, addToast, onClose, onCreateTask, model FNXC:Insights 2026-06-23-19:20: The refresh action must be icon-only in the Insights header. Keeping the visible label out of this button preserves room for the title and neighboring controls while aria-label/title retain the accessible command name. + + FNXC:Insights 2026-06-23-00:23: + Insights header filter chips need compact visible labels. Keep the descriptive accessibility copy, but show Backlog instead of Backlog Health and Archived instead of Show Archived/Hide Archived. */} - {backlogHealthOnly ? t("insights.allInsights", "All Insights") : t("insights.backlogHealth", "Backlog Health")} ({backlogHealthCount}) + {backlogHealthOnly ? t("insights.allInsights", "All Insights") : t("insights.backlogHealth", "Backlog")} ({backlogHealthCount}) )} {onClose && ( @@ -525,7 +528,7 @@ export function InsightsView({ projectId, addToast, onClose, onCreateTask, model data-testid="toggle-archived-insights" > - {showArchived ? t("insights.hideArchivedLabel", "Hide Archived") : t("insights.showArchivedLabel", "Show Archived ({{count}})", { count: archivedCount })} + {showArchived ? t("insights.hideArchivedLabel", "Archived") : t("insights.showArchivedLabel", "Archived ({{count}})", { count: archivedCount })} )} + + {onNewTask ? ( + + ) : null} + + ); + const renderBulkEditToolbars = () => ( <>
@@ -1904,29 +1926,8 @@ export function ListView({ {isMobile && ( <>
- {renderWorkflowSelector()} - - {onNewTask ? ( - - ) : null} -
- {selectedColumn - ? t("listView.statsInColumn", "{{count}} of {{total}} tasks in {{column}}", { count: filteredCount, total: tasks.length, column: getListColumnLabel(selectedColumn) }) - : t("listView.stats", "{{count}} of {{total}} tasks", { count: filteredCount, total: tasks.length })} -
+ {renderPrimaryActionCluster()}
{viewOptionsOpen ? (
{renderViewOptionsPanel("list-view-options-panel-mobile")}
@@ -1957,38 +1958,13 @@ export function ListView({ {!isMobile && (