Cloud|

@k2b/ui

Files

Generic file trees, browser composition, and content-aware previews.

FileBrowserPanel@k2b/uiFileTree@k2b/uiFileView@k2b/ui

A fully interactive, path-first browser over an application-owned in-memory source. Create, upload, rename, move, delete, edit, save, and download without a backend.

Files

Loading files...

Select a file

Pick a file from the tree to view it.

TSX

Copy
const source: FileSource = {
list: async () => entries,
read: async (path) => content[path],
write: async (path, value) => update(path, value),
rename: async (from, to) => move(from, to),
remove: async (path) => remove(path),
upload: async (dir, files) => upload(dir, files),
};
<FileBrowserPanel source={source} initialPath="/README.md" />

FileBrowserPanel, FileTree, and FileView present application-owned file data. They do not depend on Cloud storage, routes, permissions, or mutation APIs.

Use file components

Use FileBrowserPanel for the ready tree-and-preview composition over a FileSource. Use FileTree for a different layout or selection flow. Use FileView when a page already owns the selected file.

Import

tsx
import {
  FileBrowserPanel,
  FileTree,
  FileView,
  canPreviewFile,
  formatFileViewSize,
  getFileViewPreviewKind,
  openFileBrowser,
  type FileBrowserPanelProps,
  type FileSource,
  type FileTreeActions,
  type FileTreeEntry,
  type FileTreeProps,
  type FileViewContent,
  type FileViewFile,
  type FileViewPreviewKind,
  type FileViewProps,
  type FileViewRenderer,
  type FileViewRendererProps,
} from "@k2b/ui";

FileSource

FileSource is the asynchronous boundary:

ts
type FileSource = {
  list(): Promise<FileTreeEntry[]>;
  read(path: string): Promise<FileViewContent>;
  write?(path: string, content: string, encoding?: "utf8" | "base64"):
    Promise<void>;
  remove?(path: string): Promise<void>;
  rename?(from: string, to: string): Promise<void>;
  upload?(dirPath: string, files: File[]): Promise<void>;
  downloadHref?(path: string): string | null;
  isReadOnly?(path: string): boolean;
};

Only supplied capabilities receive matching controls. readOnly on FileBrowserPanel hides every mutation even when the source implements it. isReadOnly can protect individual paths such as generated inputs. Capabilities are independent: for example, a source may offer rename or remove without offering write.

The host authenticates every operation and checks authorization again inside the source. Hiding a control is not an authorization boundary.

Path-first tree

Each FileTreeEntry has an absolute-style path such as /src/app.tsx. Folders are derived from file paths; explicit { path, kind: "folder" } entries represent empty folders.

selectedPath and onSelect own selection. expandedPaths and onExpandedChange provide controlled expansion; otherwise folders start expanded. FileTreeActions enables rename, remove, create, move, and download affordances individually.

contextMenu adds application-specific menu items without replacing the built-in actions.

File previews

FileView receives a FileViewFile, an asynchronous load function, and optional preview and download URLs. Text, Markdown, JSON, delimited text, images, PDF, audio, and video use built-in renderers. Supplying save enables editing for compatible text renderers.

Pass revision to refetch a FileView whose path did not change. A FileBrowserPanel forwards its refreshKey to both the file list and the selected preview.

Markdown files edit through MarkdownEditor. Other UTF-8 source and text files use a plain monospace textarea inside the same editor chrome, so Markdown formatting controls and completions are not offered for code.

FileViewContent is either UTF-8 or base64:

ts
type FileViewContent = {
  encoding: "utf8" | "base64";
  content: string;
  mediaType: string;
};

getFileViewPreviewKind returns the inferred FileViewPreviewKind. canPreviewFile also checks the built-in size limits. formatFileViewSize produces the compact size label used by the preview.

Pass renderers to FileView or FileBrowserPanel to add application-specific renderers before the built-ins. Extensions are scoped to that component instance, so concurrent SSR requests and independently mounted applications cannot mutate each other's renderer set.

FileView reports local edits through onDirtyChange. FileBrowserPanel uses that signal to guard selection changes; pass confirmDiscard to own the confirmation copy and policy. Without it, the shared confirmation prompt is used.

Dialog helper

openFileBrowser({ source, title, subtitle, icon }) opens the shared panel in a dialog and resolves when it closes. The supplied source has the same capability and authorization responsibilities as an inline panel.

Accessibility

FileTree exposes tree and tree-item roles with arrow-key navigation. Selection, expansion, context actions, preview actions, and text editing remain keyboard reachable.

Runtime

The static tree and preview shells render on the server. Resource loading, selection, editing, drag and drop, media controls, prompts, and dialogs require hydration.

Example

tsx
const entries = (): FileTreeEntry[] =>
  Object.entries(contentByPath).map(([path, content]) => ({
    path,
    mediaType: path.endsWith(".md") ? "text/markdown" : "text/plain",
    size: new TextEncoder().encode(content).byteLength,
  }));

const source: FileSource = {
  list: async () => entries(),
  read: async (path) => ({
    encoding: "utf8",
    content: contentByPath[path] ?? "",
    mediaType: path.endsWith(".md") ? "text/markdown" : "text/plain",
  }),
  write: async (path, content) => {
    contentByPath[path] = content;
  },
  rename: async (from, to) => {
    contentByPath[to] = contentByPath[from] ?? "";
    delete contentByPath[from];
  },
  remove: async (path) => {
    delete contentByPath[path];
  },
};

<FileBrowserPanel
  source={source}
  initialPath="/README.md"
  renderers={applicationRenderers}
  confirmDiscard={(path) => confirm(`Discard changes to ${path}?`)}
/>;

Direct composition

tsx
<FileTree
  entries={entries()}
  selectedPath={selectedPath()}
  onSelect={(entry) => setSelectedPath(entry.path)}
/>

<FileView
  file={{ path: selectedPath(), mediaType: "text/plain" }}
  load={() => source.read(selectedPath())}
  save={(content) => source.write!(selectedPath(), content)}
  renderers={applicationRenderers}
/>

Connect a coding agent

The Cloud skill provides compact working instructions. MCP supplies exact current documentation when details matter.

CLI command
bunx skills add https://docs.example.com

This command installs the working instructions published by this website in the selected coding agent.

For exact, current details, also connect the MCP server through one of the agent tabs.

Installation uses the open-source Vercel Skills CLI.

The skill and MCP complement each other: the skill describes workflows, while MCP supplies current documentation.