feat(icons): file icons and tab glyphs in color, from the host's own artwork
Issue #611 asked for file-type icons like the official explorer's, and #429 built a colored theme for them. DSH 0.1.5-rc.2 makes most of that work unnecessary: `@deepseek-ai/dsh-client-ui-primitives` now exports `FileTypeIcon` / `CodeFileIcon` with the host's own 48-type full-color code artwork plus category-colored sheet glyphs (and its classifier behind them). So the plugin renders the host's icons instead of shipping a color table, and color becomes the only behaviour rather than an opt-in theme. File icons (src/client/file-icons.tsx) - `builtinFileIcon(path, size)` → `<FileTypeIcon path size />`; folders use the host's folder category. The plugin's own per-extension VSCodicon map is gone. - The public `registerFileIcon` API from #531 is unchanged and still outranks the built-ins, so an external plugin can take over any row. One semantic consequence, now documented and tested: because the host classifier covers every path, a registered catch-all (`exts: []`) claims rows it used to lose to the built-in map. An unregistered catch-all (the old icon theme's shape) is unaffected. - Nothing is lazy any more: the artwork lives in a platform module the client already holds, so `lib/client-file-icons.js` and its 563-entry dataset (255 kB) are deleted outright — the core bundle grew by 10 kB (0.85 → 0.86 MB). - `src/client/file-icon-theme.ts`, the `fileIconTheme` pref (prefs-shared / config / client prefs), the settings-page select row and the six `settingsFileIconTheme*` keys in all 20 dictionaries are removed with it. Tab glyphs (src/client/builtins/tab-icons.tsx) - Every built-in tab type now declares a colored glyph: files (the host's folder artwork), changes/diff (commit), tasks (checklist), side chat (discussion), terminal (shell), browser (globe). - The color comes from a `--dsw-alias-*` token through a wrapper class, never from a literal — the skin contract (§12) holds and `tests/theme.spec.ts` enforces both halves (no literals in the module, every color declaration in the stylesheet resolves to a token). Native right Sidebar chips (src/client/native/tab-adapter.tsx) - The host's tab definition has no icon field, but the `sidebar.right.pane.tab.title` slot IS the chip's content, so the plugin draws the glyph itself: an editor tab with a path shows that file's icon, every other tab shows its descriptor's icon — the same `descriptor.icon` the workbench strip and the guide capsules already render, so the three cannot drift. The glyph is `aria-hidden`, which keeps the chip's accessible name exactly the title the mount lane matches on. Gates: typecheck / lint / check:consumer-types green; pnpm test 127 files / 1342 passed / 9 skipped (3 new chip cases, 1 rewritten built-in case).
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Colors for the built-in tab glyphs (src/client/builtins/tab-icons.tsx).
|
||||
*
|
||||
* Every declaration resolves to a `--dsw-alias-*` token, so all skins stay in
|
||||
* charge — including the token's own fallback chain, which keeps the glyph
|
||||
* legible on a host that does not define the brand/state families. The plugin
|
||||
* never writes a color literal here; `tests/theme.spec.ts` enforces both
|
||||
* halves of that rule.
|
||||
*
|
||||
* The chips sit on the panel surface, so a token that is itself translucent
|
||||
* would wash out at 14px; each rule therefore prefers an opaque state/brand
|
||||
* token and falls back to the primary ink.
|
||||
*/
|
||||
|
||||
.files {
|
||||
color: var(--dsw-alias-brand-primary, var(--dsw-alias-label-primary));
|
||||
}
|
||||
|
||||
.changes {
|
||||
color: var(--dsw-alias-state-success-primary, var(--dsw-alias-brand-primary, var(--dsw-alias-label-primary)));
|
||||
}
|
||||
|
||||
.tasks {
|
||||
color: var(--dsw-alias-state-warn-primary, var(--dsw-alias-label-primary));
|
||||
}
|
||||
|
||||
.sidechat {
|
||||
color: var(--dsw-alias-state-business-primary, var(--dsw-alias-brand-primary, var(--dsw-alias-label-primary)));
|
||||
}
|
||||
|
||||
.terminal {
|
||||
color: var(--dsw-alias-label-primary);
|
||||
}
|
||||
|
||||
.browser {
|
||||
color: var(--dsw-alias-state-business-primary, var(--dsw-alias-brand-primary, var(--dsw-alias-label-primary)));
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* The built-in tab glyphs, in color.
|
||||
*
|
||||
* Every built-in tab type and the incoming 8th (changes) declare their icon
|
||||
* here, so the three surfaces that draw a tab's glyph — the bottom workbench's
|
||||
* tab strip, the native right Sidebar's guide capsules and its tab chips — all
|
||||
* read the same colorful glyph from `descriptor.icon`.
|
||||
*
|
||||
* The color always arrives from a theme token, never from the plugin: the
|
||||
* glyph is a VSCodicon drawn in `currentColor`, and the wrapper class supplies
|
||||
* that color. A skin therefore keeps control of everything (the guide's §12);
|
||||
* this module is what `tests/theme.spec.ts` checks for color literals.
|
||||
*
|
||||
* `files` is the one exception in kind rather than in color: it renders DSH's
|
||||
* own folder artwork (`FileTypeIcon`), matching the file rows this tab shows.
|
||||
*/
|
||||
import type { ReactNode } from 'react'
|
||||
import { FileTypeIcon } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import styles from './tab-icons.module.css'
|
||||
import {
|
||||
VscCommentDiscussion,
|
||||
VscGitCommit,
|
||||
VscGlobe,
|
||||
VscTasklist,
|
||||
VscTerminal,
|
||||
} from 'react-icons/vsc'
|
||||
/** The styled wrapper classes; typed so a renamed rule fails the build. */
|
||||
const css = styles as Record<'files' | 'changes' | 'tasks' | 'sidechat' | 'terminal' | 'browser', string>
|
||||
|
||||
/** One tab type's glyph, sized by the caller's surface (14px in a strip). */
|
||||
export type TabIcon = (size: number) => ReactNode
|
||||
|
||||
/** Surround a glyph with the class that hands it its token-driven color. */
|
||||
function themed(className: string, glyph: ReactNode): ReactNode {
|
||||
return <span className={className}>{glyph}</span>
|
||||
}
|
||||
|
||||
/** The Files tab: the host's folder artwork, like the rows it opens. */
|
||||
export const filesTabIcon: TabIcon = (size) => (
|
||||
<span className={css.files}>
|
||||
<FileTypeIcon kind="folder" size={size} />
|
||||
</span>
|
||||
)
|
||||
|
||||
/** Changes / diff: the commit glyph, green like the diff affordances. */
|
||||
export const changesTabIcon: TabIcon = (size) =>
|
||||
themed(css.changes, <VscGitCommit size={size} />)
|
||||
|
||||
/** Tasks (subagent) — the live-activity amber. */
|
||||
export const tasksTabIcon: TabIcon = (size) =>
|
||||
themed(css.tasks, <VscTasklist size={size} />)
|
||||
|
||||
/** Side chat — the conversational/secondary accent. */
|
||||
export const sidechatTabIcon: TabIcon = (size) =>
|
||||
themed(css.sidechat, <VscCommentDiscussion size={size} />)
|
||||
|
||||
/** Terminal — primary ink, the shell is text. */
|
||||
export const terminalTabIcon: TabIcon = (size) =>
|
||||
themed(css.terminal, <VscTerminal size={size} />)
|
||||
|
||||
/** Browser — the same secondary accent as the side chat's sibling surfaces. */
|
||||
export const browserTabIcon: TabIcon = (size) =>
|
||||
themed(css.browser, <VscGlobe size={size} />)
|
||||
@@ -8,8 +8,11 @@
|
||||
* `browser:<n>` the same way (no quota). The editor IS the files window
|
||||
* (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) => <IconFolderOpen16 size={size} />,
|
||||
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) => <IconFolderOpenOutline16 size={size} />,
|
||||
title: () => t('settingsFileIconThemeBuiltin'),
|
||||
desc: () => t('settingsFileIconThemeBuiltinDesc'),
|
||||
},
|
||||
{
|
||||
value: 'colored',
|
||||
icon: (size: number) => <IconSparkle16 size={size} />,
|
||||
title: () => t('settingsFileIconThemeColored'),
|
||||
desc: () => t('settingsFileIconThemeColoredDesc'),
|
||||
},
|
||||
],
|
||||
}],
|
||||
render: ({ pluginSettings, updatePluginSetting }) => (
|
||||
<OpenWithSettings pluginSettings={pluginSettings} updatePluginSetting={updatePluginSetting} />
|
||||
@@ -172,7 +152,7 @@ export function builtinTabs(ctx: Context, options: BuiltinTabOptions = {}): read
|
||||
id: 'git',
|
||||
title: () => t('changes'),
|
||||
description: () => t('guideDescGit'),
|
||||
icon: (size: number) => <IconDiffOutline16 size={size} />,
|
||||
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) => <IconThinkOutline16 size={size} />,
|
||||
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) => <IconNewChatOutline16 size={size} />,
|
||||
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) => <IconTerminalOutline16 size={size} />,
|
||||
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) => <IconGlobeOutline16 size={size} />,
|
||||
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) => <IconDiffOutline16 size={size} />,
|
||||
icon: changesTabIcon,
|
||||
order: -1,
|
||||
hidden: true,
|
||||
dedupeKey: (tab) => tab.id,
|
||||
|
||||
Reference in New Issue
Block a user