Browse Source

fix(canvas-tui): Polish file selector dropdown UI

- Remove duplicate current file from dropdown list
- Use subtle · · · separator instead of heavy line
- Align dropdown text with header filename
- Add vertical padding to content area
- Restructure Header so dropdown renders below border
- Delete unused FileSelector component (merged into Header)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
0xDarkMatter 3 months ago
parent
commit
b055110e79

+ 8 - 6
canvas-tui/src/app.tsx

@@ -103,6 +103,9 @@ export const App: React.FC<AppProps> = ({ watchPath, watchDir, enableMouse = tru
       exit();
       exit();
     }
     }
 
 
+    // Other files (excluding current) for dropdown navigation
+    const otherFiles = files.filter(f => f.path !== currentFilePath);
+
     // Tab - toggle file selector focus
     // Tab - toggle file selector focus
     if (key.tab) {
     if (key.tab) {
       if (isDropdownFocused) {
       if (isDropdownFocused) {
@@ -113,9 +116,8 @@ export const App: React.FC<AppProps> = ({ watchPath, watchDir, enableMouse = tru
         // Focus and open dropdown
         // Focus and open dropdown
         setIsDropdownFocused(true);
         setIsDropdownFocused(true);
         setIsDropdownOpen(true);
         setIsDropdownOpen(true);
-        // Set selected index to current file
-        const currentIndex = files.findIndex(f => f.path === currentFilePath);
-        setSelectedFileIndex(currentIndex >= 0 ? currentIndex : 0);
+        // Start at first file
+        setSelectedFileIndex(0);
       }
       }
       return;
       return;
     }
     }
@@ -134,12 +136,12 @@ export const App: React.FC<AppProps> = ({ watchPath, watchDir, enableMouse = tru
         return;
         return;
       }
       }
       if (key.downArrow) {
       if (key.downArrow) {
-        setSelectedFileIndex(prev => Math.min(files.length - 1, prev + 1));
+        setSelectedFileIndex(prev => Math.min(otherFiles.length - 1, prev + 1));
         return;
         return;
       }
       }
-      if (key.return && files[selectedFileIndex]) {
+      if (key.return && otherFiles[selectedFileIndex]) {
         // Select file and close dropdown
         // Select file and close dropdown
-        setCurrentFilePath(files[selectedFileIndex].path);
+        setCurrentFilePath(otherFiles[selectedFileIndex].path);
         setIsDropdownOpen(false);
         setIsDropdownOpen(false);
         setIsDropdownFocused(false);
         setIsDropdownFocused(false);
         setScrollOffset(0);
         setScrollOffset(0);

+ 0 - 62
canvas-tui/src/components/FileSelector.tsx

@@ -1,62 +0,0 @@
-import React from 'react';
-import { Box, Text } from 'ink';
-import { FileInfo, truncateFilename } from '../hooks/useDirectoryFiles.js';
-
-interface FileSelectorProps {
-  files: FileInfo[];
-  currentFile: string;
-  selectedIndex: number;
-  isOpen: boolean;
-  isFocused: boolean;
-}
-
-const MAX_DISPLAY_LENGTH = 30;
-
-export const FileSelector: React.FC<FileSelectorProps> = ({
-  files,
-  currentFile,
-  selectedIndex,
-  isOpen,
-  isFocused,
-}) => {
-  // Get current filename from path
-  const currentFileName = currentFile.split(/[/\\]/).pop() || 'No file';
-  const displayName = truncateFilename(currentFileName, MAX_DISPLAY_LENGTH);
-
-  if (!isOpen) {
-    // Closed state - just show current file with arrow
-    return (
-      <Text color="gray">
-        {displayName} ▼
-      </Text>
-    );
-  }
-
-  // Open state - render as single text block to avoid Ink layout issues
-  const lines: string[] = [];
-  const pad = '   '; // Right padding
-
-  // Current file
-  lines.push(`${displayName} ▲${pad}`);
-
-  // Separator
-  lines.push(`─────────────────────${pad}`);
-
-  // Show files or "no files" message
-  if (files.length === 0) {
-    lines.push(`  (no files in drafts/)${pad}`);
-  } else {
-    files.slice(0, 6).forEach((file, index) => {
-      const isSelected = index === selectedIndex;
-      const name = truncateFilename(file.name, MAX_DISPLAY_LENGTH);
-      const marker = isSelected ? '▸' : ' ';
-      lines.push(`${marker} ${name}${isSelected ? ' ◂' : '  '}${pad}`);
-    });
-
-    if (files.length > 6) {
-      lines.push(`  +${files.length - 6} more${pad}`);
-    }
-  }
-
-  return <Text>{lines.join('\n')}</Text>;
-};

+ 55 - 10
canvas-tui/src/components/Header.tsx

@@ -1,7 +1,6 @@
 import React from 'react';
 import React from 'react';
 import { Box, Text } from 'ink';
 import { Box, Text } from 'ink';
-import { FileSelector } from './FileSelector.js';
-import { FileInfo } from '../hooks/useDirectoryFiles.js';
+import { FileInfo, truncateFilename } from '../hooks/useDirectoryFiles.js';
 
 
 interface HeaderProps {
 interface HeaderProps {
   title: string;
   title: string;
@@ -14,6 +13,8 @@ interface HeaderProps {
   isDropdownFocused: boolean;
   isDropdownFocused: boolean;
 }
 }
 
 
+const MAX_DISPLAY_LENGTH = 30;
+
 export const Header: React.FC<HeaderProps> = ({
 export const Header: React.FC<HeaderProps> = ({
   title,
   title,
   width,
   width,
@@ -25,23 +26,67 @@ export const Header: React.FC<HeaderProps> = ({
 }) => {
 }) => {
   const leftContent = ` ${title} `;
   const leftContent = ` ${title} `;
 
 
+  // Get current filename from path
+  const currentFileName = currentFile.split(/[/\\]/).pop() || 'No file';
+  const displayName = truncateFilename(currentFileName, MAX_DISPLAY_LENGTH);
+
+  // Filter out current file for dropdown
+  const otherFiles = files.filter(f => f.path !== currentFile);
+
   return (
   return (
     <Box flexDirection="column">
     <Box flexDirection="column">
-      {/* Header bar */}
+      {/* Header bar - always single line with border */}
       <Box borderStyle="single" borderBottom={true} borderTop={false} borderLeft={false} borderRight={false}>
       <Box borderStyle="single" borderBottom={true} borderTop={false} borderLeft={false} borderRight={false}>
         <Box width={width}>
         <Box width={width}>
           <Text bold color="blue">{leftContent}</Text>
           <Text bold color="blue">{leftContent}</Text>
           <Box flexGrow={1} />
           <Box flexGrow={1} />
-          <FileSelector
-            files={files}
-            currentFile={currentFile}
-            selectedIndex={selectedFileIndex}
-            isOpen={isDropdownOpen}
-            isFocused={isDropdownFocused}
-          />
+          <Text color="gray">{displayName} {isDropdownOpen ? '▲' : '▼'}</Text>
           <Text>    </Text>
           <Text>    </Text>
         </Box>
         </Box>
       </Box>
       </Box>
+
+      {/* Dropdown renders BELOW the header border */}
+      {isDropdownOpen && (
+        <Box justifyContent="flex-end" paddingRight={4}>
+          <DropdownList
+            files={otherFiles}
+            selectedIndex={selectedFileIndex}
+          />
+        </Box>
+      )}
     </Box>
     </Box>
   );
   );
 };
 };
+
+// Dropdown list component (only the list items, no header)
+interface DropdownListProps {
+  files: FileInfo[];
+  selectedIndex: number;
+}
+
+const DropdownList: React.FC<DropdownListProps> = ({ files, selectedIndex }) => {
+  const pad = '   ';
+
+  if (files.length === 0) {
+    return <Text color="gray">  (no other files){pad}</Text>;
+  }
+
+  const lines: string[] = [];
+
+  // Subtle separator
+  lines.push(`  · · ·${pad}`);
+
+  // File list
+  files.slice(0, 6).forEach((file, index) => {
+    const isSelected = index === selectedIndex;
+    const name = truncateFilename(file.name, MAX_DISPLAY_LENGTH);
+    const marker = isSelected ? '▸' : ' ';
+    lines.push(`${marker} ${name}${isSelected ? ' ◂' : '  '}${pad}`);
+  });
+
+  if (files.length > 6) {
+    lines.push(`  +${files.length - 6} more${pad}`);
+  }
+
+  return <Text>{lines.join('\n')}</Text>;
+};

+ 1 - 1
canvas-tui/src/components/MarkdownView.tsx

@@ -37,7 +37,7 @@ export const MarkdownView: React.FC<MarkdownViewProps> = ({
   const showScrollDown = clampedOffset + maxHeight < totalLines;
   const showScrollDown = clampedOffset + maxHeight < totalLines;
 
 
   return (
   return (
-    <Box flexDirection="column" paddingX={1}>
+    <Box flexDirection="column" paddingX={1} paddingY={1}>
       {showScrollUp && (
       {showScrollUp && (
         <Text color="gray">--- more above ({clampedOffset} lines) ---</Text>
         <Text color="gray">--- more above ({clampedOffset} lines) ---</Text>
       )}
       )}