Implement workspace-aware folder browser
This commit is contained in:
@@ -13,14 +13,17 @@ export class FileSystemBrowser {
|
||||
this.root = path.resolve(options.rootDir)
|
||||
}
|
||||
|
||||
list(relativePath: string, depth = 2): FileSystemEntry[] {
|
||||
list(relativePath: string, options: { depth?: number; includeFiles?: boolean } = {}): FileSystemEntry[] {
|
||||
const depth = options.depth ?? 2
|
||||
const includeFiles = options.includeFiles ?? true
|
||||
if (depth < 1) {
|
||||
throw new Error("Depth must be at least 1")
|
||||
}
|
||||
return this.walk(relativePath, depth)
|
||||
const normalizedPath = this.normalizeRelativePath(relativePath)
|
||||
return this.walk(normalizedPath, depth, includeFiles)
|
||||
}
|
||||
|
||||
private walk(relativePath: string, remainingDepth: number): FileSystemEntry[] {
|
||||
private walk(relativePath: string, remainingDepth: number, includeFiles: boolean): FileSystemEntry[] {
|
||||
const resolved = this.toAbsolute(relativePath)
|
||||
const entries = fs.readdirSync(resolved, { withFileTypes: true })
|
||||
|
||||
@@ -31,21 +34,39 @@ export class FileSystemBrowser {
|
||||
|
||||
const current: FileSystemEntry = {
|
||||
name: entry.name,
|
||||
path: entryPath,
|
||||
path: this.normalizeRelativePath(entryPath),
|
||||
type: entry.isDirectory() ? "directory" : "file",
|
||||
size: entry.isDirectory() ? undefined : stats.size,
|
||||
modifiedAt: stats.mtime.toISOString(),
|
||||
}
|
||||
|
||||
if (entry.isDirectory() && remainingDepth > 1) {
|
||||
const nested = this.walk(entryPath, remainingDepth - 1)
|
||||
const nested = this.walk(entryPath, remainingDepth - 1, includeFiles)
|
||||
return [current, ...nested]
|
||||
}
|
||||
|
||||
if (!entry.isDirectory() && !includeFiles) {
|
||||
return []
|
||||
}
|
||||
|
||||
return [current]
|
||||
})
|
||||
}
|
||||
|
||||
private normalizeRelativePath(input: string | undefined) {
|
||||
if (!input || input === "." || input === "./" || input === "/") {
|
||||
return "."
|
||||
}
|
||||
let normalized = input.replace(/\\+/g, "/")
|
||||
if (normalized.startsWith("./")) {
|
||||
normalized = normalized.replace(/^\.\/+/, "")
|
||||
}
|
||||
if (normalized.startsWith("/")) {
|
||||
normalized = normalized.replace(/^\/+/g, "")
|
||||
}
|
||||
return normalized === "" ? "." : normalized
|
||||
}
|
||||
|
||||
readFile(relativePath: string): string {
|
||||
const resolved = this.toAbsolute(relativePath)
|
||||
return fs.readFileSync(resolved, "utf-8")
|
||||
|
||||
@@ -9,6 +9,7 @@ interface RouteDeps {
|
||||
const FilesystemQuerySchema = z.object({
|
||||
path: z.string().optional(),
|
||||
depth: z.coerce.number().int().min(1).max(10).default(2),
|
||||
includeFiles: z.coerce.boolean().default(true),
|
||||
})
|
||||
|
||||
export function registerFilesystemRoutes(app: FastifyInstance, deps: RouteDeps) {
|
||||
@@ -17,7 +18,10 @@ export function registerFilesystemRoutes(app: FastifyInstance, deps: RouteDeps)
|
||||
const targetPath = query.path ?? "."
|
||||
|
||||
try {
|
||||
return deps.fileSystemBrowser.list(targetPath, query.depth)
|
||||
return deps.fileSystemBrowser.list(targetPath, {
|
||||
depth: query.depth,
|
||||
includeFiles: query.includeFiles,
|
||||
})
|
||||
} catch (error) {
|
||||
reply.code(400)
|
||||
return { error: (error as Error).message }
|
||||
|
||||
315
packages/ui/src/components/directory-browser-dialog.tsx
Normal file
315
packages/ui/src/components/directory-browser-dialog.tsx
Normal file
@@ -0,0 +1,315 @@
|
||||
import { Component, Show, For, createSignal, createMemo, createEffect, onCleanup } from "solid-js"
|
||||
import { ArrowUpLeft, Folder as FolderIcon, Loader2, X } from "lucide-solid"
|
||||
import type { FileSystemEntry } from "../../../cli/src/api-types"
|
||||
import { cliApi } from "../lib/api-client"
|
||||
import { getServerMeta } from "../lib/server-meta"
|
||||
|
||||
const ROOT_KEY = "."
|
||||
const ROOT_REQUEST_PATH = "/"
|
||||
const DEFAULT_DEPTH = 2
|
||||
|
||||
interface DirectoryBrowserDialogProps {
|
||||
open: boolean
|
||||
title: string
|
||||
description?: string
|
||||
onSelect: (absolutePath: string) => void
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
function normalizeRelativePath(input?: string) {
|
||||
if (!input || input === "." || input === "./" || input === "/") {
|
||||
return "."
|
||||
}
|
||||
let normalized = input.replace(/\\+/g, "/")
|
||||
if (normalized.startsWith("./")) {
|
||||
normalized = normalized.replace(/^\.\/+/, "")
|
||||
}
|
||||
if (normalized.startsWith("/")) {
|
||||
normalized = normalized.replace(/^\/+/g, "")
|
||||
}
|
||||
return normalized === "" ? "." : normalized
|
||||
}
|
||||
|
||||
function getParentPath(relativePath: string) {
|
||||
const normalized = normalizeRelativePath(relativePath)
|
||||
if (normalized === ".") {
|
||||
return "."
|
||||
}
|
||||
const segments = normalized.split("/")
|
||||
segments.pop()
|
||||
return segments.length === 0 ? "." : segments.join("/")
|
||||
}
|
||||
|
||||
function resolveAbsolutePath(root: string, relativePath: string) {
|
||||
if (!root) {
|
||||
return relativePath
|
||||
}
|
||||
if (!relativePath || relativePath === "." || relativePath === "./" || relativePath === "/") {
|
||||
return root
|
||||
}
|
||||
const separator = root.includes("\\") ? "\\" : "/"
|
||||
const trimmedRoot = root.endsWith(separator) ? root : `${root}${separator}`
|
||||
const normalized = relativePath.replace(/[\\/]+/g, separator).replace(/^[\\/]+/, "")
|
||||
return `${trimmedRoot}${normalized}`
|
||||
}
|
||||
|
||||
type FolderRow =
|
||||
| { type: "up"; path: string }
|
||||
| { type: "folder"; entry: FileSystemEntry }
|
||||
|
||||
const DirectoryBrowserDialog: Component<DirectoryBrowserDialogProps> = (props) => {
|
||||
const [rootPath, setRootPath] = createSignal("")
|
||||
const [loading, setLoading] = createSignal(false)
|
||||
const [error, setError] = createSignal<string | null>(null)
|
||||
const [directoryChildren, setDirectoryChildren] = createSignal<Map<string, FileSystemEntry[]>>(new Map())
|
||||
const [loadingPaths, setLoadingPaths] = createSignal<Set<string>>(new Set())
|
||||
const [loadedPaths, setLoadedPaths] = createSignal<Set<string>>(new Set())
|
||||
const [currentPath, setCurrentPath] = createSignal(ROOT_KEY)
|
||||
|
||||
function resetState() {
|
||||
setDirectoryChildren(new Map<string, FileSystemEntry[]>())
|
||||
setLoadingPaths(new Set<string>())
|
||||
setLoadedPaths(new Set<string>())
|
||||
setCurrentPath(ROOT_KEY)
|
||||
setError(null)
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
if (!props.open) {
|
||||
return
|
||||
}
|
||||
resetState()
|
||||
void initialize()
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === "Escape") {
|
||||
event.preventDefault()
|
||||
props.onClose()
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown)
|
||||
onCleanup(() => {
|
||||
window.removeEventListener("keydown", handleKeyDown)
|
||||
})
|
||||
})
|
||||
|
||||
async function initialize() {
|
||||
setLoading(true)
|
||||
try {
|
||||
const meta = await getServerMeta()
|
||||
setRootPath(meta.workspaceRoot)
|
||||
await ensureDirectoryLoaded(ROOT_KEY)
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : "Unable to load filesystem"
|
||||
setError(message)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureDirectoryLoaded(path: string) {
|
||||
const normalized = normalizeRelativePath(path)
|
||||
if (loadedPaths().has(normalized)) {
|
||||
return
|
||||
}
|
||||
await loadDirectory(normalized)
|
||||
}
|
||||
|
||||
async function loadDirectory(path: string) {
|
||||
const normalized = normalizeRelativePath(path)
|
||||
if (loadingPaths().has(normalized)) {
|
||||
return
|
||||
}
|
||||
|
||||
setLoadingPaths((prev) => {
|
||||
const next = new Set(prev)
|
||||
next.add(normalized)
|
||||
return next
|
||||
})
|
||||
|
||||
try {
|
||||
const requestPath = normalized === ROOT_KEY ? ROOT_REQUEST_PATH : normalized
|
||||
const entries = await cliApi.listFileSystem(requestPath, { depth: DEFAULT_DEPTH, includeFiles: false })
|
||||
mergeDirectoryEntries(normalized, entries)
|
||||
setLoadedPaths((prev) => {
|
||||
const next = new Set(prev)
|
||||
next.add(normalized)
|
||||
return next
|
||||
})
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : "Unable to load filesystem"
|
||||
setError(message)
|
||||
throw err
|
||||
} finally {
|
||||
setLoadingPaths((prev) => {
|
||||
const next = new Set(prev)
|
||||
next.delete(normalized)
|
||||
return next
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function mergeDirectoryEntries(basePath: string, entries: FileSystemEntry[]) {
|
||||
const grouped = new Map<string, FileSystemEntry[]>([[basePath, []]])
|
||||
for (const entry of entries) {
|
||||
if (entry.type !== "directory") {
|
||||
continue
|
||||
}
|
||||
const normalizedEntryPath = normalizeRelativePath(entry.path)
|
||||
const parentPath = getParentPath(normalizedEntryPath)
|
||||
const siblings = grouped.get(parentPath) ?? []
|
||||
siblings.push({ ...entry, path: normalizedEntryPath })
|
||||
grouped.set(parentPath, siblings)
|
||||
}
|
||||
|
||||
setDirectoryChildren((prev) => {
|
||||
const next = new Map(prev)
|
||||
for (const [parent, children] of grouped.entries()) {
|
||||
const sorted = children.slice().sort((a, b) => a.name.localeCompare(b.name))
|
||||
next.set(parent, sorted)
|
||||
}
|
||||
return next
|
||||
})
|
||||
}
|
||||
|
||||
function handleEntrySelect(relativePath: string) {
|
||||
const absolute = resolveAbsolutePath(rootPath(), relativePath)
|
||||
props.onSelect(absolute)
|
||||
}
|
||||
|
||||
function isPathLoading(path: string) {
|
||||
return loadingPaths().has(normalizeRelativePath(path))
|
||||
}
|
||||
|
||||
const folderRows = createMemo<FolderRow[]>(() => {
|
||||
const rows: FolderRow[] = []
|
||||
if (currentPath() !== ROOT_KEY) {
|
||||
rows.push({ type: "up", path: getParentPath(currentPath()) })
|
||||
}
|
||||
const children = directoryChildren().get(currentPath()) ?? []
|
||||
for (const entry of children) {
|
||||
rows.push({ type: "folder", entry })
|
||||
}
|
||||
return rows
|
||||
})
|
||||
|
||||
function handleNavigateTo(path: string) {
|
||||
const normalized = normalizeRelativePath(path)
|
||||
setCurrentPath(normalized)
|
||||
void ensureDirectoryLoaded(normalized)
|
||||
}
|
||||
|
||||
function handleNavigateUp() {
|
||||
handleNavigateTo(getParentPath(currentPath()))
|
||||
}
|
||||
|
||||
const currentAbsolutePath = createMemo(() => resolveAbsolutePath(rootPath(), currentPath()))
|
||||
|
||||
function handleOverlayClick(event: MouseEvent) {
|
||||
if (event.target === event.currentTarget) {
|
||||
props.onClose()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Show when={props.open}>
|
||||
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-6" onClick={handleOverlayClick}>
|
||||
<div class="modal-surface directory-browser-modal" role="dialog" aria-modal="true">
|
||||
<div class="panel directory-browser-panel">
|
||||
<div class="directory-browser-header">
|
||||
<div class="directory-browser-heading">
|
||||
<h3 class="directory-browser-title">{props.title}</h3>
|
||||
<p class="directory-browser-description">
|
||||
{props.description || "Browse folders under the configured workspace root."}
|
||||
</p>
|
||||
</div>
|
||||
<button type="button" class="directory-browser-close" aria-label="Close" onClick={props.onClose}>
|
||||
<X class="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="panel-body directory-browser-body">
|
||||
<Show when={rootPath()}>
|
||||
<div class="directory-browser-current">
|
||||
<div class="directory-browser-current-meta">
|
||||
<span class="directory-browser-current-label">Current folder</span>
|
||||
<span class="directory-browser-current-path">{currentAbsolutePath()}</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="selector-button selector-button-secondary directory-browser-select directory-browser-current-select"
|
||||
onClick={() => handleEntrySelect(currentPath())}
|
||||
>
|
||||
Select Current
|
||||
</button>
|
||||
</div>
|
||||
</Show>
|
||||
<Show
|
||||
when={!loading() && !error()}
|
||||
fallback={
|
||||
<div class="panel-empty-state flex-1">
|
||||
<Show when={loading()} fallback={<span class="text-red-500">{error()}</span>}>
|
||||
<div class="directory-browser-loading">
|
||||
<Loader2 class="w-5 h-5 animate-spin" />
|
||||
<span>Loading folders…</span>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Show
|
||||
when={folderRows().length > 0}
|
||||
fallback={<div class="panel-empty-state flex-1">No folders available.</div>}
|
||||
>
|
||||
<div class="panel-list panel-list--fill flex-1 min-h-0 overflow-auto directory-browser-list" role="listbox">
|
||||
<For each={folderRows()}>
|
||||
{(item) => {
|
||||
const isFolder = item.type === "folder"
|
||||
const label = isFolder ? item.entry.name || item.entry.path : "Up one level"
|
||||
const navigate = () => (isFolder ? handleNavigateTo(item.entry.path) : handleNavigateUp())
|
||||
return (
|
||||
<div class="panel-list-item" role="option">
|
||||
<div class="panel-list-item-content directory-browser-row">
|
||||
<button type="button" class="directory-browser-row-main" onClick={navigate}>
|
||||
<div class="directory-browser-row-icon">
|
||||
<Show when={!isFolder} fallback={<FolderIcon class="w-4 h-4" />}>
|
||||
<ArrowUpLeft class="w-4 h-4" />
|
||||
</Show>
|
||||
</div>
|
||||
<div class="directory-browser-row-text">
|
||||
<span class="directory-browser-row-name">{label}</span>
|
||||
</div>
|
||||
<Show when={isFolder && isPathLoading(item.entry.path)}>
|
||||
<Loader2 class="directory-browser-row-spinner animate-spin" />
|
||||
</Show>
|
||||
</button>
|
||||
{isFolder ? (
|
||||
<button
|
||||
type="button"
|
||||
class="selector-button selector-button-secondary directory-browser-select"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
handleEntrySelect(item.entry.path)
|
||||
}}
|
||||
>
|
||||
Select
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
|
||||
export default DirectoryBrowserDialog
|
||||
@@ -2,7 +2,7 @@ import { Component, createSignal, Show, For, onMount, onCleanup, createEffect }
|
||||
import { Folder, Clock, Trash2, FolderPlus, Settings, ChevronRight } from "lucide-solid"
|
||||
import { useConfig } from "../stores/preferences"
|
||||
import AdvancedSettingsModal from "./advanced-settings-modal"
|
||||
import FileSystemBrowserDialog from "./filesystem-browser-dialog"
|
||||
import DirectoryBrowserDialog from "./directory-browser-dialog"
|
||||
import Kbd from "./kbd"
|
||||
|
||||
const codeNomadLogo = new URL("../images/CodeNomad-Icon.png", import.meta.url).href
|
||||
@@ -386,11 +386,10 @@ const FolderSelectionView: Component<FolderSelectionViewProps> = (props) => {
|
||||
isLoading={props.isLoading}
|
||||
/>
|
||||
|
||||
<FileSystemBrowserDialog
|
||||
<DirectoryBrowserDialog
|
||||
open={isFolderBrowserOpen()}
|
||||
mode="directories"
|
||||
title="Browse for Folder"
|
||||
description="Select any directory exposed by the CLI server."
|
||||
title="Select Workspace"
|
||||
description="Select workspace to start coding."
|
||||
onClose={() => setIsFolderBrowserOpen(false)}
|
||||
onSelect={handleBrowserSelect}
|
||||
/>
|
||||
|
||||
@@ -130,11 +130,14 @@ export const cliApi = {
|
||||
body: JSON.stringify({ path }),
|
||||
})
|
||||
},
|
||||
listFileSystem(relativePath = ".", options?: { depth?: number }): Promise<FileSystemEntry[]> {
|
||||
listFileSystem(relativePath = ".", options?: { depth?: number; includeFiles?: boolean }): Promise<FileSystemEntry[]> {
|
||||
const params = new URLSearchParams({ path: relativePath })
|
||||
if (options?.depth) {
|
||||
params.set("depth", String(options.depth))
|
||||
}
|
||||
if (options?.includeFiles !== undefined) {
|
||||
params.set("includeFiles", String(options.includeFiles))
|
||||
}
|
||||
return request<FileSystemEntry[]>(`/api/filesystem?${params.toString()}`)
|
||||
},
|
||||
readInstanceData(id: string): Promise<InstanceData> {
|
||||
|
||||
166
packages/ui/src/styles/components/directory-browser.css
Normal file
166
packages/ui/src/styles/components/directory-browser.css
Normal file
@@ -0,0 +1,166 @@
|
||||
.directory-browser-modal {
|
||||
width: min(960px, 90vw);
|
||||
height: min(85vh, 900px);
|
||||
max-height: 90vh;
|
||||
border-radius: var(--radius-xl);
|
||||
}
|
||||
|
||||
.directory-browser-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.directory-browser-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-xl);
|
||||
padding: 1.5rem;
|
||||
border-bottom: 1px solid var(--border-base);
|
||||
background-color: var(--surface-secondary);
|
||||
}
|
||||
|
||||
.directory-browser-heading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-sm);
|
||||
}
|
||||
|
||||
.directory-browser-title {
|
||||
font-size: var(--font-size-2xl);
|
||||
line-height: var(--line-height-tight);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.directory-browser-description {
|
||||
font-size: var(--font-size-xl);
|
||||
line-height: var(--line-height-relaxed);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.directory-browser-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: 1.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-lg);
|
||||
background-color: var(--surface-base);
|
||||
}
|
||||
|
||||
.directory-browser-current {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-md);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.directory-browser-current-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2xs);
|
||||
}
|
||||
|
||||
.directory-browser-current-label {
|
||||
font-size: var(--font-size-sm);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.directory-browser-current-path {
|
||||
font-family: var(--font-family-mono);
|
||||
font-size: var(--font-size-base);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.directory-browser-current-select {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.directory-browser-close {
|
||||
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: var(--radius-full);
|
||||
border: 1px solid var(--border-base);
|
||||
background-color: var(--surface-base);
|
||||
color: var(--text-primary);
|
||||
transition: background-color 0.15s ease;
|
||||
}
|
||||
|
||||
.directory-browser-close:hover {
|
||||
background-color: var(--surface-hover);
|
||||
}
|
||||
|
||||
.directory-browser-list {
|
||||
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.directory-browser-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-md);
|
||||
}
|
||||
|
||||
.directory-browser-row-main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-md);
|
||||
text-align: left;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text-primary);
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.directory-browser-row-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: var(--radius-lg);
|
||||
background-color: var(--surface-secondary);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.directory-browser-row-name {
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
.directory-browser-row-spinner {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.directory-browser-select {
|
||||
width: auto;
|
||||
min-width: 90px;
|
||||
}
|
||||
|
||||
|
||||
.directory-browser-select:hover {
|
||||
background-color: var(--selection-highlight-bg);
|
||||
border-color: var(--accent-primary);
|
||||
color: var(--accent-primary);
|
||||
}
|
||||
|
||||
.directory-browser-loading {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-sm);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
@@ -224,12 +224,15 @@
|
||||
}
|
||||
|
||||
.selector-button-secondary {
|
||||
background-color: var(--surface-secondary);
|
||||
background-color: var(--surface-base);
|
||||
color: var(--text-primary);
|
||||
border-color: var(--border-base);
|
||||
}
|
||||
|
||||
.selector-button-secondary:hover:not(:disabled) {
|
||||
background-color: var(--surface-hover);
|
||||
background-color: var(--surface-secondary);
|
||||
border-color: var(--border-base);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.selector-button-secondary:disabled {
|
||||
|
||||
@@ -4,3 +4,4 @@
|
||||
@import "./components/dropdown.css";
|
||||
@import "./components/selector.css";
|
||||
@import "./components/env-vars.css";
|
||||
@import "./components/directory-browser.css";
|
||||
|
||||
Reference in New Issue
Block a user