diff --git a/src/bundle-route.ts b/src/bundle-route.ts index 24b9147..4f098ec 100644 --- a/src/bundle-route.ts +++ b/src/bundle-route.ts @@ -19,7 +19,7 @@ import { fileURLToPath } from 'node:url' import type { Context, SidebarHttpRequest, SidebarHttpResponse } from './context-types.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] /** Directory of this host-half module (lib/ — the chunk scripts live next to it). */ diff --git a/src/client/FileTree.tsx b/src/client/FileTree.tsx index ef09235..2873f51 100644 --- a/src/client/FileTree.tsx +++ b/src/client/FileTree.tsx @@ -149,7 +149,7 @@ export function FileTree(props: { busy: boolean /** * 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). */ service?: BetterSidebarService diff --git a/src/client/builtins/tab-icons.module.css b/src/client/builtins/tab-icons.module.css new file mode 100644 index 0000000..1b74378 --- /dev/null +++ b/src/client/builtins/tab-icons.module.css @@ -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))); +} diff --git a/src/client/builtins/tab-icons.tsx b/src/client/builtins/tab-icons.tsx new file mode 100644 index 0000000..2381dc8 --- /dev/null +++ b/src/client/builtins/tab-icons.tsx @@ -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 {glyph} +} + +/** The Files tab: the host's folder artwork, like the rows it opens. */ +export const filesTabIcon: TabIcon = (size) => ( + + + +) + +/** Changes / diff: the commit glyph, green like the diff affordances. */ +export const changesTabIcon: TabIcon = (size) => + themed(css.changes, ) + +/** Tasks (subagent) — the live-activity amber. */ +export const tasksTabIcon: TabIcon = (size) => + themed(css.tasks, ) + +/** Side chat — the conversational/secondary accent. */ +export const sidechatTabIcon: TabIcon = (size) => + themed(css.sidechat, ) + +/** Terminal — primary ink, the shell is text. */ +export const terminalTabIcon: TabIcon = (size) => + themed(css.terminal, ) + +/** Browser — the same secondary accent as the side chat's sibling surfaces. */ +export const browserTabIcon: TabIcon = (size) => + themed(css.browser, ) diff --git a/src/client/builtins/tabs.tsx b/src/client/builtins/tabs.tsx index eed3ec3..55e12ec 100644 --- a/src/client/builtins/tabs.tsx +++ b/src/client/builtins/tabs.tsx @@ -8,8 +8,11 @@ * `browser:` the same way (no quota). The editor IS the files window * (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 { + browserTabIcon, changesTabIcon, filesTabIcon, sidechatTabIcon, tasksTabIcon, terminalTabIcon, +} from './tab-icons.tsx' import { allLeaves, isAgentTabId, type SidebarState } from '../state.ts' import { t } from '../locales.ts' import { openSidebarFile } from '../intercept.tsx' @@ -22,7 +25,6 @@ import { SubagentView } from '../SubagentView.tsx' import { consumeSidechatSeed, SideChatView, sidechatThreadIdOf } from '../SideChatView.tsx' import { api } from '../api.ts' 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 type { ComponentType } from 'react' 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 // the file. Visible in the + menu in the explorer's old slot. title: () => t('files'), - icon: (size: number) => , + icon: filesTabIcon, order: 10, hidden: false, dedupeKey: (tab) => tab.path, @@ -122,28 +124,6 @@ export function builtinTabs(ctx: Context, options: BuiltinTabOptions = {}): read key: 'workspaceFence', title: () => t('settingsFenceTitle'), 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) => , - title: () => t('settingsFileIconThemeBuiltin'), - desc: () => t('settingsFileIconThemeBuiltinDesc'), - }, - { - value: 'colored', - icon: (size: number) => , - title: () => t('settingsFileIconThemeColored'), - desc: () => t('settingsFileIconThemeColoredDesc'), - }, - ], }], render: ({ pluginSettings, updatePluginSetting }) => ( @@ -172,7 +152,7 @@ export function builtinTabs(ctx: Context, options: BuiltinTabOptions = {}): read id: 'git', title: () => t('changes'), description: () => t('guideDescGit'), - icon: (size: number) => , + icon: changesTabIcon, order: 20, single: true, badge: (_ctx, scope) => { @@ -195,7 +175,7 @@ export function builtinTabs(ctx: Context, options: BuiltinTabOptions = {}): read id: 'subagent', title: () => t('subagent'), description: () => t('guideDescSubagent'), - icon: (size: number) => , + icon: tasksTabIcon, order: 30, single: true, // 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', title: () => t('sideChat'), description: () => t('guideDescSidechat'), - icon: (size: number) => , + icon: sidechatTabIcon, order: 35, // Codex-style: EVERY side conversation is its own tab. A plain open // 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', title: () => t('terminal'), description: () => t('guideDescTerminal'), - icon: (size: number) => , + icon: terminalTabIcon, order: 40, available: (_ctx, _scope, state) => uiTerminalCount(state) < TERMINAL_LIMIT, // Declarative settings: the model-facing terminal tools switch, the @@ -335,7 +315,7 @@ export function builtinTabs(ctx: Context, options: BuiltinTabOptions = {}): read id: 'browser', title: () => t('browser'), description: () => t('guideDescBrowser'), - icon: (size: number) => , + icon: browserTabIcon, order: 50, // Declarative settings: the sandbox escape hatch, the link-takeover // MASTER switch, and the per-protocol takeover switches (http on / @@ -379,7 +359,7 @@ export function builtinTabs(ctx: Context, options: BuiltinTabOptions = {}): read { id: 'diff', title: () => t('changes'), - icon: (size: number) => , + icon: changesTabIcon, order: -1, hidden: true, dedupeKey: (tab) => tab.id, diff --git a/src/client/chunk-loader.ts b/src/client/chunk-loader.ts index e5181be..d89fdbf 100644 --- a/src/client/chunk-loader.ts +++ b/src/client/chunk-loader.ts @@ -50,7 +50,7 @@ * client.js); an edit that does land while a core HMR happens is caught by * 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). */ export type ChunkExports = Record diff --git a/src/client/chunks/file-icons.tsx b/src/client/chunks/file-icons.tsx deleted file mode 100644 index c4b93f7..0000000 --- a/src/client/chunks/file-icons.tsx +++ /dev/null @@ -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 -} - -// ── Brand file icons (extension → { icon, brand color }) ──────────────── - -export const FILE_ICON_BY_EXT: Partial> = { - // 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> = { - // 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 = { - // 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() - 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 - ? - : , - }) - } - - return () => { for (const dispose of disposers) dispose() } -} - -/** Regroup one name→{Icon,color} table into Icon → color → keys. */ -function groupByGlyph(table: Partial>): Map> { - const grouped = new Map>() - 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 -} diff --git a/src/client/file-icon-theme.ts b/src/client/file-icon-theme.ts deleted file mode 100644 index 9753a5f..0000000 --- a/src/client/file-icon-theme.ts +++ /dev/null @@ -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() - }, - } -} diff --git a/src/client/file-icons.tsx b/src/client/file-icons.tsx index 7b8a1fd..5fe1699 100644 --- a/src/client/file-icons.tsx +++ b/src/client/file-icons.tsx @@ -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 - * skin contract as every other icon this plugin draws (visual values ride the - * `--dsw-alias-*` tokens, never hardcoded colors). External plugins that want - * colorful icons register their own `FileIconDescriptor`s through - * `ctx.betterSidebar.registerFileIcon` (they own their colors); this map is - * only the fallback nobody claimed. + * The built-in set is DSH's OWN file-type artwork: `FileTypeIcon` from + * `@deepseek-ai/dsh-client-ui-primitives` classifies a path and renders the + * same glyphs the host's own explorer shows — the 48 full-color code and + * configuration categories, plus the category-colored sheet glyphs for + * markdown, images, PDFs, office documents, video, folders and the generic + * 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 { - 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 -} - -/** The built-in directory glyphs (what a folder row shows unregistered). */ -export function builtinFolderIcon(open: boolean, size: number): ReactNode { - return open ? : -} - -/** One glyph shared by a group of extensions. */ -type GlyphGroup = [readonly string[], GlyphFactory] +import { FileTypeIcon } from '@deepseek-ai/dsh-client-ui-primitives' /** - * Extension → glyph groups. Extensions are lowercase without the leading dot, - * matched case-insensitively by the caller's normalization. Group order is - * documentation only: the flattened lookup below is a Map, so every extension - * key must stay unique (a duplicate would be last-write-wins). The reserved - * folder values (`'folder'`/`'folder-open'`) never appear here — directories - * resolve through `builtinFolderIcon`. - */ -const GROUPS: readonly GlyphGroup[] = [ - [['md', 'markdown', 'mdx'], (size) => ], - [['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg', 'bmp', 'ico', 'avif', 'tiff'], (size) => ], - [['pdf'], (size) => ], - [['json', 'jsonc', 'json5'], (size) => ], - [['html', 'htm', 'xhtml', 'xml', 'jsx', 'tsx'], (size) => ], - [['css', 'scss', 'sass', 'less', 'styl'], (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) => ], - [['yml', 'yaml', 'toml', 'ini', 'cfg', 'conf', 'properties', 'editorconfig'], (size) => ], - [['sql', 'db', 'sqlite', 'sqlite3'], (size) => ], - [['lock'], (size) => ], - [['zip', 'tar', 'gz', 'tgz', 'bz2', 'xz', '7z', 'rar'], (size) => ], -] - -const LOOKUP: ReadonlyMap = 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). + * A file row: the host's own classifier and artwork for any path. Unknown + * extensions land on the generic document glyph, exactly like the host's + * explorer. + * @param path - the row's path (any separator; the classifier reads the basename). + * @param size - the square edge in px. + * @returns the host's file-type glyph. */ export function builtinFileIcon(path: string, size: number): ReactNode { - const glyph = LOOKUP.get(extOf(path)) - return glyph === undefined ? fallbackFileIcon(size) : glyph(size) + return +} + +/** + * 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 +} + +/** + * 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 } diff --git a/src/client/index.tsx b/src/client/index.tsx index 87e2f14..d5c82a3 100644 --- a/src/client/index.tsx +++ b/src/client/index.tsx @@ -24,7 +24,6 @@ import { registerBottomToggle } from './sidebar/bottom-toggle.tsx' import { createNativeSurface } from './native/surface.ts' import { registerLinkInterception } from './link-intercept.ts' import { registerImeGuard } from './ime-guard.ts' -import { createFileIconThemeLoader } from './file-icon-theme.ts' import { registerSettingsNavIcon } from './settings-nav-icon.ts' import { loadBootDecision } from './prefs.ts' import { SideCardSection } from './SideCardSection.tsx' @@ -189,21 +188,6 @@ export function apply(ctx: Context): void { () => registerBuiltins(ctx, service, { terminalTitle: () => terminalTitle }), '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 // 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 diff --git a/src/client/locales-ar.ts b/src/client/locales-ar.ts index 32082b8..45dc7bb 100644 --- a/src/client/locales-ar.ts +++ b/src/client/locales-ar.ts @@ -260,12 +260,6 @@ export const ar: Record = { settingsToolsDesc: 'عند التفعيل، يمكن للنموذج إنشاء وتشغيل طرفيات الشريط الجانبي عبر أدوات terminal_* الثمانية (معطّل افتراضياً)', settingsFenceTitle: 'فحص مسارات مساحة العمل', settingsFenceDesc: 'عند التفعيل، تصل وظائف الملفات في الشريط الجانبي إلى مسارات داخل مساحة عمل الجلسة فقط (الافتراضي)؛ عند الإيقاف تصبح ملفات المضيف كلها متاحة — وتحصل سكربتات الصفحة على النفاذ نفسه طوال ذلك', - settingsFileIconThemeTitle: 'أيقونات الملفات', - settingsFileIconThemeDesc: 'نمط الأيقونات لشجرة الملفات وتبويبات الملفات (تُحمَّل السمة الملوّنة عند الطلب، لا عند بدء التشغيل)', - settingsFileIconThemeBuiltin: 'مدمج (أحادي اللون)', - settingsFileIconThemeBuiltinDesc: 'أيقونات أحادية اللون تتبع رموز السمة، تغطي أنواع التعليمات البرمجية والمستندات والوسائط الشائعة', - settingsFileIconThemeColored: 'أيقونات العلامات الملوّنة', - settingsFileIconThemeColoredDesc: 'ألوان العلامات حسب النوع (مثل .tsx → React و package.json → npm)؛ تُحمَّل حزمة الأيقونات عند أول تفعيل', fenceErrorReason: 'هذا المسار خارج مساحة عمل الجلسة وقد حجبه الفحص', fenceDisableAction: 'إيقاف فحص مساحة العمل', settingsBottomTerminalTitle: 'فتح طرفية تلقائياً عند أول توسيع للوحة السفلية', diff --git a/src/client/locales-de.ts b/src/client/locales-de.ts index 593df05..acc8364 100644 --- a/src/client/locales-de.ts +++ b/src/client/locales-de.ts @@ -245,12 +245,6 @@ export const de: Record = { settingsToolsDesc: 'Wenn aktiviert, kann das Modell über die 8 terminal_*-Werkzeuge Terminale in der Seitenleiste erstellen und steuern (standardmäßig deaktiviert)', 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', - 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', fenceDisableAction: 'Workspace-Kontrolle ausschalten', settingsBottomTerminalTitle: 'Beim ersten Ausklappen des unteren Panels automatisch ein Terminal öffnen', diff --git a/src/client/locales-fr.ts b/src/client/locales-fr.ts index af49406..0425d45 100644 --- a/src/client/locales-fr.ts +++ b/src/client/locales-fr.ts @@ -252,12 +252,6 @@ export const fr: Record = { 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', 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', fenceDisableAction: 'Désactiver le contrôle de l\'espace de travail', settingsBottomTerminalTitle: 'Ouvrir automatiquement un terminal au premier déploiement du panneau inférieur', diff --git a/src/client/locales-hi.ts b/src/client/locales-hi.ts index 2f274b0..63b095b 100644 --- a/src/client/locales-hi.ts +++ b/src/client/locales-hi.ts @@ -259,12 +259,6 @@ export const hi: Record = { settingsToolsDesc: 'सक्षम होने पर, मॉडल 8 terminal_* टूल्स से साइडबार टर्मिनल बना और चला सकता है (डिफ़ॉल्ट रूप से बंद)', settingsFenceTitle: 'वर्कस्पेस पाथ जाँच', settingsFenceDesc: 'चालू होने पर साइडबार की फ़ाइल सुविधाएँ केवल सेशन वर्कस्पेस के भीतर के पाथ तक पहुँचती हैं (डिफ़ॉल्ट); बंद करने पर होस्ट की कोई भी फ़ाइल खोली जा सकती है — उस दौरान पेज के स्क्रिप्ट को भी वही पहुँच मिल जाती है', - settingsFileIconThemeTitle: 'फ़ाइल आइकन', - settingsFileIconThemeDesc: 'फ़ाइल ट्री और फ़ाइल टैब के आइकन की शैली (रंगीन थीम माँग पर लोड होती है, शुरू होते समय नहीं)', - settingsFileIconThemeBuiltin: 'अंतर्निहित (एकरंगी)', - settingsFileIconThemeBuiltinDesc: 'स्किन टोकन के अनुसार एकरंगी आइकन, सामान्य कोड / दस्तावेज़ / मीडिया प्रकारों के लिए', - settingsFileIconThemeColored: 'रंगीन ब्रांड आइकन', - settingsFileIconThemeColoredDesc: 'प्रकार के अनुसार ब्रांड रंग (जैसे .tsx → React, package.json → npm); पहली बार चालू करने पर आइकन पैक लोड होता है', fenceErrorReason: 'यह पाथ सेशन वर्कस्पेस के बाहर है और जाँच द्वारा अवरुद्ध किया गया', fenceDisableAction: 'वर्कस्पेस जाँच बंद करें', settingsBottomTerminalTitle: 'निचला पैनल पहली बार खुलने पर स्वतः टर्मिनल खोलें', diff --git a/src/client/locales-id.ts b/src/client/locales-id.ts index c5f6ef8..7babc43 100644 --- a/src/client/locales-id.ts +++ b/src/client/locales-id.ts @@ -257,12 +257,6 @@ export const id: Record = { settingsToolsDesc: 'Saat diaktifkan, model dapat membuat dan mengendalikan terminal sidebar melalui 8 alat terminal_* (nonaktif secara default)', 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', - 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', fenceDisableAction: 'Matikan pemeriksaan workspace', settingsBottomTerminalTitle: 'Buka terminal otomatis saat panel bawah pertama kali dibentangkan', diff --git a/src/client/locales-it.ts b/src/client/locales-it.ts index 48ba34d..c1a92f6 100644 --- a/src/client/locales-it.ts +++ b/src/client/locales-it.ts @@ -250,12 +250,6 @@ export const it: Record = { 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', 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', fenceDisableAction: 'Disattiva il controllo del workspace', settingsBottomTerminalTitle: 'Apri automaticamente un terminale alla prima espansione del pannello inferiore', diff --git a/src/client/locales-ja.ts b/src/client/locales-ja.ts index 3e78bf3..71483ff 100644 --- a/src/client/locales-ja.ts +++ b/src/client/locales-ja.ts @@ -259,12 +259,6 @@ export const ja: Record = { settingsToolsDesc: 'オンにすると、モデルが terminal_create 等 8 個のツールでサイドバーターミナルを作成・操作可能(デフォルトオフ)', settingsFenceTitle: 'ワークスペースパスガード', settingsFenceDesc: 'オンの場合、サイドバーのファイル機能はセッションワークスペース内のパスのみアクセス可能(デフォルト)。オフにするとホスト上の任意のファイルにアクセスできるが、オフの間はページ内スクリプトも同じ権限を得る', - settingsFileIconThemeTitle: 'ファイルアイコン', - settingsFileIconThemeDesc: 'ファイルツリーとファイルタブのアイコンスタイル(カラーテーマは必要時に読み込み、起動時には読み込みません)', - settingsFileIconThemeBuiltin: '標準(モノクロ)', - settingsFileIconThemeBuiltinDesc: 'スキントークンに従うモノクロアイコン。一般的なコード / ドキュメント / メディア形式に対応', - settingsFileIconThemeColored: 'カラーブランドアイコン', - settingsFileIconThemeColoredDesc: '種類ごとにブランドカラーで表示(例:.tsx → React、package.json → npm)。初回有効時にアイコンパックを読み込みます', fenceErrorReason: 'このパスはセッションワークスペースの外にあり、ワークスペースガードによりブロックされました', fenceDisableAction: 'ワークスペースガードをオフにする', settingsBottomTerminalTitle: '下パネル初回展開時に自動でターミナルを開く', diff --git a/src/client/locales-ko.ts b/src/client/locales-ko.ts index 71b65d0..658c390 100644 --- a/src/client/locales-ko.ts +++ b/src/client/locales-ko.ts @@ -251,12 +251,6 @@ export const ko: Record = { settingsToolsDesc: '켜면 모델이 terminal_create 등 8개 도구로 사이드바 터미널을 만들고 조작할 수 있습니다(기본 꺼짐)', settingsFenceTitle: '워크스페이스 경로 검사', settingsFenceDesc: '켜면 사이드바의 파일 기능이 세션 워크스페이스 내 경로만 접근합니다(기본값). 끄면 호스트의 모든 파일에 접근할 수 있으며, 꺼져 있는 동안 페이지 내 스크립트도 동일한 권한을 얻습니다', - settingsFileIconThemeTitle: '파일 아이콘', - settingsFileIconThemeDesc: '파일 트리와 파일 탭의 아이콘 스타일 (컬러 테마는 필요할 때 로드되며 시작 시에는 로드되지 않습니다)', - settingsFileIconThemeBuiltin: '기본(단색)', - settingsFileIconThemeBuiltinDesc: '스킨 토큰을 따르는 단색 아이콘으로 주요 코드 / 문서 / 미디어 형식을 지원합니다', - settingsFileIconThemeColored: '컬러 브랜드 아이콘', - settingsFileIconThemeColoredDesc: '유형별 브랜드 색상 (예: .tsx → React, package.json → npm), 처음 켤 때 아이콘 팩을 로드합니다', fenceErrorReason: '이 경로는 세션 워크스페이스 밖에 있어 검사에 의해 차단되었습니다', fenceDisableAction: '워크스페이스 검사 끄기', settingsBottomTerminalTitle: '하단 패널을 처음 펼칠 때 터미널 자동 열기', diff --git a/src/client/locales-nl.ts b/src/client/locales-nl.ts index fcdb497..043a105 100644 --- a/src/client/locales-nl.ts +++ b/src/client/locales-nl.ts @@ -257,12 +257,6 @@ export const nl: Record = { settingsToolsDesc: 'Indien ingeschakeld kan het model zijbalk-terminals aanmaken en besturen via de 8 terminal_*-tools (standaard uit)', 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', - 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', fenceDisableAction: 'Workspace-controle uitzetten', settingsBottomTerminalTitle: 'Automatisch een terminal openen bij de eerste uitklap van het onderpaneel', diff --git a/src/client/locales-pl.ts b/src/client/locales-pl.ts index 78d48c5..e09bab1 100644 --- a/src/client/locales-pl.ts +++ b/src/client/locales-pl.ts @@ -261,12 +261,6 @@ export const pl: Record = { 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', 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ę', fenceDisableAction: 'Wyłącz kontrolę obszaru roboczego', settingsBottomTerminalTitle: 'Automatycznie otwieraj terminal przy pierwszym rozwinięciu panelu dolnego', diff --git a/src/client/locales-pt.ts b/src/client/locales-pt.ts index 6168379..ebdc6e7 100644 --- a/src/client/locales-pt.ts +++ b/src/client/locales-pt.ts @@ -242,12 +242,6 @@ export const pt: Record = { 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', 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', fenceDisableAction: 'Desativar o controle do workspace', settingsBottomTerminalTitle: 'Abrir um terminal automaticamente na primeira expansão do painel inferior', diff --git a/src/client/locales-ru.ts b/src/client/locales-ru.ts index 7397c7a..3fd5149 100644 --- a/src/client/locales-ru.ts +++ b/src/client/locales-ru.ts @@ -256,12 +256,6 @@ export const ru: Record = { settingsToolsDesc: 'Если включено, модель может через 8 инструментов (terminal_create и др.) создавать терминалы боковой панели и управлять ими (по умолчанию выключено)', settingsFenceTitle: 'Контроль путей рабочей области', settingsFenceDesc: 'Включено — файловые функции боковой панели достигают только путей внутри рабочей области сессии (по умолчанию); выключено — доступны любые файлы хоста, а скрипты страницы получают тот же доступ всё это время', - settingsFileIconThemeTitle: 'Значки файлов', - settingsFileIconThemeDesc: 'Стиль значков дерева файлов и вкладок (цветная тема загружается по требованию, а не при запуске)', - settingsFileIconThemeBuiltin: 'Встроенные (монохромные)', - settingsFileIconThemeBuiltinDesc: 'Монохромные значки на основе токенов темы: код, документы и медиа', - settingsFileIconThemeColored: 'Цветные значки брендов', - settingsFileIconThemeColoredDesc: 'Цвета брендов по типу (например, .tsx → React, package.json → npm); набор значков загружается при первом включении', fenceErrorReason: 'Этот путь вне рабочей области сессии и заблокирован контролем', fenceDisableAction: 'Отключить контроль рабочей области', settingsBottomTerminalTitle: 'Автооткрытие терминала при первом разворачивании нижней панели', diff --git a/src/client/locales-sv.ts b/src/client/locales-sv.ts index d9a8313..66d5edd 100644 --- a/src/client/locales-sv.ts +++ b/src/client/locales-sv.ts @@ -242,12 +242,6 @@ export const sv: Record = { 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', 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', fenceDisableAction: 'Stäng av workspace-kontrollen', settingsBottomTerminalTitle: 'Öppna automatiskt en terminal vid bottenpanelens första expandering', diff --git a/src/client/locales-th.ts b/src/client/locales-th.ts index be8c173..72b31fd 100644 --- a/src/client/locales-th.ts +++ b/src/client/locales-th.ts @@ -259,12 +259,6 @@ export const th: Record = { settingsToolsDesc: 'เมื่อเปิดใช้ โมเดลสามารถสร้างและควบคุมเทอร์มินัลแถบด้านข้างผ่านเครื่องมือ terminal_* 8 ตัว (ปิดเป็นค่าเริ่มต้น)', settingsFenceTitle: 'การตรวจสอบเส้นทางเวิร์กสเปซ', settingsFenceDesc: 'เมื่อเปิด ฟีเจอร์ไฟล์ของแถบข้างเข้าถึงได้เฉพาะเส้นทางในเวิร์กสเปซของเซสชัน (ค่าเริ่มต้น) เมื่อปิด จะเข้าถึงไฟล์ใดก็ได้บนโฮสต์ — สคริปต์ในหน้าก็ได้ขอบเขตเดียวกันตลอดช่วงนั้น', - settingsFileIconThemeTitle: 'ไอคอนไฟล์', - settingsFileIconThemeDesc: 'สไตล์ไอคอนของแผนผังไฟล์และแท็บไฟล์ (ธีมสีจะโหลดเมื่อต้องการ ไม่โหลดตอนเริ่มต้น)', - settingsFileIconThemeBuiltin: 'ในตัว (สีเดียว)', - settingsFileIconThemeBuiltinDesc: 'ไอคอนสีเดียวตามโทเค็นธีม ครอบคลุมชนิดโค้ด / เอกสาร / สื่อทั่วไป', - settingsFileIconThemeColored: 'ไอคอนแบรนด์แบบมีสี', - settingsFileIconThemeColoredDesc: 'สีแบรนด์ตามชนิด (เช่น .tsx → React, package.json → npm) ชุดไอคอนจะโหลดเมื่อเปิดใช้ครั้งแรก', fenceErrorReason: 'เส้นทางนี้อยู่นอกเวิร์กสเปซของเซสชันและถูกบล็อกโดยการตรวจสอบ', fenceDisableAction: 'ปิดการตรวจสอบเวิร์กสเปซ', settingsBottomTerminalTitle: 'เปิดเทอร์มินัลอัตโนมัติเมื่อพาเนลล่างขยายครั้งแรก', diff --git a/src/client/locales-tr.ts b/src/client/locales-tr.ts index 1bfe080..e12ca75 100644 --- a/src/client/locales-tr.ts +++ b/src/client/locales-tr.ts @@ -259,12 +259,6 @@ export const tr: Record = { 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', 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', fenceDisableAction: 'Çalışma alanı denetimini kapat', settingsBottomTerminalTitle: 'Alt panelin ilk genişletilmesinde otomatik terminal aç', diff --git a/src/client/locales-vi.ts b/src/client/locales-vi.ts index 8d6dc42..78e72ca 100644 --- a/src/client/locales-vi.ts +++ b/src/client/locales-vi.ts @@ -259,12 +259,6 @@ export const vi: Record = { 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', 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', fenceDisableAction: 'Tắt kiểm tra workspace', settingsBottomTerminalTitle: 'Tự động mở terminal khi panel dưới mở rộng lần đầu', diff --git a/src/client/locales-zh-HK.ts b/src/client/locales-zh-HK.ts index 47b1ee7..8c03489 100644 --- a/src/client/locales-zh-HK.ts +++ b/src/client/locales-zh-HK.ts @@ -274,12 +274,6 @@ export const zhHK: Record = { settingsToolsDesc: '開啟後,模型可透過 terminal_create 等 8 個工具建立並操作側邊欄終端(預設關閉)', settingsFenceTitle: '工作區路徑檢測', settingsFenceDesc: '開啟後,側欄的檔案功能僅能存取工作階段工作區內的路徑(預設);關閉後可存取主機上任意檔案——關閉期間頁面內指令碼也將獲得同等存取能力', - settingsFileIconThemeTitle: '檔案圖示', - settingsFileIconThemeDesc: '檔案樹與檔案分頁的圖示風格(彩色主題按需載入,不影響啟動)', - settingsFileIconThemeBuiltin: '內建(單色)', - settingsFileIconThemeBuiltinDesc: '跟隨皮膚權杖的單色圖示,覆蓋常見程式碼 / 文件 / 媒體類型', - settingsFileIconThemeColored: '彩色品牌圖示', - settingsFileIconThemeColoredDesc: '按類型顯示品牌配色圖示(如 .tsx → React、package.json → npm);首次開啟時載入圖示包', fenceErrorReason: '此路徑在工作階段工作區之外,已被工作區檢測攔截', fenceDisableAction: '關閉工作區檢測', settingsBottomTerminalTitle: '底部面板首次展開自動開終端', diff --git a/src/client/locales-zh-MO.ts b/src/client/locales-zh-MO.ts index 6658557..4615010 100644 --- a/src/client/locales-zh-MO.ts +++ b/src/client/locales-zh-MO.ts @@ -274,12 +274,6 @@ export const zhMO: Record = { settingsToolsDesc: '開啟後,模型可透過 terminal_create 等 8 個工具建立並操作側邊欄終端(預設關閉)', settingsFenceTitle: '工作區路徑檢測', settingsFenceDesc: '開啟後,側欄的檔案功能僅能存取工作階段工作區內的路徑(預設);關閉後可存取主機上任意檔案——關閉期間頁面內指令碼也將獲得同等存取能力', - settingsFileIconThemeTitle: '檔案圖示', - settingsFileIconThemeDesc: '檔案樹與檔案分頁的圖示風格(彩色主題按需載入,不影響啟動)', - settingsFileIconThemeBuiltin: '內建(單色)', - settingsFileIconThemeBuiltinDesc: '跟隨皮膚權杖的單色圖示,覆蓋常見程式碼 / 文件 / 媒體類型', - settingsFileIconThemeColored: '彩色品牌圖示', - settingsFileIconThemeColoredDesc: '按類型顯示品牌配色圖示(如 .tsx → React、package.json → npm);首次開啟時載入圖示包', fenceErrorReason: '此路徑在工作階段工作區之外,已被工作區檢測攔截', fenceDisableAction: '關閉工作區檢測', settingsBottomTerminalTitle: '底部面板首次展開自動開終端', diff --git a/src/client/locales-zh-TW.ts b/src/client/locales-zh-TW.ts index 23679b5..23e7f6d 100644 --- a/src/client/locales-zh-TW.ts +++ b/src/client/locales-zh-TW.ts @@ -274,12 +274,6 @@ export const zhTW: Record = { settingsToolsDesc: '開啟後,模型可透過 terminal_create 等 8 個工具建立並操作側邊欄終端(預設關閉)', settingsFenceTitle: '工作區路徑檢測', settingsFenceDesc: '開啟後,側欄的檔案功能僅能存取工作階段工作區內的路徑(預設);關閉後可存取主機上任意檔案——關閉期間頁面內指令碼也將獲得同等存取能力', - settingsFileIconThemeTitle: '檔案圖示', - settingsFileIconThemeDesc: '檔案樹與檔案分頁的圖示風格(彩色主題按需載入,不影響啟動)', - settingsFileIconThemeBuiltin: '內建(單色)', - settingsFileIconThemeBuiltinDesc: '跟隨面板權杖的單色圖示,涵蓋常見程式碼 / 文件 / 媒體類型', - settingsFileIconThemeColored: '彩色品牌圖示', - settingsFileIconThemeColoredDesc: '依類型顯示品牌配色圖示(如 .tsx → React、package.json → npm);首次開啟時載入圖示套件', fenceErrorReason: '此路徑在工作階段工作區之外,已被工作區檢測攔截', fenceDisableAction: '關閉工作區檢測', settingsBottomTerminalTitle: '底部面板首次展開自動開終端', diff --git a/src/client/locales.ts b/src/client/locales.ts index 7d5fb80..f418f86 100644 --- a/src/client/locales.ts +++ b/src/client/locales.ts @@ -271,12 +271,6 @@ export const zh = { settingsToolsDesc: '开启后,模型可通过 terminal_create 等 8 个工具创建并操作侧边栏终端(默认关闭)', settingsFenceTitle: '工作区路径检测', settingsFenceDesc: '开启后,侧栏的文件功能仅能访问会话工作区内的路径(默认);关闭后可访问主机上任意文件——关闭期间页面内脚本也将获得同等访问能力', - settingsFileIconThemeTitle: '文件图标', - settingsFileIconThemeDesc: '文件树与文件 tab 的图标风格(彩色主题按需加载,不影响启动)', - settingsFileIconThemeBuiltin: '内置(单色)', - settingsFileIconThemeBuiltinDesc: '跟随皮肤令牌的单色图标,覆盖常见代码 / 文档 / 媒体类型', - settingsFileIconThemeColored: '彩色品牌图标', - settingsFileIconThemeColoredDesc: '按类型显示品牌配色图标(如 .tsx → React、package.json → npm);首次开启时加载图标包', fenceErrorReason: '此路径在会话工作区之外,已被工作区检测拦截', fenceDisableAction: '关闭工作区检测', settingsBottomTerminalTitle: '底部面板首次展开自动开终端', @@ -719,12 +713,6 @@ export const en: Record = { settingsToolsDesc: 'When enabled, the model can create and drive sidebar terminals through the 8 terminal_* tools (off by default)', 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', - 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', fenceDisableAction: 'Turn off the workspace fence', settingsBottomTerminalTitle: 'Auto-open a terminal on the bottom panel\'s first expansion', diff --git a/src/client/native/index.ts b/src/client/native/index.ts index c807134..cd574ef 100644 --- a/src/client/native/index.ts +++ b/src/client/native/index.ts @@ -162,7 +162,7 @@ export function registerNativeSurface(deps: NativeSurfaceDeps): () => void { ctx.slots.inject('sidebar.right.pane.tab.title', () => ctx.slots.register({ name: 'sidebar.right.pane.tab.title', key: id, - inject: () => ({ records }), + inject: () => ({ records, service, descriptorId: injected.descriptorId }), }, NativeTabTitle)), ] diff --git a/src/client/native/tab-adapter.tsx b/src/client/native/tab-adapter.tsx index 1000278..89bfa21 100644 --- a/src/client/native/tab-adapter.tsx +++ b/src/client/native/tab-adapter.tsx @@ -31,6 +31,12 @@ import type { BetterSidebarService } from '../service.ts' import type { SidebarStore, SidebarTab, TabType } from '../state.ts' 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`. * 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. */ export interface NativeTitleInjected { 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 - * in-place file switch, the side chat on the thread's first prompt). Without - * this registration the chip would keep the title captured when the tab - * opened. + * A live tab chip: the type's glyph followed by the synthetic record's title + * (the editor rewrites it on an in-place file switch, the side chat on the + * thread's first prompt). Without this registration the chip would keep the + * 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 { - const { records, useTabInfo } = props + const { records, service, descriptorId, useTabInfo } = props const nativeTab = useTabInfo().tab - return useSyncExternalStore( + const version = useSyncExternalStore( 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. */} + + {title} + ) } diff --git a/src/client/prefs.ts b/src/client/prefs.ts index e91f8fb..4e6416f 100644 --- a/src/client/prefs.ts +++ b/src/client/prefs.ts @@ -74,9 +74,6 @@ export function parsePrefs(value: unknown): SidebarPrefs { workspaceFence: typeof record.workspaceFence === 'boolean' ? record.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 // declares the field WITHOUT a default, so documents written by older // plugin versions resolve without it — migrate from the legacy fields: diff --git a/src/client/service.ts b/src/client/service.ts index 9ee2f71..e37402d 100644 --- a/src/client/service.ts +++ b/src/client/service.ts @@ -27,7 +27,7 @@ import { type SidebarSnapshot, type SidebarState, type SidebarStore, type SidebarTab, type TabType, } from './state.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 { SidebarPrefs } from '../prefs-shared.ts' @@ -491,9 +491,11 @@ export interface BetterSidebarService { * the whole chain with per-factory crash isolation: * 1. a specific registered name or extension (priority desc, registration * order), - * 2. the built-in monochrome glyph map (md/media/pdf/json/code/...), - * 3. the best registered global default (`exts: []`, priority desc), - * 4. the generic `VscFile`. + * 2. the best registered global default (`exts: []`, priority desc) — an + * external plugin that registers a catch-all owns every row the host's + * 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 * 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 - // or extension registration → built-in glyph → best catch-all → stock - // VscFile. The catch-all ranks by priority desc then registration order - // (first wins); an unclaimed extension reaching it is exactly "nothing - // specified falls back to the (registered or stock) default". + // or extension registration → registered catch-all → the host's own + // file-type artwork. The catch-all ranks by priority desc then registration + // order (first wins), which is what lets an external plugin own "every + // extension I did not name" without also owning the ones DSH draws. const fileIcon = (path: string, size: number): ReactNode => { const specific = matchFileIcon(path) if (specific !== undefined) { const icon = safeIcon(specific, path, size) if (icon !== undefined) return icon } - const builtin = builtinFileIconOf(path) - if (builtin !== undefined) return builtin(size) for (const d of rankedFileIcons()) { if (d.exts !== undefined && d.exts.length === 0) { const icon = safeIcon(d, path, size) if (icon !== undefined) return icon } } - return fallbackFileIcon(size) + return builtinFileIcon(path, size) } // 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 // states. const folderIcon = (path: string, open: boolean, size: number): ReactNode => { diff --git a/src/client/sidebar.module.css b/src/client/sidebar.module.css index b3b296a..a72905a 100644 --- a/src/client/sidebar.module.css +++ b/src/client/sidebar.module.css @@ -420,6 +420,17 @@ body[data-dsh-sidebar-dragging] .bottomPanel { 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 switching never tears down terminals/editors (state survives). */ .paneTab { diff --git a/src/config.ts b/src/config.ts index c7039b9..07e5d4a 100644 --- a/src/config.ts +++ b/src/config.ts @@ -118,7 +118,6 @@ export const PrefsSchema: z = z.object({ 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), workspaceFence: z.boolean().default(true), - fileIconTheme: z.string().default('builtin'), terminalShell: z.string().default(''), terminalShellArgs: z.string().default(''), titleBarScheme: z.union([z.const('auto'), z.const('web'), z.const('preset'), z.const('custom')]), diff --git a/src/prefs-shared.ts b/src/prefs-shared.ts index 438e2dd..d08a9aa 100644 --- a/src/prefs-shared.ts +++ b/src/prefs-shared.ts @@ -79,14 +79,6 @@ export interface SidebarPrefs { * one-click global off + retry. */ 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 * executable name). Empty (default) keeps the legacy resolution order: @@ -256,7 +248,6 @@ export const SIDEBAR_PREFS_DEFAULTS: SidebarPrefs = { terminalFontSize: TERMINAL_FONT_SIZE_DEFAULT, editorExplorer: false, workspaceFence: true, - fileIconTheme: 'builtin', terminalShell: '', terminalShellArgs: '', titleBarScheme: 'auto', diff --git a/tests/builtins.spec.ts b/tests/builtins.spec.ts index 7746244..76c3a94 100644 --- a/tests/builtins.spec.ts +++ b/tests/builtins.spec.ts @@ -136,7 +136,7 @@ describe('built-in tab registrations', () => { it('the editor tab declares its merged-mode (embedded file tree) setting', () => { const { service } = setup() 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]?.desc).toBeDefined() // The merged mode is an iconed select (merged vs separate), not a switch. diff --git a/tests/bundle-route.spec.ts b/tests/bundle-route.spec.ts index b27e9b2..501585d 100644 --- a/tests/bundle-route.spec.ts +++ b/tests/bundle-route.spec.ts @@ -150,6 +150,6 @@ describe('/sidebar/bundle route', () => { }) 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']) }) }) diff --git a/tests/file-icon-theme.spec.tsx b/tests/file-icon-theme.spec.tsx deleted file mode 100644 index bb743a4..0000000 --- a/tests/file-icon-theme.spec.tsx +++ /dev/null @@ -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 -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 => { 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() - }) -}) diff --git a/tests/file-icons.spec.tsx b/tests/file-icons.spec.tsx index ec1a60a..6b4b622 100644 --- a/tests/file-icons.spec.tsx +++ b/tests/file-icons.spec.tsx @@ -1,13 +1,13 @@ /** * Tests for the file-icon feature: the external registration API * (registerFileIcon — registry lifecycle, reserved folder exts, and the - * authoritative fileIcon/folderIcon resolver chains) and the built-in - * per-extension glyph map (extension normalization, group hits, and the - * generic VscFile fallback). + * authoritative fileIcon/folderIcon resolver chains) and the built-in glyphs, + * which are DSH's own `FileTypeIcon` artwork rather than a plugin-owned + * extension table (see src/client/file-icons.tsx). */ import { describe, it, expect, vi } from 'vitest' 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) const g = globalThis as Record @@ -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', () => { const service = createBetterSidebarService(createSidebarStore()) service.registerFileIcon({ id: 'md', exts: ['md'], icon: marker }) 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()) service.registerFileIcon({ id: 'all', exts: [], icon: marker }) - expect(glyphOf(service.fileIcon('/w/README.md', 14))).toBe(VscMarkdown) - expect(glyphOf(service.fileIcon('/w/logo.png', 14))).toBe(VscFileMedia) - // An extension the builtin map does not cover reaches the catch-all. + expect(glyphOf(service.fileIcon('/w/README.md', 14))).toBe('span') + expect(glyphOf(service.fileIcon('/w/logo.png', 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()) service.registerFileIcon({ id: 'md-decline', exts: ['md'], icon: () => undefined }) // 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-take', exts: [], icon: marker }) // The declining catch-all is skipped, the next one takes the row. 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()) - expect(glyphOf(service.fileIcon('/w/pkg.json', 14))).toBe(VscJson) - expect(glyphOf(service.fileIcon('/w/main.ts', 14))).toBe(VscFileCode) - expect(glyphOf(service.fileIcon('/w/Makefile', 14))).toBe(VscFile) + expect(glyphOf(service.fileIcon('/w/pkg.json', 14))).toBe(FileTypeIcon) + expect(glyphOf(service.fileIcon('/w/main.ts', 14))).toBe(FileTypeIcon) + expect(glyphOf(service.fileIcon('/w/Makefile', 14))).toBe(FileTypeIcon) }) 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()) service.registerFileIcon({ id: 'boom-md', exts: ['md'], icon: () => { throw new Error('boom') } }) // 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') } }) - // Catch-all throws → falls to the stock VscFile. - expect(glyphOf(service.fileIcon('/w/Makefile', 14))).toBe(VscFile) + // Catch-all throws → falls to the host's file-type artwork. + expect(glyphOf(service.fileIcon('/w/Makefile', 14))).toBe(FileTypeIcon) expect(errorSpy).toHaveBeenCalled() } finally { errorSpy.mockRestore() @@ -167,8 +171,8 @@ describe('fileIcon resolver chain (specific → builtin → catch-all → VscFil const service = createBetterSidebarService(createSidebarStore()) service.registerFileIcon({ id: 'folders', exts: ['folder', 'folder-open'], icon: marker }) expect(service.matchFileIcon('/w/x.folder')).toBeUndefined() - // The real file falls through to the stock default, not the folder icon. - expect(glyphOf(service.fileIcon('/w/x.folder', 14))).toBe(VscFile) + // The real file falls through to the host's artwork, not the folder glyph. + 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()) solo.registerFileIcon({ id: 'named', folderNames: ['src'], icon: marker }) 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', () => { @@ -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', () => { const service = createBetterSidebarService(createSidebarStore()) - expect(glyphOf(service.folderIcon('/w/src', false, 14))).toBe(VscFolder) - expect(glyphOf(service.folderIcon('/w/src', true, 14))).toBe(VscFolderOpened) - expect(glyphOf(builtinFolderIcon(false, 14))).toBe(VscFolder) - expect(glyphOf(builtinFolderIcon(true, 14))).toBe(VscFolderOpened) + expect(glyphOf(service.folderIcon('/w/src', false, 14))).toBe(FileTypeIcon) + expect(glyphOf(service.folderIcon('/w/src', true, 14))).toBe(FileTypeIcon) + expect(glyphOf(builtinFolderIcon(false, 14))).toBe(FileTypeIcon) + expect(glyphOf(builtinFolderIcon(true, 14))).toBe(FileTypeIcon) }) 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 }) expect(glyphOf(service.folderIcon('/w/src', false, 14))).toBe('span') // 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 }) expect(glyphOf(service.folderIcon('/w/src', true, 14))).toBe('b') 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', () => { const service = createBetterSidebarService(createSidebarStore()) 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', () => { @@ -256,7 +260,7 @@ describe('folderIcon resolver (registered folder/folder-open → builtin glyphs) try { const service = createBetterSidebarService(createSidebarStore()) 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() } finally { errorSpy.mockRestore() @@ -264,19 +268,25 @@ describe('folderIcon resolver (registered folder/folder-open → builtin glyphs) }) }) -describe('built-in glyph map (builtinFileIcon)', () => { - it('maps representative extensions to their group glyph', () => { - expect(glyphOf(builtinFileIcon('/w/README.md', 14))).toBe(VscMarkdown) - expect(glyphOf(builtinFileIcon('/w/logo.png', 14))).toBe(VscFileMedia) - expect(glyphOf(builtinFileIcon('/w/logo.SVG', 14))).toBe(VscFileMedia) - expect(glyphOf(builtinFileIcon('/w/pkg.json', 14))).toBe(VscJson) - expect(glyphOf(builtinFileIcon('/w/main.ts', 14))).toBe(VscFileCode) - expect(glyphOf(builtinFileIcon('/w/main.py', 14))).toBe(VscFileCode) +describe('built-in glyphs are the host’s own artwork', () => { + it('every path goes through the host FileTypeIcon (it classifies, the host draws)', () => { + for (const path of ['/w/README.md', '/w/logo.png', '/w/main.ts', '/w/pkg.json', '/w/data.xyzunknown', '/w/Makefile']) { + expect(glyphOf(builtinFileIcon(path, 14)), path).toBe(FileTypeIcon) + } + expect(glyphOf(fallbackFileIcon(14))).toBe(FileTypeIcon) }) - it('falls back to the generic VscFile for unknown extensions and paths without one', () => { - expect(glyphOf(builtinFileIcon('/w/data.xyzunknown', 14))).toBe(VscFile) - expect(glyphOf(builtinFileIcon('/w/Makefile', 14))).toBe(VscFile) - expect(glyphOf(fallbackFileIcon(14))).toBe(VscFile) + it('hands the row’s path and size to the host classifier', () => { + const props = (builtinFileIcon('/w/main.ts', 14) as ReactElement).props as { path: string; size: number } + expect(props.path).toBe('/w/main.ts') + 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) }) }) diff --git a/tests/manifest-consistency.spec.ts b/tests/manifest-consistency.spec.ts index 4d691b1..04ba007 100644 --- a/tests/manifest-consistency.spec.ts +++ b/tests/manifest-consistency.spec.ts @@ -71,7 +71,7 @@ function bundleId(file: string): string { } /** 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). */ function chunkSlot(file: string): string { diff --git a/tests/native-surface.spec.ts b/tests/native-surface.spec.ts index b1b210b..468cb51 100644 --- a/tests/native-surface.spec.ts +++ b/tests/native-surface.spec.ts @@ -8,7 +8,7 @@ import { describe, expect, it, vi } from 'vitest' import { createElement } from 'react' import { createRoot, type Root } from 'react-dom/client' 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 { createBetterSidebarService, type SidebarSurface } from '../src/client/service.ts' import { createSidebarStore } from '../src/client/state.ts' @@ -310,3 +310,111 @@ describe('NativeTabBody full-height host wrapper', () => { 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, + service: ReturnType, + 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() + }) +}) diff --git a/tests/smoke.spec.ts b/tests/smoke.spec.ts index d49141f..16caa48 100644 --- a/tests/smoke.spec.ts +++ b/tests/smoke.spec.ts @@ -1000,7 +1000,6 @@ describe('side card settings routes', () => { terminalFontSize: 13, editorExplorer: false, workspaceFence: true, - fileIconTheme: 'builtin', terminalShell: '', terminalShellArgs: '', titleBarCompat: false, diff --git a/tests/theme.spec.ts b/tests/theme.spec.ts index 330db57..65cf0e3 100644 --- a/tests/theme.spec.ts +++ b/tests/theme.spec.ts @@ -85,36 +85,48 @@ describe('effectiveTokenValue', () => { /** * Skin contract (guide §12): every visual value rides a `--dsw-alias-*` / - * `--dsw-font-*` / `--ds-*` token. The ONE documented exception is the - * optional colored icon theme: brand colors are identity, not chrome, so - * they are hardcoded — but only inside the lazy `file-icons` chunk, which - * the core bundle must never import. This guards that boundary. + * `--dsw-font-*` / `--ds-*` token, and the plugin draws no color of its own. + * + * File icons are no exception to check any more: the built-in set IS DSH's + * `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. const ROOT = process.cwd() -describe('skin contract boundary for hardcoded icon colors', () => { - const coreIconModules = ['src/client/file-icons.tsx', 'src/client/file-icon-theme.ts'] - - it('the core icon modules carry no color literals', () => { - for (const file of coreIconModules) { +describe('skin contract: the plugin owns no color of its own', () => { + it('the icon modules carry no color literals', () => { + for (const file of ['src/client/file-icons.tsx', 'src/client/icons.tsx']) { const source = readFileSync(resolve(ROOT, file), 'utf8') expect(source, file).not.toMatch(/#[0-9a-fA-F]{3,8}\b/) expect(source, file).not.toMatch(/\brgba?\(/) } }) - it('only the lazy chunk may carry the colored dataset, and the core never imports it statically', () => { - const chunk = readFileSync(resolve(ROOT, 'src/client/chunks/file-icons.tsx'), 'utf8') - expect(chunk).toMatch(/#[0-9a-fA-F]{6}/) - const clientDir = resolve(ROOT, 'src/client') - const offenders: string[] = [] - for (const entry of readdirSync(clientDir, { recursive: true, withFileTypes: true })) { - if (!entry.isFile() || !/\.tsx?$/.test(entry.name)) continue - const path = resolve(entry.parentPath, entry.name) - if (path.includes('/chunks/')) continue - if (/from '[^']*chunks\/file-icons/.test(readFileSync(path, 'utf8'))) offenders.push(path) + it('the colored tab glyphs take their color from theme tokens, never from a literal', () => { + const module = readFileSync(resolve(ROOT, 'src/client/builtins/tab-icons.tsx'), 'utf8') + expect(module).not.toMatch(/#[0-9a-fA-F]{3,8}\b/) + expect(module).not.toMatch(/\brgba?\(/) + // Each glyph is wrapped in a themed class, which is how the color arrives. + const styles = readFileSync(resolve(ROOT, 'src/client/builtins/tab-icons.module.css'), 'utf8') + const classes = [...module.matchAll(/css\.([a-zA-Z]+)/g)].map(m => m[1]) + expect(classes.length).toBeGreaterThan(0) + for (const name of new Set(classes)) expect(styles, name).toContain(`.${name}`) + // 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([]) }) }) diff --git a/tsdown.config.ts b/tsdown.config.ts index 9c80b5e..881a45d 100644 --- a/tsdown.config.ts +++ b/tsdown.config.ts @@ -211,11 +211,6 @@ function chunkBundle(name: string): UserConfig { inputOptions: { resolve: { 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), @@ -329,7 +324,7 @@ function makeCssPlugin(pluginId: string): BuildPlugin { } /** 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 [ {