From 0aaf3d7a14d09e70dc0a50f55b4820febe8a9838 Mon Sep 17 00:00:00 2001 From: Ronald Roy Date: Sun, 18 Jan 2026 08:05:55 -0800 Subject: [PATCH 1/7] feat: combine breadcrumb and Adjust button on same row Reduces vertical space usage by placing the breadcrumb navigation and Adjust button in a shared toolbar row instead of separate rows. Co-Authored-By: Claude Opus 4.5 --- frontend/src/components/MarkdownViewer.css | 46 +++++++++++++++------- frontend/src/components/MarkdownViewer.tsx | 7 ++-- 2 files changed, 34 insertions(+), 19 deletions(-) diff --git a/frontend/src/components/MarkdownViewer.css b/frontend/src/components/MarkdownViewer.css index 235aaab9..dcb2a211 100644 --- a/frontend/src/components/MarkdownViewer.css +++ b/frontend/src/components/MarkdownViewer.css @@ -22,22 +22,48 @@ overflow-y: auto; } +/* Toolbar with breadcrumb and action buttons */ +.markdown-viewer__toolbar { + display: flex; + align-items: center; + gap: var(--spacing-sm); + padding: var(--spacing-xs) var(--spacing-md); + background: var(--glass-bg-quartary); + backdrop-filter: blur(var(--glass-blur)); + border-bottom: 1px solid var(--glass-border); + min-height: 44px; +} + +@supports not (backdrop-filter: blur(10px)) { + .markdown-viewer__toolbar { + background-color: var(--color-surface); + } +} + /* Breadcrumb navigation */ .markdown-viewer__breadcrumb { display: flex; align-items: center; flex-wrap: wrap; + flex: 1; + min-width: 0; gap: var(--spacing-xs); + font-size: var(--text-sm); +} + +/* Standalone breadcrumb (loading/error states) */ +.markdown-viewer--loading .markdown-viewer__breadcrumb, +.markdown-viewer--error .markdown-viewer__breadcrumb { padding: var(--spacing-sm) var(--spacing-md); background: var(--glass-bg-quartary); backdrop-filter: blur(var(--glass-blur)); border-bottom: 1px solid var(--glass-border); - font-size: var(--text-sm); min-height: 44px; } @supports not (backdrop-filter: blur(10px)) { - .markdown-viewer__breadcrumb { + .markdown-viewer--loading .markdown-viewer__breadcrumb, + .markdown-viewer--error .markdown-viewer__breadcrumb { background-color: var(--color-surface); } } @@ -363,7 +389,8 @@ backdrop-filter: blur(4px); } - .markdown-viewer__breadcrumb { + .markdown-viewer--loading .markdown-viewer__breadcrumb, + .markdown-viewer--error .markdown-viewer__breadcrumb { padding: var(--spacing-xs) var(--spacing-sm); backdrop-filter: blur(4px); } @@ -380,17 +407,6 @@ overflow: hidden; } -/* View header with Adjust button (REQ-F-1) */ -.markdown-viewer__view-header { - display: flex; - align-items: center; - justify-content: flex-end; - padding: var(--spacing-xs) var(--spacing-md); - background: var(--glass-bg-tertiary); - border-bottom: 1px solid var(--glass-border); - min-height: 44px; -} - /* Adjust header with Save/Cancel buttons (REQ-F-3) */ .markdown-viewer__adjust-header { display: flex; @@ -522,7 +538,7 @@ /* Mobile adjustments for adjust mode */ @media (max-width: 767px) { - .markdown-viewer__view-header, + .markdown-viewer__toolbar, .markdown-viewer__adjust-header { padding: var(--spacing-xs) var(--spacing-sm); } diff --git a/frontend/src/components/MarkdownViewer.tsx b/frontend/src/components/MarkdownViewer.tsx index 681eeb4e..cb6de1c8 100644 --- a/frontend/src/components/MarkdownViewer.tsx +++ b/frontend/src/components/MarkdownViewer.tsx @@ -534,10 +534,9 @@ export function MarkdownViewer({ // Normal view mode with Adjust button (REQ-F-1) return (
- - - {/* Header with Adjust button (REQ-F-1) */} -
+ {/* Toolbar with breadcrumb and Adjust button on same row (REQ-F-1) */} +
+ - - {browser.currentPath || "No file selected"} - -
+ {/* Mobile header - only shown when no file is selected */} + {!browser.currentPath && ( +
+ + + No file selected + +
+ )}
{isImageFile(browser.currentPath) ? ( - + ) : isVideoFile(browser.currentPath) ? ( - + ) : isPdfFile(browser.currentPath) ? ( - + ) : isJsonFile(browser.currentPath) ? ( - + ) : isTxtFile(browser.currentPath) ? ( - + ) : isCsvFile(browser.currentPath) ? ( - + ) : isMarkdownFile(browser.currentPath) || !browser.currentPath ? ( - + ) : ( - + )}
{/* Recall Widgets - collapsible panel shown when viewing files that match widget source patterns */} diff --git a/frontend/src/components/CsvViewer.css b/frontend/src/components/CsvViewer.css index a5197d18..7f1d49ec 100644 --- a/frontend/src/components/CsvViewer.css +++ b/frontend/src/components/CsvViewer.css @@ -23,22 +23,50 @@ overflow-y: auto; } +/* Toolbar with breadcrumb */ +.csv-viewer__toolbar { + display: flex; + align-items: center; + gap: var(--spacing-sm); + padding: var(--spacing-xs) var(--spacing-md); + background: var(--glass-bg-quartary); + backdrop-filter: blur(var(--glass-blur)); + border-bottom: 1px solid var(--glass-border); + min-height: 44px; +} + +@supports not (backdrop-filter: blur(10px)) { + .csv-viewer__toolbar { + background-color: var(--color-surface); + } +} + /* Breadcrumb navigation */ .csv-viewer__breadcrumb { display: flex; align-items: center; flex-wrap: wrap; + flex: 1; + min-width: 0; gap: var(--spacing-xs); + font-size: var(--text-sm); +} + +/* Standalone breadcrumb (loading/error/parse-error states) */ +.csv-viewer--loading .csv-viewer__breadcrumb, +.csv-viewer--error .csv-viewer__breadcrumb, +.csv-viewer--parse-error .csv-viewer__breadcrumb { padding: var(--spacing-sm) var(--spacing-md); background: var(--glass-bg-quartary); backdrop-filter: blur(var(--glass-blur)); border-bottom: 1px solid var(--glass-border); - font-size: var(--text-sm); min-height: 44px; } @supports not (backdrop-filter: blur(10px)) { - .csv-viewer__breadcrumb { + .csv-viewer--loading .csv-viewer__breadcrumb, + .csv-viewer--error .csv-viewer__breadcrumb, + .csv-viewer--parse-error .csv-viewer__breadcrumb { background-color: var(--color-surface); } } diff --git a/frontend/src/components/CsvViewer.tsx b/frontend/src/components/CsvViewer.tsx index 0b419d57..1ba1fdd3 100644 --- a/frontend/src/components/CsvViewer.tsx +++ b/frontend/src/components/CsvViewer.tsx @@ -16,6 +16,8 @@ import "./CsvViewer.css"; export interface CsvViewerProps { /** Callback when a path is navigated (breadcrumb) */ onNavigate?: (path: string) => void; + /** Callback to open mobile file browser (only shown on mobile) */ + onMobileMenuClick?: () => void; } /** @@ -239,7 +241,7 @@ function parseCsv(content: string, delimiter: string): ParseResult { * - Warning banners for data issues * - Fallback to raw content for malformed files */ -export function CsvViewer({ onNavigate }: CsvViewerProps): ReactNode { +export function CsvViewer({ onNavigate, onMobileMenuClick }: CsvViewerProps): ReactNode { const { browser, setCurrentPath } = useSession(); const { currentPath, @@ -330,7 +332,31 @@ export function CsvViewer({ onNavigate }: CsvViewerProps): ReactNode { // Normal view mode - render table return (
- +
+ {onMobileMenuClick && ( + + )} + +
{currentFileTruncated && (
diff --git a/frontend/src/components/DownloadViewer.css b/frontend/src/components/DownloadViewer.css index 9b88cd75..70293a40 100644 --- a/frontend/src/components/DownloadViewer.css +++ b/frontend/src/components/DownloadViewer.css @@ -6,19 +6,47 @@ .download-viewer { display: flex; - align-items: center; - justify-content: center; + flex-direction: column; height: 100%; - padding: var(--spacing-lg); +} + +.download-viewer__header { + display: flex; + align-items: center; + gap: var(--spacing-sm); + padding: var(--spacing-xs) var(--spacing-md); + background: var(--glass-bg-quartary); + backdrop-filter: blur(var(--glass-blur)); + border-bottom: 1px solid var(--glass-border); + min-height: 44px; +} + +@supports not (backdrop-filter: blur(10px)) { + .download-viewer__header { + background-color: var(--color-surface); + } +} + +.download-viewer__path { + flex: 1; + font-size: var(--text-sm); + color: var(--color-text); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } .download-viewer__content { display: flex; flex-direction: column; + flex: 1; align-items: center; + justify-content: center; text-align: center; gap: var(--spacing-md); max-width: 300px; + margin: 0 auto; + padding: var(--spacing-lg); } .download-viewer__icon { @@ -30,14 +58,6 @@ height: 64px; } -.download-viewer__filename { - font-size: var(--font-size-lg); - font-weight: 500; - color: var(--text-primary); - word-break: break-word; - margin: 0; -} - .download-viewer__message { font-size: var(--font-size-sm); color: var(--text-muted); diff --git a/frontend/src/components/DownloadViewer.tsx b/frontend/src/components/DownloadViewer.tsx index 67225e1e..59ee7261 100644 --- a/frontend/src/components/DownloadViewer.tsx +++ b/frontend/src/components/DownloadViewer.tsx @@ -14,6 +14,8 @@ export interface DownloadViewerProps { path: string; /** Base URL for vault assets (e.g., /vault/{vaultId}/assets) */ assetBaseUrl: string; + /** Callback to open mobile file browser (only shown on mobile) */ + onMobileMenuClick?: () => void; } /** @@ -22,18 +24,42 @@ export interface DownloadViewerProps { * Uses the existing asset serving endpoint to provide the download, * with the download attribute to trigger browser download behavior. */ -export function DownloadViewer({ path, assetBaseUrl }: DownloadViewerProps): ReactNode { +export function DownloadViewer({ path, assetBaseUrl, onMobileMenuClick }: DownloadViewerProps): ReactNode { const downloadUrl = `${assetBaseUrl}/${encodeAssetPath(path)}`; const fileName = path.split("/").pop() ?? path; const extension = fileName.includes(".") ? fileName.split(".").pop()?.toUpperCase() : "FILE"; return (
+
+ {onMobileMenuClick && ( + + )} + {fileName} +
-

{fileName}

No preview available for {extension} files

diff --git a/frontend/src/components/ImageViewer.tsx b/frontend/src/components/ImageViewer.tsx index b79bcd97..16f0ac52 100644 --- a/frontend/src/components/ImageViewer.tsx +++ b/frontend/src/components/ImageViewer.tsx @@ -14,6 +14,8 @@ export interface ImageViewerProps { path: string; /** Base URL for vault assets (e.g., /vault/{vaultId}/assets) */ assetBaseUrl: string; + /** Callback to open mobile file browser (only shown on mobile) */ + onMobileMenuClick?: () => void; } /** @@ -22,7 +24,7 @@ export interface ImageViewerProps { * Uses the existing asset serving endpoint to fetch the image, * leveraging the same infrastructure used for embedded markdown images. */ -export function ImageViewer({ path, assetBaseUrl }: ImageViewerProps): ReactNode { +export function ImageViewer({ path, assetBaseUrl, onMobileMenuClick }: ImageViewerProps): ReactNode { const [isLoading, setIsLoading] = useState(true); const [hasError, setHasError] = useState(false); @@ -48,6 +50,28 @@ export function ImageViewer({ path, assetBaseUrl }: ImageViewerProps): ReactNode return (
+ {onMobileMenuClick && ( + + )} {fileName}
diff --git a/frontend/src/components/JsonViewer.css b/frontend/src/components/JsonViewer.css index f73acf77..661d8765 100644 --- a/frontend/src/components/JsonViewer.css +++ b/frontend/src/components/JsonViewer.css @@ -27,17 +27,25 @@ display: flex; align-items: center; flex-wrap: wrap; + flex: 1; + min-width: 0; gap: var(--spacing-xs); + font-size: var(--text-sm); +} + +/* Standalone breadcrumb (loading/error states) */ +.json-viewer--loading .json-viewer__breadcrumb, +.json-viewer--error .json-viewer__breadcrumb { padding: var(--spacing-sm) var(--spacing-md); background: var(--glass-bg-quartary); backdrop-filter: blur(var(--glass-blur)); border-bottom: 1px solid var(--glass-border); - font-size: var(--text-sm); min-height: 44px; } @supports not (backdrop-filter: blur(10px)) { - .json-viewer__breadcrumb { + .json-viewer--loading .json-viewer__breadcrumb, + .json-viewer--error .json-viewer__breadcrumb { background-color: var(--color-surface); } } @@ -230,17 +238,24 @@ overflow: hidden; } -/* View header with Adjust button */ -.json-viewer__view-header { +/* Toolbar with breadcrumb and Adjust button */ +.json-viewer__toolbar { display: flex; align-items: center; - justify-content: flex-end; + gap: var(--spacing-sm); padding: var(--spacing-xs) var(--spacing-md); - background: var(--glass-bg-tertiary); + background: var(--glass-bg-quartary); + backdrop-filter: blur(var(--glass-blur)); border-bottom: 1px solid var(--glass-border); min-height: 44px; } +@supports not (backdrop-filter: blur(10px)) { + .json-viewer__toolbar { + background-color: var(--color-surface); + } +} + /* Adjust header with Save/Cancel buttons */ .json-viewer__adjust-header { display: flex; @@ -372,11 +387,17 @@ /* Mobile adjustments for adjust mode */ @media (max-width: 767px) { - .json-viewer__view-header, + .json-viewer__toolbar, .json-viewer__adjust-header { padding: var(--spacing-xs) var(--spacing-sm); } + .json-viewer--loading .json-viewer__breadcrumb, + .json-viewer--error .json-viewer__breadcrumb { + padding: var(--spacing-xs) var(--spacing-sm); + backdrop-filter: blur(4px); + } + .json-viewer__adjust-content { padding: var(--spacing-sm); } diff --git a/frontend/src/components/JsonViewer.tsx b/frontend/src/components/JsonViewer.tsx index 3f38a3f5..1c3d375b 100644 --- a/frontend/src/components/JsonViewer.tsx +++ b/frontend/src/components/JsonViewer.tsx @@ -23,6 +23,8 @@ export interface JsonViewerProps { onNavigate?: (path: string) => void; /** Callback to save file content in adjust mode */ onSave?: (content: string) => void; + /** Callback to open mobile file browser (only shown on mobile) */ + onMobileMenuClick?: () => void; } /** @@ -114,6 +116,7 @@ function formatJson(content: string): string | null { export function JsonViewer({ onNavigate, onSave, + onMobileMenuClick, }: JsonViewerProps): ReactNode { const { browser, @@ -285,9 +288,30 @@ export function JsonViewer({ // Normal view mode return (
- - -
+
+ {onMobileMenuClick && ( + + )} + + )}