From e11a7dcdc2dd895842e52afeb46fbd11ef14ea25 Mon Sep 17 00:00:00 2001 From: Mark Otto Date: Thu, 2 Apr 2026 07:50:13 -0700 Subject: [PATCH 1/2] Revamp icons a bit more - Better match themes to those I designed for the VS Code icon theme - Replace defaults with our duo files and folders - Add demo page to show how the tiers differ --- .../trees-dev/_components/TreesDevSidebar.tsx | 1 + apps/docs/app/trees-dev/icon-tiers/page.tsx | 97 +++ .../app/trees/docs/CoreTypes/constants.ts | 4 +- apps/docs/app/trees/docs/Icons/content.mdx | 9 +- .../docs/app/trees/docs/ReactAPI/constants.ts | 2 +- .../app/trees/docs/VanillaAPI/constants.ts | 2 +- .../tree-examples/CustomIconsSection.tsx | 80 +-- .../trees/scripts/generate-built-in-icons.ts | 292 +++++---- packages/trees/src/builtInIcons.ts | 558 ++++++++++++++---- packages/trees/src/components/Icon.tsx | 18 +- packages/trees/src/components/Root.tsx | 2 +- packages/trees/src/iconConfig.ts | 6 +- packages/trees/src/style.css | 257 +++++++- .../test/ssr-declarative-shadow-dom.test.ts | 40 +- 14 files changed, 1054 insertions(+), 314 deletions(-) create mode 100644 apps/docs/app/trees-dev/icon-tiers/page.tsx diff --git a/apps/docs/app/trees-dev/_components/TreesDevSidebar.tsx b/apps/docs/app/trees-dev/_components/TreesDevSidebar.tsx index 6773e1e9b..e766e2a41 100644 --- a/apps/docs/app/trees-dev/_components/TreesDevSidebar.tsx +++ b/apps/docs/app/trees-dev/_components/TreesDevSidebar.tsx @@ -16,6 +16,7 @@ const DEMO_PAGES = [ { slug: 'drag-and-drop', label: 'Drag and Drop' }, { slug: 'git-status', label: 'Git Status' }, { slug: 'custom-icons', label: 'Custom Icons' }, + { slug: 'icon-tiers', label: 'Icon Tiers' }, { slug: 'header-slot', label: 'Header Slot' }, { slug: 'context-menu', label: 'Context Menu' }, { slug: 'virtualization', label: 'Virtualization' }, diff --git a/apps/docs/app/trees-dev/icon-tiers/page.tsx b/apps/docs/app/trees-dev/icon-tiers/page.tsx new file mode 100644 index 000000000..8a5899c0c --- /dev/null +++ b/apps/docs/app/trees-dev/icon-tiers/page.tsx @@ -0,0 +1,97 @@ +'use client'; + +import type { FileTreeBuiltInIconSet } from '@pierre/trees'; +import { FileTree as FileTreeReact } from '@pierre/trees/react'; + +const TIER_FILES = [ + // Folder states + 'closed-folder/placeholder', + 'open-folder/placeholder', + + // Standard tier — languages & common file types + 'index.ts', + 'app.js', + 'App.tsx', + 'style.css', + 'page.html', + 'data.json', + 'README.md', + 'main.go', + 'main.py', + 'app.rb', + 'lib.rs', + 'app.swift', + 'script.sh', + 'logo.png', + 'font.woff2', + '.gitignore', + 'config.mcp', + + // Complete tier — frameworks, brands, tooling + 'Layout.astro', + '.babelrc', + 'biome.json', + 'bootstrap.min.js', + '.browserslistrc', + 'bun.lock', + 'claude.md', + 'Dockerfile', + 'eslint.config.js', + 'schema.graphql', + 'next.config.ts', + '.oxlintrc.json', + 'postcss.config.js', + '.prettierrc', + 'styles.scss', + '.stylelintrc', + 'App.svelte', + 'svgo.config.js', + 'tailwind.config.ts', + 'main.tf', + 'vite.config.ts', + 'settings.code-workspace', + 'App.vue', + 'module.wasm', + 'webpack.config.js', + 'config.yml', + 'main.zig', + + // Falls through to `default` token + 'unknown.txt', +]; + +const TIERS: { set: FileTreeBuiltInIconSet; label: string }[] = [ + { set: 'minimal', label: 'Minimal' }, + { set: 'standard', label: 'Standard' }, + { set: 'complete', label: 'Complete' }, +]; + +export default function IconTiersPage() { + return ( + <> +

Icon Tiers

+
+ {TIERS.map(({ set, label }) => ( +
+

{label}

+
+ +
+
+ ))} +
+ + ); +} diff --git a/apps/docs/app/trees/docs/CoreTypes/constants.ts b/apps/docs/app/trees/docs/CoreTypes/constants.ts index 6f2fee5d2..0f775b3db 100644 --- a/apps/docs/app/trees/docs/CoreTypes/constants.ts +++ b/apps/docs/app/trees/docs/CoreTypes/constants.ts @@ -172,7 +172,7 @@ export const FILE_TREE_ICON_CONFIG_TYPE: PreloadFileOptions = { // or inject your own SVG symbols. interface FileTreeIconConfig { // Optional: use one of the built-in sets, or "none" for custom-only rules. - set?: 'simple' | 'file-type' | 'duo-tone' | 'none'; + set?: 'minimal' | 'standard' | 'complete' | 'none'; // Optional: enable built-in per-file-type colors. Default: true. colored?: boolean; @@ -213,7 +213,7 @@ interface FileTreeIconConfig { const options = { initialFiles: ['src/index.ts', 'src/components/Button.tsx'], icons: { - set: 'file-type', + set: 'standard', colored: true, spriteSheet: \` simple,{' '} - file-type, and duo-tone icon sets. You can - also enable colored: true, override the built-in - palette with CSS variables like{' '} - --trees-file-icon-color-javascript, or fall back to a - fully custom sprite. See the{' '} + Choose between the shipped minimal,{' '} + standard, and complete icon tiers. Each + tier is cumulative. Override the built-in palette with CSS variables + like --trees-file-icon-color-javascript, or fall back + to a fully custom sprite. See the{' '} FileTreeIconConfig docs {' '} @@ -85,21 +78,19 @@ export function CustomIconsSection() { } description={ - <> - Generic built-ins with a single file glyph and no file-type map. - + <>Generic file, folder, and image icons with no file types. } > - Simple + Minimal } - description={ - <>Semantic file-type icons without any extra configuration. - } + description={<>Icons for common languages and file types.} > - File-type + Standard } - description={ - <> - With built-in semantic colors enabled via{' '} - colored: true. - - } + description={<>Full, colored suite with brands and frameworks.} > - Duo-tone + Complete = { - default: { fileType: 'file.svg', duoTone: 'file-duo.svg' }, - typescript: { - fileType: 'lang-typescript.svg', - duoTone: 'lang-typescript-duo.svg', - }, - javascript: { - fileType: 'lang-javascript.svg', - duoTone: 'lang-javascript-duo.svg', - }, - css: { fileType: 'lang-css.svg', duoTone: 'lang-css-duo.svg' }, - react: { fileType: 'react.svg' }, - markdown: { fileType: 'lang-markdown.svg' }, - json: { fileType: 'braces.svg' }, - npm: { fileType: 'npm.svg' }, - git: { fileType: 'git.svg' }, - image: { fileType: 'image.svg', duoTone: 'image-duo.svg' }, - mcp: { fileType: 'mcp.svg' }, +const TOKEN_DEFS: Record = { + // -- standard tier: languages, common file types ------------------------- + default: { icon: 'file-duo', tier: 'standard' }, + bash: { icon: 'bash-duo', tier: 'standard' }, + css: { icon: 'lang-css-duo', tier: 'standard' }, + font: { icon: 'font', tier: 'standard' }, + git: { icon: 'git', tier: 'standard' }, + go: { icon: 'lang-go', tier: 'standard' }, + html: { icon: 'lang-html-duo', tier: 'standard' }, + image: { icon: 'image-duo', tier: 'standard' }, + javascript: { icon: 'lang-javascript-duo', tier: 'standard' }, + json: { icon: 'braces', tier: 'standard' }, + markdown: { icon: 'lang-markdown', tier: 'standard' }, + mcp: { icon: 'mcp', tier: 'standard' }, + python: { icon: 'lang-python', tier: 'standard' }, + ruby: { icon: 'lang-ruby', tier: 'standard' }, + rust: { icon: 'lang-rust', tier: 'standard' }, + swift: { icon: 'lang-swift', tier: 'standard' }, + typescript: { icon: 'lang-typescript-duo', tier: 'standard' }, + + // -- complete tier: frameworks, brands, tooling ------------------------- + astro: { icon: 'astro', tier: 'complete' }, + babel: { icon: 'babel', tier: 'complete' }, + biome: { icon: 'biome', tier: 'complete' }, + bootstrap: { icon: 'bootstrap-duo', tier: 'complete' }, + browserslist: { icon: 'browserslist-duo', tier: 'complete' }, + bun: { icon: 'bun-duo', tier: 'complete' }, + claude: { icon: 'claude', tier: 'complete' }, + docker: { icon: 'docker', tier: 'complete' }, + eslint: { icon: 'eslint', tier: 'complete' }, + graphql: { icon: 'graphql', tier: 'complete' }, + nextjs: { icon: 'nextjs', tier: 'complete' }, + oxc: { icon: 'oxc', tier: 'complete' }, + postcss: { icon: 'postcss', tier: 'complete' }, + prettier: { icon: 'prettier', tier: 'complete' }, + react: { icon: 'react', tier: 'complete' }, + sass: { icon: 'sass', tier: 'complete' }, + stylelint: { icon: 'stylelint', tier: 'complete' }, + svelte: { icon: 'svelte', tier: 'complete' }, + svgo: { icon: 'svgo', tier: 'complete' }, + tailwind: { icon: 'tailwind', tier: 'complete' }, + terraform: { icon: 'terraform', tier: 'complete' }, + vite: { icon: 'vite', tier: 'complete' }, + vscode: { icon: 'vscode', tier: 'complete' }, + vue: { icon: 'vue', tier: 'complete' }, + wasm: { icon: 'wasm-duo', tier: 'complete' }, + webpack: { icon: 'webpack', tier: 'complete' }, + yml: { icon: 'yml', tier: 'complete' }, + zig: { icon: 'zig', tier: 'complete' }, }; -const TOKENS = Object.keys(TOKEN_SVG_MAP).sort(); - -// Reverse map: theme icon name → our token. -// Excludes file-text-duo (its extensions fall through to 'default' automatically) -// and lang-html-duo (no html token in our system). -const THEME_ICON_TO_TOKEN: Record = { - 'image-duo': 'image', - 'lang-javascript-duo': 'javascript', - 'lang-typescript-duo': 'typescript', - 'lang-css-duo': 'css', - 'lang-markdown': 'markdown', - braces: 'json', - git: 'git', - react: 'react', - npm: 'npm', -}; +const SORTED_TOKENS = Object.keys(TOKEN_DEFS).sort(); + +// Reverse map: theme icon name → our token name +const ICON_TO_TOKEN: Record = {}; +for (const [token, def] of Object.entries(TOKEN_DEFS)) { + ICON_TO_TOKEN[def.icon] = token; +} // Manual additions not covered by the theme data const MANUAL_EXTENSION_TOKENS: Record = { mcp: 'mcp', - svg: 'image', 'mdx.tsx': 'markdown', }; @@ -79,7 +107,11 @@ const MANUAL_FILENAME_TOKENS: Record = { // --------------------------------------------------------------------------- function readSvg(filename: string): string { - return readFileSync(join(svgsDir, filename), 'utf8'); + const path = join(svgsDir, filename); + if (!existsSync(path)) { + throw new Error(`SVG not found: ${path}`); + } + return readFileSync(path, 'utf8'); } function extractSvgInner(svg: string): string { @@ -127,25 +159,41 @@ async function loadThemeTier(filename: string): Promise { return mod.default as ThemeEntry[]; } +/** + * Builds the file-extension and file-name → token lookup tables by walking + * the theme tier data. Also produces an overrides map for extensions whose + * token changes between the standard and complete tiers (e.g. tsx → typescript + * at standard, tsx → react at complete). + */ async function buildTokenMaps(): Promise<{ extensionTokens: Record; fileNameTokens: Record; + completeExtOverrides: Record; }> { const minimal = await loadThemeTier('minimal.mjs'); - const defaults = await loadThemeTier('default.mjs'); + const standards = await loadThemeTier('default.mjs'); const complete = await loadThemeTier('complete.mjs'); - const allEntries = [...minimal, ...defaults, ...complete]; const extensionTokens: Record = {}; const fileNameTokens: Record = {}; + const completeExtOverrides: Record = {}; - for (const entry of allEntries) { - const token = THEME_ICON_TO_TOKEN[entry.name]; - if (token == null) continue; + function processEntry(entry: ThemeEntry, target: 'base' | 'complete') { + const token = ICON_TO_TOKEN[entry.name]; + if (token == null) return; if (entry.fileExtensions != null) { for (const ext of entry.fileExtensions) { - extensionTokens[ext] = token; + if (target === 'complete') { + const existing = extensionTokens[ext]; + if (existing != null && existing !== token) { + completeExtOverrides[ext] = token; + } else if (existing == null) { + extensionTokens[ext] = token; + } + } else { + extensionTokens[ext] = token; + } } } if (entry.fileNames != null) { @@ -155,6 +203,13 @@ async function buildTokenMaps(): Promise<{ } } + for (const entry of [...minimal, ...standards]) { + processEntry(entry, 'base'); + } + for (const entry of complete) { + processEntry(entry, 'complete'); + } + for (const [ext, token] of Object.entries(MANUAL_EXTENSION_TOKENS)) { extensionTokens[ext] = token; } @@ -162,7 +217,7 @@ async function buildTokenMaps(): Promise<{ fileNameTokens[name] = token; } - return { extensionTokens, fileNameTokens }; + return { extensionTokens, fileNameTokens, completeExtOverrides }; } // --------------------------------------------------------------------------- @@ -170,42 +225,34 @@ async function buildTokenMaps(): Promise<{ // --------------------------------------------------------------------------- function generateSymbolConstants(): { - fileTypeSymbols: string[]; - duoToneSymbols: string[]; + standardSymbols: string[]; + completeOnlySymbols: string[]; declarations: string; } { - const fileTypeSymbols: string[] = []; - const duoToneSymbols: string[] = []; + const standardSymbols: string[] = []; + const completeOnlySymbols: string[] = []; const lines: string[] = []; - for (const token of TOKENS) { - const entry = TOKEN_SVG_MAP[token]; - const ftId = `file-tree-builtin-file-type-${token}`; - const dtId = `file-tree-builtin-duo-tone-${token}`; - const ftVarName = `ft_${token.replace(/-/g, '_')}`; - const dtVarName = `dt_${token.replace(/-/g, '_')}`; - - const ftSymbol = svgToSymbol(entry.fileType, ftId); - lines.push(`const ${ftVarName} = \`${ftSymbol}\`;`); - fileTypeSymbols.push(ftVarName); - - if (entry.duoTone != null) { - const dtSymbol = svgToSymbol(entry.duoTone, dtId); - lines.push(`const ${dtVarName} = \`${dtSymbol}\`;`); - duoToneSymbols.push(dtVarName); - } else { - lines.push( - `const ${dtVarName} = ${ftVarName}.replaceAll('${ftId}', '${dtId}');` - ); - duoToneSymbols.push(dtVarName); - } + for (const token of SORTED_TOKENS) { + const def = TOKEN_DEFS[token]; + const symbolId = `file-tree-builtin-${token}`; + const varName = `sym_${token.replace(/-/g, '_')}`; + const svgFile = `${def.icon}.svg`; + const symbol = svgToSymbol(svgFile, symbolId); + lines.push(`const ${varName} = \`${symbol}\`;`); lines.push(''); + + if (def.tier === 'standard') { + standardSymbols.push(varName); + } else { + completeOnlySymbols.push(varName); + } } return { - fileTypeSymbols, - duoToneSymbols, + standardSymbols, + completeOnlySymbols, declarations: lines.join('\n'), }; } @@ -216,11 +263,16 @@ function formatRecord(entries: Record, indent: string): string { } async function generate(): Promise { - const { extensionTokens, fileNameTokens } = await buildTokenMaps(); - const { fileTypeSymbols, duoToneSymbols, declarations } = + const { extensionTokens, fileNameTokens, completeExtOverrides } = + await buildTokenMaps(); + const { standardSymbols, completeOnlySymbols, declarations } = generateSymbolConstants(); - const tokenType = TOKENS.map((t) => ` | '${t}'`).join('\n'); + const tokenType = SORTED_TOKENS.map((t) => ` | '${t}'`).join('\n'); + + const standardTokensList = SORTED_TOKENS.filter( + (t) => TOKEN_DEFS[t].tier === 'standard' + ); return `// @generated by scripts/generate-built-in-icons.ts — do not edit manually import type { FileTreeBuiltInIconSet } from './iconConfig'; @@ -228,7 +280,7 @@ import type { FileTreeBuiltInIconSet } from './iconConfig'; export type BuiltInFileIconToken = ${tokenType}; -const SIMPLE_SVG_SPRITE_SHEET = \`