From 4f37dabbe7a856c1edf4c40f5cd637baddafe250 Mon Sep 17 00:00:00 2001 From: hypercross Date: Sat, 8 Aug 2026 12:32:11 +0800 Subject: [PATCH] feat(web): add iconify tag icons for object classes Replace the text class tag in the tree and inspector with one or more MDI icons (hover shows the class name). Bundle only the used icons via a generator script instead of the full set. --- apps/web/package.json | 3 + apps/web/src/components/ObjectTree.tsx | 11 +++- apps/web/src/components/objectIcons.test.ts | 16 ++++++ apps/web/src/components/objectIcons.tsx | 28 +++++++++ apps/web/src/components/objectIconsData.ts | 49 ++++++++++++++++ apps/web/src/pages/ModPage.tsx | 11 +++- pnpm-lock.yaml | 58 +++++++++++++++++++ scripts/generate-object-icons.mjs | 63 +++++++++++++++++++++ 8 files changed, 235 insertions(+), 4 deletions(-) create mode 100644 apps/web/src/components/objectIcons.test.ts create mode 100644 apps/web/src/components/objectIcons.tsx create mode 100644 apps/web/src/components/objectIconsData.ts create mode 100644 scripts/generate-object-icons.mjs diff --git a/apps/web/package.json b/apps/web/package.json index 28b3ec9..20228cd 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -12,6 +12,9 @@ "lint": "echo \"no lint configured\"" }, "dependencies": { + "@iconify-json/mdi": "^1.2.3", + "@iconify/react": "^6.0.2", + "@iconify/utils": "^3.1.4", "@tts/extract": "workspace:*", "@tts/shared": "workspace:*", "react": "^19.2.8", diff --git a/apps/web/src/components/ObjectTree.tsx b/apps/web/src/components/ObjectTree.tsx index 05e1c51..dcac417 100644 --- a/apps/web/src/components/ObjectTree.tsx +++ b/apps/web/src/components/ObjectTree.tsx @@ -1,5 +1,7 @@ import { useState } from 'react'; +import { Icon } from '@iconify/react'; import type { ObjectTreeNode } from '@tts/extract'; +import { iconsForObject } from './objectIcons'; interface Props { nodes: ObjectTreeNode[]; @@ -70,8 +72,13 @@ function TreeNode({ onClick={() => onSelect(node.object.GUID)} className="block min-w-0 flex-1 truncate py-1 text-left text-sm" > - - {node.object.Name} + + {iconsForObject(node.object.Name).map((icon) => ( + + ))} {node.label} diff --git a/apps/web/src/components/objectIcons.test.ts b/apps/web/src/components/objectIcons.test.ts new file mode 100644 index 0000000..17c3ee7 --- /dev/null +++ b/apps/web/src/components/objectIcons.test.ts @@ -0,0 +1,16 @@ +import { describe, expect, it } from 'vitest'; +import { iconsForObject } from './objectIcons'; + +describe('iconsForObject', () => { + it('maps known classes to icons', () => { + expect(iconsForObject('Card')).toEqual(['mdi:cards-outline']); + expect(iconsForObject('Custom_Model_Bag')).toEqual([ + 'mdi:bag-personal-outline', + 'mdi:cube-outline', + ]); + }); + + it('falls back to the help icon for unknown classes', () => { + expect(iconsForObject('Unknown_Thing')).toEqual(['mdi:help-circle-outline']); + }); +}); \ No newline at end of file diff --git a/apps/web/src/components/objectIcons.tsx b/apps/web/src/components/objectIcons.tsx new file mode 100644 index 0000000..7b7624a --- /dev/null +++ b/apps/web/src/components/objectIcons.tsx @@ -0,0 +1,28 @@ +import './objectIconsData'; + +/** + * Map of TTS object class (`TTSObject.Name`) to one or more MDI icon names. + * Icons are registered in `objectIcons.ts` (generated) and referenced here by + * name. Unknown classes fall back to a help icon. + */ +const OBJECT_ICONS: Record = { + Card: ['mdi:cards-outline'], + Deck: ['mdi:cards'], + Custom_Deck: ['mdi:cards'], + Bag: ['mdi:bag-personal-outline'], + Custom_Model_Bag: ['mdi:bag-personal-outline', 'mdi:cube-outline'], + Custom_Model: ['mdi:cube-outline'], + Custom_Token: ['mdi:circle-outline'], + Custom_Image: ['mdi:image-outline'], + Custom_PDF: ['mdi:file-pdf-box'], + Dice: ['mdi:dice-6'], + Figurine: ['mdi:chess-knight'], + Board: ['mdi:table-large'], +}; + +const FALLBACK = ['mdi:help-circle-outline']; + +/** Resolve the icon name(s) for an object class. */ +export function iconsForObject(name: string): string[] { + return OBJECT_ICONS[name] ?? FALLBACK; +} \ No newline at end of file diff --git a/apps/web/src/components/objectIconsData.ts b/apps/web/src/components/objectIconsData.ts new file mode 100644 index 0000000..3550247 --- /dev/null +++ b/apps/web/src/components/objectIconsData.ts @@ -0,0 +1,49 @@ +// Generated by scripts/generate-object-icons.mjs — do not edit by hand. +// Re-run `node scripts/generate-object-icons.mjs` after changing ICON_NAMES. +import { addCollection } from '@iconify/react'; + +const mdiSubset = { + "prefix": "mdi", + "width": 24, + "height": 24, + "icons": { + "cards-outline": { + "body": "" + }, + "cards": { + "body": "" + }, + "bag-personal-outline": { + "body": "" + }, + "cube-outline": { + "body": "" + }, + "cube": { + "body": "" + }, + "circle-outline": { + "body": "" + }, + "image-outline": { + "body": "" + }, + "file-pdf-box": { + "body": "" + }, + "dice-6": { + "body": "" + }, + "chess-knight": { + "body": "" + }, + "table-large": { + "body": "" + }, + "help-circle-outline": { + "body": "" + } + } +}; + +addCollection(mdiSubset); diff --git a/apps/web/src/pages/ModPage.tsx b/apps/web/src/pages/ModPage.tsx index 5a76309..a2e97fe 100644 --- a/apps/web/src/pages/ModPage.tsx +++ b/apps/web/src/pages/ModPage.tsx @@ -1,4 +1,5 @@ import { useEffect, useMemo, useState } from 'react'; +import { Icon } from '@iconify/react'; import { useParams } from 'react-router-dom'; import { buildTree, collectRefs } from '@tts/extract'; import { useModStore } from '../stores/modStore'; @@ -6,6 +7,7 @@ import { useSearchStore } from '../stores/searchStore'; import { modFileUrl } from '../api'; import ObjectTree from '../components/ObjectTree'; import { resolveViewer } from '../components/viewers'; +import { iconsForObject } from '../components/objectIcons'; export default function ModPage() { const { id } = useParams<{ id: string }>(); @@ -60,8 +62,13 @@ export default function ModPage() { {selected ? ( <>
- - {selected.object.Name} + + {iconsForObject(selected.object.Name).map((icon) => ( + + ))}

{selected.label}

diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 431c871..42910ad 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -45,6 +45,15 @@ importers: apps/web: dependencies: + '@iconify-json/mdi': + specifier: ^1.2.3 + version: 1.2.3 + '@iconify/react': + specifier: ^6.0.2 + version: 6.0.2(react@19.2.8) + '@iconify/utils': + specifier: ^3.1.4 + version: 3.1.4 '@tts/extract': specifier: workspace:* version: link:../../packages/extract @@ -124,6 +133,9 @@ importers: packages: + '@antfu/install-pkg@1.1.0': + resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==} + '@esbuild/aix-ppc64@0.28.1': resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} engines: {node: '>=18'} @@ -286,6 +298,20 @@ packages: peerDependencies: hono: ^4 + '@iconify-json/mdi@1.2.3': + resolution: {integrity: sha512-O3cLwbDOK7NNDf2ihaQOH5F9JglnulNDFV7WprU2dSoZu3h3cWH//h74uQAB87brHmvFVxIOkuBX2sZSzYhScg==} + + '@iconify/react@6.0.2': + resolution: {integrity: sha512-SMmC2sactfpJD427WJEDN6PMyznTFMhByK9yLW0gOTtnjzzbsi/Ke/XqsumsavFPwNiXs8jSiYeZTmLCLwO+Fg==} + peerDependencies: + react: '>=16' + + '@iconify/types@2.0.0': + resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} + + '@iconify/utils@3.1.4': + resolution: {integrity: sha512-b1S7B1k9ohZ+iNTi2ATxbRYG9fTrJmUT0rc46bvVnNxqNRGW7dyo/vRREwyniI5IRN2RSJHDcm+s3BjWrSAjHw==} + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -623,6 +649,9 @@ packages: resolution: {integrity: sha512-kdJoFVv2xmayw6cY09H7AbMJMt8Jn5jdlEdXsP7AGBdF2DIptVlKlOLKXP41yPip4/a3yQPv9gVcJYI8YY04dw==} engines: {node: '>=16.9.0'} + import-meta-resolve@4.2.0: + resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==} + jiti@2.7.0: resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} hasBin: true @@ -787,6 +816,9 @@ packages: resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==} engines: {node: '>=12.20.0'} + package-manager-detector@1.8.0: + resolution: {integrity: sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A==} + pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -998,6 +1030,11 @@ packages: snapshots: + '@antfu/install-pkg@1.1.0': + dependencies: + package-manager-detector: 1.8.0 + tinyexec: 1.3.0 + '@esbuild/aix-ppc64@0.28.1': optional: true @@ -1080,6 +1117,23 @@ snapshots: dependencies: hono: 4.13.1 + '@iconify-json/mdi@1.2.3': + dependencies: + '@iconify/types': 2.0.0 + + '@iconify/react@6.0.2(react@19.2.8)': + dependencies: + '@iconify/types': 2.0.0 + react: 19.2.8 + + '@iconify/types@2.0.0': {} + + '@iconify/utils@3.1.4': + dependencies: + '@antfu/install-pkg': 1.1.0 + '@iconify/types': 2.0.0 + import-meta-resolve: 4.2.0 + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -1349,6 +1403,8 @@ snapshots: hono@4.13.1: {} + import-meta-resolve@4.2.0: {} + jiti@2.7.0: {} lightningcss-android-arm64@1.32.0: @@ -1457,6 +1513,8 @@ snapshots: obug@2.1.4: {} + package-manager-detector@1.8.0: {} + pathe@2.0.3: {} picocolors@1.1.1: {} diff --git a/scripts/generate-object-icons.mjs b/scripts/generate-object-icons.mjs new file mode 100644 index 0000000..69c067a --- /dev/null +++ b/scripts/generate-object-icons.mjs @@ -0,0 +1,63 @@ +// Generates apps/web/src/components/objectIcons.ts from the MDI icon set. +// Extracts only the icons referenced in ICON_NAMES so the bundle stays small, +// then registers them with Iconify's offline storage at import time. +import { readFileSync, writeFileSync, existsSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname, resolve } from 'node:path'; +import { createRequire } from 'node:module'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +// Resolve deps from apps/web, where the iconify packages are hoisted. +const webPkg = resolve(__dirname, '../apps/web/package.json'); +const require = createRequire(webPkg); + +// Resolve the mdi icons.json via the package entry (works from the pnpm store). +const mdiPkg = require.resolve('@iconify-json/mdi/icons.json'); +const icons = JSON.parse(readFileSync(mdiPkg, 'utf8')); + +const ICON_NAMES = [ + 'cards-outline', + 'cards', + 'bag-personal-outline', + 'cube-outline', + 'cube', + 'circle-outline', + 'image-outline', + 'file-pdf-box', + 'dice-6', + 'chess-knight', + 'table-large', + 'help-circle-outline', +]; + +// Extract just the icons we need, keeping the set's prefix/dimensions. +const subset = { + prefix: icons.prefix, + width: icons.width, + height: icons.height, + icons: Object.fromEntries( + ICON_NAMES.filter((name) => icons.icons[name]).map((name) => [ + name, + icons.icons[name], + ]), + ), +}; + +const missing = ICON_NAMES.filter((name) => !icons.icons[name]); +if (missing.length > 0) { + throw new Error(`Missing icons in mdi set: ${missing.join(', ')}`); +} + +const out = resolve(__dirname, '../apps/web/src/components/objectIconsData.ts'); +const body = `// Generated by scripts/generate-object-icons.mjs — do not edit by hand. +// Re-run \`node scripts/generate-object-icons.mjs\` after changing ICON_NAMES. +import { addCollection } from '@iconify/react'; + +const mdiSubset = ${JSON.stringify(subset, null, 2)}; + +addCollection(mdiSubset); +`; + +writeFileSync(out, body); +console.log(`Wrote ${out} (${ICON_NAMES.length} icons)`); \ No newline at end of file