Browse Source

feat(canvas): Improve file selector dropdown styling

- Scan drafts/ subdirectory for documents
- Minimal dropdown: current file, separator, file list
- Wider display (30 chars) with right padding
- Selection markers (▸ ◂) instead of inverse text
- Single text block rendering to avoid Ink layout bugs

🤖 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
24f4eb20d9

+ 31 - 34
canvas-tui/src/components/FileSelector.tsx

@@ -10,7 +10,7 @@ interface FileSelectorProps {
   isFocused: boolean;
 }
 
-const MAX_DISPLAY_LENGTH = 20;
+const MAX_DISPLAY_LENGTH = 30;
 
 export const FileSelector: React.FC<FileSelectorProps> = ({
   files,
@@ -23,39 +23,36 @@ export const FileSelector: React.FC<FileSelectorProps> = ({
   const currentFileName = currentFile.split(/[/\\]/).pop() || 'No file';
   const displayName = truncateFilename(currentFileName, MAX_DISPLAY_LENGTH);
 
-  return (
-    <Box flexDirection="column" alignItems="flex-end">
-      {/* Selector - just filename with arrow */}
-      <Text
-        inverse={isFocused}
-        color={isFocused ? undefined : 'gray'}
-      >
-        {displayName} {isOpen ? '▲' : '▼'}
+  if (!isOpen) {
+    // Closed state - just show current file with arrow
+    return (
+      <Text color="gray">
+        {displayName} ▼
       </Text>
+    );
+  }
 
-      {/* Dropdown - simple list */}
-      {isOpen && files.length > 0 && (
-        <Box flexDirection="column" marginTop={0}>
-          {files.slice(0, 6).map((file, index) => {
-            const isSelected = index === selectedIndex;
-            const isCurrent = file.name === currentFileName;
-            const name = truncateFilename(file.name, MAX_DISPLAY_LENGTH);
-
-            return (
-              <Text
-                key={file.path}
-                inverse={isSelected}
-                color={isCurrent && !isSelected ? 'cyan' : undefined}
-              >
-                {isSelected ? '› ' : '  '}{name}
-              </Text>
-            );
-          })}
-          {files.length > 6 && (
-            <Text dimColor>  +{files.length - 6} more</Text>
-          )}
-        </Box>
-      )}
-    </Box>
-  );
+  // 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}`);
+
+  // Other files
+  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>;
 };

+ 4 - 6
canvas-tui/src/components/Header.tsx

@@ -25,16 +25,13 @@ export const Header: React.FC<HeaderProps> = ({
 }) => {
   const leftContent = ` ${title} `;
 
-  // Reserve space for file selector (~30 chars)
-  const selectorWidth = 30;
-  const padding = Math.max(0, width - leftContent.length - selectorWidth - 2);
-
   return (
     <Box flexDirection="column">
+      {/* Header bar */}
       <Box borderStyle="single" borderBottom={true} borderTop={false} borderLeft={false} borderRight={false}>
-        <Box width={width} justifyContent="space-between">
+        <Box width={width}>
           <Text bold color="blue">{leftContent}</Text>
-          <Text>{' '.repeat(padding)}</Text>
+          <Box flexGrow={1} />
           <FileSelector
             files={files}
             currentFile={currentFile}
@@ -42,6 +39,7 @@ export const Header: React.FC<HeaderProps> = ({
             isOpen={isDropdownOpen}
             isFocused={isDropdownFocused}
           />
+          <Text>    </Text>
         </Box>
       </Box>
     </Box>

+ 10 - 3
canvas-tui/src/hooks/useDirectoryFiles.ts

@@ -18,9 +18,16 @@ export function useDirectoryFiles(dirPath: string): FileInfo[] {
   useEffect(() => {
     const scanDirectory = () => {
       try {
-        // Resolve to absolute path
+        // Resolve to absolute path, scan 'drafts' subdirectory
         const absoluteDir = path.resolve(dirPath);
-        const entries = fs.readdirSync(absoluteDir, { withFileTypes: true });
+        const draftsDir = path.join(absoluteDir, 'drafts');
+
+        // Create drafts dir if it doesn't exist
+        if (!fs.existsSync(draftsDir)) {
+          fs.mkdirSync(draftsDir, { recursive: true });
+        }
+
+        const entries = fs.readdirSync(draftsDir, { withFileTypes: true });
         const fileInfos: FileInfo[] = [];
 
         for (const entry of entries) {
@@ -29,7 +36,7 @@ export function useDirectoryFiles(dirPath: string): FileInfo[] {
           const ext = path.extname(entry.name).toLowerCase();
           if (ext !== '.md' && ext !== '.txt') continue;
 
-          const filePath = path.join(absoluteDir, entry.name);
+          const filePath = path.join(draftsDir, entry.name);
           const stats = fs.statSync(filePath);
 
           fileInfos.push({