feat(icons): file icons and tab glyphs in color, from the host's own artwork
Issue #611 asked for file-type icons like the official explorer's, and #429 built a colored theme for them. DSH 0.1.5-rc.2 makes most of that work unnecessary: `@deepseek-ai/dsh-client-ui-primitives` now exports `FileTypeIcon` / `CodeFileIcon` with the host's own 48-type full-color code artwork plus category-colored sheet glyphs (and its classifier behind them). So the plugin renders the host's icons instead of shipping a color table, and color becomes the only behaviour rather than an opt-in theme. File icons (src/client/file-icons.tsx) - `builtinFileIcon(path, size)` → `<FileTypeIcon path size />`; folders use the host's folder category. The plugin's own per-extension VSCodicon map is gone. - The public `registerFileIcon` API from #531 is unchanged and still outranks the built-ins, so an external plugin can take over any row. One semantic consequence, now documented and tested: because the host classifier covers every path, a registered catch-all (`exts: []`) claims rows it used to lose to the built-in map. An unregistered catch-all (the old icon theme's shape) is unaffected. - Nothing is lazy any more: the artwork lives in a platform module the client already holds, so `lib/client-file-icons.js` and its 563-entry dataset (255 kB) are deleted outright — the core bundle grew by 10 kB (0.85 → 0.86 MB). - `src/client/file-icon-theme.ts`, the `fileIconTheme` pref (prefs-shared / config / client prefs), the settings-page select row and the six `settingsFileIconTheme*` keys in all 20 dictionaries are removed with it. Tab glyphs (src/client/builtins/tab-icons.tsx) - Every built-in tab type now declares a colored glyph: files (the host's folder artwork), changes/diff (commit), tasks (checklist), side chat (discussion), terminal (shell), browser (globe). - The color comes from a `--dsw-alias-*` token through a wrapper class, never from a literal — the skin contract (§12) holds and `tests/theme.spec.ts` enforces both halves (no literals in the module, every color declaration in the stylesheet resolves to a token). Native right Sidebar chips (src/client/native/tab-adapter.tsx) - The host's tab definition has no icon field, but the `sidebar.right.pane.tab.title` slot IS the chip's content, so the plugin draws the glyph itself: an editor tab with a path shows that file's icon, every other tab shows its descriptor's icon — the same `descriptor.icon` the workbench strip and the guide capsules already render, so the three cannot drift. The glyph is `aria-hidden`, which keeps the chip's accessible name exactly the title the mount lane matches on. Gates: typecheck / lint / check:consumer-types green; pnpm test 127 files / 1342 passed / 9 skipped (3 new chip cases, 1 rewritten built-in case).
This commit is contained in:
+1
-1
@@ -19,7 +19,7 @@ import { fileURLToPath } from 'node:url'
|
|||||||
import type { Context, SidebarHttpRequest, SidebarHttpResponse } from './context-types.ts'
|
import type { Context, SidebarHttpRequest, SidebarHttpResponse } from './context-types.ts'
|
||||||
|
|
||||||
/** The chunk names the client may request (mirror of src/client/chunk-loader.ts). */
|
/** The chunk names the client may request (mirror of src/client/chunk-loader.ts). */
|
||||||
export const CHUNK_NAMES = ['terminal', 'editor', 'mermaid', 'locale', 'file-icons'] as const
|
export const CHUNK_NAMES = ['terminal', 'editor', 'mermaid', 'locale'] as const
|
||||||
export type ChunkName = (typeof CHUNK_NAMES)[number]
|
export type ChunkName = (typeof CHUNK_NAMES)[number]
|
||||||
|
|
||||||
/** Directory of this host-half module (lib/ — the chunk scripts live next to it). */
|
/** Directory of this host-half module (lib/ — the chunk scripts live next to it). */
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ export function FileTree(props: {
|
|||||||
busy: boolean
|
busy: boolean
|
||||||
/**
|
/**
|
||||||
* The sidebar registry service: when present, externally registered file
|
* The sidebar registry service: when present, externally registered file
|
||||||
* icons (`registerFileIcon`) outrank the built-in glyph map on file rows.
|
* icons (`registerFileIcon`) outrank the host's file-type artwork on file rows.
|
||||||
* Absent → the built-ins alone (the host always passes it today).
|
* Absent → the built-ins alone (the host always passes it today).
|
||||||
*/
|
*/
|
||||||
service?: BetterSidebarService
|
service?: BetterSidebarService
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
/*
|
||||||
|
* Colors for the built-in tab glyphs (src/client/builtins/tab-icons.tsx).
|
||||||
|
*
|
||||||
|
* Every declaration resolves to a `--dsw-alias-*` token, so all skins stay in
|
||||||
|
* charge — including the token's own fallback chain, which keeps the glyph
|
||||||
|
* legible on a host that does not define the brand/state families. The plugin
|
||||||
|
* never writes a color literal here; `tests/theme.spec.ts` enforces both
|
||||||
|
* halves of that rule.
|
||||||
|
*
|
||||||
|
* The chips sit on the panel surface, so a token that is itself translucent
|
||||||
|
* would wash out at 14px; each rule therefore prefers an opaque state/brand
|
||||||
|
* token and falls back to the primary ink.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.files {
|
||||||
|
color: var(--dsw-alias-brand-primary, var(--dsw-alias-label-primary));
|
||||||
|
}
|
||||||
|
|
||||||
|
.changes {
|
||||||
|
color: var(--dsw-alias-state-success-primary, var(--dsw-alias-brand-primary, var(--dsw-alias-label-primary)));
|
||||||
|
}
|
||||||
|
|
||||||
|
.tasks {
|
||||||
|
color: var(--dsw-alias-state-warn-primary, var(--dsw-alias-label-primary));
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidechat {
|
||||||
|
color: var(--dsw-alias-state-business-primary, var(--dsw-alias-brand-primary, var(--dsw-alias-label-primary)));
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal {
|
||||||
|
color: var(--dsw-alias-label-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.browser {
|
||||||
|
color: var(--dsw-alias-state-business-primary, var(--dsw-alias-brand-primary, var(--dsw-alias-label-primary)));
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
/**
|
||||||
|
* The built-in tab glyphs, in color.
|
||||||
|
*
|
||||||
|
* Every built-in tab type and the incoming 8th (changes) declare their icon
|
||||||
|
* here, so the three surfaces that draw a tab's glyph — the bottom workbench's
|
||||||
|
* tab strip, the native right Sidebar's guide capsules and its tab chips — all
|
||||||
|
* read the same colorful glyph from `descriptor.icon`.
|
||||||
|
*
|
||||||
|
* The color always arrives from a theme token, never from the plugin: the
|
||||||
|
* glyph is a VSCodicon drawn in `currentColor`, and the wrapper class supplies
|
||||||
|
* that color. A skin therefore keeps control of everything (the guide's §12);
|
||||||
|
* this module is what `tests/theme.spec.ts` checks for color literals.
|
||||||
|
*
|
||||||
|
* `files` is the one exception in kind rather than in color: it renders DSH's
|
||||||
|
* own folder artwork (`FileTypeIcon`), matching the file rows this tab shows.
|
||||||
|
*/
|
||||||
|
import type { ReactNode } from 'react'
|
||||||
|
import { FileTypeIcon } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||||
|
import styles from './tab-icons.module.css'
|
||||||
|
import {
|
||||||
|
VscCommentDiscussion,
|
||||||
|
VscGitCommit,
|
||||||
|
VscGlobe,
|
||||||
|
VscTasklist,
|
||||||
|
VscTerminal,
|
||||||
|
} from 'react-icons/vsc'
|
||||||
|
/** The styled wrapper classes; typed so a renamed rule fails the build. */
|
||||||
|
const css = styles as Record<'files' | 'changes' | 'tasks' | 'sidechat' | 'terminal' | 'browser', string>
|
||||||
|
|
||||||
|
/** One tab type's glyph, sized by the caller's surface (14px in a strip). */
|
||||||
|
export type TabIcon = (size: number) => ReactNode
|
||||||
|
|
||||||
|
/** Surround a glyph with the class that hands it its token-driven color. */
|
||||||
|
function themed(className: string, glyph: ReactNode): ReactNode {
|
||||||
|
return <span className={className}>{glyph}</span>
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The Files tab: the host's folder artwork, like the rows it opens. */
|
||||||
|
export const filesTabIcon: TabIcon = (size) => (
|
||||||
|
<span className={css.files}>
|
||||||
|
<FileTypeIcon kind="folder" size={size} />
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
|
||||||
|
/** Changes / diff: the commit glyph, green like the diff affordances. */
|
||||||
|
export const changesTabIcon: TabIcon = (size) =>
|
||||||
|
themed(css.changes, <VscGitCommit size={size} />)
|
||||||
|
|
||||||
|
/** Tasks (subagent) — the live-activity amber. */
|
||||||
|
export const tasksTabIcon: TabIcon = (size) =>
|
||||||
|
themed(css.tasks, <VscTasklist size={size} />)
|
||||||
|
|
||||||
|
/** Side chat — the conversational/secondary accent. */
|
||||||
|
export const sidechatTabIcon: TabIcon = (size) =>
|
||||||
|
themed(css.sidechat, <VscCommentDiscussion size={size} />)
|
||||||
|
|
||||||
|
/** Terminal — primary ink, the shell is text. */
|
||||||
|
export const terminalTabIcon: TabIcon = (size) =>
|
||||||
|
themed(css.terminal, <VscTerminal size={size} />)
|
||||||
|
|
||||||
|
/** Browser — the same secondary accent as the side chat's sibling surfaces. */
|
||||||
|
export const browserTabIcon: TabIcon = (size) =>
|
||||||
|
themed(css.browser, <VscGlobe size={size} />)
|
||||||
@@ -8,8 +8,11 @@
|
|||||||
* `browser:<n>` the same way (no quota). The editor IS the files window
|
* `browser:<n>` the same way (no quota). The editor IS the files window
|
||||||
* (the old standalone explorer merged into it).
|
* (the old standalone explorer merged into it).
|
||||||
*/
|
*/
|
||||||
import { IconCodeOutline16, IconFolderOpen16, IconFolderOpenOutline16, IconNewChatOutline16, IconPanelLeftOutline16, IconSparkle16, IconThinkOutline16 } from '@deepseek-ai/dsh-client-ui-primitives'
|
import { IconCodeOutline16, IconPanelLeftOutline16 } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||||
import type { Context } from '../../context-types.ts'
|
import type { Context } from '../../context-types.ts'
|
||||||
|
import {
|
||||||
|
browserTabIcon, changesTabIcon, filesTabIcon, sidechatTabIcon, tasksTabIcon, terminalTabIcon,
|
||||||
|
} from './tab-icons.tsx'
|
||||||
import { allLeaves, isAgentTabId, type SidebarState } from '../state.ts'
|
import { allLeaves, isAgentTabId, type SidebarState } from '../state.ts'
|
||||||
import { t } from '../locales.ts'
|
import { t } from '../locales.ts'
|
||||||
import { openSidebarFile } from '../intercept.tsx'
|
import { openSidebarFile } from '../intercept.tsx'
|
||||||
@@ -22,7 +25,6 @@ import { SubagentView } from '../SubagentView.tsx'
|
|||||||
import { consumeSidechatSeed, SideChatView, sidechatThreadIdOf } from '../SideChatView.tsx'
|
import { consumeSidechatSeed, SideChatView, sidechatThreadIdOf } from '../SideChatView.tsx'
|
||||||
import { api } from '../api.ts'
|
import { api } from '../api.ts'
|
||||||
import { BrowserView } from '../BrowserView.tsx'
|
import { BrowserView } from '../BrowserView.tsx'
|
||||||
import { IconTerminalOutline16, IconDiffOutline16, IconGlobeOutline16 } from '../icons.tsx'
|
|
||||||
import { TERMINAL_FONT_SIZE_MAX, TERMINAL_FONT_SIZE_MIN } from '../../prefs-shared.ts'
|
import { TERMINAL_FONT_SIZE_MAX, TERMINAL_FONT_SIZE_MIN } from '../../prefs-shared.ts'
|
||||||
import type { ComponentType } from 'react'
|
import type { ComponentType } from 'react'
|
||||||
import type { SessionScope } from '../api.ts'
|
import type { SessionScope } from '../api.ts'
|
||||||
@@ -87,7 +89,7 @@ export function builtinTabs(ctx: Context, options: BuiltinTabOptions = {}): read
|
|||||||
// explorer (empty hint + docked tree); with a path it previews/edits
|
// explorer (empty hint + docked tree); with a path it previews/edits
|
||||||
// the file. Visible in the + menu in the explorer's old slot.
|
// the file. Visible in the + menu in the explorer's old slot.
|
||||||
title: () => t('files'),
|
title: () => t('files'),
|
||||||
icon: (size: number) => <IconFolderOpen16 size={size} />,
|
icon: filesTabIcon,
|
||||||
order: 10,
|
order: 10,
|
||||||
hidden: false,
|
hidden: false,
|
||||||
dedupeKey: (tab) => tab.path,
|
dedupeKey: (tab) => tab.path,
|
||||||
@@ -122,28 +124,6 @@ export function builtinTabs(ctx: Context, options: BuiltinTabOptions = {}): read
|
|||||||
key: 'workspaceFence',
|
key: 'workspaceFence',
|
||||||
title: () => t('settingsFenceTitle'),
|
title: () => t('settingsFenceTitle'),
|
||||||
desc: () => t('settingsFenceDesc'),
|
desc: () => t('settingsFenceDesc'),
|
||||||
}, {
|
|
||||||
// The file-icon theme: monochrome built-in glyphs (default) vs the
|
|
||||||
// optional colored brand dataset, which is fetched from the lazy
|
|
||||||
// `file-icons` chunk only when picked (see file-icon-theme.ts).
|
|
||||||
key: 'fileIconTheme',
|
|
||||||
type: 'select',
|
|
||||||
title: () => t('settingsFileIconThemeTitle'),
|
|
||||||
desc: () => t('settingsFileIconThemeDesc'),
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
value: 'builtin',
|
|
||||||
icon: (size: number) => <IconFolderOpenOutline16 size={size} />,
|
|
||||||
title: () => t('settingsFileIconThemeBuiltin'),
|
|
||||||
desc: () => t('settingsFileIconThemeBuiltinDesc'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'colored',
|
|
||||||
icon: (size: number) => <IconSparkle16 size={size} />,
|
|
||||||
title: () => t('settingsFileIconThemeColored'),
|
|
||||||
desc: () => t('settingsFileIconThemeColoredDesc'),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}],
|
}],
|
||||||
render: ({ pluginSettings, updatePluginSetting }) => (
|
render: ({ pluginSettings, updatePluginSetting }) => (
|
||||||
<OpenWithSettings pluginSettings={pluginSettings} updatePluginSetting={updatePluginSetting} />
|
<OpenWithSettings pluginSettings={pluginSettings} updatePluginSetting={updatePluginSetting} />
|
||||||
@@ -172,7 +152,7 @@ export function builtinTabs(ctx: Context, options: BuiltinTabOptions = {}): read
|
|||||||
id: 'git',
|
id: 'git',
|
||||||
title: () => t('changes'),
|
title: () => t('changes'),
|
||||||
description: () => t('guideDescGit'),
|
description: () => t('guideDescGit'),
|
||||||
icon: (size: number) => <IconDiffOutline16 size={size} />,
|
icon: changesTabIcon,
|
||||||
order: 20,
|
order: 20,
|
||||||
single: true,
|
single: true,
|
||||||
badge: (_ctx, scope) => {
|
badge: (_ctx, scope) => {
|
||||||
@@ -195,7 +175,7 @@ export function builtinTabs(ctx: Context, options: BuiltinTabOptions = {}): read
|
|||||||
id: 'subagent',
|
id: 'subagent',
|
||||||
title: () => t('subagent'),
|
title: () => t('subagent'),
|
||||||
description: () => t('guideDescSubagent'),
|
description: () => t('guideDescSubagent'),
|
||||||
icon: (size: number) => <IconThinkOutline16 size={size} />,
|
icon: tasksTabIcon,
|
||||||
order: 30,
|
order: 30,
|
||||||
single: true,
|
single: true,
|
||||||
// Declarative settings: the auto-open switches render under this row in
|
// Declarative settings: the auto-open switches render under this row in
|
||||||
@@ -224,7 +204,7 @@ export function builtinTabs(ctx: Context, options: BuiltinTabOptions = {}): read
|
|||||||
id: 'sidechat',
|
id: 'sidechat',
|
||||||
title: () => t('sideChat'),
|
title: () => t('sideChat'),
|
||||||
description: () => t('guideDescSidechat'),
|
description: () => t('guideDescSidechat'),
|
||||||
icon: (size: number) => <IconNewChatOutline16 size={size} />,
|
icon: sidechatTabIcon,
|
||||||
order: 35,
|
order: 35,
|
||||||
// Codex-style: EVERY side conversation is its own tab. A plain open
|
// Codex-style: EVERY side conversation is its own tab. A plain open
|
||||||
// mints a fresh tab flagged `autoCreate` (the view creates the EMPTY
|
// mints a fresh tab flagged `autoCreate` (the view creates the EMPTY
|
||||||
@@ -270,7 +250,7 @@ export function builtinTabs(ctx: Context, options: BuiltinTabOptions = {}): read
|
|||||||
id: 'terminal',
|
id: 'terminal',
|
||||||
title: () => t('terminal'),
|
title: () => t('terminal'),
|
||||||
description: () => t('guideDescTerminal'),
|
description: () => t('guideDescTerminal'),
|
||||||
icon: (size: number) => <IconTerminalOutline16 size={size} />,
|
icon: terminalTabIcon,
|
||||||
order: 40,
|
order: 40,
|
||||||
available: (_ctx, _scope, state) => uiTerminalCount(state) < TERMINAL_LIMIT,
|
available: (_ctx, _scope, state) => uiTerminalCount(state) < TERMINAL_LIMIT,
|
||||||
// Declarative settings: the model-facing terminal tools switch, the
|
// Declarative settings: the model-facing terminal tools switch, the
|
||||||
@@ -335,7 +315,7 @@ export function builtinTabs(ctx: Context, options: BuiltinTabOptions = {}): read
|
|||||||
id: 'browser',
|
id: 'browser',
|
||||||
title: () => t('browser'),
|
title: () => t('browser'),
|
||||||
description: () => t('guideDescBrowser'),
|
description: () => t('guideDescBrowser'),
|
||||||
icon: (size: number) => <IconGlobeOutline16 size={size} />,
|
icon: browserTabIcon,
|
||||||
order: 50,
|
order: 50,
|
||||||
// Declarative settings: the sandbox escape hatch, the link-takeover
|
// Declarative settings: the sandbox escape hatch, the link-takeover
|
||||||
// MASTER switch, and the per-protocol takeover switches (http on /
|
// MASTER switch, and the per-protocol takeover switches (http on /
|
||||||
@@ -379,7 +359,7 @@ export function builtinTabs(ctx: Context, options: BuiltinTabOptions = {}): read
|
|||||||
{
|
{
|
||||||
id: 'diff',
|
id: 'diff',
|
||||||
title: () => t('changes'),
|
title: () => t('changes'),
|
||||||
icon: (size: number) => <IconDiffOutline16 size={size} />,
|
icon: changesTabIcon,
|
||||||
order: -1,
|
order: -1,
|
||||||
hidden: true,
|
hidden: true,
|
||||||
dedupeKey: (tab) => tab.id,
|
dedupeKey: (tab) => tab.id,
|
||||||
|
|||||||
@@ -50,7 +50,7 @@
|
|||||||
* client.js); an edit that does land while a core HMR happens is caught by
|
* client.js); an edit that does land while a core HMR happens is caught by
|
||||||
* the ETag comparison on the next activation.
|
* the ETag comparison on the next activation.
|
||||||
*/
|
*/
|
||||||
export type ChunkName = 'terminal' | 'editor' | 'mermaid' | 'locale' | 'file-icons'
|
export type ChunkName = 'terminal' | 'editor' | 'mermaid' | 'locale'
|
||||||
|
|
||||||
/** The module exports a chunk factory provides (namespace-ish record). */
|
/** The module exports a chunk factory provides (namespace-ish record). */
|
||||||
export type ChunkExports = Record<string, unknown>
|
export type ChunkExports = Record<string, unknown>
|
||||||
|
|||||||
@@ -1,773 +0,0 @@
|
|||||||
/**
|
|
||||||
* Lazy chunk entry: the colored file-icon theme dataset — brand and generic
|
|
||||||
* glyphs paired with their display color (extensions / exact file names /
|
|
||||||
* directory names), consumed by `registerColoredFileIcons` in the core half
|
|
||||||
* when the user turns the theme on.
|
|
||||||
*
|
|
||||||
* Data provenance: the tables below come from PR #429 (@fenter,
|
|
||||||
* `feat/file-type-icons`, the 563-entry table); they are reused here as the
|
|
||||||
* plugin's built-in OPTIONAL icon theme. The case-variant folder keys
|
|
||||||
* (`Android`/`android`, `IOS`/`ios`) were folded into one rule because the
|
|
||||||
* resolver matches names case-insensitively.
|
|
||||||
*
|
|
||||||
* Why a chunk: ~160 react-icons glyphs (Simple Icons brands + VSCodicons) and
|
|
||||||
* ~700 lines of table would otherwise sit in the startup bundle for a theme
|
|
||||||
* most users never enable. The core half only fetches this on demand.
|
|
||||||
*
|
|
||||||
* Skin contract: this module is the ONE documented exception to the
|
|
||||||
* token-only rule (guide §12) — brand colors are identity, not chrome, and
|
|
||||||
* the theme is opt-in, so they cannot ride `--dsw-alias-*`. Nothing here is
|
|
||||||
* imported by the core bundle; keep it that way.
|
|
||||||
*/
|
|
||||||
import type { ReactNode } from 'react'
|
|
||||||
import type { BetterSidebarService, FileIconDescriptor } from '../service.ts'
|
|
||||||
import {
|
|
||||||
SiAngular, SiApple, SiAstro, SiBabel, SiBiome, SiBun, SiC, SiClojure,
|
|
||||||
SiCmake, SiCoffeescript, SiComposer, SiConventionalcommits, SiCplusplus,
|
|
||||||
SiCrystal, SiCss, SiCypress, SiDart, SiDjango, SiDocker, SiDocusaurus,
|
|
||||||
SiDotenv, SiDotnet, SiElixir, SiElm, SiErlang, SiEsbuild, SiEslint,
|
|
||||||
SiGatsby, SiGit, SiGithub, SiGitlab, SiGnubash, SiGo, SiGodotengine,
|
|
||||||
SiGooglecloud, SiGradle, SiGraphql, SiGrunt, SiGulp, SiHaskell, SiHelm,
|
|
||||||
SiHtml5, SiJavascript, SiJenkins, SiJest, SiJulia, SiKotlin, SiKubernetes,
|
|
||||||
SiLerna, SiLess, SiLua, SiMarkdown, SiMysql, SiNetlify, SiNextdotjs, SiNim,
|
|
||||||
SiNixos, SiNodedotjs, SiNpm, SiNuxt, SiNx, SiOcaml, SiPerl, SiPhp, SiPnpm,
|
|
||||||
SiPostcss, SiPrettier, SiPrisma, SiPython, SiRacket, SiReact, SiRemix,
|
|
||||||
SiRollupdotjs, SiRuby, SiRust, SiSass, SiScala, SiSolidity, SiSqlite,
|
|
||||||
SiStorybook, SiStylelint, SiSvelte, SiSwc, SiSwift, SiTailwindcss, SiTauri,
|
|
||||||
SiTerraform, SiToml, SiTurbo, SiTypescript, SiVercel, SiVim, SiVite,
|
|
||||||
SiVitepress, SiVitest, SiVuedotjs, SiWebpack, SiYaml, SiYarn, SiZig
|
|
||||||
} from 'react-icons/si'
|
|
||||||
import {
|
|
||||||
VscBook, VscFile, VscFileBinary, VscFileCode, VscFileMedia, VscFilePdf,
|
|
||||||
VscFileText, VscFileZip, VscFolder, VscFolderOpened, VscJson, VscKey,
|
|
||||||
VscMap, VscMusic, VscSettings, VscTerminalCmd, VscTerminalPowershell
|
|
||||||
} from 'react-icons/vsc'
|
|
||||||
import type { IconType } from 'react-icons'
|
|
||||||
|
|
||||||
interface IconEntry {
|
|
||||||
Icon: IconType
|
|
||||||
color: string
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Render one colored icon at the tree's 14px size. */
|
|
||||||
export function colored(entry: IconEntry, size = 14): ReactNode {
|
|
||||||
const { Icon, color } = entry
|
|
||||||
return <Icon size={size} style={{ color }} />
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Brand file icons (extension → { icon, brand color }) ────────────────
|
|
||||||
|
|
||||||
export const FILE_ICON_BY_EXT: Partial<Record<string, IconEntry>> = {
|
|
||||||
// TypeScript / JavaScript
|
|
||||||
ts: { Icon: SiTypescript, color: '#3178C6' },
|
|
||||||
tsx: { Icon: SiReact, color: '#61DAFB' },
|
|
||||||
js: { Icon: SiJavascript, color: '#F7DF1E' },
|
|
||||||
jsx: { Icon: SiReact, color: '#61DAFB' },
|
|
||||||
mjs: { Icon: SiJavascript, color: '#F7DF1E' },
|
|
||||||
cjs: { Icon: SiJavascript, color: '#F7DF1E' },
|
|
||||||
mtjs: { Icon: SiJavascript, color: '#F7DF1E' },
|
|
||||||
cts: { Icon: SiTypescript, color: '#3178C6' },
|
|
||||||
// Web frameworks
|
|
||||||
vue: { Icon: SiVuedotjs, color: '#42B883' },
|
|
||||||
svelte: { Icon: SiSvelte, color: '#FF3E00' },
|
|
||||||
astro: { Icon: SiAstro, color: '#FF5E00' },
|
|
||||||
html: { Icon: SiHtml5, color: '#E34F26' },
|
|
||||||
htm: { Icon: SiHtml5, color: '#E34F26' },
|
|
||||||
xhtml: { Icon: SiHtml5, color: '#E34F26' },
|
|
||||||
css: { Icon: SiCss, color: '#1572B6' },
|
|
||||||
scss: { Icon: SiSass, color: '#CC6699' },
|
|
||||||
sass: { Icon: SiSass, color: '#CC6699' },
|
|
||||||
less: { Icon: SiLess, color: '#1D365D' },
|
|
||||||
styl: { Icon: VscFileCode, color: '#BF1B1B' },
|
|
||||||
pug: { Icon: VscFileCode, color: '#A86454' },
|
|
||||||
jade: { Icon: VscFileCode, color: '#A86454' },
|
|
||||||
hbs: { Icon: VscFileCode, color: '#F0772B' },
|
|
||||||
handlebars: { Icon: VscFileCode, color: '#F0772B' },
|
|
||||||
ejs: { Icon: VscFileCode, color: '#E44D26' },
|
|
||||||
eta: { Icon: VscFileCode, color: '#E44D26' },
|
|
||||||
njk: { Icon: VscFileCode, color: '#2C7873' },
|
|
||||||
nunjucks: { Icon: VscFileCode, color: '#2C7873' },
|
|
||||||
twig: { Icon: VscFileCode, color: '#41C47B' },
|
|
||||||
liquid: { Icon: VscFileCode, color: '#41C47B' },
|
|
||||||
// Systems
|
|
||||||
py: { Icon: SiPython, color: '#3776AB' },
|
|
||||||
pyi: { Icon: SiPython, color: '#3776AB' },
|
|
||||||
pyw: { Icon: SiPython, color: '#3776AB' },
|
|
||||||
rb: { Icon: SiRuby, color: '#CC342D' },
|
|
||||||
go: { Icon: SiGo, color: '#00ADD8' },
|
|
||||||
rs: { Icon: SiRust, color: '#DEA584' },
|
|
||||||
php: { Icon: SiPhp, color: '#777BB4' },
|
|
||||||
swift: { Icon: SiSwift, color: '#F05138' },
|
|
||||||
kt: { Icon: SiKotlin, color: '#7F52FF' },
|
|
||||||
kts: { Icon: SiKotlin, color: '#7F52FF' },
|
|
||||||
dart: { Icon: SiDart, color: '#0175C2' },
|
|
||||||
lua: { Icon: SiLua, color: '#2C2D72' },
|
|
||||||
scala: { Icon: SiScala, color: '#DC322F' },
|
|
||||||
clj: { Icon: SiClojure, color: '#5F5F5F' },
|
|
||||||
cljs: { Icon: SiClojure, color: '#5F5F5F' },
|
|
||||||
cljc: { Icon: SiClojure, color: '#5F5F5F' },
|
|
||||||
edn: { Icon: SiClojure, color: '#5F5F5F' },
|
|
||||||
pl: { Icon: SiPerl, color: '#393939' },
|
|
||||||
pm: { Icon: SiPerl, color: '#393939' },
|
|
||||||
hs: { Icon: SiHaskell, color: '#5E5086' },
|
|
||||||
erl: { Icon: SiErlang, color: '#A90533' },
|
|
||||||
jl: { Icon: SiJulia, color: '#9558B2' },
|
|
||||||
ex: { Icon: SiElixir, color: '#4B275F' },
|
|
||||||
exs: { Icon: SiElixir, color: '#4B275F' },
|
|
||||||
elm: { Icon: SiElm, color: '#60B5CC' },
|
|
||||||
nim: { Icon: SiNim, color: '#FFE95B' },
|
|
||||||
zig: { Icon: SiZig, color: '#F7A41D' },
|
|
||||||
ml: { Icon: SiOcaml, color: '#EC672F' },
|
|
||||||
mli: { Icon: SiOcaml, color: '#EC672F' },
|
|
||||||
sol: { Icon: SiSolidity, color: '#363636' },
|
|
||||||
rkt: { Icon: SiRacket, color: '#9C1C1C' },
|
|
||||||
vim: { Icon: SiVim, color: '#019733' },
|
|
||||||
viml: { Icon: SiVim, color: '#019733' },
|
|
||||||
ahk: { Icon: VscFileCode, color: '#334455' },
|
|
||||||
feature: { Icon: VscFileCode, color: '#00C356' },
|
|
||||||
groovy: { Icon: VscFileCode, color: '#4298B8' },
|
|
||||||
gradle: { Icon: SiGradle, color: '#02303A' },
|
|
||||||
proto: { Icon: VscFileCode, color: '#4285F4' },
|
|
||||||
thrift: { Icon: VscFileCode, color: '#6B7280' },
|
|
||||||
nix: { Icon: SiNixos, color: '#7EB1D6' },
|
|
||||||
// C / C++
|
|
||||||
c: { Icon: SiC, color: '#A8B9CC' },
|
|
||||||
h: { Icon: SiC, color: '#A8B9CC' },
|
|
||||||
cpp: { Icon: SiCplusplus, color: '#00599C' },
|
|
||||||
cc: { Icon: SiCplusplus, color: '#00599C' },
|
|
||||||
cxx: { Icon: SiCplusplus, color: '#00599C' },
|
|
||||||
hpp: { Icon: SiCplusplus, color: '#00599C' },
|
|
||||||
hxx: { Icon: SiCplusplus, color: '#00599C' },
|
|
||||||
ino: { Icon: VscFileCode, color: '#00878F' },
|
|
||||||
cs: { Icon: SiDotnet, color: '#512BD4' },
|
|
||||||
fs: { Icon: SiDotnet, color: '#512BD4' },
|
|
||||||
fsx: { Icon: SiDotnet, color: '#512BD4' },
|
|
||||||
vb: { Icon: SiDotnet, color: '#512BD4' },
|
|
||||||
// Shell
|
|
||||||
sh: { Icon: SiGnubash, color: '#4EAA25' },
|
|
||||||
bash: { Icon: SiGnubash, color: '#4EAA25' },
|
|
||||||
zsh: { Icon: SiGnubash, color: '#4EAA25' },
|
|
||||||
fish: { Icon: SiGnubash, color: '#4EAA25' },
|
|
||||||
ps1: { Icon: VscTerminalPowershell, color: '#012456' },
|
|
||||||
psd1: { Icon: VscTerminalPowershell, color: '#012456' },
|
|
||||||
psm1: { Icon: VscTerminalPowershell, color: '#012456' },
|
|
||||||
bat: { Icon: VscTerminalCmd, color: '#C8C8C8' },
|
|
||||||
cmd: { Icon: VscTerminalCmd, color: '#C8C8C8' },
|
|
||||||
awk: { Icon: VscFileCode, color: '#4EAA25' },
|
|
||||||
// Data / config
|
|
||||||
json: { Icon: VscJson, color: '#F7DF1E' },
|
|
||||||
jsonc: { Icon: VscJson, color: '#F7DF1E' },
|
|
||||||
json5: { Icon: VscJson, color: '#F7DF1E' },
|
|
||||||
yaml: { Icon: SiYaml, color: '#CB171E' },
|
|
||||||
yml: { Icon: SiYaml, color: '#CB171E' },
|
|
||||||
toml: { Icon: SiToml, color: '#9C4121' },
|
|
||||||
ini: { Icon: VscSettings, color: '#8B5CF6' },
|
|
||||||
conf: { Icon: VscSettings, color: '#8B5CF6' },
|
|
||||||
cfg: { Icon: VscSettings, color: '#8B5CF6' },
|
|
||||||
env: { Icon: SiDotenv, color: '#ECD53F' },
|
|
||||||
xml: { Icon: VscFileCode, color: '#E3792B' },
|
|
||||||
plist: { Icon: VscSettings, color: '#6B7280' },
|
|
||||||
properties: { Icon: VscSettings, color: '#8B5CF6' },
|
|
||||||
csv: { Icon: VscFileText, color: '#2CB553' },
|
|
||||||
tsv: { Icon: VscFileText, color: '#2CB553' },
|
|
||||||
// Docs
|
|
||||||
md: { Icon: SiMarkdown, color: '#0844B8' },
|
|
||||||
markdown: { Icon: SiMarkdown, color: '#0844B8' },
|
|
||||||
mdx: { Icon: SiMarkdown, color: '#0844B8' },
|
|
||||||
txt: { Icon: VscFileText, color: '#6B7280' },
|
|
||||||
log: { Icon: VscFileText, color: '#6B7280' },
|
|
||||||
pdf: { Icon: VscFilePdf, color: '#E53935' },
|
|
||||||
rst: { Icon: VscFileText, color: '#7D3CBE' },
|
|
||||||
adoc: { Icon: VscFileText, color: '#E40046' },
|
|
||||||
asciidoc: { Icon: VscFileText, color: '#E40046' },
|
|
||||||
tex: { Icon: VscFileText, color: '#3D6117' },
|
|
||||||
latex: { Icon: VscFileText, color: '#3D6117' },
|
|
||||||
org: { Icon: VscFileText, color: '#77D0B0' },
|
|
||||||
rtf: { Icon: VscFileText, color: '#6B7280' },
|
|
||||||
// Office
|
|
||||||
doc: { Icon: VscFileText, color: '#2B579A' },
|
|
||||||
docx: { Icon: VscFileText, color: '#2B579A' },
|
|
||||||
xls: { Icon: VscFileText, color: '#217346' },
|
|
||||||
xlsx: { Icon: VscFileText, color: '#217346' },
|
|
||||||
ppt: { Icon: VscFileText, color: '#D24726' },
|
|
||||||
pptx: { Icon: VscFileText, color: '#D24726' },
|
|
||||||
odt: { Icon: VscFileText, color: '#18A303' },
|
|
||||||
ods: { Icon: VscFileText, color: '#18A303' },
|
|
||||||
odp: { Icon: VscFileText, color: '#18A303' },
|
|
||||||
// Images
|
|
||||||
png: { Icon: VscFileMedia, color: '#A855F7' },
|
|
||||||
jpg: { Icon: VscFileMedia, color: '#A855F7' },
|
|
||||||
jpeg: { Icon: VscFileMedia, color: '#A855F7' },
|
|
||||||
gif: { Icon: VscFileMedia, color: '#A855F7' },
|
|
||||||
webp: { Icon: VscFileMedia, color: '#A855F7' },
|
|
||||||
svg: { Icon: VscFileMedia, color: '#FFB13B' },
|
|
||||||
ico: { Icon: VscFileMedia, color: '#A855F7' },
|
|
||||||
bmp: { Icon: VscFileMedia, color: '#A855F7' },
|
|
||||||
avif: { Icon: VscFileMedia, color: '#A855F7' },
|
|
||||||
tiff: { Icon: VscFileMedia, color: '#A855F7' },
|
|
||||||
tif: { Icon: VscFileMedia, color: '#A855F7' },
|
|
||||||
heic: { Icon: VscFileMedia, color: '#A855F7' },
|
|
||||||
heif: { Icon: VscFileMedia, color: '#A855F7' },
|
|
||||||
psd: { Icon: VscFileMedia, color: '#31A8FF' },
|
|
||||||
ai: { Icon: VscFileMedia, color: '#FF9A00' },
|
|
||||||
eps: { Icon: VscFileMedia, color: '#FFB13B' },
|
|
||||||
raw: { Icon: VscFileMedia, color: '#6B7280' },
|
|
||||||
xcf: { Icon: VscFileMedia, color: '#535555' },
|
|
||||||
// Video
|
|
||||||
mp4: { Icon: VscFileMedia, color: '#FF6B6B' },
|
|
||||||
webm: { Icon: VscFileMedia, color: '#FF6B6B' },
|
|
||||||
mov: { Icon: VscFileMedia, color: '#FF6B6B' },
|
|
||||||
mkv: { Icon: VscFileMedia, color: '#FF6B6B' },
|
|
||||||
avi: { Icon: VscFileMedia, color: '#FF6B6B' },
|
|
||||||
wmv: { Icon: VscFileMedia, color: '#FF6B6B' },
|
|
||||||
flv: { Icon: VscFileMedia, color: '#FF6B6B' },
|
|
||||||
m4v: { Icon: VscFileMedia, color: '#FF6B6B' },
|
|
||||||
m3u8: { Icon: VscFileMedia, color: '#FF6B6B' },
|
|
||||||
// Audio
|
|
||||||
mp3: { Icon: VscMusic, color: '#EC4899' },
|
|
||||||
wav: { Icon: VscMusic, color: '#EC4899' },
|
|
||||||
flac: { Icon: VscMusic, color: '#EC4899' },
|
|
||||||
ogg: { Icon: VscMusic, color: '#EC4899' },
|
|
||||||
m4a: { Icon: VscMusic, color: '#EC4899' },
|
|
||||||
aac: { Icon: VscMusic, color: '#EC4899' },
|
|
||||||
opus: { Icon: VscMusic, color: '#EC4899' },
|
|
||||||
midi: { Icon: VscMusic, color: '#EC4899' },
|
|
||||||
mid: { Icon: VscMusic, color: '#EC4899' },
|
|
||||||
wma: { Icon: VscMusic, color: '#EC4899' },
|
|
||||||
aiff: { Icon: VscMusic, color: '#EC4899' },
|
|
||||||
mka: { Icon: VscMusic, color: '#EC4899' },
|
|
||||||
// Archives
|
|
||||||
zip: { Icon: VscFileZip, color: '#F59E0B' },
|
|
||||||
tar: { Icon: VscFileZip, color: '#F59E0B' },
|
|
||||||
gz: { Icon: VscFileZip, color: '#F59E0B' },
|
|
||||||
tgz: { Icon: VscFileZip, color: '#F59E0B' },
|
|
||||||
rar: { Icon: VscFileZip, color: '#F59E0B' },
|
|
||||||
'7z': { Icon: VscFileZip, color: '#F59E0B' },
|
|
||||||
bz2: { Icon: VscFileZip, color: '#F59E0B' },
|
|
||||||
xz: { Icon: VscFileZip, color: '#F59E0B' },
|
|
||||||
iso: { Icon: VscFileZip, color: '#F59E0B' },
|
|
||||||
dmg: { Icon: VscFileZip, color: '#F59E0B' },
|
|
||||||
jar: { Icon: VscFileZip, color: '#F59E0B' },
|
|
||||||
war: { Icon: VscFileZip, color: '#F59E0B' },
|
|
||||||
ear: { Icon: VscFileZip, color: '#F59E0B' },
|
|
||||||
apk: { Icon: VscFileZip, color: '#3DDC84' },
|
|
||||||
deb: { Icon: VscFileZip, color: '#A81D33' },
|
|
||||||
rpm: { Icon: VscFileZip, color: '#000000' },
|
|
||||||
msi: { Icon: VscFileZip, color: '#6B7280' },
|
|
||||||
lz: { Icon: VscFileZip, color: '#F59E0B' },
|
|
||||||
zst: { Icon: VscFileZip, color: '#F59E0B' },
|
|
||||||
cab: { Icon: VscFileZip, color: '#F59E0B' },
|
|
||||||
// Database
|
|
||||||
db: { Icon: SiSqlite, color: '#003B57' },
|
|
||||||
sqlite: { Icon: SiSqlite, color: '#003B57' },
|
|
||||||
sqlite3: { Icon: SiSqlite, color: '#003B57' },
|
|
||||||
sql: { Icon: SiMysql, color: '#4479A1' },
|
|
||||||
// Source maps
|
|
||||||
map: { Icon: VscMap, color: '#6B7280' },
|
|
||||||
// Keys / certs
|
|
||||||
pem: { Icon: VscKey, color: '#10B981' },
|
|
||||||
key: { Icon: VscKey, color: '#10B981' },
|
|
||||||
crt: { Icon: VscKey, color: '#10B981' },
|
|
||||||
pub: { Icon: VscKey, color: '#10B981' },
|
|
||||||
cer: { Icon: VscKey, color: '#10B981' },
|
|
||||||
der: { Icon: VscKey, color: '#10B981' },
|
|
||||||
p12: { Icon: VscKey, color: '#10B981' },
|
|
||||||
pfx: { Icon: VscKey, color: '#10B981' },
|
|
||||||
jks: { Icon: VscKey, color: '#10B981' },
|
|
||||||
keystore: { Icon: VscKey, color: '#10B981' },
|
|
||||||
csr: { Icon: VscKey, color: '#10B981' },
|
|
||||||
ovpn: { Icon: VscKey, color: '#10B981' },
|
|
||||||
// Fonts
|
|
||||||
ttf: { Icon: VscFileBinary, color: '#A855F7' },
|
|
||||||
otf: { Icon: VscFileBinary, color: '#A855F7' },
|
|
||||||
woff: { Icon: VscFileBinary, color: '#A855F7' },
|
|
||||||
woff2: { Icon: VscFileBinary, color: '#A855F7' },
|
|
||||||
eot: { Icon: VscFileBinary, color: '#A855F7' },
|
|
||||||
// GraphQL
|
|
||||||
graphql: { Icon: SiGraphql, color: '#E10098' },
|
|
||||||
gql: { Icon: SiGraphql, color: '#E10098' },
|
|
||||||
// Terraform / IaC
|
|
||||||
tf: { Icon: SiTerraform, color: '#7B42BC' },
|
|
||||||
tfvars: { Icon: SiTerraform, color: '#7B42BC' },
|
|
||||||
tfstate: { Icon: SiTerraform, color: '#7B42BC' },
|
|
||||||
hcl: { Icon: SiTerraform, color: '#7B42BC' },
|
|
||||||
// Godot
|
|
||||||
gd: { Icon: SiGodotengine, color: '#478CBF' },
|
|
||||||
// Crystal
|
|
||||||
cr: { Icon: SiCrystal, color: '#000000' },
|
|
||||||
// CoffeeScript
|
|
||||||
coffee: { Icon: SiCoffeescript, color: '#2F2614' },
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Special filenames (exact match, case-sensitive) ──────────────────────
|
|
||||||
|
|
||||||
export const FILE_ICON_BY_NAME: Partial<Record<string, IconEntry>> = {
|
|
||||||
// Package managers
|
|
||||||
'package.json': { Icon: SiNpm, color: '#CB3837' },
|
|
||||||
'package-lock.json': { Icon: SiNpm, color: '#CB3837' },
|
|
||||||
'pnpm-lock.yaml': { Icon: SiPnpm, color: '#F69220' },
|
|
||||||
'yarn.lock': { Icon: SiYarn, color: '#2C8EBB' },
|
|
||||||
'Cargo.lock': { Icon: SiRust, color: '#DEA584' },
|
|
||||||
'Cargo.toml': { Icon: SiRust, color: '#DEA584' },
|
|
||||||
'go.sum': { Icon: SiGo, color: '#00ADD8' },
|
|
||||||
'go.mod': { Icon: SiGo, color: '#00ADD8' },
|
|
||||||
'go.work': { Icon: SiGo, color: '#00ADD8' },
|
|
||||||
'composer.json': { Icon: SiComposer, color: '#885630' },
|
|
||||||
'composer.lock': { Icon: SiComposer, color: '#885630' },
|
|
||||||
'poetry.lock': { Icon: SiPython, color: '#3776AB' },
|
|
||||||
'uv.lock': { Icon: SiPython, color: '#3776AB' },
|
|
||||||
'bun.lockb': { Icon: SiBun, color: '#FBF0DF' },
|
|
||||||
'bun.lock': { Icon: SiBun, color: '#FBF0DF' },
|
|
||||||
'mix.lock': { Icon: SiElixir, color: '#4B275F' },
|
|
||||||
'Podfile': { Icon: SiApple, color: '#A6A6A6' },
|
|
||||||
'Podfile.lock': { Icon: SiApple, color: '#A6A6A6' },
|
|
||||||
'Package.resolved': { Icon: SiSwift, color: '#F05138' },
|
|
||||||
'packages.lock.json': { Icon: SiNodedotjs, color: '#339933' },
|
|
||||||
'flake.lock': { Icon: SiNixos, color: '#7EB1D6' },
|
|
||||||
'flake.nix': { Icon: SiNixos, color: '#7EB1D6' },
|
|
||||||
'default.nix': { Icon: SiNixos, color: '#7EB1D6' },
|
|
||||||
'shell.nix': { Icon: SiNixos, color: '#7EB1D6' },
|
|
||||||
// Git
|
|
||||||
'.gitignore': { Icon: SiGit, color: '#F05032' },
|
|
||||||
'.gitattributes': { Icon: SiGit, color: '#F05032' },
|
|
||||||
'.gitmodules': { Icon: SiGit, color: '#F05032' },
|
|
||||||
'.gitconfig': { Icon: SiGit, color: '#F05032' },
|
|
||||||
'.gitmessage': { Icon: SiGit, color: '#F05032' },
|
|
||||||
'.git-blame-ignore-revs': { Icon: SiGit, color: '#F05032' },
|
|
||||||
'.gitkeep': { Icon: VscFile, color: '#6B7280' },
|
|
||||||
'.keep': { Icon: VscFile, color: '#6B7280' },
|
|
||||||
// Docs
|
|
||||||
'LICENSE': { Icon: VscBook, color: '#6B7280' },
|
|
||||||
'LICENSE.md': { Icon: VscBook, color: '#6B7280' },
|
|
||||||
'LICENSE.txt': { Icon: VscBook, color: '#6B7280' },
|
|
||||||
'README.md': { Icon: SiMarkdown, color: '#0844B8' },
|
|
||||||
'CHANGELOG.md': { Icon: SiMarkdown, color: '#0844B8' },
|
|
||||||
'CONTRIBUTING.md': { Icon: VscBook, color: '#6B7280' },
|
|
||||||
'CODE_OF_CONDUCT.md': { Icon: VscBook, color: '#6B7280' },
|
|
||||||
'SECURITY.md': { Icon: VscBook, color: '#6B7280' },
|
|
||||||
'AUTHORS': { Icon: VscBook, color: '#6B7280' },
|
|
||||||
'MAINTAINERS': { Icon: VscBook, color: '#6B7280' },
|
|
||||||
'CODEOWNERS': { Icon: SiGithub, color: '#181717' },
|
|
||||||
// Docker
|
|
||||||
'Dockerfile': { Icon: SiDocker, color: '#2496ED' },
|
|
||||||
'Dockerfile.dev': { Icon: SiDocker, color: '#2496ED' },
|
|
||||||
'Dockerfile.prod': { Icon: SiDocker, color: '#2496ED' },
|
|
||||||
'Dockerfile.test': { Icon: SiDocker, color: '#2496ED' },
|
|
||||||
'docker-compose.yml': { Icon: SiDocker, color: '#2496ED' },
|
|
||||||
'docker-compose.yaml': { Icon: SiDocker, color: '#2496ED' },
|
|
||||||
'docker-compose.override.yml': { Icon: SiDocker, color: '#2496ED' },
|
|
||||||
'Containerfile': { Icon: SiDocker, color: '#2496ED' },
|
|
||||||
'.dockerignore': { Icon: SiDocker, color: '#2496ED' },
|
|
||||||
'.containerignore': { Icon: SiDocker, color: '#2496ED' },
|
|
||||||
// Build
|
|
||||||
'Makefile': { Icon: SiCmake, color: '#064F8C' },
|
|
||||||
'CMakeLists.txt': { Icon: SiCmake, color: '#064F8C' },
|
|
||||||
'.editorconfig': { Icon: VscSettings, color: '#E0EFEF' },
|
|
||||||
// Env
|
|
||||||
'.env': { Icon: SiDotenv, color: '#ECD53F' },
|
|
||||||
'.env.local': { Icon: SiDotenv, color: '#ECD53F' },
|
|
||||||
'.env.development': { Icon: SiDotenv, color: '#ECD53F' },
|
|
||||||
'.env.production': { Icon: SiDotenv, color: '#ECD53F' },
|
|
||||||
'.env.example': { Icon: SiDotenv, color: '#ECD53F' },
|
|
||||||
'.env.test': { Icon: SiDotenv, color: '#ECD53F' },
|
|
||||||
'.env.staging': { Icon: SiDotenv, color: '#ECD53F' },
|
|
||||||
'.env.docker': { Icon: SiDotenv, color: '#ECD53F' },
|
|
||||||
// TypeScript / JS config
|
|
||||||
'tsconfig.json': { Icon: SiTypescript, color: '#3178C6' },
|
|
||||||
'tsconfig.base.json': { Icon: SiTypescript, color: '#3178C6' },
|
|
||||||
'jsconfig.json': { Icon: SiJavascript, color: '#F7DF1E' },
|
|
||||||
// Bundlers / build tools
|
|
||||||
'vite.config.ts': { Icon: SiVite, color: '#646CFF' },
|
|
||||||
'vite.config.js': { Icon: SiVite, color: '#646CFF' },
|
|
||||||
'vite.config.mts': { Icon: SiVite, color: '#646CFF' },
|
|
||||||
'vite.config.mjs': { Icon: SiVite, color: '#646CFF' },
|
|
||||||
'webpack.config.js': { Icon: SiWebpack, color: '#1C78C0' },
|
|
||||||
'webpack.config.ts': { Icon: SiWebpack, color: '#1C78C0' },
|
|
||||||
'rollup.config.js': { Icon: SiRollupdotjs, color: '#C3413D' },
|
|
||||||
'rollup.config.mjs': { Icon: SiRollupdotjs, color: '#C3413D' },
|
|
||||||
'esbuild.config.js': { Icon: SiEsbuild, color: '#FFCF00' },
|
|
||||||
'gulpfile.js': { Icon: SiGulp, color: '#CF4647' },
|
|
||||||
'gulpfile.ts': { Icon: SiGulp, color: '#CF4647' },
|
|
||||||
'gulpfile.mjs': { Icon: SiGulp, color: '#CF4647' },
|
|
||||||
'gruntfile.js': { Icon: SiGrunt, color: '#FBA937' },
|
|
||||||
'tsup.config.ts': { Icon: VscSettings, color: '#646CFF' },
|
|
||||||
'tsdown.config.ts': { Icon: VscSettings, color: '#646CFF' },
|
|
||||||
'unbuild.config.ts': { Icon: VscSettings, color: '#646CFF' },
|
|
||||||
'farm.config.ts': { Icon: VscSettings, color: '#646CFF' },
|
|
||||||
'rspack.config.js': { Icon: VscSettings, color: '#646CFF' },
|
|
||||||
'swc.config.js': { Icon: SiSwc, color: '#E0BD6E' },
|
|
||||||
'.swcrc': { Icon: SiSwc, color: '#E0BD6E' },
|
|
||||||
'turbo.json': { Icon: SiTurbo, color: '#6DD3C6' },
|
|
||||||
'turbo.config.json': { Icon: SiTurbo, color: '#6DD3C6' },
|
|
||||||
'nx.json': { Icon: SiNx, color: '#14305F' },
|
|
||||||
'lerna.json': { Icon: SiLerna, color: '#9333EA' },
|
|
||||||
'biome.json': { Icon: SiBiome, color: '#60A5FA' },
|
|
||||||
// Frameworks
|
|
||||||
'next.config.js': { Icon: SiNextdotjs, color: '#000000' },
|
|
||||||
'next.config.mjs': { Icon: SiNextdotjs, color: '#000000' },
|
|
||||||
'next.config.ts': { Icon: SiNextdotjs, color: '#000000' },
|
|
||||||
'nuxt.config.ts': { Icon: SiNuxt, color: '#00DC82' },
|
|
||||||
'nuxt.config.js': { Icon: SiNuxt, color: '#00DC82' },
|
|
||||||
'angular.json': { Icon: SiAngular, color: '#DD0031' },
|
|
||||||
'svelte.config.js': { Icon: SiSvelte, color: '#FF3E00' },
|
|
||||||
'astro.config.mjs': { Icon: SiAstro, color: '#FF5E00' },
|
|
||||||
'astro.config.ts': { Icon: SiAstro, color: '#FF5E00' },
|
|
||||||
'remix.config.js': { Icon: SiRemix, color: '#3992FF' },
|
|
||||||
'gatsby-config.js': { Icon: SiGatsby, color: '#663399' },
|
|
||||||
'gatsby-config.ts': { Icon: SiGatsby, color: '#663399' },
|
|
||||||
'tailwind.config.js': { Icon: SiTailwindcss, color: '#06B6D4' },
|
|
||||||
'tailwind.config.ts': { Icon: SiTailwindcss, color: '#06B6D4' },
|
|
||||||
'tauri.conf.json': { Icon: SiTauri, color: '#FFC131' },
|
|
||||||
'vercel.json': { Icon: SiVercel, color: '#000000' },
|
|
||||||
'netlify.toml': { Icon: SiNetlify, color: '#00C7B7' },
|
|
||||||
'docusaurus.config.js': { Icon: SiDocusaurus, color: '#3ECCF4' },
|
|
||||||
'docusaurus.config.ts': { Icon: SiDocusaurus, color: '#3ECCF4' },
|
|
||||||
'vitepress.config.ts': { Icon: SiVitepress, color: '#5672CD' },
|
|
||||||
'vitepress.config.js': { Icon: SiVitepress, color: '#5672CD' },
|
|
||||||
// Testing
|
|
||||||
'playwright.config.ts': { Icon: VscSettings, color: '#2EAD33' },
|
|
||||||
'playwright.config.js': { Icon: VscSettings, color: '#2EAD33' },
|
|
||||||
'vitest.config.ts': { Icon: SiVitest, color: '#FCC00B' },
|
|
||||||
'vitest.config.js': { Icon: SiVitest, color: '#FCC00B' },
|
|
||||||
'jest.config.js': { Icon: SiJest, color: '#C21325' },
|
|
||||||
'jest.config.ts': { Icon: SiJest, color: '#C21325' },
|
|
||||||
'cypress.config.ts': { Icon: SiCypress, color: '#17202C' },
|
|
||||||
'cypress.config.js': { Icon: SiCypress, color: '#17202C' },
|
|
||||||
// Linting / formatting
|
|
||||||
'.eslintrc.js': { Icon: SiEslint, color: '#4B32C3' },
|
|
||||||
'.eslintrc.json': { Icon: SiEslint, color: '#4B32C3' },
|
|
||||||
'.eslintrc.cjs': { Icon: SiEslint, color: '#4B32C3' },
|
|
||||||
'.eslintrc.mjs': { Icon: SiEslint, color: '#4B32C3' },
|
|
||||||
'.eslintrc.yml': { Icon: SiEslint, color: '#4B32C3' },
|
|
||||||
'eslint.config.js': { Icon: SiEslint, color: '#4B32C3' },
|
|
||||||
'eslint.config.mjs': { Icon: SiEslint, color: '#4B32C3' },
|
|
||||||
'eslint.config.ts': { Icon: SiEslint, color: '#4B32C3' },
|
|
||||||
'.eslintignore': { Icon: SiEslint, color: '#4B32C3' },
|
|
||||||
'.prettierrc': { Icon: SiPrettier, color: '#F7B93E' },
|
|
||||||
'.prettierrc.json': { Icon: SiPrettier, color: '#F7B93E' },
|
|
||||||
'.prettierrc.js': { Icon: SiPrettier, color: '#F7B93E' },
|
|
||||||
'.prettierrc.cjs': { Icon: SiPrettier, color: '#F7B93E' },
|
|
||||||
'.prettierrc.mjs': { Icon: SiPrettier, color: '#F7B93E' },
|
|
||||||
'.prettierrc.toml': { Icon: SiPrettier, color: '#F7B93E' },
|
|
||||||
'.prettierrc.json5': { Icon: SiPrettier, color: '#F7B93E' },
|
|
||||||
'prettier.config.js': { Icon: SiPrettier, color: '#F7B93E' },
|
|
||||||
'prettier.config.mjs': { Icon: SiPrettier, color: '#F7B93E' },
|
|
||||||
'.prettierignore': { Icon: SiPrettier, color: '#F7B93E' },
|
|
||||||
'.stylelintrc': { Icon: SiStylelint, color: '#263A6F' },
|
|
||||||
'.stylelintrc.json': { Icon: SiStylelint, color: '#263A6F' },
|
|
||||||
'.stylelintrc.js': { Icon: SiStylelint, color: '#263A6F' },
|
|
||||||
'.stylelintignore': { Icon: SiStylelint, color: '#263A6F' },
|
|
||||||
'babel.config.js': { Icon: SiBabel, color: '#F9DC3D' },
|
|
||||||
'babel.config.json': { Icon: SiBabel, color: '#F9DC3D' },
|
|
||||||
'.babelrc': { Icon: SiBabel, color: '#F9DC3D' },
|
|
||||||
'.babelrc.json': { Icon: SiBabel, color: '#F9DC3D' },
|
|
||||||
'.babelrc.cjs': { Icon: SiBabel, color: '#F9DC3D' },
|
|
||||||
'postcss.config.js': { Icon: SiPostcss, color: '#DD3A8C' },
|
|
||||||
'postcss.config.ts': { Icon: SiPostcss, color: '#DD3A8C' },
|
|
||||||
'.postcssrc': { Icon: SiPostcss, color: '#DD3A8C' },
|
|
||||||
'.postcssrc.json': { Icon: SiPostcss, color: '#DD3A8C' },
|
|
||||||
// Other config
|
|
||||||
'.npmrc': { Icon: SiNpm, color: '#CB3837' },
|
|
||||||
'.yarnrc': { Icon: SiYarn, color: '#2C8EBB' },
|
|
||||||
'.pnpmrc': { Icon: SiPnpm, color: '#F69220' },
|
|
||||||
'.npmignore': { Icon: SiNpm, color: '#CB3837' },
|
|
||||||
'.nvmrc': { Icon: SiNodedotjs, color: '#339933' },
|
|
||||||
'.node-version': { Icon: SiNodedotjs, color: '#339933' },
|
|
||||||
'.ruby-version': { Icon: SiRuby, color: '#CC342D' },
|
|
||||||
'.python-version': { Icon: SiPython, color: '#3776AB' },
|
|
||||||
'.lintstagedrc': { Icon: VscSettings, color: '#F7B500' },
|
|
||||||
'.lintstagedrc.json': { Icon: VscSettings, color: '#F7B500' },
|
|
||||||
'.commitlintrc': { Icon: SiConventionalcommits, color: '#FE5197' },
|
|
||||||
'.commitlintrc.json': { Icon: SiConventionalcommits, color: '#FE5197' },
|
|
||||||
'.releaserc': { Icon: VscSettings, color: '#F7B500' },
|
|
||||||
'.releaserc.json': { Icon: VscSettings, color: '#F7B500' },
|
|
||||||
'.czrc': { Icon: VscSettings, color: '#F7B500' },
|
|
||||||
// Ruby / Python
|
|
||||||
'Gemfile': { Icon: SiRuby, color: '#CC342D' },
|
|
||||||
'Gemfile.lock': { Icon: SiRuby, color: '#CC342D' },
|
|
||||||
'Rakefile': { Icon: SiRuby, color: '#CC342D' },
|
|
||||||
'requirements.txt': { Icon: SiPython, color: '#3776AB' },
|
|
||||||
'setup.py': { Icon: SiPython, color: '#3776AB' },
|
|
||||||
'pyproject.toml': { Icon: SiPython, color: '#3776AB' },
|
|
||||||
'manage.py': { Icon: SiDjango, color: '#092E20' },
|
|
||||||
// CI/CD
|
|
||||||
'Jenkinsfile': { Icon: SiJenkins, color: '#D24939' },
|
|
||||||
'.gitlab-ci.yml': { Icon: SiGitlab, color: '#FC6D26' },
|
|
||||||
'.travis.yml': { Icon: VscSettings, color: '#3EAAAF' },
|
|
||||||
'azure-pipelines.yml': { Icon: VscSettings, color: '#0078D4' },
|
|
||||||
'bitbucket-pipelines.yml': { Icon: VscSettings, color: '#0052CC' },
|
|
||||||
'.drone.yml': { Icon: VscSettings, color: '#212121' },
|
|
||||||
'cloudbuild.yaml': { Icon: SiGooglecloud, color: '#4285F4' },
|
|
||||||
// K8s / Helm
|
|
||||||
'Chart.yaml': { Icon: SiHelm, color: '#0F1689' },
|
|
||||||
'values.yaml': { Icon: SiHelm, color: '#0F1689' },
|
|
||||||
'kustomization.yaml': { Icon: SiKubernetes, color: '#326CE5' },
|
|
||||||
'kustomization.yml': { Icon: SiKubernetes, color: '#326CE5' },
|
|
||||||
'skaffold.yaml': { Icon: SiKubernetes, color: '#326CE5' },
|
|
||||||
'.helmignore': { Icon: SiHelm, color: '#0F1689' },
|
|
||||||
// Serverless
|
|
||||||
'serverless.yml': { Icon: VscSettings, color: '#6B7280' },
|
|
||||||
'sam.yaml': { Icon: VscSettings, color: '#6B7280' },
|
|
||||||
'appspec.yml': { Icon: VscSettings, color: '#6B7280' },
|
|
||||||
// Prisma
|
|
||||||
'schema.prisma': { Icon: SiPrisma, color: '#5D8AA8' },
|
|
||||||
// Misc
|
|
||||||
'Procfile': { Icon: SiNodedotjs, color: '#339933' },
|
|
||||||
'.storybook/main.js': { Icon: SiStorybook, color: '#FF4785' },
|
|
||||||
'.storybook/main.ts': { Icon: SiStorybook, color: '#FF4785' },
|
|
||||||
'.storybook/preview.js': { Icon: SiStorybook, color: '#FF4785' },
|
|
||||||
'.storybook/preview.ts': { Icon: SiStorybook, color: '#FF4785' },
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Special folder colors ───────────────────────────────────────────────
|
|
||||||
|
|
||||||
/** Folder name → tint color (applied to VscFolder / VscFolderOpened). */
|
|
||||||
export const FOLDER_COLOR: Record<string, string> = {
|
|
||||||
// Package / dependency
|
|
||||||
node_modules: '#CB3837',
|
|
||||||
packages: '#CB3837',
|
|
||||||
// Source
|
|
||||||
src: '#3178C6',
|
|
||||||
source: '#3178C6',
|
|
||||||
app: '#3178C6',
|
|
||||||
apps: '#3178C6',
|
|
||||||
core: '#A855F7',
|
|
||||||
common: '#6B7280',
|
|
||||||
shared: '#6B7280',
|
|
||||||
internal: '#6B7280',
|
|
||||||
modules: '#3178C6',
|
|
||||||
plugins: '#42B883',
|
|
||||||
// Git
|
|
||||||
'.git': '#F05032',
|
|
||||||
'.github': '#F05032',
|
|
||||||
'.husky': '#F05032',
|
|
||||||
// Output
|
|
||||||
public: '#42B883',
|
|
||||||
static: '#42B883',
|
|
||||||
dist: '#E34F26',
|
|
||||||
build: '#E34F26',
|
|
||||||
out: '#E34F26',
|
|
||||||
'.output': '#E34F26',
|
|
||||||
'.next': '#000000',
|
|
||||||
'.nuxt': '#00DC82',
|
|
||||||
'.svelte-kit': '#FF3E00',
|
|
||||||
'.astro': '#FF5E00',
|
|
||||||
'.turbo': '#6DD3C6',
|
|
||||||
'.parcel-cache': '#6B7280',
|
|
||||||
'.vercel': '#000000',
|
|
||||||
'.netlify': '#00C7B7',
|
|
||||||
'.serverless': '#6B7280',
|
|
||||||
'.terraform': '#7B42BC',
|
|
||||||
'.terragrunt-cache': '#7B42BC',
|
|
||||||
'.docusaurus': '#3ECCF4',
|
|
||||||
'.cache': '#6B7280',
|
|
||||||
// Testing
|
|
||||||
test: '#F7DF1E',
|
|
||||||
tests: '#F7DF1E',
|
|
||||||
__tests__: '#F7DF1E',
|
|
||||||
__mocks__: '#F7DF1E',
|
|
||||||
__snapshots__: '#F7DF1E',
|
|
||||||
__pacts__: '#F7DF1E',
|
|
||||||
spec: '#F7DF1E',
|
|
||||||
specs: '#F7DF1E',
|
|
||||||
mocks: '#F7DF1E',
|
|
||||||
fixtures: '#F7DF1E',
|
|
||||||
e2e: '#F7DF1E',
|
|
||||||
cypress: '#17202C',
|
|
||||||
'.nyc_output': '#F7DF1E',
|
|
||||||
coverage: '#42B883',
|
|
||||||
// IDE / editor
|
|
||||||
'.vscode': '#3178C6',
|
|
||||||
'.idea': '#3178C6',
|
|
||||||
'.devcontainer': '#3178C6',
|
|
||||||
'.codesandbox': '#6B7280',
|
|
||||||
'.stackblitz': '#6B7280',
|
|
||||||
'.gitpod': '#6B7280',
|
|
||||||
// Assets
|
|
||||||
assets: '#A855F7',
|
|
||||||
images: '#A855F7',
|
|
||||||
img: '#A855F7',
|
|
||||||
icons: '#A855F7',
|
|
||||||
fonts: '#A855F7',
|
|
||||||
media: '#A855F7',
|
|
||||||
resources: '#A855F7',
|
|
||||||
content: '#A855F7',
|
|
||||||
// Styles
|
|
||||||
styles: '#CC6699',
|
|
||||||
stylesheets: '#CC6699',
|
|
||||||
css: '#CC6699',
|
|
||||||
themes: '#CC6699',
|
|
||||||
templates: '#CC6699',
|
|
||||||
partials: '#CC6699',
|
|
||||||
layouts: '#CC6699',
|
|
||||||
// Components
|
|
||||||
components: '#42B883',
|
|
||||||
composables: '#42B883',
|
|
||||||
hooks: '#42B883',
|
|
||||||
// Library
|
|
||||||
lib: '#6B7280',
|
|
||||||
libs: '#6B7280',
|
|
||||||
vendor: '#6B7280',
|
|
||||||
// Docs
|
|
||||||
docs: '#0844B8',
|
|
||||||
documentation: '#0844B8',
|
|
||||||
examples: '#6B7280',
|
|
||||||
playground: '#42B883',
|
|
||||||
benchmarks: '#4EAA25',
|
|
||||||
// Scripts
|
|
||||||
scripts: '#4EAA25',
|
|
||||||
bin: '#4EAA25',
|
|
||||||
tools: '#4EAA25',
|
|
||||||
// Config
|
|
||||||
config: '#F7B500',
|
|
||||||
configs: '#F7B500',
|
|
||||||
'.config': '#F7B500',
|
|
||||||
// i18n
|
|
||||||
locales: '#F7B500',
|
|
||||||
i18n: '#F7B500',
|
|
||||||
translations: '#F7B500',
|
|
||||||
lang: '#F7B500',
|
|
||||||
language: '#F7B500',
|
|
||||||
// Routing
|
|
||||||
pages: '#3178C6',
|
|
||||||
views: '#3178C6',
|
|
||||||
routes: '#3178C6',
|
|
||||||
router: '#3178C6',
|
|
||||||
// State
|
|
||||||
store: '#A855F7',
|
|
||||||
stores: '#A855F7',
|
|
||||||
state: '#A855F7',
|
|
||||||
// Utils
|
|
||||||
utils: '#6B7280',
|
|
||||||
helpers: '#6B7280',
|
|
||||||
types: '#3178C6',
|
|
||||||
typings: '#3178C6',
|
|
||||||
// Backend
|
|
||||||
api: '#42B883',
|
|
||||||
server: '#42B883',
|
|
||||||
services: '#42B883',
|
|
||||||
client: '#3178C6',
|
|
||||||
clients: '#3178C6',
|
|
||||||
middleware: '#CC6699',
|
|
||||||
controllers: '#42B883',
|
|
||||||
models: '#A855F7',
|
|
||||||
entities: '#A855F7',
|
|
||||||
repositories: '#A855F7',
|
|
||||||
migrations: '#4479A1',
|
|
||||||
seeds: '#4479A1',
|
|
||||||
factories: '#4479A1',
|
|
||||||
// Jobs / workers
|
|
||||||
tasks: '#4EAA25',
|
|
||||||
jobs: '#4EAA25',
|
|
||||||
workers: '#4EAA25',
|
|
||||||
queues: '#4EAA25',
|
|
||||||
// Events
|
|
||||||
events: '#F7DF1E',
|
|
||||||
listeners: '#F7DF1E',
|
|
||||||
observers: '#F7DF1E',
|
|
||||||
notifications: '#F7DF1E',
|
|
||||||
mail: '#F7DF1E',
|
|
||||||
emails: '#F7DF1E',
|
|
||||||
channels: '#F7DF1E',
|
|
||||||
commands: '#4EAA25',
|
|
||||||
console: '#4EAA25',
|
|
||||||
cron: '#4EAA25',
|
|
||||||
// Temp / data
|
|
||||||
pub: '#42B883',
|
|
||||||
tmp: '#6B7280',
|
|
||||||
temp: '#6B7280',
|
|
||||||
cache: '#6B7280',
|
|
||||||
logs: '#6B7280',
|
|
||||||
data: '#A855F7',
|
|
||||||
backup: '#6B7280',
|
|
||||||
www: '#42B883',
|
|
||||||
webroot: '#42B883',
|
|
||||||
// CI/CD
|
|
||||||
'.gitlab': '#FC6D26',
|
|
||||||
'.circleci': '#6B7280',
|
|
||||||
'.travis': '#6B7280',
|
|
||||||
'.drone': '#6B7280',
|
|
||||||
'.azure': '#0078D4',
|
|
||||||
// Mobile
|
|
||||||
android: '#3DDC84',
|
|
||||||
ios: '#000000',
|
|
||||||
Platforms: '#3178C6',
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Registration ────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register the dataset through the public `registerFileIcon` API: one
|
|
||||||
* descriptor per distinct glyph+color (the resolver walks descriptors per
|
|
||||||
* row, so 563 rules collapse into ~250 registrations), plus one per folder
|
|
||||||
* tint. Returns a disposer that removes every registration.
|
|
||||||
*
|
|
||||||
* Folder tints ride `folderNames` only — they claim the directories they
|
|
||||||
* name, never every folder, so unnamed directories keep the built-in glyphs.
|
|
||||||
*/
|
|
||||||
export function registerColoredFileIcons(service: BetterSidebarService): () => void {
|
|
||||||
const disposers: (() => void)[] = []
|
|
||||||
const register = (descriptor: FileIconDescriptor): void => {
|
|
||||||
disposers.push(service.registerFileIcon(descriptor))
|
|
||||||
}
|
|
||||||
let seq = 0
|
|
||||||
|
|
||||||
// Extensions: group the table by (glyph, color) → one descriptor per group.
|
|
||||||
for (const [Icon, byColor] of groupByGlyph(FILE_ICON_BY_EXT)) {
|
|
||||||
for (const [color, exts] of byColor) {
|
|
||||||
register({
|
|
||||||
id: `colored:ext:${seq++}`,
|
|
||||||
exts,
|
|
||||||
icon: (_path, size) => colored({ Icon, color }, size),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exact file names (package.json, Dockerfile, …): same grouping, name rules.
|
|
||||||
for (const [Icon, byColor] of groupByGlyph(FILE_ICON_BY_NAME)) {
|
|
||||||
for (const [color, names] of byColor) {
|
|
||||||
register({
|
|
||||||
id: `colored:name:${seq++}`,
|
|
||||||
names,
|
|
||||||
icon: (_path, size) => colored({ Icon, color }, size),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Directory tints: VscFolder/VscFolderOpened in the rule's color, grouped
|
|
||||||
// by color (the folder table maps name → color, not name → glyph).
|
|
||||||
const foldersByColor = new Map<string, string[]>()
|
|
||||||
for (const [name, color] of Object.entries(FOLDER_COLOR)) {
|
|
||||||
const names = foldersByColor.get(color)
|
|
||||||
if (names === undefined) foldersByColor.set(color, [name])
|
|
||||||
else names.push(name)
|
|
||||||
}
|
|
||||||
for (const [color, folderNames] of foldersByColor) {
|
|
||||||
register({
|
|
||||||
id: `colored:folder:${seq++}`,
|
|
||||||
folderNames,
|
|
||||||
icon: (_path, size, open) => open === true
|
|
||||||
? <VscFolderOpened size={size} style={{ color }} />
|
|
||||||
: <VscFolder size={size} style={{ color }} />,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => { for (const dispose of disposers) dispose() }
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Regroup one name→{Icon,color} table into Icon → color → keys. */
|
|
||||||
function groupByGlyph(table: Partial<Record<string, IconEntry>>): Map<IconType, Map<string, string[]>> {
|
|
||||||
const grouped = new Map<IconType, Map<string, string[]>>()
|
|
||||||
for (const [key, entry] of Object.entries(table)) {
|
|
||||||
if (entry === undefined) continue
|
|
||||||
let byColor = grouped.get(entry.Icon)
|
|
||||||
if (byColor === undefined) {
|
|
||||||
byColor = new Map()
|
|
||||||
grouped.set(entry.Icon, byColor)
|
|
||||||
}
|
|
||||||
const keys = byColor.get(entry.color)
|
|
||||||
if (keys === undefined) byColor.set(entry.color, [key])
|
|
||||||
else keys.push(key)
|
|
||||||
}
|
|
||||||
return grouped
|
|
||||||
}
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
/**
|
|
||||||
* The optional colored file-icon theme (pref `fileIconTheme`).
|
|
||||||
*
|
|
||||||
* `'builtin'` (the default) is the monochrome glyph map the tree already
|
|
||||||
* uses — nothing to do. `'colored'` fetches the lazy `file-icons` chunk
|
|
||||||
* (~160 brand glyphs + 563 rules) and registers it through the public
|
|
||||||
* `registerFileIcon` API; switching back disposes those registrations. The
|
|
||||||
* chunk is only fetched when the user picks the theme, so the startup bundle
|
|
||||||
* never carries icon data it may not render.
|
|
||||||
*
|
|
||||||
* The loader is generation-guarded: a fast toggle (or an unload) while the
|
|
||||||
* chunk is still in flight must not register the stale dataset.
|
|
||||||
*/
|
|
||||||
import { loadChunk } from './chunk-loader.ts'
|
|
||||||
import type { BetterSidebarService } from './service.ts'
|
|
||||||
|
|
||||||
/** The pref value selecting the built-in monochrome glyphs (default). */
|
|
||||||
export const FILE_ICON_THEME_BUILTIN = 'builtin'
|
|
||||||
/** The pref value selecting the optional colored brand-icon dataset. */
|
|
||||||
export const FILE_ICON_THEME_COLORED = 'colored'
|
|
||||||
|
|
||||||
/** A registrar exported by the lazy `file-icons` chunk. */
|
|
||||||
type ColoredRegistrar = (service: BetterSidebarService) => () => void
|
|
||||||
|
|
||||||
export interface FileIconThemeLoader {
|
|
||||||
/** Apply a pref value (unknown values behave as the built-in theme). */
|
|
||||||
set(theme: string): void
|
|
||||||
/** Drop any registration and stop reacting to in-flight chunk loads. */
|
|
||||||
dispose(): void
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Bind `service` to the `fileIconTheme` pref. Call `set` on activation and on
|
|
||||||
* every prefs change; call `dispose` when the plugin unloads.
|
|
||||||
*/
|
|
||||||
export function createFileIconThemeLoader(service: BetterSidebarService): FileIconThemeLoader {
|
|
||||||
/** Disposer of the currently registered colored dataset, if any. */
|
|
||||||
let unregister: (() => void) | undefined
|
|
||||||
/** Bumped on every set/dispose: a load that lands on a stale generation is dropped. */
|
|
||||||
let generation = 0
|
|
||||||
/** The theme the pref currently asks for (repeated set() calls are no-ops). */
|
|
||||||
let current: string | undefined
|
|
||||||
|
|
||||||
const dropRegistrations = (): void => {
|
|
||||||
const dispose = unregister
|
|
||||||
unregister = undefined
|
|
||||||
if (dispose !== undefined) dispose()
|
|
||||||
}
|
|
||||||
|
|
||||||
const set = (theme: string): void => {
|
|
||||||
if (theme === current) return
|
|
||||||
current = theme
|
|
||||||
const generationAtSet = ++generation
|
|
||||||
if (theme !== FILE_ICON_THEME_COLORED) {
|
|
||||||
dropRegistrations()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
loadChunk('file-icons').then((chunk) => {
|
|
||||||
if (generationAtSet !== generation) return
|
|
||||||
const register = chunk.registerColoredFileIcons as ColoredRegistrar | undefined
|
|
||||||
if (typeof register !== 'function') {
|
|
||||||
console.error('[dsh-better-sidebar] file-icons chunk exports no registerColoredFileIcons')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
unregister = register(service)
|
|
||||||
}).catch((error: unknown) => {
|
|
||||||
if (generationAtSet !== generation) return
|
|
||||||
console.error('[dsh-better-sidebar] file-icons chunk failed to load:', error)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
set,
|
|
||||||
dispose: () => {
|
|
||||||
generation += 1
|
|
||||||
current = undefined
|
|
||||||
dropRegistrations()
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+53
-72
@@ -1,81 +1,62 @@
|
|||||||
/**
|
/**
|
||||||
* Built-in per-extension file glyphs for the file tree (feature: file icons).
|
* Built-in file and folder glyphs for the file tree (feature: file icons).
|
||||||
*
|
*
|
||||||
* All glyphs are VSCodicons consumed as `currentColor` monochrome — the same
|
* The built-in set is DSH's OWN file-type artwork: `FileTypeIcon` from
|
||||||
* skin contract as every other icon this plugin draws (visual values ride the
|
* `@deepseek-ai/dsh-client-ui-primitives` classifies a path and renders the
|
||||||
* `--dsw-alias-*` tokens, never hardcoded colors). External plugins that want
|
* same glyphs the host's own explorer shows — the 48 full-color code and
|
||||||
* colorful icons register their own `FileIconDescriptor`s through
|
* configuration categories, plus the category-colored sheet glyphs for
|
||||||
* `ctx.betterSidebar.registerFileIcon` (they own their colors); this map is
|
* markdown, images, PDFs, office documents, video, folders and the generic
|
||||||
* only the fallback nobody claimed.
|
* fallback. Colored artwork is content, not chrome, and the host draws it
|
||||||
|
* with its own palette, so this module carries no color literals and stays
|
||||||
|
* inside the skin contract (the guide's §12).
|
||||||
|
*
|
||||||
|
* Nothing here needs a lazy chunk: the artwork belongs to a platform module
|
||||||
|
* the client already holds in its frozen module table, so the core bundle
|
||||||
|
* gains a function call rather than 500-odd icon rules.
|
||||||
|
*
|
||||||
|
* External plugins that want their own glyphs (per extension, per exact file
|
||||||
|
* name, per directory name) register `FileIconDescriptor`s through
|
||||||
|
* `ctx.betterSidebar.registerFileIcon`; their registrations win over these.
|
||||||
*/
|
*/
|
||||||
import type { ReactNode } from 'react'
|
import type { ReactNode } from 'react'
|
||||||
import {
|
import { FileTypeIcon } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||||
VscDatabase, VscFile, VscFileCode, VscFileMedia, VscFilePdf, VscFileZip,
|
|
||||||
VscFolder, VscFolderOpened, VscJson, VscLock, VscMarkdown, VscSettings,
|
|
||||||
} from 'react-icons/vsc'
|
|
||||||
import { extOf } from './paths.ts'
|
|
||||||
|
|
||||||
/** One glyph shared by a group of extensions. */
|
|
||||||
type GlyphFactory = (size: number) => ReactNode
|
|
||||||
|
|
||||||
/** The generic file glyph — the fallback for every unclaimed extension. */
|
|
||||||
export function fallbackFileIcon(size: number): ReactNode {
|
|
||||||
return <VscFile size={size} />
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The built-in directory glyphs (what a folder row shows unregistered). */
|
|
||||||
export function builtinFolderIcon(open: boolean, size: number): ReactNode {
|
|
||||||
return open ? <VscFolderOpened size={size} /> : <VscFolder size={size} />
|
|
||||||
}
|
|
||||||
|
|
||||||
/** One glyph shared by a group of extensions. */
|
|
||||||
type GlyphGroup = [readonly string[], GlyphFactory]
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extension → glyph groups. Extensions are lowercase without the leading dot,
|
* A file row: the host's own classifier and artwork for any path. Unknown
|
||||||
* matched case-insensitively by the caller's normalization. Group order is
|
* extensions land on the generic document glyph, exactly like the host's
|
||||||
* documentation only: the flattened lookup below is a Map, so every extension
|
* explorer.
|
||||||
* key must stay unique (a duplicate would be last-write-wins). The reserved
|
* @param path - the row's path (any separator; the classifier reads the basename).
|
||||||
* folder values (`'folder'`/`'folder-open'`) never appear here — directories
|
* @param size - the square edge in px.
|
||||||
* resolve through `builtinFolderIcon`.
|
* @returns the host's file-type glyph.
|
||||||
*/
|
|
||||||
const GROUPS: readonly GlyphGroup[] = [
|
|
||||||
[['md', 'markdown', 'mdx'], (size) => <VscMarkdown size={size} />],
|
|
||||||
[['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg', 'bmp', 'ico', 'avif', 'tiff'], (size) => <VscFileMedia size={size} />],
|
|
||||||
[['pdf'], (size) => <VscFilePdf size={size} />],
|
|
||||||
[['json', 'jsonc', 'json5'], (size) => <VscJson size={size} />],
|
|
||||||
[['html', 'htm', 'xhtml', 'xml', 'jsx', 'tsx'], (size) => <VscFileCode size={size} />],
|
|
||||||
[['css', 'scss', 'sass', 'less', 'styl'], (size) => <VscFileCode size={size} />],
|
|
||||||
[['js', 'mjs', 'cjs', 'ts', 'mts', 'cts', 'py', 'rs', 'go', 'java', 'kt', 'kts', 'swift', 'rb', 'php', 'c', 'h', 'cpp', 'hpp', 'cs', 'm', 'mm', 'scala', 'sh', 'bash', 'zsh', 'fish', 'lua', 'pl', 'r', 'jl', 'dart', 'ex', 'exs', 'erl', 'hs', 'clj', 'cljs', 'vb', 'fs'], (size) => <VscFileCode size={size} />],
|
|
||||||
[['yml', 'yaml', 'toml', 'ini', 'cfg', 'conf', 'properties', 'editorconfig'], (size) => <VscSettings size={size} />],
|
|
||||||
[['sql', 'db', 'sqlite', 'sqlite3'], (size) => <VscDatabase size={size} />],
|
|
||||||
[['lock'], (size) => <VscLock size={size} />],
|
|
||||||
[['zip', 'tar', 'gz', 'tgz', 'bz2', 'xz', '7z', 'rar'], (size) => <VscFileZip size={size} />],
|
|
||||||
]
|
|
||||||
|
|
||||||
const LOOKUP: ReadonlyMap<string, GlyphFactory> = new Map(
|
|
||||||
GROUPS.flatMap(([exts, glyph]) => exts.map((ext) => [ext, glyph] as const)),
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The built-in glyph FACTORY claiming a path's extension, or undefined when
|
|
||||||
* the extension is unclaimed (the caller decides the fallback — the generic
|
|
||||||
* `VscFile` or a registered catch-all). Exposed as a separate function so
|
|
||||||
* the service's resolver can distinguish "builtin claims it" from "builtin
|
|
||||||
* is just its own fallback" without comparing ReactNodes by reference.
|
|
||||||
*/
|
|
||||||
export function builtinFileIconOf(path: string): GlyphFactory | undefined {
|
|
||||||
return LOOKUP.get(extOf(path))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The built-in glyph for a path: its extension's group glyph, or the generic
|
|
||||||
* `VscFile` when nothing claims it. Pure and synchronous — callers run this
|
|
||||||
* on every render, so it stays a Map lookup. The no-service fallback path
|
|
||||||
* for surfaces without the registry (and the last-but-one link of the
|
|
||||||
* service's `fileIcon` chain).
|
|
||||||
*/
|
*/
|
||||||
export function builtinFileIcon(path: string, size: number): ReactNode {
|
export function builtinFileIcon(path: string, size: number): ReactNode {
|
||||||
const glyph = LOOKUP.get(extOf(path))
|
return <FileTypeIcon path={path} size={size} />
|
||||||
return glyph === undefined ? fallbackFileIcon(size) : glyph(size)
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A directory row: the host's folder glyph.
|
||||||
|
*
|
||||||
|
* The host ships ONE folder drawing (`kind: 'folder'` resolves to its
|
||||||
|
* monochrome `IconFolderClose16`, which rides `currentColor` and therefore
|
||||||
|
* still follows the skin), and the classifier never returns a folder kind of
|
||||||
|
* its own. The expansion state is already legible from the tree's own chevron
|
||||||
|
* and row affordances, so this deliberately does not invent a second folder
|
||||||
|
* drawing.
|
||||||
|
* @param _open - whether the row is expanded (accepted for API compatibility).
|
||||||
|
* @param size - the square edge in px.
|
||||||
|
* @returns the host's folder glyph.
|
||||||
|
*/
|
||||||
|
export function builtinFolderIcon(_open: boolean, size: number): ReactNode {
|
||||||
|
return <FileTypeIcon kind="folder" size={size} />
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The last-resort glyph: the host's generic document, for the resolver's leaf
|
||||||
|
* when no registration and no built-in match applies (and for surfaces that
|
||||||
|
* render a file row without a service).
|
||||||
|
* @param size - the square edge in px.
|
||||||
|
* @returns the host's generic file glyph.
|
||||||
|
*/
|
||||||
|
export function fallbackFileIcon(size: number): ReactNode {
|
||||||
|
return <FileTypeIcon kind="other" size={size} />
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import { registerBottomToggle } from './sidebar/bottom-toggle.tsx'
|
|||||||
import { createNativeSurface } from './native/surface.ts'
|
import { createNativeSurface } from './native/surface.ts'
|
||||||
import { registerLinkInterception } from './link-intercept.ts'
|
import { registerLinkInterception } from './link-intercept.ts'
|
||||||
import { registerImeGuard } from './ime-guard.ts'
|
import { registerImeGuard } from './ime-guard.ts'
|
||||||
import { createFileIconThemeLoader } from './file-icon-theme.ts'
|
|
||||||
import { registerSettingsNavIcon } from './settings-nav-icon.ts'
|
import { registerSettingsNavIcon } from './settings-nav-icon.ts'
|
||||||
import { loadBootDecision } from './prefs.ts'
|
import { loadBootDecision } from './prefs.ts'
|
||||||
import { SideCardSection } from './SideCardSection.tsx'
|
import { SideCardSection } from './SideCardSection.tsx'
|
||||||
@@ -189,21 +188,6 @@ export function apply(ctx: Context): void {
|
|||||||
() => registerBuiltins(ctx, service, { terminalTitle: () => terminalTitle }),
|
() => registerBuiltins(ctx, service, { terminalTitle: () => terminalTitle }),
|
||||||
'dsh-better-sidebar: register built-in tabs and viewers',
|
'dsh-better-sidebar: register built-in tabs and viewers',
|
||||||
)
|
)
|
||||||
// Optional colored file-icon theme: the `fileIconTheme` pref decides whether
|
|
||||||
// the lazy `file-icons` chunk is fetched and its dataset registered through
|
|
||||||
// `registerFileIcon`. The loader reacts to prefs writes and is disposed with
|
|
||||||
// the fiber (HMR-safe); 'builtin' (the default) keeps the startup path free
|
|
||||||
// of icon data.
|
|
||||||
ctx.effect(() => {
|
|
||||||
const loader = createFileIconThemeLoader(service)
|
|
||||||
const sync = (): void => { loader.set(sidebarStore.getPrefs().fileIconTheme) }
|
|
||||||
sync()
|
|
||||||
const unsubscribe = service.subscribeState(sync)
|
|
||||||
return () => {
|
|
||||||
unsubscribe()
|
|
||||||
loader.dispose()
|
|
||||||
}
|
|
||||||
}, 'dsh-better-sidebar: colored file-icon theme')
|
|
||||||
// A failure anywhere in the client lifecycle must never take the app down
|
// A failure anywhere in the client lifecycle must never take the app down
|
||||||
// silently: log with the plugin prefix and pin a visible diagnostic strip
|
// silently: log with the plugin prefix and pin a visible diagnostic strip
|
||||||
// to the page so a blank panel is never the only symptom. This strip is
|
// to the page so a blank panel is never the only symptom. This strip is
|
||||||
|
|||||||
@@ -260,12 +260,6 @@ export const ar: Record<string, string> = {
|
|||||||
settingsToolsDesc: 'عند التفعيل، يمكن للنموذج إنشاء وتشغيل طرفيات الشريط الجانبي عبر أدوات terminal_* الثمانية (معطّل افتراضياً)',
|
settingsToolsDesc: 'عند التفعيل، يمكن للنموذج إنشاء وتشغيل طرفيات الشريط الجانبي عبر أدوات terminal_* الثمانية (معطّل افتراضياً)',
|
||||||
settingsFenceTitle: 'فحص مسارات مساحة العمل',
|
settingsFenceTitle: 'فحص مسارات مساحة العمل',
|
||||||
settingsFenceDesc: 'عند التفعيل، تصل وظائف الملفات في الشريط الجانبي إلى مسارات داخل مساحة عمل الجلسة فقط (الافتراضي)؛ عند الإيقاف تصبح ملفات المضيف كلها متاحة — وتحصل سكربتات الصفحة على النفاذ نفسه طوال ذلك',
|
settingsFenceDesc: 'عند التفعيل، تصل وظائف الملفات في الشريط الجانبي إلى مسارات داخل مساحة عمل الجلسة فقط (الافتراضي)؛ عند الإيقاف تصبح ملفات المضيف كلها متاحة — وتحصل سكربتات الصفحة على النفاذ نفسه طوال ذلك',
|
||||||
settingsFileIconThemeTitle: 'أيقونات الملفات',
|
|
||||||
settingsFileIconThemeDesc: 'نمط الأيقونات لشجرة الملفات وتبويبات الملفات (تُحمَّل السمة الملوّنة عند الطلب، لا عند بدء التشغيل)',
|
|
||||||
settingsFileIconThemeBuiltin: 'مدمج (أحادي اللون)',
|
|
||||||
settingsFileIconThemeBuiltinDesc: 'أيقونات أحادية اللون تتبع رموز السمة، تغطي أنواع التعليمات البرمجية والمستندات والوسائط الشائعة',
|
|
||||||
settingsFileIconThemeColored: 'أيقونات العلامات الملوّنة',
|
|
||||||
settingsFileIconThemeColoredDesc: 'ألوان العلامات حسب النوع (مثل .tsx → React و package.json → npm)؛ تُحمَّل حزمة الأيقونات عند أول تفعيل',
|
|
||||||
fenceErrorReason: 'هذا المسار خارج مساحة عمل الجلسة وقد حجبه الفحص',
|
fenceErrorReason: 'هذا المسار خارج مساحة عمل الجلسة وقد حجبه الفحص',
|
||||||
fenceDisableAction: 'إيقاف فحص مساحة العمل',
|
fenceDisableAction: 'إيقاف فحص مساحة العمل',
|
||||||
settingsBottomTerminalTitle: 'فتح طرفية تلقائياً عند أول توسيع للوحة السفلية',
|
settingsBottomTerminalTitle: 'فتح طرفية تلقائياً عند أول توسيع للوحة السفلية',
|
||||||
|
|||||||
@@ -245,12 +245,6 @@ export const de: Record<string, string> = {
|
|||||||
settingsToolsDesc: 'Wenn aktiviert, kann das Modell über die 8 terminal_*-Werkzeuge Terminale in der Seitenleiste erstellen und steuern (standardmäßig deaktiviert)',
|
settingsToolsDesc: 'Wenn aktiviert, kann das Modell über die 8 terminal_*-Werkzeuge Terminale in der Seitenleiste erstellen und steuern (standardmäßig deaktiviert)',
|
||||||
settingsFenceTitle: 'Workspace-Pfadkontrolle',
|
settingsFenceTitle: 'Workspace-Pfadkontrolle',
|
||||||
settingsFenceDesc: 'Wenn aktiv, erreichen die Dateifunktionen der Seitenleiste nur Pfade innerhalb des Session-Workspaces (Standard); ausgeschaltet sind Dateien überall auf dem Host erreichbar — Skripte der Seite erhalten solange dieselbe Reichweite',
|
settingsFenceDesc: 'Wenn aktiv, erreichen die Dateifunktionen der Seitenleiste nur Pfade innerhalb des Session-Workspaces (Standard); ausgeschaltet sind Dateien überall auf dem Host erreichbar — Skripte der Seite erhalten solange dieselbe Reichweite',
|
||||||
settingsFileIconThemeTitle: 'Dateisymbole',
|
|
||||||
settingsFileIconThemeDesc: 'Symbolstil für Dateibaum und Datei-Tabs (das Farbthema lädt bei Bedarf, nie beim Start)',
|
|
||||||
settingsFileIconThemeBuiltin: 'Integriert (einfarbig)',
|
|
||||||
settingsFileIconThemeBuiltinDesc: 'Token-gesteuerte einfarbige Symbole für gängige Code-, Dokument- und Medientypen',
|
|
||||||
settingsFileIconThemeColored: 'Farbige Markensymbole',
|
|
||||||
settingsFileIconThemeColoredDesc: 'Markenfarben je Typ (z. B. .tsx → React, package.json → npm); das Icon-Paket lädt beim ersten Aktivieren',
|
|
||||||
fenceErrorReason: 'Dieser Pfad liegt außerhalb des Session-Workspaces und wurde von der Workspace-Kontrolle blockiert',
|
fenceErrorReason: 'Dieser Pfad liegt außerhalb des Session-Workspaces und wurde von der Workspace-Kontrolle blockiert',
|
||||||
fenceDisableAction: 'Workspace-Kontrolle ausschalten',
|
fenceDisableAction: 'Workspace-Kontrolle ausschalten',
|
||||||
settingsBottomTerminalTitle: 'Beim ersten Ausklappen des unteren Panels automatisch ein Terminal öffnen',
|
settingsBottomTerminalTitle: 'Beim ersten Ausklappen des unteren Panels automatisch ein Terminal öffnen',
|
||||||
|
|||||||
@@ -252,12 +252,6 @@ export const fr: Record<string, string> = {
|
|||||||
settingsToolsDesc: 'Une fois activé, le modèle peut créer et piloter des terminaux de la barre latérale via les 8 outils terminal_* (désactivé par défaut)',
|
settingsToolsDesc: 'Une fois activé, le modèle peut créer et piloter des terminaux de la barre latérale via les 8 outils terminal_* (désactivé par défaut)',
|
||||||
settingsFenceTitle: 'Contrôle des chemins de l\'espace de travail',
|
settingsFenceTitle: 'Contrôle des chemins de l\'espace de travail',
|
||||||
settingsFenceDesc: 'Activé, les fonctions fichiers de la barre latérale n\'atteignent que les chemins dans l\'espace de travail de la session (par défaut) ; désactivé, les fichiers de tout l\'hôte deviennent accessibles — les scripts de la page obtiennent la même portée pendant ce temps',
|
settingsFenceDesc: 'Activé, les fonctions fichiers de la barre latérale n\'atteignent que les chemins dans l\'espace de travail de la session (par défaut) ; désactivé, les fichiers de tout l\'hôte deviennent accessibles — les scripts de la page obtiennent la même portée pendant ce temps',
|
||||||
settingsFileIconThemeTitle: 'Icônes de fichiers',
|
|
||||||
settingsFileIconThemeDesc: 'Style des icônes de l\'arborescence et des onglets de fichiers (le thème coloré se charge à la demande, jamais au démarrage)',
|
|
||||||
settingsFileIconThemeBuiltin: 'Intégré (monochrome)',
|
|
||||||
settingsFileIconThemeBuiltinDesc: 'Icônes monochromes pilotées par les jetons de thème, pour les types code / document / média courants',
|
|
||||||
settingsFileIconThemeColored: 'Icônes de marque colorées',
|
|
||||||
settingsFileIconThemeColoredDesc: 'Couleurs de marque par type (ex. .tsx → React, package.json → npm) ; le pack d\'icônes se charge à la première activation',
|
|
||||||
fenceErrorReason: 'Ce chemin est hors de l\'espace de travail de la session et a été bloqué par le contrôle',
|
fenceErrorReason: 'Ce chemin est hors de l\'espace de travail de la session et a été bloqué par le contrôle',
|
||||||
fenceDisableAction: 'Désactiver le contrôle de l\'espace de travail',
|
fenceDisableAction: 'Désactiver le contrôle de l\'espace de travail',
|
||||||
settingsBottomTerminalTitle: 'Ouvrir automatiquement un terminal au premier déploiement du panneau inférieur',
|
settingsBottomTerminalTitle: 'Ouvrir automatiquement un terminal au premier déploiement du panneau inférieur',
|
||||||
|
|||||||
@@ -259,12 +259,6 @@ export const hi: Record<string, string> = {
|
|||||||
settingsToolsDesc: 'सक्षम होने पर, मॉडल 8 terminal_* टूल्स से साइडबार टर्मिनल बना और चला सकता है (डिफ़ॉल्ट रूप से बंद)',
|
settingsToolsDesc: 'सक्षम होने पर, मॉडल 8 terminal_* टूल्स से साइडबार टर्मिनल बना और चला सकता है (डिफ़ॉल्ट रूप से बंद)',
|
||||||
settingsFenceTitle: 'वर्कस्पेस पाथ जाँच',
|
settingsFenceTitle: 'वर्कस्पेस पाथ जाँच',
|
||||||
settingsFenceDesc: 'चालू होने पर साइडबार की फ़ाइल सुविधाएँ केवल सेशन वर्कस्पेस के भीतर के पाथ तक पहुँचती हैं (डिफ़ॉल्ट); बंद करने पर होस्ट की कोई भी फ़ाइल खोली जा सकती है — उस दौरान पेज के स्क्रिप्ट को भी वही पहुँच मिल जाती है',
|
settingsFenceDesc: 'चालू होने पर साइडबार की फ़ाइल सुविधाएँ केवल सेशन वर्कस्पेस के भीतर के पाथ तक पहुँचती हैं (डिफ़ॉल्ट); बंद करने पर होस्ट की कोई भी फ़ाइल खोली जा सकती है — उस दौरान पेज के स्क्रिप्ट को भी वही पहुँच मिल जाती है',
|
||||||
settingsFileIconThemeTitle: 'फ़ाइल आइकन',
|
|
||||||
settingsFileIconThemeDesc: 'फ़ाइल ट्री और फ़ाइल टैब के आइकन की शैली (रंगीन थीम माँग पर लोड होती है, शुरू होते समय नहीं)',
|
|
||||||
settingsFileIconThemeBuiltin: 'अंतर्निहित (एकरंगी)',
|
|
||||||
settingsFileIconThemeBuiltinDesc: 'स्किन टोकन के अनुसार एकरंगी आइकन, सामान्य कोड / दस्तावेज़ / मीडिया प्रकारों के लिए',
|
|
||||||
settingsFileIconThemeColored: 'रंगीन ब्रांड आइकन',
|
|
||||||
settingsFileIconThemeColoredDesc: 'प्रकार के अनुसार ब्रांड रंग (जैसे .tsx → React, package.json → npm); पहली बार चालू करने पर आइकन पैक लोड होता है',
|
|
||||||
fenceErrorReason: 'यह पाथ सेशन वर्कस्पेस के बाहर है और जाँच द्वारा अवरुद्ध किया गया',
|
fenceErrorReason: 'यह पाथ सेशन वर्कस्पेस के बाहर है और जाँच द्वारा अवरुद्ध किया गया',
|
||||||
fenceDisableAction: 'वर्कस्पेस जाँच बंद करें',
|
fenceDisableAction: 'वर्कस्पेस जाँच बंद करें',
|
||||||
settingsBottomTerminalTitle: 'निचला पैनल पहली बार खुलने पर स्वतः टर्मिनल खोलें',
|
settingsBottomTerminalTitle: 'निचला पैनल पहली बार खुलने पर स्वतः टर्मिनल खोलें',
|
||||||
|
|||||||
@@ -257,12 +257,6 @@ export const id: Record<string, string> = {
|
|||||||
settingsToolsDesc: 'Saat diaktifkan, model dapat membuat dan mengendalikan terminal sidebar melalui 8 alat terminal_* (nonaktif secara default)',
|
settingsToolsDesc: 'Saat diaktifkan, model dapat membuat dan mengendalikan terminal sidebar melalui 8 alat terminal_* (nonaktif secara default)',
|
||||||
settingsFenceTitle: 'Pemeriksaan path workspace',
|
settingsFenceTitle: 'Pemeriksaan path workspace',
|
||||||
settingsFenceDesc: 'Saat aktif, fitur berkas sidebar hanya menjangkau path di dalam workspace sesi (default); saat nonaktif, berkas di mana pun di host dapat diakses — skrip halaman memperoleh jangkauan yang sama selama itu',
|
settingsFenceDesc: 'Saat aktif, fitur berkas sidebar hanya menjangkau path di dalam workspace sesi (default); saat nonaktif, berkas di mana pun di host dapat diakses — skrip halaman memperoleh jangkauan yang sama selama itu',
|
||||||
settingsFileIconThemeTitle: 'Ikon berkas',
|
|
||||||
settingsFileIconThemeDesc: 'Gaya ikon untuk pohon berkas dan tab berkas (tema berwarna dimuat saat diperlukan, tidak saat mulai)',
|
|
||||||
settingsFileIconThemeBuiltin: 'Bawaan (monokrom)',
|
|
||||||
settingsFileIconThemeBuiltinDesc: 'Ikon monokrom mengikuti token tema, mencakup tipe kode / dokumen / media umum',
|
|
||||||
settingsFileIconThemeColored: 'Ikon merek berwarna',
|
|
||||||
settingsFileIconThemeColoredDesc: 'Warna merek per tipe (mis. .tsx → React, package.json → npm); paket ikon dimuat saat pertama diaktifkan',
|
|
||||||
fenceErrorReason: 'Path ini berada di luar workspace sesi dan diblokir oleh pemeriksaan',
|
fenceErrorReason: 'Path ini berada di luar workspace sesi dan diblokir oleh pemeriksaan',
|
||||||
fenceDisableAction: 'Matikan pemeriksaan workspace',
|
fenceDisableAction: 'Matikan pemeriksaan workspace',
|
||||||
settingsBottomTerminalTitle: 'Buka terminal otomatis saat panel bawah pertama kali dibentangkan',
|
settingsBottomTerminalTitle: 'Buka terminal otomatis saat panel bawah pertama kali dibentangkan',
|
||||||
|
|||||||
@@ -250,12 +250,6 @@ export const it: Record<string, string> = {
|
|||||||
settingsToolsDesc: 'Se attivato, il modello può creare e gestire terminali della barra laterale attraverso gli 8 strumenti terminal_* (disattivato per impostazione predefinita)',
|
settingsToolsDesc: 'Se attivato, il modello può creare e gestire terminali della barra laterale attraverso gli 8 strumenti terminal_* (disattivato per impostazione predefinita)',
|
||||||
settingsFenceTitle: 'Controllo dei percorsi del workspace',
|
settingsFenceTitle: 'Controllo dei percorsi del workspace',
|
||||||
settingsFenceDesc: 'Attivo, le funzioni file della barra laterale raggiungono solo i percorsi nel workspace di sessione (predefinito); disattivo, ogni file dell\'host è raggiungibile — gli script della pagina ottengono la stessa portata nel frattempo',
|
settingsFenceDesc: 'Attivo, le funzioni file della barra laterale raggiungono solo i percorsi nel workspace di sessione (predefinito); disattivo, ogni file dell\'host è raggiungibile — gli script della pagina ottengono la stessa portata nel frattempo',
|
||||||
settingsFileIconThemeTitle: 'Icone dei file',
|
|
||||||
settingsFileIconThemeDesc: 'Stile delle icone per l\'albero dei file e le schede (il tema colorato si carica su richiesta, mai all\'avvio)',
|
|
||||||
settingsFileIconThemeBuiltin: 'Integrato (monocromatico)',
|
|
||||||
settingsFileIconThemeBuiltinDesc: 'Icone monocromatiche basate sui token del tema, per i tipi di codice / documento / media più comuni',
|
|
||||||
settingsFileIconThemeColored: 'Icone di marca colorate',
|
|
||||||
settingsFileIconThemeColoredDesc: 'Colori di marca per tipo (es. .tsx → React, package.json → npm); il pacchetto si carica alla prima attivazione',
|
|
||||||
fenceErrorReason: 'Questo percorso è fuori dal workspace di sessione ed è stato bloccato dal controllo',
|
fenceErrorReason: 'Questo percorso è fuori dal workspace di sessione ed è stato bloccato dal controllo',
|
||||||
fenceDisableAction: 'Disattiva il controllo del workspace',
|
fenceDisableAction: 'Disattiva il controllo del workspace',
|
||||||
settingsBottomTerminalTitle: 'Apri automaticamente un terminale alla prima espansione del pannello inferiore',
|
settingsBottomTerminalTitle: 'Apri automaticamente un terminale alla prima espansione del pannello inferiore',
|
||||||
|
|||||||
@@ -259,12 +259,6 @@ export const ja: Record<string, string> = {
|
|||||||
settingsToolsDesc: 'オンにすると、モデルが terminal_create 等 8 個のツールでサイドバーターミナルを作成・操作可能(デフォルトオフ)',
|
settingsToolsDesc: 'オンにすると、モデルが terminal_create 等 8 個のツールでサイドバーターミナルを作成・操作可能(デフォルトオフ)',
|
||||||
settingsFenceTitle: 'ワークスペースパスガード',
|
settingsFenceTitle: 'ワークスペースパスガード',
|
||||||
settingsFenceDesc: 'オンの場合、サイドバーのファイル機能はセッションワークスペース内のパスのみアクセス可能(デフォルト)。オフにするとホスト上の任意のファイルにアクセスできるが、オフの間はページ内スクリプトも同じ権限を得る',
|
settingsFenceDesc: 'オンの場合、サイドバーのファイル機能はセッションワークスペース内のパスのみアクセス可能(デフォルト)。オフにするとホスト上の任意のファイルにアクセスできるが、オフの間はページ内スクリプトも同じ権限を得る',
|
||||||
settingsFileIconThemeTitle: 'ファイルアイコン',
|
|
||||||
settingsFileIconThemeDesc: 'ファイルツリーとファイルタブのアイコンスタイル(カラーテーマは必要時に読み込み、起動時には読み込みません)',
|
|
||||||
settingsFileIconThemeBuiltin: '標準(モノクロ)',
|
|
||||||
settingsFileIconThemeBuiltinDesc: 'スキントークンに従うモノクロアイコン。一般的なコード / ドキュメント / メディア形式に対応',
|
|
||||||
settingsFileIconThemeColored: 'カラーブランドアイコン',
|
|
||||||
settingsFileIconThemeColoredDesc: '種類ごとにブランドカラーで表示(例:.tsx → React、package.json → npm)。初回有効時にアイコンパックを読み込みます',
|
|
||||||
fenceErrorReason: 'このパスはセッションワークスペースの外にあり、ワークスペースガードによりブロックされました',
|
fenceErrorReason: 'このパスはセッションワークスペースの外にあり、ワークスペースガードによりブロックされました',
|
||||||
fenceDisableAction: 'ワークスペースガードをオフにする',
|
fenceDisableAction: 'ワークスペースガードをオフにする',
|
||||||
settingsBottomTerminalTitle: '下パネル初回展開時に自動でターミナルを開く',
|
settingsBottomTerminalTitle: '下パネル初回展開時に自動でターミナルを開く',
|
||||||
|
|||||||
@@ -251,12 +251,6 @@ export const ko: Record<string, string> = {
|
|||||||
settingsToolsDesc: '켜면 모델이 terminal_create 등 8개 도구로 사이드바 터미널을 만들고 조작할 수 있습니다(기본 꺼짐)',
|
settingsToolsDesc: '켜면 모델이 terminal_create 등 8개 도구로 사이드바 터미널을 만들고 조작할 수 있습니다(기본 꺼짐)',
|
||||||
settingsFenceTitle: '워크스페이스 경로 검사',
|
settingsFenceTitle: '워크스페이스 경로 검사',
|
||||||
settingsFenceDesc: '켜면 사이드바의 파일 기능이 세션 워크스페이스 내 경로만 접근합니다(기본값). 끄면 호스트의 모든 파일에 접근할 수 있으며, 꺼져 있는 동안 페이지 내 스크립트도 동일한 권한을 얻습니다',
|
settingsFenceDesc: '켜면 사이드바의 파일 기능이 세션 워크스페이스 내 경로만 접근합니다(기본값). 끄면 호스트의 모든 파일에 접근할 수 있으며, 꺼져 있는 동안 페이지 내 스크립트도 동일한 권한을 얻습니다',
|
||||||
settingsFileIconThemeTitle: '파일 아이콘',
|
|
||||||
settingsFileIconThemeDesc: '파일 트리와 파일 탭의 아이콘 스타일 (컬러 테마는 필요할 때 로드되며 시작 시에는 로드되지 않습니다)',
|
|
||||||
settingsFileIconThemeBuiltin: '기본(단색)',
|
|
||||||
settingsFileIconThemeBuiltinDesc: '스킨 토큰을 따르는 단색 아이콘으로 주요 코드 / 문서 / 미디어 형식을 지원합니다',
|
|
||||||
settingsFileIconThemeColored: '컬러 브랜드 아이콘',
|
|
||||||
settingsFileIconThemeColoredDesc: '유형별 브랜드 색상 (예: .tsx → React, package.json → npm), 처음 켤 때 아이콘 팩을 로드합니다',
|
|
||||||
fenceErrorReason: '이 경로는 세션 워크스페이스 밖에 있어 검사에 의해 차단되었습니다',
|
fenceErrorReason: '이 경로는 세션 워크스페이스 밖에 있어 검사에 의해 차단되었습니다',
|
||||||
fenceDisableAction: '워크스페이스 검사 끄기',
|
fenceDisableAction: '워크스페이스 검사 끄기',
|
||||||
settingsBottomTerminalTitle: '하단 패널을 처음 펼칠 때 터미널 자동 열기',
|
settingsBottomTerminalTitle: '하단 패널을 처음 펼칠 때 터미널 자동 열기',
|
||||||
|
|||||||
@@ -257,12 +257,6 @@ export const nl: Record<string, string> = {
|
|||||||
settingsToolsDesc: 'Indien ingeschakeld kan het model zijbalk-terminals aanmaken en besturen via de 8 terminal_*-tools (standaard uit)',
|
settingsToolsDesc: 'Indien ingeschakeld kan het model zijbalk-terminals aanmaken en besturen via de 8 terminal_*-tools (standaard uit)',
|
||||||
settingsFenceTitle: 'Workspace-padcontrole',
|
settingsFenceTitle: 'Workspace-padcontrole',
|
||||||
settingsFenceDesc: 'Aan: de bestandsfuncties van de zijbalk bereiken alleen paden binnen de sessie-workspace (standaard); uit: elk bestand op de host is bereikbaar — paginascripts krijgen dat bereik zolang het uit staat',
|
settingsFenceDesc: 'Aan: de bestandsfuncties van de zijbalk bereiken alleen paden binnen de sessie-workspace (standaard); uit: elk bestand op de host is bereikbaar — paginascripts krijgen dat bereik zolang het uit staat',
|
||||||
settingsFileIconThemeTitle: 'Bestandspictogrammen',
|
|
||||||
settingsFileIconThemeDesc: 'Pictogramstijl voor de bestandsboom en bestandstabbladen (het kleurenthema wordt op verzoek geladen, nooit bij het starten)',
|
|
||||||
settingsFileIconThemeBuiltin: 'Ingebouwd (monochroom)',
|
|
||||||
settingsFileIconThemeBuiltinDesc: 'Monochrome pictogrammen volgens de token-regels, voor gangbare code-, document- en mediatypen',
|
|
||||||
settingsFileIconThemeColored: 'Gekleurde merkpictogrammen',
|
|
||||||
settingsFileIconThemeColoredDesc: 'Merkkleuren per type (bijv. .tsx → React, package.json → npm); het pictogrammenpakket wordt geladen bij de eerste activering',
|
|
||||||
fenceErrorReason: 'Dit pad ligt buiten de sessie-workspace en is geblokkeerd door de controle',
|
fenceErrorReason: 'Dit pad ligt buiten de sessie-workspace en is geblokkeerd door de controle',
|
||||||
fenceDisableAction: 'Workspace-controle uitzetten',
|
fenceDisableAction: 'Workspace-controle uitzetten',
|
||||||
settingsBottomTerminalTitle: 'Automatisch een terminal openen bij de eerste uitklap van het onderpaneel',
|
settingsBottomTerminalTitle: 'Automatisch een terminal openen bij de eerste uitklap van het onderpaneel',
|
||||||
|
|||||||
@@ -261,12 +261,6 @@ export const pl: Record<string, string> = {
|
|||||||
settingsToolsDesc: 'Po włączeniu model może tworzyć i sterować terminalami panelu bocznego przez 8 narzędzi terminal_* (domyślnie wyłączone)',
|
settingsToolsDesc: 'Po włączeniu model może tworzyć i sterować terminalami panelu bocznego przez 8 narzędzi terminal_* (domyślnie wyłączone)',
|
||||||
settingsFenceTitle: 'Kontrola ścieżek obszaru roboczego',
|
settingsFenceTitle: 'Kontrola ścieżek obszaru roboczego',
|
||||||
settingsFenceDesc: 'Włączona: funkcje plików paska bocznego sięgają tylko ścieżek w obszarze roboczym sesji (domyślnie); wyłączona: dostępny jest dowolny plik hosta — skrypty strony zyskują ten sam zasięg przez ten czas',
|
settingsFenceDesc: 'Włączona: funkcje plików paska bocznego sięgają tylko ścieżek w obszarze roboczym sesji (domyślnie); wyłączona: dostępny jest dowolny plik hosta — skrypty strony zyskują ten sam zasięg przez ten czas',
|
||||||
settingsFileIconThemeTitle: 'Ikony plików',
|
|
||||||
settingsFileIconThemeDesc: 'Styl ikon drzewa plików i kart plików (kolorowy motyw wczytuje się na żądanie, nigdy przy starcie)',
|
|
||||||
settingsFileIconThemeBuiltin: 'Wbudowane (monochromatyczne)',
|
|
||||||
settingsFileIconThemeBuiltinDesc: 'Monochromatyczne ikony zgodne z tokenami motywu, dla typowych plików kodu / dokumentów / multimediów',
|
|
||||||
settingsFileIconThemeColored: 'Kolorowe ikony marek',
|
|
||||||
settingsFileIconThemeColoredDesc: 'Kolory marek według typu (np. .tsx → React, package.json → npm); paczka ikon wczytuje się przy pierwszym włączeniu',
|
|
||||||
fenceErrorReason: 'Ta ścieżka jest poza obszarem roboczym sesji i została zablokowana przez kontrolę',
|
fenceErrorReason: 'Ta ścieżka jest poza obszarem roboczym sesji i została zablokowana przez kontrolę',
|
||||||
fenceDisableAction: 'Wyłącz kontrolę obszaru roboczego',
|
fenceDisableAction: 'Wyłącz kontrolę obszaru roboczego',
|
||||||
settingsBottomTerminalTitle: 'Automatycznie otwieraj terminal przy pierwszym rozwinięciu panelu dolnego',
|
settingsBottomTerminalTitle: 'Automatycznie otwieraj terminal przy pierwszym rozwinięciu panelu dolnego',
|
||||||
|
|||||||
@@ -242,12 +242,6 @@ export const pt: Record<string, string> = {
|
|||||||
settingsToolsDesc: 'Quando ativado, o modelo pode criar e operar terminais da barra lateral por meio das 8 ferramentas terminal_* (desativado por padrão)',
|
settingsToolsDesc: 'Quando ativado, o modelo pode criar e operar terminais da barra lateral por meio das 8 ferramentas terminal_* (desativado por padrão)',
|
||||||
settingsFenceTitle: 'Controle de caminhos do workspace',
|
settingsFenceTitle: 'Controle de caminhos do workspace',
|
||||||
settingsFenceDesc: 'Ativado, os recursos de arquivos da barra lateral só alcançam caminhos dentro do workspace da sessão (padrão); desativado, arquivos de todo o host ficam acessíveis — scripts da página ganham o mesmo alcance enquanto isso',
|
settingsFenceDesc: 'Ativado, os recursos de arquivos da barra lateral só alcançam caminhos dentro do workspace da sessão (padrão); desativado, arquivos de todo o host ficam acessíveis — scripts da página ganham o mesmo alcance enquanto isso',
|
||||||
settingsFileIconThemeTitle: 'Ícones de arquivo',
|
|
||||||
settingsFileIconThemeDesc: 'Estilo dos ícones da árvore de arquivos e das abas (o tema colorido é carregado sob demanda, nunca na inicialização)',
|
|
||||||
settingsFileIconThemeBuiltin: 'Integrado (monocromático)',
|
|
||||||
settingsFileIconThemeBuiltinDesc: 'Ícones monocromáticos guiados por tokens, para tipos comuns de código / documento / mídia',
|
|
||||||
settingsFileIconThemeColored: 'Ícones de marca coloridos',
|
|
||||||
settingsFileIconThemeColoredDesc: 'Cores de marca por tipo (ex.: .tsx → React, package.json → npm); o pacote é carregado ao ativar pela primeira vez',
|
|
||||||
fenceErrorReason: 'Este caminho está fora do workspace da sessão e foi bloqueado pelo controle',
|
fenceErrorReason: 'Este caminho está fora do workspace da sessão e foi bloqueado pelo controle',
|
||||||
fenceDisableAction: 'Desativar o controle do workspace',
|
fenceDisableAction: 'Desativar o controle do workspace',
|
||||||
settingsBottomTerminalTitle: 'Abrir um terminal automaticamente na primeira expansão do painel inferior',
|
settingsBottomTerminalTitle: 'Abrir um terminal automaticamente na primeira expansão do painel inferior',
|
||||||
|
|||||||
@@ -256,12 +256,6 @@ export const ru: Record<string, string> = {
|
|||||||
settingsToolsDesc: 'Если включено, модель может через 8 инструментов (terminal_create и др.) создавать терминалы боковой панели и управлять ими (по умолчанию выключено)',
|
settingsToolsDesc: 'Если включено, модель может через 8 инструментов (terminal_create и др.) создавать терминалы боковой панели и управлять ими (по умолчанию выключено)',
|
||||||
settingsFenceTitle: 'Контроль путей рабочей области',
|
settingsFenceTitle: 'Контроль путей рабочей области',
|
||||||
settingsFenceDesc: 'Включено — файловые функции боковой панели достигают только путей внутри рабочей области сессии (по умолчанию); выключено — доступны любые файлы хоста, а скрипты страницы получают тот же доступ всё это время',
|
settingsFenceDesc: 'Включено — файловые функции боковой панели достигают только путей внутри рабочей области сессии (по умолчанию); выключено — доступны любые файлы хоста, а скрипты страницы получают тот же доступ всё это время',
|
||||||
settingsFileIconThemeTitle: 'Значки файлов',
|
|
||||||
settingsFileIconThemeDesc: 'Стиль значков дерева файлов и вкладок (цветная тема загружается по требованию, а не при запуске)',
|
|
||||||
settingsFileIconThemeBuiltin: 'Встроенные (монохромные)',
|
|
||||||
settingsFileIconThemeBuiltinDesc: 'Монохромные значки на основе токенов темы: код, документы и медиа',
|
|
||||||
settingsFileIconThemeColored: 'Цветные значки брендов',
|
|
||||||
settingsFileIconThemeColoredDesc: 'Цвета брендов по типу (например, .tsx → React, package.json → npm); набор значков загружается при первом включении',
|
|
||||||
fenceErrorReason: 'Этот путь вне рабочей области сессии и заблокирован контролем',
|
fenceErrorReason: 'Этот путь вне рабочей области сессии и заблокирован контролем',
|
||||||
fenceDisableAction: 'Отключить контроль рабочей области',
|
fenceDisableAction: 'Отключить контроль рабочей области',
|
||||||
settingsBottomTerminalTitle: 'Автооткрытие терминала при первом разворачивании нижней панели',
|
settingsBottomTerminalTitle: 'Автооткрытие терминала при первом разворачивании нижней панели',
|
||||||
|
|||||||
@@ -242,12 +242,6 @@ export const sv: Record<string, string> = {
|
|||||||
settingsToolsDesc: 'När aktiverat kan modellen skapa och styra sidopanelsterminaler via de 8 terminal_*-verktygen (av som standard)',
|
settingsToolsDesc: 'När aktiverat kan modellen skapa och styra sidopanelsterminaler via de 8 terminal_*-verktygen (av som standard)',
|
||||||
settingsFenceTitle: 'Kontroll av workspace-sökvägar',
|
settingsFenceTitle: 'Kontroll av workspace-sökvägar',
|
||||||
settingsFenceDesc: 'På: sidopanelens filfunktioner når bara sökvägar i sessionens workspace (standard); av: vilken fil som helst på värden kan nås — sidans skript får samma räckvidd så länge',
|
settingsFenceDesc: 'På: sidopanelens filfunktioner når bara sökvägar i sessionens workspace (standard); av: vilken fil som helst på värden kan nås — sidans skript får samma räckvidd så länge',
|
||||||
settingsFileIconThemeTitle: 'Filikoner',
|
|
||||||
settingsFileIconThemeDesc: 'Ikonstil för filträdet och filflikarna (färgtemat laddas vid behov, aldrig vid start)',
|
|
||||||
settingsFileIconThemeBuiltin: 'Inbyggd (monokrom)',
|
|
||||||
settingsFileIconThemeBuiltinDesc: 'Monokroma ikoner som följer temats tokens, för vanliga kod-, dokument- och mediatyper',
|
|
||||||
settingsFileIconThemeColored: 'Färgade varumärkesikoner',
|
|
||||||
settingsFileIconThemeColoredDesc: 'Varumärkesfärger per typ (t.ex. .tsx → React, package.json → npm); ikonpaketet laddas första gången du aktiverar det',
|
|
||||||
fenceErrorReason: 'Denna sökväg ligger utanför sessionens workspace och blockerades av kontrollen',
|
fenceErrorReason: 'Denna sökväg ligger utanför sessionens workspace och blockerades av kontrollen',
|
||||||
fenceDisableAction: 'Stäng av workspace-kontrollen',
|
fenceDisableAction: 'Stäng av workspace-kontrollen',
|
||||||
settingsBottomTerminalTitle: 'Öppna automatiskt en terminal vid bottenpanelens första expandering',
|
settingsBottomTerminalTitle: 'Öppna automatiskt en terminal vid bottenpanelens första expandering',
|
||||||
|
|||||||
@@ -259,12 +259,6 @@ export const th: Record<string, string> = {
|
|||||||
settingsToolsDesc: 'เมื่อเปิดใช้ โมเดลสามารถสร้างและควบคุมเทอร์มินัลแถบด้านข้างผ่านเครื่องมือ terminal_* 8 ตัว (ปิดเป็นค่าเริ่มต้น)',
|
settingsToolsDesc: 'เมื่อเปิดใช้ โมเดลสามารถสร้างและควบคุมเทอร์มินัลแถบด้านข้างผ่านเครื่องมือ terminal_* 8 ตัว (ปิดเป็นค่าเริ่มต้น)',
|
||||||
settingsFenceTitle: 'การตรวจสอบเส้นทางเวิร์กสเปซ',
|
settingsFenceTitle: 'การตรวจสอบเส้นทางเวิร์กสเปซ',
|
||||||
settingsFenceDesc: 'เมื่อเปิด ฟีเจอร์ไฟล์ของแถบข้างเข้าถึงได้เฉพาะเส้นทางในเวิร์กสเปซของเซสชัน (ค่าเริ่มต้น) เมื่อปิด จะเข้าถึงไฟล์ใดก็ได้บนโฮสต์ — สคริปต์ในหน้าก็ได้ขอบเขตเดียวกันตลอดช่วงนั้น',
|
settingsFenceDesc: 'เมื่อเปิด ฟีเจอร์ไฟล์ของแถบข้างเข้าถึงได้เฉพาะเส้นทางในเวิร์กสเปซของเซสชัน (ค่าเริ่มต้น) เมื่อปิด จะเข้าถึงไฟล์ใดก็ได้บนโฮสต์ — สคริปต์ในหน้าก็ได้ขอบเขตเดียวกันตลอดช่วงนั้น',
|
||||||
settingsFileIconThemeTitle: 'ไอคอนไฟล์',
|
|
||||||
settingsFileIconThemeDesc: 'สไตล์ไอคอนของแผนผังไฟล์และแท็บไฟล์ (ธีมสีจะโหลดเมื่อต้องการ ไม่โหลดตอนเริ่มต้น)',
|
|
||||||
settingsFileIconThemeBuiltin: 'ในตัว (สีเดียว)',
|
|
||||||
settingsFileIconThemeBuiltinDesc: 'ไอคอนสีเดียวตามโทเค็นธีม ครอบคลุมชนิดโค้ด / เอกสาร / สื่อทั่วไป',
|
|
||||||
settingsFileIconThemeColored: 'ไอคอนแบรนด์แบบมีสี',
|
|
||||||
settingsFileIconThemeColoredDesc: 'สีแบรนด์ตามชนิด (เช่น .tsx → React, package.json → npm) ชุดไอคอนจะโหลดเมื่อเปิดใช้ครั้งแรก',
|
|
||||||
fenceErrorReason: 'เส้นทางนี้อยู่นอกเวิร์กสเปซของเซสชันและถูกบล็อกโดยการตรวจสอบ',
|
fenceErrorReason: 'เส้นทางนี้อยู่นอกเวิร์กสเปซของเซสชันและถูกบล็อกโดยการตรวจสอบ',
|
||||||
fenceDisableAction: 'ปิดการตรวจสอบเวิร์กสเปซ',
|
fenceDisableAction: 'ปิดการตรวจสอบเวิร์กสเปซ',
|
||||||
settingsBottomTerminalTitle: 'เปิดเทอร์มินัลอัตโนมัติเมื่อพาเนลล่างขยายครั้งแรก',
|
settingsBottomTerminalTitle: 'เปิดเทอร์มินัลอัตโนมัติเมื่อพาเนลล่างขยายครั้งแรก',
|
||||||
|
|||||||
@@ -259,12 +259,6 @@ export const tr: Record<string, string> = {
|
|||||||
settingsToolsDesc: 'Etkinleştirildiğinde model, 8 terminal_* aracı üzerinden kenar çubuğu terminalleri oluşturup kullanabilir (varsayılan kapalı)',
|
settingsToolsDesc: 'Etkinleştirildiğinde model, 8 terminal_* aracı üzerinden kenar çubuğu terminalleri oluşturup kullanabilir (varsayılan kapalı)',
|
||||||
settingsFenceTitle: 'Çalışma alanı yol denetimi',
|
settingsFenceTitle: 'Çalışma alanı yol denetimi',
|
||||||
settingsFenceDesc: 'Açıkken kenar çubuğunun dosya işlevleri yalnızca oturum çalışma alanındaki yollara erişir (varsayılan); kapatıldığında ana makinedeki her dosyaya erişilebilir — o süre boyunca sayfadaki betikler de aynı erişimi kazanır',
|
settingsFenceDesc: 'Açıkken kenar çubuğunun dosya işlevleri yalnızca oturum çalışma alanındaki yollara erişir (varsayılan); kapatıldığında ana makinedeki her dosyaya erişilebilir — o süre boyunca sayfadaki betikler de aynı erişimi kazanır',
|
||||||
settingsFileIconThemeTitle: 'Dosya simgeleri',
|
|
||||||
settingsFileIconThemeDesc: 'Dosya ağacı ve dosya sekmeleri için simge stili (renkli tema isteğe bağlı yüklenir, başlangıçta değil)',
|
|
||||||
settingsFileIconThemeBuiltin: 'Yerleşik (tek renk)',
|
|
||||||
settingsFileIconThemeBuiltinDesc: 'Tema belirteçlerine uyan tek renk simgeler; yaygın kod / belge / medya türlerini kapsar',
|
|
||||||
settingsFileIconThemeColored: 'Renkli marka simgeleri',
|
|
||||||
settingsFileIconThemeColoredDesc: 'Türe göre marka renkleri (örn. .tsx → React, package.json → npm); simge paketi ilk açılışta yüklenir',
|
|
||||||
fenceErrorReason: 'Bu yol oturum çalışma alanının dışında ve denetim tarafından engellendi',
|
fenceErrorReason: 'Bu yol oturum çalışma alanının dışında ve denetim tarafından engellendi',
|
||||||
fenceDisableAction: 'Çalışma alanı denetimini kapat',
|
fenceDisableAction: 'Çalışma alanı denetimini kapat',
|
||||||
settingsBottomTerminalTitle: 'Alt panelin ilk genişletilmesinde otomatik terminal aç',
|
settingsBottomTerminalTitle: 'Alt panelin ilk genişletilmesinde otomatik terminal aç',
|
||||||
|
|||||||
@@ -259,12 +259,6 @@ export const vi: Record<string, string> = {
|
|||||||
settingsToolsDesc: 'Khi bật, mô hình có thể tạo và điều khiển terminal thanh bên qua 8 công cụ terminal_* (mặc định tắt)',
|
settingsToolsDesc: 'Khi bật, mô hình có thể tạo và điều khiển terminal thanh bên qua 8 công cụ terminal_* (mặc định tắt)',
|
||||||
settingsFenceTitle: 'Kiểm tra đường dẫn workspace',
|
settingsFenceTitle: 'Kiểm tra đường dẫn workspace',
|
||||||
settingsFenceDesc: 'Khi bật, các tính năng tệp của thanh bên chỉ truy cập đường dẫn trong workspace của phiên (mặc định); khi tắt, mọi tệp trên máy chủ đều có thể truy cập — script trong trang cũng có quyền tương ứng trong thời gian đó',
|
settingsFenceDesc: 'Khi bật, các tính năng tệp của thanh bên chỉ truy cập đường dẫn trong workspace của phiên (mặc định); khi tắt, mọi tệp trên máy chủ đều có thể truy cập — script trong trang cũng có quyền tương ứng trong thời gian đó',
|
||||||
settingsFileIconThemeTitle: 'Biểu tượng tệp',
|
|
||||||
settingsFileIconThemeDesc: 'Kiểu biểu tượng cho cây tệp và tab tệp (chủ đề màu được tải khi cần, không tải lúc khởi động)',
|
|
||||||
settingsFileIconThemeBuiltin: 'Tích hợp (đơn sắc)',
|
|
||||||
settingsFileIconThemeBuiltinDesc: 'Biểu tượng đơn sắc theo token giao diện, bao phủ các loại mã / tài liệu / media phổ biến',
|
|
||||||
settingsFileIconThemeColored: 'Biểu tượng thương hiệu nhiều màu',
|
|
||||||
settingsFileIconThemeColoredDesc: 'Màu thương hiệu theo loại (ví dụ .tsx → React, package.json → npm); gói biểu tượng tải trong lần bật đầu tiên',
|
|
||||||
fenceErrorReason: 'Đường dẫn này nằm ngoài workspace của phiên và đã bị chặn bởi kiểm tra',
|
fenceErrorReason: 'Đường dẫn này nằm ngoài workspace của phiên và đã bị chặn bởi kiểm tra',
|
||||||
fenceDisableAction: 'Tắt kiểm tra workspace',
|
fenceDisableAction: 'Tắt kiểm tra workspace',
|
||||||
settingsBottomTerminalTitle: 'Tự động mở terminal khi panel dưới mở rộng lần đầu',
|
settingsBottomTerminalTitle: 'Tự động mở terminal khi panel dưới mở rộng lần đầu',
|
||||||
|
|||||||
@@ -274,12 +274,6 @@ export const zhHK: Record<string, string> = {
|
|||||||
settingsToolsDesc: '開啟後,模型可透過 terminal_create 等 8 個工具建立並操作側邊欄終端(預設關閉)',
|
settingsToolsDesc: '開啟後,模型可透過 terminal_create 等 8 個工具建立並操作側邊欄終端(預設關閉)',
|
||||||
settingsFenceTitle: '工作區路徑檢測',
|
settingsFenceTitle: '工作區路徑檢測',
|
||||||
settingsFenceDesc: '開啟後,側欄的檔案功能僅能存取工作階段工作區內的路徑(預設);關閉後可存取主機上任意檔案——關閉期間頁面內指令碼也將獲得同等存取能力',
|
settingsFenceDesc: '開啟後,側欄的檔案功能僅能存取工作階段工作區內的路徑(預設);關閉後可存取主機上任意檔案——關閉期間頁面內指令碼也將獲得同等存取能力',
|
||||||
settingsFileIconThemeTitle: '檔案圖示',
|
|
||||||
settingsFileIconThemeDesc: '檔案樹與檔案分頁的圖示風格(彩色主題按需載入,不影響啟動)',
|
|
||||||
settingsFileIconThemeBuiltin: '內建(單色)',
|
|
||||||
settingsFileIconThemeBuiltinDesc: '跟隨皮膚權杖的單色圖示,覆蓋常見程式碼 / 文件 / 媒體類型',
|
|
||||||
settingsFileIconThemeColored: '彩色品牌圖示',
|
|
||||||
settingsFileIconThemeColoredDesc: '按類型顯示品牌配色圖示(如 .tsx → React、package.json → npm);首次開啟時載入圖示包',
|
|
||||||
fenceErrorReason: '此路徑在工作階段工作區之外,已被工作區檢測攔截',
|
fenceErrorReason: '此路徑在工作階段工作區之外,已被工作區檢測攔截',
|
||||||
fenceDisableAction: '關閉工作區檢測',
|
fenceDisableAction: '關閉工作區檢測',
|
||||||
settingsBottomTerminalTitle: '底部面板首次展開自動開終端',
|
settingsBottomTerminalTitle: '底部面板首次展開自動開終端',
|
||||||
|
|||||||
@@ -274,12 +274,6 @@ export const zhMO: Record<string, string> = {
|
|||||||
settingsToolsDesc: '開啟後,模型可透過 terminal_create 等 8 個工具建立並操作側邊欄終端(預設關閉)',
|
settingsToolsDesc: '開啟後,模型可透過 terminal_create 等 8 個工具建立並操作側邊欄終端(預設關閉)',
|
||||||
settingsFenceTitle: '工作區路徑檢測',
|
settingsFenceTitle: '工作區路徑檢測',
|
||||||
settingsFenceDesc: '開啟後,側欄的檔案功能僅能存取工作階段工作區內的路徑(預設);關閉後可存取主機上任意檔案——關閉期間頁面內指令碼也將獲得同等存取能力',
|
settingsFenceDesc: '開啟後,側欄的檔案功能僅能存取工作階段工作區內的路徑(預設);關閉後可存取主機上任意檔案——關閉期間頁面內指令碼也將獲得同等存取能力',
|
||||||
settingsFileIconThemeTitle: '檔案圖示',
|
|
||||||
settingsFileIconThemeDesc: '檔案樹與檔案分頁的圖示風格(彩色主題按需載入,不影響啟動)',
|
|
||||||
settingsFileIconThemeBuiltin: '內建(單色)',
|
|
||||||
settingsFileIconThemeBuiltinDesc: '跟隨皮膚權杖的單色圖示,覆蓋常見程式碼 / 文件 / 媒體類型',
|
|
||||||
settingsFileIconThemeColored: '彩色品牌圖示',
|
|
||||||
settingsFileIconThemeColoredDesc: '按類型顯示品牌配色圖示(如 .tsx → React、package.json → npm);首次開啟時載入圖示包',
|
|
||||||
fenceErrorReason: '此路徑在工作階段工作區之外,已被工作區檢測攔截',
|
fenceErrorReason: '此路徑在工作階段工作區之外,已被工作區檢測攔截',
|
||||||
fenceDisableAction: '關閉工作區檢測',
|
fenceDisableAction: '關閉工作區檢測',
|
||||||
settingsBottomTerminalTitle: '底部面板首次展開自動開終端',
|
settingsBottomTerminalTitle: '底部面板首次展開自動開終端',
|
||||||
|
|||||||
@@ -274,12 +274,6 @@ export const zhTW: Record<string, string> = {
|
|||||||
settingsToolsDesc: '開啟後,模型可透過 terminal_create 等 8 個工具建立並操作側邊欄終端(預設關閉)',
|
settingsToolsDesc: '開啟後,模型可透過 terminal_create 等 8 個工具建立並操作側邊欄終端(預設關閉)',
|
||||||
settingsFenceTitle: '工作區路徑檢測',
|
settingsFenceTitle: '工作區路徑檢測',
|
||||||
settingsFenceDesc: '開啟後,側欄的檔案功能僅能存取工作階段工作區內的路徑(預設);關閉後可存取主機上任意檔案——關閉期間頁面內指令碼也將獲得同等存取能力',
|
settingsFenceDesc: '開啟後,側欄的檔案功能僅能存取工作階段工作區內的路徑(預設);關閉後可存取主機上任意檔案——關閉期間頁面內指令碼也將獲得同等存取能力',
|
||||||
settingsFileIconThemeTitle: '檔案圖示',
|
|
||||||
settingsFileIconThemeDesc: '檔案樹與檔案分頁的圖示風格(彩色主題按需載入,不影響啟動)',
|
|
||||||
settingsFileIconThemeBuiltin: '內建(單色)',
|
|
||||||
settingsFileIconThemeBuiltinDesc: '跟隨面板權杖的單色圖示,涵蓋常見程式碼 / 文件 / 媒體類型',
|
|
||||||
settingsFileIconThemeColored: '彩色品牌圖示',
|
|
||||||
settingsFileIconThemeColoredDesc: '依類型顯示品牌配色圖示(如 .tsx → React、package.json → npm);首次開啟時載入圖示套件',
|
|
||||||
fenceErrorReason: '此路徑在工作階段工作區之外,已被工作區檢測攔截',
|
fenceErrorReason: '此路徑在工作階段工作區之外,已被工作區檢測攔截',
|
||||||
fenceDisableAction: '關閉工作區檢測',
|
fenceDisableAction: '關閉工作區檢測',
|
||||||
settingsBottomTerminalTitle: '底部面板首次展開自動開終端',
|
settingsBottomTerminalTitle: '底部面板首次展開自動開終端',
|
||||||
|
|||||||
@@ -271,12 +271,6 @@ export const zh = {
|
|||||||
settingsToolsDesc: '开启后,模型可通过 terminal_create 等 8 个工具创建并操作侧边栏终端(默认关闭)',
|
settingsToolsDesc: '开启后,模型可通过 terminal_create 等 8 个工具创建并操作侧边栏终端(默认关闭)',
|
||||||
settingsFenceTitle: '工作区路径检测',
|
settingsFenceTitle: '工作区路径检测',
|
||||||
settingsFenceDesc: '开启后,侧栏的文件功能仅能访问会话工作区内的路径(默认);关闭后可访问主机上任意文件——关闭期间页面内脚本也将获得同等访问能力',
|
settingsFenceDesc: '开启后,侧栏的文件功能仅能访问会话工作区内的路径(默认);关闭后可访问主机上任意文件——关闭期间页面内脚本也将获得同等访问能力',
|
||||||
settingsFileIconThemeTitle: '文件图标',
|
|
||||||
settingsFileIconThemeDesc: '文件树与文件 tab 的图标风格(彩色主题按需加载,不影响启动)',
|
|
||||||
settingsFileIconThemeBuiltin: '内置(单色)',
|
|
||||||
settingsFileIconThemeBuiltinDesc: '跟随皮肤令牌的单色图标,覆盖常见代码 / 文档 / 媒体类型',
|
|
||||||
settingsFileIconThemeColored: '彩色品牌图标',
|
|
||||||
settingsFileIconThemeColoredDesc: '按类型显示品牌配色图标(如 .tsx → React、package.json → npm);首次开启时加载图标包',
|
|
||||||
fenceErrorReason: '此路径在会话工作区之外,已被工作区检测拦截',
|
fenceErrorReason: '此路径在会话工作区之外,已被工作区检测拦截',
|
||||||
fenceDisableAction: '关闭工作区检测',
|
fenceDisableAction: '关闭工作区检测',
|
||||||
settingsBottomTerminalTitle: '底部面板首次展开自动开终端',
|
settingsBottomTerminalTitle: '底部面板首次展开自动开终端',
|
||||||
@@ -719,12 +713,6 @@ export const en: Record<keyof typeof zh, string> = {
|
|||||||
settingsToolsDesc: 'When enabled, the model can create and drive sidebar terminals through the 8 terminal_* tools (off by default)',
|
settingsToolsDesc: 'When enabled, the model can create and drive sidebar terminals through the 8 terminal_* tools (off by default)',
|
||||||
settingsFenceTitle: 'Workspace path fence',
|
settingsFenceTitle: 'Workspace path fence',
|
||||||
settingsFenceDesc: 'On, the sidebar\'s file features only reach paths inside the session workspace (default); off, any file on the host is reachable — page scripts gain the same reach while it is off',
|
settingsFenceDesc: 'On, the sidebar\'s file features only reach paths inside the session workspace (default); off, any file on the host is reachable — page scripts gain the same reach while it is off',
|
||||||
settingsFileIconThemeTitle: 'File icons',
|
|
||||||
settingsFileIconThemeDesc: 'Icon style for the file tree and file tabs (the colored theme loads on demand, never at startup)',
|
|
||||||
settingsFileIconThemeBuiltin: 'Built-in (monochrome)',
|
|
||||||
settingsFileIconThemeBuiltinDesc: 'Token-driven monochrome glyphs covering common code / document / media types',
|
|
||||||
settingsFileIconThemeColored: 'Colored brand icons',
|
|
||||||
settingsFileIconThemeColoredDesc: 'Per-type brand colors (e.g. .tsx → React, package.json → npm); the icon pack loads the first time you enable it',
|
|
||||||
fenceErrorReason: 'This path is outside the session workspace and was blocked by the workspace fence',
|
fenceErrorReason: 'This path is outside the session workspace and was blocked by the workspace fence',
|
||||||
fenceDisableAction: 'Turn off the workspace fence',
|
fenceDisableAction: 'Turn off the workspace fence',
|
||||||
settingsBottomTerminalTitle: 'Auto-open a terminal on the bottom panel\'s first expansion',
|
settingsBottomTerminalTitle: 'Auto-open a terminal on the bottom panel\'s first expansion',
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ export function registerNativeSurface(deps: NativeSurfaceDeps): () => void {
|
|||||||
ctx.slots.inject('sidebar.right.pane.tab.title', () => ctx.slots.register({
|
ctx.slots.inject('sidebar.right.pane.tab.title', () => ctx.slots.register({
|
||||||
name: 'sidebar.right.pane.tab.title',
|
name: 'sidebar.right.pane.tab.title',
|
||||||
key: id,
|
key: id,
|
||||||
inject: () => ({ records }),
|
inject: () => ({ records, service, descriptorId: injected.descriptorId }),
|
||||||
}, NativeTabTitle)),
|
}, NativeTabTitle)),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,12 @@ import type { BetterSidebarService } from '../service.ts'
|
|||||||
import type { SidebarStore, SidebarTab, TabType } from '../state.ts'
|
import type { SidebarStore, SidebarTab, TabType } from '../state.ts'
|
||||||
import css from '../sidebar.module.css'
|
import css from '../sidebar.module.css'
|
||||||
|
|
||||||
|
/** The chip glyph's size: the tab strip's own icon scale. */
|
||||||
|
const CHIP_ICON_SIZE = 14
|
||||||
|
|
||||||
|
/** The editor kind: a chip for a file row shows the file's own glyph. */
|
||||||
|
const EDITOR_KIND = 'editor'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The plugin-side seed a native open carries in `navigation.params`.
|
* The plugin-side seed a native open carries in `navigation.params`.
|
||||||
* JSON-shaped by convention (the native surface does not validate it).
|
* JSON-shaped by convention (the native surface does not validate it).
|
||||||
@@ -330,20 +336,52 @@ export function NativeTabBody(props: NativeBodyInjected & NativeBodyFrameworkPro
|
|||||||
/** What a title registration injects. */
|
/** What a title registration injects. */
|
||||||
export interface NativeTitleInjected {
|
export interface NativeTitleInjected {
|
||||||
readonly records: NativeTabRecords
|
readonly records: NativeTabRecords
|
||||||
|
readonly service: BetterSidebarService
|
||||||
|
/** The descriptor id this title belongs to (one registration per descriptor). */
|
||||||
|
readonly descriptorId: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A live tab chip: the synthetic record's title (the editor rewrites it on an
|
* A live tab chip: the type's glyph followed by the synthetic record's title
|
||||||
* in-place file switch, the side chat on the thread's first prompt). Without
|
* (the editor rewrites it on an in-place file switch, the side chat on the
|
||||||
* this registration the chip would keep the title captured when the tab
|
* thread's first prompt). Without this registration the chip would keep the
|
||||||
* opened.
|
* title captured when the tab opened.
|
||||||
|
*
|
||||||
|
* The host's tab definition has no icon field — a chip is drawn from the
|
||||||
|
* `title` text alone — but this slot IS the chip's content, so the glyph is
|
||||||
|
* ours to add. Placement follows the plugin's own semantics: an editor tab
|
||||||
|
* with a path shows that file's icon (the same glyph the tree row shows), and
|
||||||
|
* every other tab shows its descriptor's icon. Both ride
|
||||||
|
* `descriptor.icon`, so the workbench strip, the guide capsules and the chip
|
||||||
|
* cannot drift apart.
|
||||||
*/
|
*/
|
||||||
export function NativeTabTitle(props: NativeTitleInjected & NativeBodyFrameworkProps): ReactNode {
|
export function NativeTabTitle(props: NativeTitleInjected & NativeBodyFrameworkProps): ReactNode {
|
||||||
const { records, useTabInfo } = props
|
const { records, service, descriptorId, useTabInfo } = props
|
||||||
const nativeTab = useTabInfo().tab
|
const nativeTab = useTabInfo().tab
|
||||||
return useSyncExternalStore(
|
const version = useSyncExternalStore(
|
||||||
listener => records.subscribe(listener),
|
listener => records.subscribe(listener),
|
||||||
() => records.get(nativeTab.id)?.tab.title ?? nativeTab.title,
|
() => records.versionOf(nativeTab.id),
|
||||||
|
)
|
||||||
|
const record = records.get(nativeTab.id)
|
||||||
|
const title = record?.tab.title ?? nativeTab.title
|
||||||
|
// `version` is read so a title/path/meta mutation re-renders the chip; the
|
||||||
|
// icon itself is derived from the record, never stored.
|
||||||
|
void version
|
||||||
|
const descriptor = service.getTab(descriptorId) ?? service.getTab(record?.tab.type ?? nativeTab.kind)
|
||||||
|
const path = record?.tab.path
|
||||||
|
const icon = path !== undefined && descriptorId === EDITOR_KIND
|
||||||
|
? service.fileIcon(path, CHIP_ICON_SIZE)
|
||||||
|
: undefined
|
||||||
|
const glyph = icon ?? (typeof descriptor?.icon === 'function'
|
||||||
|
? descriptor.icon(CHIP_ICON_SIZE)
|
||||||
|
: descriptor?.icon)
|
||||||
|
if (glyph === undefined || glyph === null) return title
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Decorative: the chip's accessible name stays the title. */}
|
||||||
|
<span className={css.chipIcon} aria-hidden="true">{glyph}</span>
|
||||||
|
{title}
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -74,9 +74,6 @@ export function parsePrefs(value: unknown): SidebarPrefs {
|
|||||||
workspaceFence: typeof record.workspaceFence === 'boolean'
|
workspaceFence: typeof record.workspaceFence === 'boolean'
|
||||||
? record.workspaceFence
|
? record.workspaceFence
|
||||||
: SIDEBAR_PREFS_DEFAULTS.workspaceFence,
|
: SIDEBAR_PREFS_DEFAULTS.workspaceFence,
|
||||||
fileIconTheme: typeof record.fileIconTheme === 'string'
|
|
||||||
? record.fileIconTheme
|
|
||||||
: SIDEBAR_PREFS_DEFAULTS.fileIconTheme,
|
|
||||||
// The title-bar scheme (auto | web | preset | custom). The schema
|
// The title-bar scheme (auto | web | preset | custom). The schema
|
||||||
// declares the field WITHOUT a default, so documents written by older
|
// declares the field WITHOUT a default, so documents written by older
|
||||||
// plugin versions resolve without it — migrate from the legacy fields:
|
// plugin versions resolve without it — migrate from the legacy fields:
|
||||||
|
|||||||
+12
-12
@@ -27,7 +27,7 @@ import {
|
|||||||
type SidebarSnapshot, type SidebarState, type SidebarStore, type SidebarTab, type TabType,
|
type SidebarSnapshot, type SidebarState, type SidebarStore, type SidebarTab, type TabType,
|
||||||
} from './state.ts'
|
} from './state.ts'
|
||||||
import { baseName, extOf } from './paths.ts'
|
import { baseName, extOf } from './paths.ts'
|
||||||
import { builtinFileIconOf, builtinFolderIcon, fallbackFileIcon } from './file-icons.tsx'
|
import { builtinFileIcon, builtinFolderIcon } from './file-icons.tsx'
|
||||||
import type { SessionScope } from './api.ts'
|
import type { SessionScope } from './api.ts'
|
||||||
import type { SidebarPrefs } from '../prefs-shared.ts'
|
import type { SidebarPrefs } from '../prefs-shared.ts'
|
||||||
|
|
||||||
@@ -491,9 +491,11 @@ export interface BetterSidebarService {
|
|||||||
* the whole chain with per-factory crash isolation:
|
* the whole chain with per-factory crash isolation:
|
||||||
* 1. a specific registered name or extension (priority desc, registration
|
* 1. a specific registered name or extension (priority desc, registration
|
||||||
* order),
|
* order),
|
||||||
* 2. the built-in monochrome glyph map (md/media/pdf/json/code/...),
|
* 2. the best registered global default (`exts: []`, priority desc) — an
|
||||||
* 3. the best registered global default (`exts: []`, priority desc),
|
* external plugin that registers a catch-all owns every row the host's
|
||||||
* 4. the generic `VscFile`.
|
* classifier would otherwise draw,
|
||||||
|
* 3. the host's own `FileTypeIcon` artwork (feature `fileIcons`, DSH's
|
||||||
|
* classifier and glyphs — the plugin ships no extension table).
|
||||||
* A throwing factory is logged (console.error) and skipped — the caller
|
* A throwing factory is logged (console.error) and skipped — the caller
|
||||||
* always gets a valid ReactNode.
|
* always gets a valid ReactNode.
|
||||||
*/
|
*/
|
||||||
@@ -799,29 +801,27 @@ export function createBetterSidebarService(store: SidebarStore): BetterSidebarSe
|
|||||||
}
|
}
|
||||||
|
|
||||||
// The authoritative file-icon chain (see the interface doc): specific name
|
// The authoritative file-icon chain (see the interface doc): specific name
|
||||||
// or extension registration → built-in glyph → best catch-all → stock
|
// or extension registration → registered catch-all → the host's own
|
||||||
// VscFile. The catch-all ranks by priority desc then registration order
|
// file-type artwork. The catch-all ranks by priority desc then registration
|
||||||
// (first wins); an unclaimed extension reaching it is exactly "nothing
|
// order (first wins), which is what lets an external plugin own "every
|
||||||
// specified falls back to the (registered or stock) default".
|
// extension I did not name" without also owning the ones DSH draws.
|
||||||
const fileIcon = (path: string, size: number): ReactNode => {
|
const fileIcon = (path: string, size: number): ReactNode => {
|
||||||
const specific = matchFileIcon(path)
|
const specific = matchFileIcon(path)
|
||||||
if (specific !== undefined) {
|
if (specific !== undefined) {
|
||||||
const icon = safeIcon(specific, path, size)
|
const icon = safeIcon(specific, path, size)
|
||||||
if (icon !== undefined) return icon
|
if (icon !== undefined) return icon
|
||||||
}
|
}
|
||||||
const builtin = builtinFileIconOf(path)
|
|
||||||
if (builtin !== undefined) return builtin(size)
|
|
||||||
for (const d of rankedFileIcons()) {
|
for (const d of rankedFileIcons()) {
|
||||||
if (d.exts !== undefined && d.exts.length === 0) {
|
if (d.exts !== undefined && d.exts.length === 0) {
|
||||||
const icon = safeIcon(d, path, size)
|
const icon = safeIcon(d, path, size)
|
||||||
if (icon !== undefined) return icon
|
if (icon !== undefined) return icon
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return fallbackFileIcon(size)
|
return builtinFileIcon(path, size)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Directory rows: registered folderNames/folder/folder-open icon, else the
|
// Directory rows: registered folderNames/folder/folder-open icon, else the
|
||||||
// built-in VSCodicons glyphs. The row's path feeds the factory (a theme may
|
// host's folder glyph. The row's path feeds the factory (a registration may
|
||||||
// vary icons per directory) and `open` lets one descriptor render both
|
// vary icons per directory) and `open` lets one descriptor render both
|
||||||
// states.
|
// states.
|
||||||
const folderIcon = (path: string, open: boolean, size: number): ReactNode => {
|
const folderIcon = (path: string, open: boolean, size: number): ReactNode => {
|
||||||
|
|||||||
@@ -420,6 +420,17 @@ body[data-dsh-sidebar-dragging] .bottomPanel {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* The native tab chip's glyph (drawn by the plugin inside the host's
|
||||||
|
`sidebar.right.pane.tab.title` slot, which is the chip's content): sized to
|
||||||
|
the text, aligned on its baseline box, and separated from the title the way
|
||||||
|
the workbench strip separates its own glyph from its label. */
|
||||||
|
.chipIcon {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-right: 6px;
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
|
||||||
/* One tab's mounted content; inactive tabs stay mounted but hidden so
|
/* One tab's mounted content; inactive tabs stay mounted but hidden so
|
||||||
switching never tears down terminals/editors (state survives). */
|
switching never tears down terminals/editors (state survives). */
|
||||||
.paneTab {
|
.paneTab {
|
||||||
|
|||||||
@@ -118,7 +118,6 @@ export const PrefsSchema: z<SidebarPrefs> = z.object({
|
|||||||
terminalFontSize: z.number().step(1).min(TERMINAL_FONT_SIZE_MIN).max(TERMINAL_FONT_SIZE_MAX).default(TERMINAL_FONT_SIZE_DEFAULT),
|
terminalFontSize: z.number().step(1).min(TERMINAL_FONT_SIZE_MIN).max(TERMINAL_FONT_SIZE_MAX).default(TERMINAL_FONT_SIZE_DEFAULT),
|
||||||
editorExplorer: z.boolean().default(false),
|
editorExplorer: z.boolean().default(false),
|
||||||
workspaceFence: z.boolean().default(true),
|
workspaceFence: z.boolean().default(true),
|
||||||
fileIconTheme: z.string().default('builtin'),
|
|
||||||
terminalShell: z.string().default(''),
|
terminalShell: z.string().default(''),
|
||||||
terminalShellArgs: z.string().default(''),
|
terminalShellArgs: z.string().default(''),
|
||||||
titleBarScheme: z.union([z.const('auto'), z.const('web'), z.const('preset'), z.const('custom')]),
|
titleBarScheme: z.union([z.const('auto'), z.const('web'), z.const('preset'), z.const('custom')]),
|
||||||
|
|||||||
@@ -79,14 +79,6 @@ export interface SidebarPrefs {
|
|||||||
* one-click global off + retry.
|
* one-click global off + retry.
|
||||||
*/
|
*/
|
||||||
workspaceFence: boolean
|
workspaceFence: boolean
|
||||||
/**
|
|
||||||
* The file-icon theme (v0.19.0+). `'builtin'` (default) is the monochrome
|
|
||||||
* VSCodicons glyph map every surface already uses; `'colored'` swaps in the
|
|
||||||
* optional brand-colored dataset, fetched from the lazy `file-icons` chunk
|
|
||||||
* only when selected (an unknown id behaves as `'builtin'`). The switch
|
|
||||||
* lives under the files tab's gear in the Side card settings.
|
|
||||||
*/
|
|
||||||
fileIconTheme: string
|
|
||||||
/**
|
/**
|
||||||
* The shell the UI and agent terminals spawn (absolute path or bare
|
* The shell the UI and agent terminals spawn (absolute path or bare
|
||||||
* executable name). Empty (default) keeps the legacy resolution order:
|
* executable name). Empty (default) keeps the legacy resolution order:
|
||||||
@@ -256,7 +248,6 @@ export const SIDEBAR_PREFS_DEFAULTS: SidebarPrefs = {
|
|||||||
terminalFontSize: TERMINAL_FONT_SIZE_DEFAULT,
|
terminalFontSize: TERMINAL_FONT_SIZE_DEFAULT,
|
||||||
editorExplorer: false,
|
editorExplorer: false,
|
||||||
workspaceFence: true,
|
workspaceFence: true,
|
||||||
fileIconTheme: 'builtin',
|
|
||||||
terminalShell: '',
|
terminalShell: '',
|
||||||
terminalShellArgs: '',
|
terminalShellArgs: '',
|
||||||
titleBarScheme: 'auto',
|
titleBarScheme: 'auto',
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ describe('built-in tab registrations', () => {
|
|||||||
it('the editor tab declares its merged-mode (embedded file tree) setting', () => {
|
it('the editor tab declares its merged-mode (embedded file tree) setting', () => {
|
||||||
const { service } = setup()
|
const { service } = setup()
|
||||||
const toggles = service.getTab('editor')?.settings?.toggles ?? []
|
const toggles = service.getTab('editor')?.settings?.toggles ?? []
|
||||||
expect(toggles.map(t => t.key)).toEqual(['editorExplorer', 'workspaceFence', 'fileIconTheme'])
|
expect(toggles.map(t => t.key)).toEqual(['editorExplorer', 'workspaceFence'])
|
||||||
expect(toggles[0]?.title).toBeDefined()
|
expect(toggles[0]?.title).toBeDefined()
|
||||||
expect(toggles[0]?.desc).toBeDefined()
|
expect(toggles[0]?.desc).toBeDefined()
|
||||||
// The merged mode is an iconed select (merged vs separate), not a switch.
|
// The merged mode is an iconed select (merged vs separate), not a switch.
|
||||||
|
|||||||
@@ -150,6 +150,6 @@ describe('/sidebar/bundle route', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('exports the chunk allowlist (mirror of src/client/chunk-loader.ts)', () => {
|
it('exports the chunk allowlist (mirror of src/client/chunk-loader.ts)', () => {
|
||||||
expect([...CHUNK_NAMES]).toEqual(['terminal', 'editor', 'mermaid', 'locale', 'file-icons'])
|
expect([...CHUNK_NAMES]).toEqual(['terminal', 'editor', 'mermaid', 'locale'])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,176 +0,0 @@
|
|||||||
/**
|
|
||||||
* The optional colored file-icon theme: the lazy-chunk loader that turns the
|
|
||||||
* `fileIconTheme` pref into a register/dispose of the colored dataset, and
|
|
||||||
* the dataset's own registration through the public `registerFileIcon` API.
|
|
||||||
*
|
|
||||||
* The loader tests drive a fake chunk (no fetch); the dataset tests use the
|
|
||||||
* real chunk module against a real service, asserting representative rules
|
|
||||||
* from each table (extension / exact file name / directory name).
|
|
||||||
*/
|
|
||||||
import { describe, it, expect, vi, afterEach } from 'vitest'
|
|
||||||
import type { ReactElement, ReactNode } from 'react'
|
|
||||||
import { SiNpm, SiReact } from 'react-icons/si'
|
|
||||||
import { VscFolder, VscFolderOpened } from 'react-icons/vsc'
|
|
||||||
|
|
||||||
// Mock browser globals (SidebarStore.reduce → schedulePersist uses window.setTimeout)
|
|
||||||
const g = globalThis as Record<string, unknown>
|
|
||||||
if (g.window === undefined) {
|
|
||||||
g.window = {
|
|
||||||
clearTimeout: () => {},
|
|
||||||
setTimeout: (_fn: () => void) => 0,
|
|
||||||
innerWidth: 1024,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (g.localStorage === undefined) {
|
|
||||||
g.localStorage = { getItem: () => null, setItem: () => {} }
|
|
||||||
}
|
|
||||||
|
|
||||||
import { registerChunkForTests, resetChunks } from '../src/client/chunk-loader.ts'
|
|
||||||
import { createBetterSidebarService } from '../src/client/service.ts'
|
|
||||||
import { createSidebarStore } from '../src/client/state.ts'
|
|
||||||
import {
|
|
||||||
createFileIconThemeLoader,
|
|
||||||
FILE_ICON_THEME_BUILTIN,
|
|
||||||
FILE_ICON_THEME_COLORED,
|
|
||||||
} from '../src/client/file-icon-theme.ts'
|
|
||||||
import { registerColoredFileIcons } from '../src/client/chunks/file-icons.tsx'
|
|
||||||
|
|
||||||
/** The component type of a rendered icon element (identity of the glyph). */
|
|
||||||
const glyphOf = (node: ReactNode): unknown => (node as ReactElement).type
|
|
||||||
/** The inline color of a rendered icon element (undefined when untinted). */
|
|
||||||
const colorOf = (node: ReactNode): unknown => (node as ReactElement).props.style?.color
|
|
||||||
|
|
||||||
/** Flush the queued promise chain (loadChunk → loader's `.then`). */
|
|
||||||
const settle = async (): Promise<void> => { await new Promise((resolve) => { setTimeout(resolve, 0) }) }
|
|
||||||
|
|
||||||
afterEach(() => { resetChunks() })
|
|
||||||
|
|
||||||
describe('file-icon theme loader (pref → lazy chunk → registerFileIcon)', () => {
|
|
||||||
it('registers the dataset when the colored theme is selected, and disposes on builtin', async () => {
|
|
||||||
const service = createBetterSidebarService(createSidebarStore())
|
|
||||||
let registrations = 0
|
|
||||||
let disposals = 0
|
|
||||||
registerChunkForTests('file-icons', async () => ({
|
|
||||||
registerColoredFileIcons: (target: typeof service) => {
|
|
||||||
expect(target).toBe(service)
|
|
||||||
registrations += 1
|
|
||||||
return () => { disposals += 1 }
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
const loader = createFileIconThemeLoader(service)
|
|
||||||
expect(service.getFileIcons()).toHaveLength(0)
|
|
||||||
loader.set(FILE_ICON_THEME_COLORED)
|
|
||||||
await settle()
|
|
||||||
expect(registrations).toBe(1)
|
|
||||||
loader.set(FILE_ICON_THEME_BUILTIN)
|
|
||||||
expect(disposals).toBe(1)
|
|
||||||
loader.set(FILE_ICON_THEME_COLORED)
|
|
||||||
await settle()
|
|
||||||
expect(registrations).toBe(2)
|
|
||||||
loader.dispose()
|
|
||||||
expect(disposals).toBe(2)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('treats an unknown pref value as the built-in theme', async () => {
|
|
||||||
const service = createBetterSidebarService(createSidebarStore())
|
|
||||||
let registrations = 0
|
|
||||||
registerChunkForTests('file-icons', async () => ({
|
|
||||||
registerColoredFileIcons: () => { registrations += 1; return () => {} },
|
|
||||||
}))
|
|
||||||
const loader = createFileIconThemeLoader(service)
|
|
||||||
loader.set('no-such-theme')
|
|
||||||
await settle()
|
|
||||||
expect(registrations).toBe(0)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('drops a chunk load that lands after the pref moved back to builtin', async () => {
|
|
||||||
const service = createBetterSidebarService(createSidebarStore())
|
|
||||||
let registrations = 0
|
|
||||||
let release: (() => void) | undefined
|
|
||||||
registerChunkForTests('file-icons', () => new Promise((resolve) => {
|
|
||||||
release = () => resolve({ registerColoredFileIcons: () => { registrations += 1; return () => {} } })
|
|
||||||
}))
|
|
||||||
const loader = createFileIconThemeLoader(service)
|
|
||||||
loader.set(FILE_ICON_THEME_COLORED)
|
|
||||||
loader.set(FILE_ICON_THEME_BUILTIN)
|
|
||||||
release?.()
|
|
||||||
await settle()
|
|
||||||
expect(registrations).toBe(0)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('does not re-register on a repeated set of the same value', async () => {
|
|
||||||
const service = createBetterSidebarService(createSidebarStore())
|
|
||||||
let registrations = 0
|
|
||||||
registerChunkForTests('file-icons', async () => ({
|
|
||||||
registerColoredFileIcons: () => { registrations += 1; return () => {} },
|
|
||||||
}))
|
|
||||||
const loader = createFileIconThemeLoader(service)
|
|
||||||
loader.set(FILE_ICON_THEME_COLORED)
|
|
||||||
loader.set(FILE_ICON_THEME_COLORED)
|
|
||||||
await settle()
|
|
||||||
expect(registrations).toBe(1)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('logs and keeps the built-in glyphs when the chunk fails to load', async () => {
|
|
||||||
const service = createBetterSidebarService(createSidebarStore())
|
|
||||||
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
||||||
try {
|
|
||||||
registerChunkForTests('file-icons', async () => { throw new Error('offline') })
|
|
||||||
const loader = createFileIconThemeLoader(service)
|
|
||||||
loader.set(FILE_ICON_THEME_COLORED)
|
|
||||||
await settle()
|
|
||||||
expect(errorSpy).toHaveBeenCalled()
|
|
||||||
expect(service.getFileIcons()).toHaveLength(0)
|
|
||||||
} finally {
|
|
||||||
errorSpy.mockRestore()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
it('logs and keeps the built-in glyphs when the chunk exports no registrar', async () => {
|
|
||||||
const service = createBetterSidebarService(createSidebarStore())
|
|
||||||
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
||||||
try {
|
|
||||||
registerChunkForTests('file-icons', async () => ({}))
|
|
||||||
const loader = createFileIconThemeLoader(service)
|
|
||||||
loader.set(FILE_ICON_THEME_COLORED)
|
|
||||||
await settle()
|
|
||||||
expect(errorSpy).toHaveBeenCalled()
|
|
||||||
expect(service.getFileIcons()).toHaveLength(0)
|
|
||||||
} finally {
|
|
||||||
errorSpy.mockRestore()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('colored dataset registration (563 rules through registerFileIcon)', () => {
|
|
||||||
it('registers the extension, file-name and directory rules and disposes them', () => {
|
|
||||||
const service = createBetterSidebarService(createSidebarStore())
|
|
||||||
const dispose = registerColoredFileIcons(service)
|
|
||||||
expect(service.getFileIcons().length).toBeGreaterThan(100)
|
|
||||||
|
|
||||||
// Extension rule: .tsx → React blue, and it outranks the built-in glyph.
|
|
||||||
const tsx = service.fileIcon('/w/App.tsx', 14)
|
|
||||||
expect(glyphOf(tsx)).toBe(SiReact)
|
|
||||||
expect(colorOf(tsx)).toBe('#61DAFB')
|
|
||||||
|
|
||||||
// Exact file name rule: package.json → npm red (name beats the .json ext).
|
|
||||||
const pkg = service.fileIcon('/w/package.json', 14)
|
|
||||||
expect(glyphOf(pkg)).toBe(SiNpm)
|
|
||||||
expect(colorOf(pkg)).toBe('#CB3837')
|
|
||||||
|
|
||||||
// Directory rule: node_modules is tinted, an unnamed dir keeps the glyph.
|
|
||||||
const nodeModules = service.folderIcon('/w/node_modules', false, 14)
|
|
||||||
expect(glyphOf(nodeModules)).toBe(VscFolder)
|
|
||||||
expect(colorOf(nodeModules)).toBe('#CB3837')
|
|
||||||
expect(glyphOf(service.folderIcon('/w/node_modules', true, 14))).toBe(VscFolderOpened)
|
|
||||||
expect(glyphOf(service.folderIcon('/w/zzz-unnamed-dir', false, 14))).toBe(VscFolder)
|
|
||||||
expect(colorOf(service.folderIcon('/w/zzz-unnamed-dir', false, 14))).toBeUndefined()
|
|
||||||
|
|
||||||
// An extension the dataset does not cover still falls back to the built-in.
|
|
||||||
expect(glyphOf(service.fileIcon('/w/data.unknownext', 14))).toBeDefined()
|
|
||||||
|
|
||||||
dispose()
|
|
||||||
expect(service.getFileIcons()).toHaveLength(0)
|
|
||||||
expect(colorOf(service.fileIcon('/w/App.tsx', 14))).toBeUndefined()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
+50
-40
@@ -1,13 +1,13 @@
|
|||||||
/**
|
/**
|
||||||
* Tests for the file-icon feature: the external registration API
|
* Tests for the file-icon feature: the external registration API
|
||||||
* (registerFileIcon — registry lifecycle, reserved folder exts, and the
|
* (registerFileIcon — registry lifecycle, reserved folder exts, and the
|
||||||
* authoritative fileIcon/folderIcon resolver chains) and the built-in
|
* authoritative fileIcon/folderIcon resolver chains) and the built-in glyphs,
|
||||||
* per-extension glyph map (extension normalization, group hits, and the
|
* which are DSH's own `FileTypeIcon` artwork rather than a plugin-owned
|
||||||
* generic VscFile fallback).
|
* extension table (see src/client/file-icons.tsx).
|
||||||
*/
|
*/
|
||||||
import { describe, it, expect, vi } from 'vitest'
|
import { describe, it, expect, vi } from 'vitest'
|
||||||
import type { ReactElement, ReactNode } from 'react'
|
import type { ReactElement, ReactNode } from 'react'
|
||||||
import { VscFile, VscFileCode, VscFileMedia, VscFolder, VscFolderOpened, VscJson, VscMarkdown } from 'react-icons/vsc'
|
import { FileTypeIcon } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||||
|
|
||||||
// Mock browser globals (SidebarStore.reduce → schedulePersist uses window.setTimeout)
|
// Mock browser globals (SidebarStore.reduce → schedulePersist uses window.setTimeout)
|
||||||
const g = globalThis as Record<string, unknown>
|
const g = globalThis as Record<string, unknown>
|
||||||
@@ -100,19 +100,23 @@ describe('file icon registration API', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('fileIcon resolver chain (specific → builtin → catch-all → VscFile)', () => {
|
describe('fileIcon resolver chain (specific → catch-all → the host artwork)', () => {
|
||||||
it('a specific registration beats the builtin glyph', () => {
|
it('a specific registration beats the builtin glyph', () => {
|
||||||
const service = createBetterSidebarService(createSidebarStore())
|
const service = createBetterSidebarService(createSidebarStore())
|
||||||
service.registerFileIcon({ id: 'md', exts: ['md'], icon: marker })
|
service.registerFileIcon({ id: 'md', exts: ['md'], icon: marker })
|
||||||
expect(glyphOf(service.fileIcon('/w/README.md', 14))).toBe('span')
|
expect(glyphOf(service.fileIcon('/w/README.md', 14))).toBe('span')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('the builtin glyph beats a registered catch-all (the default only claims unclaimed extensions)', () => {
|
it('a registered catch-all claims every row the plugin itself would draw', () => {
|
||||||
|
// The plugin ships no extension table any more: the host's classifier
|
||||||
|
// covers every path, so "the built-in set already claims this one" is no
|
||||||
|
// longer a reason to keep a catch-all out. A catch-all therefore wins the
|
||||||
|
// fallback position outright, and an unregistered tree keeps the host's
|
||||||
|
// artwork (the next case).
|
||||||
const service = createBetterSidebarService(createSidebarStore())
|
const service = createBetterSidebarService(createSidebarStore())
|
||||||
service.registerFileIcon({ id: 'all', exts: [], icon: marker })
|
service.registerFileIcon({ id: 'all', exts: [], icon: marker })
|
||||||
expect(glyphOf(service.fileIcon('/w/README.md', 14))).toBe(VscMarkdown)
|
expect(glyphOf(service.fileIcon('/w/README.md', 14))).toBe('span')
|
||||||
expect(glyphOf(service.fileIcon('/w/logo.png', 14))).toBe(VscFileMedia)
|
expect(glyphOf(service.fileIcon('/w/logo.png', 14))).toBe('span')
|
||||||
// An extension the builtin map does not cover reaches the catch-all.
|
|
||||||
expect(glyphOf(service.fileIcon('/w/Makefile', 14))).toBe('span')
|
expect(glyphOf(service.fileIcon('/w/Makefile', 14))).toBe('span')
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -133,18 +137,18 @@ describe('fileIcon resolver chain (specific → builtin → catch-all → VscFil
|
|||||||
const service = createBetterSidebarService(createSidebarStore())
|
const service = createBetterSidebarService(createSidebarStore())
|
||||||
service.registerFileIcon({ id: 'md-decline', exts: ['md'], icon: () => undefined })
|
service.registerFileIcon({ id: 'md-decline', exts: ['md'], icon: () => undefined })
|
||||||
// The specific registration declined → the built-in glyph claims it.
|
// The specific registration declined → the built-in glyph claims it.
|
||||||
expect(glyphOf(service.fileIcon('/w/README.md', 14))).toBe(VscMarkdown)
|
expect(glyphOf(service.fileIcon('/w/README.md', 14))).toBe(FileTypeIcon)
|
||||||
service.registerFileIcon({ id: 'all-decline', exts: [], priority: 10, icon: () => undefined })
|
service.registerFileIcon({ id: 'all-decline', exts: [], priority: 10, icon: () => undefined })
|
||||||
service.registerFileIcon({ id: 'all-take', exts: [], icon: marker })
|
service.registerFileIcon({ id: 'all-take', exts: [], icon: marker })
|
||||||
// The declining catch-all is skipped, the next one takes the row.
|
// The declining catch-all is skipped, the next one takes the row.
|
||||||
expect(glyphOf(service.fileIcon('/w/Makefile', 14))).toBe('span')
|
expect(glyphOf(service.fileIcon('/w/Makefile', 14))).toBe('span')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('with no registration at all the chain is the built-in one', () => {
|
it('with no registration at all the chain is the host’s artwork', () => {
|
||||||
const service = createBetterSidebarService(createSidebarStore())
|
const service = createBetterSidebarService(createSidebarStore())
|
||||||
expect(glyphOf(service.fileIcon('/w/pkg.json', 14))).toBe(VscJson)
|
expect(glyphOf(service.fileIcon('/w/pkg.json', 14))).toBe(FileTypeIcon)
|
||||||
expect(glyphOf(service.fileIcon('/w/main.ts', 14))).toBe(VscFileCode)
|
expect(glyphOf(service.fileIcon('/w/main.ts', 14))).toBe(FileTypeIcon)
|
||||||
expect(glyphOf(service.fileIcon('/w/Makefile', 14))).toBe(VscFile)
|
expect(glyphOf(service.fileIcon('/w/Makefile', 14))).toBe(FileTypeIcon)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('a throwing factory is skipped at every level (console.error, next link wins)', () => {
|
it('a throwing factory is skipped at every level (console.error, next link wins)', () => {
|
||||||
@@ -153,10 +157,10 @@ describe('fileIcon resolver chain (specific → builtin → catch-all → VscFil
|
|||||||
const service = createBetterSidebarService(createSidebarStore())
|
const service = createBetterSidebarService(createSidebarStore())
|
||||||
service.registerFileIcon({ id: 'boom-md', exts: ['md'], icon: () => { throw new Error('boom') } })
|
service.registerFileIcon({ id: 'boom-md', exts: ['md'], icon: () => { throw new Error('boom') } })
|
||||||
// Specific throws → falls to the builtin glyph.
|
// Specific throws → falls to the builtin glyph.
|
||||||
expect(glyphOf(service.fileIcon('/w/README.md', 14))).toBe(VscMarkdown)
|
expect(glyphOf(service.fileIcon('/w/README.md', 14))).toBe(FileTypeIcon)
|
||||||
service.registerFileIcon({ id: 'boom-all', exts: [], priority: 10, icon: () => { throw new Error('boom') } })
|
service.registerFileIcon({ id: 'boom-all', exts: [], priority: 10, icon: () => { throw new Error('boom') } })
|
||||||
// Catch-all throws → falls to the stock VscFile.
|
// Catch-all throws → falls to the host's file-type artwork.
|
||||||
expect(glyphOf(service.fileIcon('/w/Makefile', 14))).toBe(VscFile)
|
expect(glyphOf(service.fileIcon('/w/Makefile', 14))).toBe(FileTypeIcon)
|
||||||
expect(errorSpy).toHaveBeenCalled()
|
expect(errorSpy).toHaveBeenCalled()
|
||||||
} finally {
|
} finally {
|
||||||
errorSpy.mockRestore()
|
errorSpy.mockRestore()
|
||||||
@@ -167,8 +171,8 @@ describe('fileIcon resolver chain (specific → builtin → catch-all → VscFil
|
|||||||
const service = createBetterSidebarService(createSidebarStore())
|
const service = createBetterSidebarService(createSidebarStore())
|
||||||
service.registerFileIcon({ id: 'folders', exts: ['folder', 'folder-open'], icon: marker })
|
service.registerFileIcon({ id: 'folders', exts: ['folder', 'folder-open'], icon: marker })
|
||||||
expect(service.matchFileIcon('/w/x.folder')).toBeUndefined()
|
expect(service.matchFileIcon('/w/x.folder')).toBeUndefined()
|
||||||
// The real file falls through to the stock default, not the folder icon.
|
// The real file falls through to the host's artwork, not the folder glyph.
|
||||||
expect(glyphOf(service.fileIcon('/w/x.folder', 14))).toBe(VscFile)
|
expect(glyphOf(service.fileIcon('/w/x.folder', 14))).toBe(FileTypeIcon)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -203,7 +207,7 @@ describe('name matching (names / folderNames — the icon-theme half)', () => {
|
|||||||
const solo = createBetterSidebarService(createSidebarStore())
|
const solo = createBetterSidebarService(createSidebarStore())
|
||||||
solo.registerFileIcon({ id: 'named', folderNames: ['src'], icon: marker })
|
solo.registerFileIcon({ id: 'named', folderNames: ['src'], icon: marker })
|
||||||
expect(solo.matchFolderIcon(false, 'lib')).toBeUndefined()
|
expect(solo.matchFolderIcon(false, 'lib')).toBeUndefined()
|
||||||
expect(glyphOf(solo.folderIcon('/w/lib', false, 14))).toBe(VscFolder)
|
expect(glyphOf(solo.folderIcon('/w/lib', false, 14))).toBe(FileTypeIcon)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('the folder factory receives the open flag so one descriptor renders both states', () => {
|
it('the folder factory receives the open flag so one descriptor renders both states', () => {
|
||||||
@@ -223,13 +227,13 @@ describe('name matching (names / folderNames — the icon-theme half)', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('folderIcon resolver (registered folder/folder-open → builtin glyphs)', () => {
|
describe('folderIcon resolver (registered folder/folder-open → the host folder glyph)', () => {
|
||||||
it('unregistered directories show the builtin glyphs', () => {
|
it('unregistered directories show the builtin glyphs', () => {
|
||||||
const service = createBetterSidebarService(createSidebarStore())
|
const service = createBetterSidebarService(createSidebarStore())
|
||||||
expect(glyphOf(service.folderIcon('/w/src', false, 14))).toBe(VscFolder)
|
expect(glyphOf(service.folderIcon('/w/src', false, 14))).toBe(FileTypeIcon)
|
||||||
expect(glyphOf(service.folderIcon('/w/src', true, 14))).toBe(VscFolderOpened)
|
expect(glyphOf(service.folderIcon('/w/src', true, 14))).toBe(FileTypeIcon)
|
||||||
expect(glyphOf(builtinFolderIcon(false, 14))).toBe(VscFolder)
|
expect(glyphOf(builtinFolderIcon(false, 14))).toBe(FileTypeIcon)
|
||||||
expect(glyphOf(builtinFolderIcon(true, 14))).toBe(VscFolderOpened)
|
expect(glyphOf(builtinFolderIcon(true, 14))).toBe(FileTypeIcon)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('folder / folder-open registrations replace the dir glyphs (priority desc)', () => {
|
it('folder / folder-open registrations replace the dir glyphs (priority desc)', () => {
|
||||||
@@ -237,7 +241,7 @@ describe('folderIcon resolver (registered folder/folder-open → builtin glyphs)
|
|||||||
service.registerFileIcon({ id: 'closed', exts: ['folder'], icon: marker })
|
service.registerFileIcon({ id: 'closed', exts: ['folder'], icon: marker })
|
||||||
expect(glyphOf(service.folderIcon('/w/src', false, 14))).toBe('span')
|
expect(glyphOf(service.folderIcon('/w/src', false, 14))).toBe('span')
|
||||||
// The OPEN row is a separate reserved ext — still the builtin here.
|
// The OPEN row is a separate reserved ext — still the builtin here.
|
||||||
expect(glyphOf(service.folderIcon('/w/src', true, 14))).toBe(VscFolderOpened)
|
expect(glyphOf(service.folderIcon('/w/src', true, 14))).toBe(FileTypeIcon)
|
||||||
service.registerFileIcon({ id: 'open', exts: ['folder-open'], icon: markerB })
|
service.registerFileIcon({ id: 'open', exts: ['folder-open'], icon: markerB })
|
||||||
expect(glyphOf(service.folderIcon('/w/src', true, 14))).toBe('b')
|
expect(glyphOf(service.folderIcon('/w/src', true, 14))).toBe('b')
|
||||||
service.registerFileIcon({ id: 'closed-hi', exts: ['folder'], priority: 3, icon: markerB })
|
service.registerFileIcon({ id: 'closed-hi', exts: ['folder'], priority: 3, icon: markerB })
|
||||||
@@ -248,7 +252,7 @@ describe('folderIcon resolver (registered folder/folder-open → builtin glyphs)
|
|||||||
it('a catch-all registration never claims a directory', () => {
|
it('a catch-all registration never claims a directory', () => {
|
||||||
const service = createBetterSidebarService(createSidebarStore())
|
const service = createBetterSidebarService(createSidebarStore())
|
||||||
service.registerFileIcon({ id: 'all', exts: [], icon: marker })
|
service.registerFileIcon({ id: 'all', exts: [], icon: marker })
|
||||||
expect(glyphOf(service.folderIcon('/w/src', false, 14))).toBe(VscFolder)
|
expect(glyphOf(service.folderIcon('/w/src', false, 14))).toBe(FileTypeIcon)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('a throwing folder factory falls back to the builtin glyph', () => {
|
it('a throwing folder factory falls back to the builtin glyph', () => {
|
||||||
@@ -256,7 +260,7 @@ describe('folderIcon resolver (registered folder/folder-open → builtin glyphs)
|
|||||||
try {
|
try {
|
||||||
const service = createBetterSidebarService(createSidebarStore())
|
const service = createBetterSidebarService(createSidebarStore())
|
||||||
service.registerFileIcon({ id: 'boom', exts: ['folder'], icon: () => { throw new Error('boom') } })
|
service.registerFileIcon({ id: 'boom', exts: ['folder'], icon: () => { throw new Error('boom') } })
|
||||||
expect(glyphOf(service.folderIcon('/w/src', false, 14))).toBe(VscFolder)
|
expect(glyphOf(service.folderIcon('/w/src', false, 14))).toBe(FileTypeIcon)
|
||||||
expect(errorSpy).toHaveBeenCalled()
|
expect(errorSpy).toHaveBeenCalled()
|
||||||
} finally {
|
} finally {
|
||||||
errorSpy.mockRestore()
|
errorSpy.mockRestore()
|
||||||
@@ -264,19 +268,25 @@ describe('folderIcon resolver (registered folder/folder-open → builtin glyphs)
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('built-in glyph map (builtinFileIcon)', () => {
|
describe('built-in glyphs are the host’s own artwork', () => {
|
||||||
it('maps representative extensions to their group glyph', () => {
|
it('every path goes through the host FileTypeIcon (it classifies, the host draws)', () => {
|
||||||
expect(glyphOf(builtinFileIcon('/w/README.md', 14))).toBe(VscMarkdown)
|
for (const path of ['/w/README.md', '/w/logo.png', '/w/main.ts', '/w/pkg.json', '/w/data.xyzunknown', '/w/Makefile']) {
|
||||||
expect(glyphOf(builtinFileIcon('/w/logo.png', 14))).toBe(VscFileMedia)
|
expect(glyphOf(builtinFileIcon(path, 14)), path).toBe(FileTypeIcon)
|
||||||
expect(glyphOf(builtinFileIcon('/w/logo.SVG', 14))).toBe(VscFileMedia)
|
}
|
||||||
expect(glyphOf(builtinFileIcon('/w/pkg.json', 14))).toBe(VscJson)
|
expect(glyphOf(fallbackFileIcon(14))).toBe(FileTypeIcon)
|
||||||
expect(glyphOf(builtinFileIcon('/w/main.ts', 14))).toBe(VscFileCode)
|
|
||||||
expect(glyphOf(builtinFileIcon('/w/main.py', 14))).toBe(VscFileCode)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('falls back to the generic VscFile for unknown extensions and paths without one', () => {
|
it('hands the row’s path and size to the host classifier', () => {
|
||||||
expect(glyphOf(builtinFileIcon('/w/data.xyzunknown', 14))).toBe(VscFile)
|
const props = (builtinFileIcon('/w/main.ts', 14) as ReactElement).props as { path: string; size: number }
|
||||||
expect(glyphOf(builtinFileIcon('/w/Makefile', 14))).toBe(VscFile)
|
expect(props.path).toBe('/w/main.ts')
|
||||||
expect(glyphOf(fallbackFileIcon(14))).toBe(VscFile)
|
expect(props.size).toBe(14)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders the host folder category for directories', () => {
|
||||||
|
const props = (builtinFolderIcon(false, 14) as ReactElement).props as { kind: string; size: number }
|
||||||
|
expect(props.kind).toBe('folder')
|
||||||
|
expect(props.size).toBe(14)
|
||||||
|
// Both expansion states resolve to the host's folder drawing.
|
||||||
|
expect(glyphOf(builtinFolderIcon(true, 14))).toBe(FileTypeIcon)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ function bundleId(file: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** The lazy chunk bundle names (mirror of src/bundle-route.ts CHUNK_NAMES). */
|
/** The lazy chunk bundle names (mirror of src/bundle-route.ts CHUNK_NAMES). */
|
||||||
const CHUNK_FILES = ['terminal', 'editor', 'mermaid', 'file-icons'].map(name => `lib/client-${name}.js`)
|
const CHUNK_FILES = ['terminal', 'editor', 'mermaid'].map(name => `lib/client-${name}.js`)
|
||||||
|
|
||||||
/** The global registry slot a built chunk script assigns (its factory key). */
|
/** The global registry slot a built chunk script assigns (its factory key). */
|
||||||
function chunkSlot(file: string): string {
|
function chunkSlot(file: string): string {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { describe, expect, it, vi } from 'vitest'
|
|||||||
import { createElement } from 'react'
|
import { createElement } from 'react'
|
||||||
import { createRoot, type Root } from 'react-dom/client'
|
import { createRoot, type Root } from 'react-dom/client'
|
||||||
import { act } from 'react-dom/test-utils'
|
import { act } from 'react-dom/test-utils'
|
||||||
import { createNativeTabRecords, NativeTabBody } from '../src/client/native/tab-adapter.tsx'
|
import { createNativeTabRecords, NativeTabBody, NativeTabTitle } from '../src/client/native/tab-adapter.tsx'
|
||||||
import { registerNativeSurface } from '../src/client/native/index.ts'
|
import { registerNativeSurface } from '../src/client/native/index.ts'
|
||||||
import { createBetterSidebarService, type SidebarSurface } from '../src/client/service.ts'
|
import { createBetterSidebarService, type SidebarSurface } from '../src/client/service.ts'
|
||||||
import { createSidebarStore } from '../src/client/state.ts'
|
import { createSidebarStore } from '../src/client/state.ts'
|
||||||
@@ -310,3 +310,111 @@ describe('NativeTabBody full-height host wrapper', () => {
|
|||||||
host.remove()
|
host.remove()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The native tab CHIP: the host's tab definition carries no icon field, so
|
||||||
|
* the plugin draws the glyph itself inside the `sidebar.right.pane.tab.title`
|
||||||
|
* slot (which IS the chip's content). These cases pin the placement rule —
|
||||||
|
* an editor tab with a path shows the FILE's glyph, every other tab shows its
|
||||||
|
* descriptor's glyph — and the accessible-name boundary: the glyph is
|
||||||
|
* decorative, so the chip's name stays exactly the title the e2e lane matches
|
||||||
|
* with `getByRole('tab', { name })`.
|
||||||
|
*/
|
||||||
|
describe('NativeTabTitle (the chip glyph)', () => {
|
||||||
|
const renderTitle = (
|
||||||
|
records: ReturnType<typeof createNativeTabRecords>,
|
||||||
|
service: ReturnType<typeof createBetterSidebarService>,
|
||||||
|
info: unknown,
|
||||||
|
descriptorId: string,
|
||||||
|
): { host: HTMLDivElement; unmount: () => void } => {
|
||||||
|
const host = document.createElement('div')
|
||||||
|
document.body.appendChild(host)
|
||||||
|
let root: Root | undefined
|
||||||
|
act(() => {
|
||||||
|
root = createRoot(host)
|
||||||
|
root.render(createElement(NativeTabTitle, {
|
||||||
|
records,
|
||||||
|
service,
|
||||||
|
descriptorId,
|
||||||
|
useTabInfo: () => info as never,
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
host,
|
||||||
|
unmount: () => {
|
||||||
|
act(() => { root?.unmount() })
|
||||||
|
host.remove()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const nativeInfo = (id: string, kind: string, title: string) => ({
|
||||||
|
tab: {
|
||||||
|
id,
|
||||||
|
kind,
|
||||||
|
title,
|
||||||
|
contentId: `sidebar://${id}`,
|
||||||
|
visible: true,
|
||||||
|
navigation: { address: `sidebar://${id}`, params: undefined, revision: 0 },
|
||||||
|
signal: new AbortController().signal,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
it('draws the descriptor glyph before the live title', () => {
|
||||||
|
const records = createNativeTabRecords()
|
||||||
|
const service = createBetterSidebarService(createSidebarStore())
|
||||||
|
service.registerTab({
|
||||||
|
id: 'stub-tab',
|
||||||
|
title: () => 'Stub',
|
||||||
|
icon: (size: number) => createElement('i', { 'data-stub-icon': size }),
|
||||||
|
component: () => createElement('div'),
|
||||||
|
})
|
||||||
|
records.ensure({ id: 'chip-1', kind: 'stub-tab', title: 'Stub', params: undefined, scope })
|
||||||
|
|
||||||
|
const { host, unmount } = renderTitle(records, service, nativeInfo('chip-1', 'stub-tab', 'Stub'), 'stub-tab')
|
||||||
|
const chip = host.querySelector('[aria-hidden="true"]')
|
||||||
|
expect(chip, 'the chip must carry a decorative glyph').not.toBeNull()
|
||||||
|
expect(chip!.querySelector('[data-stub-icon="14"]'), 'the descriptor icon renders at the chip scale').not.toBeNull()
|
||||||
|
expect(host.textContent, 'the title follows the glyph').toBe('Stub')
|
||||||
|
expect(host.querySelector('[role="tab"]'), 'the chip itself is the host’s element, not the plugin’s').toBeNull()
|
||||||
|
unmount()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('an editor tab with a path shows the FILE glyph instead of the type glyph', () => {
|
||||||
|
const records = createNativeTabRecords()
|
||||||
|
const service = createBetterSidebarService(createSidebarStore())
|
||||||
|
service.registerTab({
|
||||||
|
id: 'editor',
|
||||||
|
title: () => 'Files',
|
||||||
|
icon: (size: number) => createElement('i', { 'data-type-icon': size }),
|
||||||
|
component: () => createElement('div'),
|
||||||
|
})
|
||||||
|
records.ensure({
|
||||||
|
id: 'chip-2',
|
||||||
|
kind: 'editor',
|
||||||
|
title: 'notes.md',
|
||||||
|
params: { path: '/work/notes.md' },
|
||||||
|
scope,
|
||||||
|
})
|
||||||
|
|
||||||
|
const { host, unmount } = renderTitle(records, service, nativeInfo('chip-2', 'editor', 'notes.md'), 'editor')
|
||||||
|
expect(host.querySelector('[data-type-icon]'), 'the type glyph must NOT be used for a file tab').toBeNull()
|
||||||
|
// The file glyph is the host's own FileTypeIcon (its component identity is
|
||||||
|
// asserted through the resolver tests); here the point is that a glyph is
|
||||||
|
// drawn and the title is intact.
|
||||||
|
expect(host.querySelector('[aria-hidden="true"]')).not.toBeNull()
|
||||||
|
expect(host.textContent).toBe('notes.md')
|
||||||
|
unmount()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('falls back to the title alone when the type is gone (unregistered descriptor)', () => {
|
||||||
|
const records = createNativeTabRecords()
|
||||||
|
const service = createBetterSidebarService(createSidebarStore())
|
||||||
|
records.ensure({ id: 'chip-3', kind: 'ghost', title: 'Ghost', params: undefined, scope })
|
||||||
|
|
||||||
|
const { host, unmount } = renderTitle(records, service, nativeInfo('chip-3', 'ghost', 'Ghost'), 'ghost')
|
||||||
|
expect(host.querySelector('[aria-hidden="true"]')).toBeNull()
|
||||||
|
expect(host.textContent).toBe('Ghost')
|
||||||
|
unmount()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@@ -1000,7 +1000,6 @@ describe('side card settings routes', () => {
|
|||||||
terminalFontSize: 13,
|
terminalFontSize: 13,
|
||||||
editorExplorer: false,
|
editorExplorer: false,
|
||||||
workspaceFence: true,
|
workspaceFence: true,
|
||||||
fileIconTheme: 'builtin',
|
|
||||||
terminalShell: '',
|
terminalShell: '',
|
||||||
terminalShellArgs: '',
|
terminalShellArgs: '',
|
||||||
titleBarCompat: false,
|
titleBarCompat: false,
|
||||||
|
|||||||
+32
-20
@@ -85,36 +85,48 @@ describe('effectiveTokenValue', () => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Skin contract (guide §12): every visual value rides a `--dsw-alias-*` /
|
* Skin contract (guide §12): every visual value rides a `--dsw-alias-*` /
|
||||||
* `--dsw-font-*` / `--ds-*` token. The ONE documented exception is the
|
* `--dsw-font-*` / `--ds-*` token, and the plugin draws no color of its own.
|
||||||
* optional colored icon theme: brand colors are identity, not chrome, so
|
*
|
||||||
* they are hardcoded — but only inside the lazy `file-icons` chunk, which
|
* File icons are no exception to check any more: the built-in set IS DSH's
|
||||||
* the core bundle must never import. This guards that boundary.
|
* `FileTypeIcon` artwork from a platform module (the host owns those pixels
|
||||||
|
* and its own palette), and everything the plugin renders around them —
|
||||||
|
* including the colored tab glyphs — rides theme tokens. So the guard is
|
||||||
|
* simply that no plugin module carries a color literal, and that no icon
|
||||||
|
* dataset sneaked back in as a chunk.
|
||||||
*/
|
*/
|
||||||
// jsdom has no file:// import.meta.url; vitest runs from the repo root.
|
// jsdom has no file:// import.meta.url; vitest runs from the repo root.
|
||||||
const ROOT = process.cwd()
|
const ROOT = process.cwd()
|
||||||
|
|
||||||
describe('skin contract boundary for hardcoded icon colors', () => {
|
describe('skin contract: the plugin owns no color of its own', () => {
|
||||||
const coreIconModules = ['src/client/file-icons.tsx', 'src/client/file-icon-theme.ts']
|
it('the icon modules carry no color literals', () => {
|
||||||
|
for (const file of ['src/client/file-icons.tsx', 'src/client/icons.tsx']) {
|
||||||
it('the core icon modules carry no color literals', () => {
|
|
||||||
for (const file of coreIconModules) {
|
|
||||||
const source = readFileSync(resolve(ROOT, file), 'utf8')
|
const source = readFileSync(resolve(ROOT, file), 'utf8')
|
||||||
expect(source, file).not.toMatch(/#[0-9a-fA-F]{3,8}\b/)
|
expect(source, file).not.toMatch(/#[0-9a-fA-F]{3,8}\b/)
|
||||||
expect(source, file).not.toMatch(/\brgba?\(/)
|
expect(source, file).not.toMatch(/\brgba?\(/)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('only the lazy chunk may carry the colored dataset, and the core never imports it statically', () => {
|
it('the colored tab glyphs take their color from theme tokens, never from a literal', () => {
|
||||||
const chunk = readFileSync(resolve(ROOT, 'src/client/chunks/file-icons.tsx'), 'utf8')
|
const module = readFileSync(resolve(ROOT, 'src/client/builtins/tab-icons.tsx'), 'utf8')
|
||||||
expect(chunk).toMatch(/#[0-9a-fA-F]{6}/)
|
expect(module).not.toMatch(/#[0-9a-fA-F]{3,8}\b/)
|
||||||
const clientDir = resolve(ROOT, 'src/client')
|
expect(module).not.toMatch(/\brgba?\(/)
|
||||||
const offenders: string[] = []
|
// Each glyph is wrapped in a themed class, which is how the color arrives.
|
||||||
for (const entry of readdirSync(clientDir, { recursive: true, withFileTypes: true })) {
|
const styles = readFileSync(resolve(ROOT, 'src/client/builtins/tab-icons.module.css'), 'utf8')
|
||||||
if (!entry.isFile() || !/\.tsx?$/.test(entry.name)) continue
|
const classes = [...module.matchAll(/css\.([a-zA-Z]+)/g)].map(m => m[1])
|
||||||
const path = resolve(entry.parentPath, entry.name)
|
expect(classes.length).toBeGreaterThan(0)
|
||||||
if (path.includes('/chunks/')) continue
|
for (const name of new Set(classes)) expect(styles, name).toContain(`.${name}`)
|
||||||
if (/from '[^']*chunks\/file-icons/.test(readFileSync(path, 'utf8'))) offenders.push(path)
|
// Every declaration that paints a color resolves to a token.
|
||||||
|
for (const declaration of styles.matchAll(/color:\s*([^;]+);/g)) {
|
||||||
|
expect(declaration[1], declaration[0]).toContain('var(--dsw-')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('no icon dataset is shipped as a lazy chunk', () => {
|
||||||
|
const chunkDir = resolve(ROOT, 'src/client/chunks')
|
||||||
|
const chunks = readdirSync(chunkDir).filter(name => /\.tsx?$/.test(name))
|
||||||
|
for (const name of chunks) {
|
||||||
|
const source = readFileSync(resolve(chunkDir, name), 'utf8')
|
||||||
|
expect(source, name).not.toMatch(/#[0-9a-fA-F]{6}\b/)
|
||||||
}
|
}
|
||||||
expect(offenders).toEqual([])
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
+1
-6
@@ -211,11 +211,6 @@ function chunkBundle(name: string): UserConfig {
|
|||||||
inputOptions: {
|
inputOptions: {
|
||||||
resolve: {
|
resolve: {
|
||||||
conditionNames: ['browser', 'import', 'require', 'default'],
|
conditionNames: ['browser', 'import', 'require', 'default'],
|
||||||
// The icon chunks import a handful of glyphs from the two react-icons
|
|
||||||
// packs; without this alias the pack's unshakeable CJS entry pulls in
|
|
||||||
// the whole set (6.4 MB for the file-icons chunk). Same reason the
|
|
||||||
// core bundle pins it.
|
|
||||||
...(name === 'file-icons' ? { alias: REACT_ICONS_ESM_ALIAS } : {}),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
noExternal: (id: string) => (CLIENT_EXTERNALS.includes(id) ? undefined : true),
|
noExternal: (id: string) => (CLIENT_EXTERNALS.includes(id) ? undefined : true),
|
||||||
@@ -329,7 +324,7 @@ function makeCssPlugin(pluginId: string): BuildPlugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** The lazy chunk names (keep in sync with src/bundle-route.ts CHUNK_NAMES). */
|
/** The lazy chunk names (keep in sync with src/bundle-route.ts CHUNK_NAMES). */
|
||||||
const CHUNKS = ['terminal', 'editor', 'mermaid', 'locale', 'file-icons']
|
const CHUNKS = ['terminal', 'editor', 'mermaid', 'locale']
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user