attn 0.1.14

A beautiful markdown viewer that launches from the CLI
<script lang="ts">
  import type { TreeNode, FileType } from './types';
  import * as Command from '$lib/components/ui/command/index.js';
  import { Kbd } from '$lib/components/ui/kbd';
  import FileIcon from '@lucide/svelte/icons/file';
  import FileTextIcon from '@lucide/svelte/icons/file-text';
  import ImageIcon from '@lucide/svelte/icons/image';
  import VideoIcon from '@lucide/svelte/icons/video';
  import MusicIcon from '@lucide/svelte/icons/music';

  interface Props {
    open: boolean;
    fileTree: TreeNode[];
    onSelect: (path: string) => void;
  }

  let { open = $bindable(false), fileTree, onSelect }: Props = $props();

  interface FlatFile {
    name: string;
    path: string;
    dir: string;
    fileType: FileType;
  }

  function flattenTree(nodes: TreeNode[], parentDir = ''): FlatFile[] {
    const result: FlatFile[] = [];
    for (const node of nodes) {
      if (node.isDir && node.children) {
        const nextParent = parentDir ? `${parentDir}/${node.name}` : node.name;
        result.push(...flattenTree(node.children, nextParent));
      } else if (!node.isDir) {
        result.push({
          name: node.name,
          path: node.path,
          dir: parentDir,
          fileType: node.fileType,
        });
      }
    }
    return result;
  }

  let files = $derived(flattenTree(fileTree));
  const isMac = navigator.platform.includes('Mac');
  const mod = isMac ? '\u2318' : 'Ctrl';

  // Group files by directory and sort for stable visual scan order.
  let grouped = $derived.by(() => {
    const groups: Record<string, FlatFile[]> = {};
    for (const file of files) {
      const key = file.dir || '/';
      if (!groups[key]) groups[key] = [];
      groups[key].push(file);
    }
    return Object
      .entries(groups)
      .map(([dir, entries]) => [
        dir,
        [...entries].sort((a, b) => a.name.localeCompare(b.name)),
      ] as const)
      .sort(([a], [b]) => a.localeCompare(b));
  });

  function handleSelect(path: string): void {
    open = false;
    onSelect(path);
  }

  function iconForType(fileType: FileType): typeof FileIcon {
    switch (fileType) {
      case 'markdown': return FileTextIcon;
      case 'image': return ImageIcon;
      case 'video': return VideoIcon;
      case 'audio': return MusicIcon;
      default: return FileIcon;
    }
  }

  function dirLabel(dir: string): string {
    return dir === '/' ? 'root' : dir;
  }
</script>

<Command.Dialog bind:open title="Open File" description="Search files to open">
  <Command.Input
    class="font-sans text-[0.97rem] tracking-[0.01em] placeholder:tracking-normal"
    placeholder="Search files..."
  />
  <Command.List class="max-h-[min(62vh,34rem)] px-2 pb-2 pt-1">
    <Command.Empty class="font-sans py-10 text-muted-foreground">
      No matching files in this workspace.
    </Command.Empty>
    {#each grouped as [dir, groupFiles] (dir)}
      <Command.Group
        heading={dirLabel(dir)}
        class="font-sans [&_[data-command-group-heading]]:text-muted-foreground/90 [&_[data-command-group-heading]]:px-2 [&_[data-command-group-heading]]:py-1 [&_[data-command-group-heading]]:font-semibold [&_[data-command-group-heading]]:tracking-[0.08em] [&_[data-command-group-heading]]:uppercase"
      >
        {#each groupFiles as file (file.path)}
          {@const Icon = iconForType(file.fileType)}
          <Command.Item
            value={file.path}
            class="group/item font-sans rounded-md border border-transparent px-2 py-2.5 aria-selected:border-border aria-selected:bg-accent/70"
            onSelect={() => handleSelect(file.path)}
          >
            <span class="bg-muted/70 text-muted-foreground inline-flex size-6 shrink-0 items-center justify-center rounded-sm border border-border/60 transition-colors aria-selected:bg-background">
              <Icon class="size-3.5 shrink-0 opacity-90" />
            </span>
            <span class="min-w-0 flex-1 truncate text-[0.96rem] font-medium leading-none tracking-[0.01em]">
              {file.name}
            </span>
            {#if file.dir}
              <span class="text-muted-foreground/90 max-w-[45%] truncate rounded-sm border border-border/70 bg-background/65 px-1.5 py-0.5 font-mono text-[0.7rem] leading-none">
                {file.dir}
              </span>
            {/if}
          </Command.Item>
        {/each}
      </Command.Group>
    {/each}
  </Command.List>
  <div class="border-border/70 text-muted-foreground bg-muted/25 flex items-center justify-between border-t px-3 py-2 font-sans text-xs">
    <span>Open files quickly</span>
    <span class="inline-flex items-center gap-1.5">
      <Kbd>{mod}</Kbd>
      <Kbd>P</Kbd>
    </span>
  </div>
</Command.Dialog>