summaryrefslogtreecommitdiffhomepage
path: root/packages/desktop/src/components/file-tree.tsx
diff options
context:
space:
mode:
authorAdam <[email protected]>2025-10-03 09:04:28 -0500
committerAdam <[email protected]>2025-10-03 09:04:28 -0500
commit3fa280d21878ae391674a21758199df3d2d8c3b5 (patch)
treef70c6ecafffeecc8e7a59dc9acef66c59a9ea54a /packages/desktop/src/components/file-tree.tsx
parent1d58b5548287a3e32ffce3abdcf0f2db08fdb155 (diff)
downloadopencode-3fa280d21878ae391674a21758199df3d2d8c3b5.tar.gz
opencode-3fa280d21878ae391674a21758199df3d2d8c3b5.zip
chore: app -> desktop
Diffstat (limited to 'packages/desktop/src/components/file-tree.tsx')
-rw-r--r--packages/desktop/src/components/file-tree.tsx110
1 files changed, 110 insertions, 0 deletions
diff --git a/packages/desktop/src/components/file-tree.tsx b/packages/desktop/src/components/file-tree.tsx
new file mode 100644
index 000000000..d31255ced
--- /dev/null
+++ b/packages/desktop/src/components/file-tree.tsx
@@ -0,0 +1,110 @@
+import { useLocal } from "@/context"
+import type { LocalFile } from "@/context/local"
+import { Collapsible, FileIcon, Tooltip } from "@/ui"
+import { For, Match, Switch, Show, type ComponentProps, type ParentProps } from "solid-js"
+import { Dynamic } from "solid-js/web"
+
+export default function FileTree(props: {
+ path: string
+ class?: string
+ nodeClass?: string
+ level?: number
+ onFileClick?: (file: LocalFile) => void
+}) {
+ const local = useLocal()
+ const level = props.level ?? 0
+
+ const Node = (p: ParentProps & ComponentProps<"div"> & { node: LocalFile; as?: "div" | "button" }) => (
+ <Dynamic
+ component={p.as ?? "div"}
+ classList={{
+ "p-0.5 w-full flex items-center gap-x-2 hover:bg-background-element cursor-pointer": true,
+ "bg-background-element": local.file.active()?.path === p.node.path,
+ [props.nodeClass ?? ""]: !!props.nodeClass,
+ }}
+ style={`padding-left: ${level * 10}px`}
+ draggable={true}
+ onDragStart={(e: any) => {
+ const evt = e as globalThis.DragEvent
+ evt.dataTransfer!.effectAllowed = "copy"
+ evt.dataTransfer!.setData("text/plain", `file:${p.node.path}`)
+
+ // Create custom drag image without margins
+ const dragImage = document.createElement("div")
+ dragImage.className =
+ "flex items-center gap-x-2 px-2 py-1 bg-background-element rounded-md border border-border-1"
+ dragImage.style.position = "absolute"
+ dragImage.style.top = "-1000px"
+
+ // Copy only the icon and text content without padding
+ const icon = e.currentTarget.querySelector("svg")
+ const text = e.currentTarget.querySelector("span")
+ if (icon && text) {
+ dragImage.innerHTML = icon.outerHTML + text.outerHTML
+ }
+
+ document.body.appendChild(dragImage)
+ evt.dataTransfer!.setDragImage(dragImage, 0, 12)
+ setTimeout(() => document.body.removeChild(dragImage), 0)
+ }}
+ {...p}
+ >
+ {p.children}
+ <span
+ classList={{
+ "text-xs whitespace-nowrap truncate": true,
+ "text-text-muted/40": p.node.ignored,
+ "text-text-muted/80": !p.node.ignored,
+ "!text-text": local.file.active()?.path === p.node.path,
+ "!text-primary": local.file.changed(p.node.path),
+ }}
+ >
+ {p.node.name}
+ </span>
+ <Show when={local.file.changed(p.node.path)}>
+ <span class="ml-auto mr-1 w-1.5 h-1.5 rounded-full bg-primary/50 shrink-0" />
+ </Show>
+ </Dynamic>
+ )
+
+ return (
+ <div class={`flex flex-col ${props.class}`}>
+ <For each={local.file.children(props.path)}>
+ {(node) => (
+ <Tooltip forceMount={false} openDelay={2000} value={node.path} placement="right">
+ <Switch>
+ <Match when={node.type === "directory"}>
+ <Collapsible
+ class="w-full"
+ forceMount={false}
+ open={local.file.node(node.path)?.expanded}
+ onOpenChange={(open) => (open ? local.file.expand(node.path) : local.file.collapse(node.path))}
+ >
+ <Collapsible.Trigger>
+ <Node node={node}>
+ <Collapsible.Arrow size={16} class="text-text-muted/60 ml-1" />
+ <FileIcon
+ node={node}
+ expanded={local.file.node(node.path).expanded}
+ class="text-text-muted/60 -ml-1"
+ />
+ </Node>
+ </Collapsible.Trigger>
+ <Collapsible.Content>
+ <FileTree path={node.path} level={level + 1} onFileClick={props.onFileClick} />
+ </Collapsible.Content>
+ </Collapsible>
+ </Match>
+ <Match when={node.type === "file"}>
+ <Node node={node} as="button" onClick={() => props.onFileClick?.(node)}>
+ <div class="w-4 shrink-0" />
+ <FileIcon node={node} class="text-primary" />
+ </Node>
+ </Match>
+ </Switch>
+ </Tooltip>
+ )}
+ </For>
+ </div>
+ )
+}