summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorDavid Hill <[email protected]>2026-02-17 18:23:04 +0000
committerDavid Hill <[email protected]>2026-02-17 18:23:04 +0000
commita685e7a805454110d92ed4da5a3799a15ea1bcb9 (patch)
tree2de6f2118ae06d5de65390109e5c5c29a538cb50
parentce7484b4f5c3de1b83db4223052bdf9ce4c0cfb9 (diff)
downloadopencode-a685e7a805454110d92ed4da5a3799a15ea1bcb9.tar.gz
opencode-a685e7a805454110d92ed4da5a3799a15ea1bcb9.zip
tui: show monochrome file icons by default in tree view, revealing colors on hover to reduce visual clutter and help users focus on code content
-rw-r--r--packages/app/src/components/file-tree.tsx28
-rw-r--r--packages/ui/src/components/file-icon.css32
-rw-r--r--packages/ui/src/components/file-icon.tsx18
3 files changed, 73 insertions, 5 deletions
diff --git a/packages/app/src/components/file-tree.tsx b/packages/app/src/components/file-tree.tsx
index 758f5a83f..1f09642ac 100644
--- a/packages/app/src/components/file-tree.tsx
+++ b/packages/app/src/components/file-tree.tsx
@@ -447,12 +447,13 @@ export default function FileTree(props: {
})
return (
- <div class={`flex flex-col gap-0.5 ${props.class ?? ""}`}>
+ <div data-component="filetree" class={`flex flex-col gap-0.5 ${props.class ?? ""}`}>
<For each={nodes()}>
{(node) => {
const expanded = () => file.tree.state(node.path)?.expanded ?? false
const deep = () => deeps().get(node.path) ?? -1
const kind = () => visibleKind(node, kinds(), marks())
+ const active = () => !!kind() && !node.ignored
return (
<Switch>
@@ -530,7 +531,30 @@ export default function FileTree(props: {
onClick={() => props.onFileClick?.(node)}
>
<div class="w-4 shrink-0" />
- <FileIcon node={node} class="text-icon-weak size-4" />
+ <Switch>
+ <Match when={node.ignored}>
+ <FileIcon
+ node={node}
+ class="size-4 filetree-icon filetree-icon--mono"
+ style="color: var(--icon-weak-base)"
+ mono
+ />
+ </Match>
+ <Match when={active()}>
+ <FileIcon
+ node={node}
+ class="size-4 filetree-icon filetree-icon--mono"
+ style={kindTextColor(kind()!)}
+ mono
+ />
+ </Match>
+ <Match when={!node.ignored}>
+ <span class="filetree-iconpair size-4">
+ <FileIcon node={node} class="size-4 filetree-icon filetree-icon--color" />
+ <FileIcon node={node} class="size-4 filetree-icon filetree-icon--mono" mono />
+ </span>
+ </Match>
+ </Switch>
</FileTreeNode>
</FileTreeNodeTooltip>
</Match>
diff --git a/packages/ui/src/components/file-icon.css b/packages/ui/src/components/file-icon.css
index e650f6dc7..078352096 100644
--- a/packages/ui/src/components/file-icon.css
+++ b/packages/ui/src/components/file-icon.css
@@ -3,3 +3,35 @@
width: 16px;
height: 16px;
}
+
+/*
+ File tree: show monochrome weak icons by default.
+ On hover, show the original file-type colors.
+*/
+[data-component="filetree"] .filetree-icon--mono {
+ color: var(--icon-base);
+}
+
+[data-component="filetree"] .filetree-iconpair {
+ position: relative;
+ display: inline-flex;
+ width: 16px;
+ height: 16px;
+}
+
+[data-component="filetree"] .filetree-iconpair [data-component="file-icon"] {
+ position: absolute;
+ inset: 0;
+}
+
+[data-component="filetree"] .filetree-iconpair .filetree-icon--color {
+ opacity: 0;
+}
+
+[data-component="filetree"]:hover .filetree-iconpair .filetree-icon--color {
+ opacity: 1;
+}
+
+[data-component="filetree"]:hover .filetree-iconpair .filetree-icon--mono {
+ opacity: 0;
+}
diff --git a/packages/ui/src/components/file-icon.tsx b/packages/ui/src/components/file-icon.tsx
index cadb7ba83..405cbe163 100644
--- a/packages/ui/src/components/file-icon.tsx
+++ b/packages/ui/src/components/file-icon.tsx
@@ -1,16 +1,20 @@
import type { Component, JSX } from "solid-js"
-import { createMemo, splitProps } from "solid-js"
+import { createMemo, splitProps, Show } from "solid-js"
import sprite from "./file-icons/sprite.svg"
import type { IconName } from "./file-icons/types"
+let filter = 0
+
export type FileIconProps = JSX.GSVGAttributes<SVGSVGElement> & {
node: { path: string; type: "file" | "directory" }
expanded?: boolean
+ mono?: boolean
}
export const FileIcon: Component<FileIconProps> = (props) => {
- const [local, rest] = splitProps(props, ["node", "class", "classList", "expanded"])
+ const [local, rest] = splitProps(props, ["node", "class", "classList", "expanded", "mono"])
const name = createMemo(() => chooseIconName(local.node.path, local.node.type, local.expanded || false))
+ const id = `file-icon-mono-${filter++}`
return (
<svg
data-component="file-icon"
@@ -20,7 +24,15 @@ export const FileIcon: Component<FileIconProps> = (props) => {
[local.class ?? ""]: !!local.class,
}}
>
- <use href={`${sprite}#${name()}`} />
+ <Show when={local.mono}>
+ <defs>
+ <filter id={id} color-interpolation-filters="sRGB">
+ <feFlood flood-color="currentColor" result="flood" />
+ <feComposite in="flood" in2="SourceAlpha" operator="in" />
+ </filter>
+ </defs>
+ </Show>
+ <use href={`${sprite}#${name()}`} filter={local.mono ? `url(#${id})` : undefined} />
</svg>
)
}