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