feat: 插件图标规范化——manifest/胶囊 icon 统一 lucide 图标名(白名单 101 个),未知名回落拼图图标;内联 svg 仅旧兼容
This commit is contained in:
@@ -12,7 +12,8 @@ import { SettingsModal } from './components/SettingsModal'
|
||||
import { Search } from './components/Search'
|
||||
import { BrowserPanel } from './components/browser/BrowserPanel'
|
||||
import { SidePanel, type PanelTabIcon } from './components/SidePanel'
|
||||
import { PluginPageFrame, PluginIcon } from './plugins/PluginPageFrame'
|
||||
import { PluginPageFrame } from './plugins/PluginPageFrame'
|
||||
import { PluginIconValue } from './plugins/plugin-icons'
|
||||
import {
|
||||
useExternalPluginComponents,
|
||||
usePluginRendererLoader,
|
||||
@@ -316,9 +317,9 @@ export default function App(): JSX.Element {
|
||||
const pluginTabIcons = useMemo(() => {
|
||||
const map = new Map<string, PanelTabIcon>()
|
||||
for (const p of panelPlugins) {
|
||||
const svg = p.manifest.icon
|
||||
const icon = p.manifest.icon
|
||||
const Icon = ({ size, className }: { size?: number; className?: string }): JSX.Element => (
|
||||
<PluginIcon svg={svg} size={size ?? 16} className={className} />
|
||||
<PluginIconValue icon={icon} size={size ?? 16} className={className} />
|
||||
)
|
||||
map.set(p.manifest.id, Icon)
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ import type {
|
||||
import type { PluginSettingsSchema, PluginSettingsSchemas, PluginSettingValues, PluginSettingsValuesMap } from '@aso/plugin-sdk'
|
||||
import { THEME_MODES, THEME_PRESETS, applyThemeColor } from '../lib/theme'
|
||||
import { usePluginList } from '../plugins/store'
|
||||
import { PluginIcon } from '../plugins/PluginPageFrame'
|
||||
import { PluginIconValue } from '../plugins/plugin-icons'
|
||||
|
||||
type PluginSettingsTab = `plugin:${string}`
|
||||
type Tab = 'general' | 'model' | 'prompt' | 'plugins' | 'skills' | 'commands' | PluginSettingsTab
|
||||
@@ -818,12 +818,16 @@ function PluginsPanel({
|
||||
|
||||
// 插件设置导航图标(svg 字符串 → 组件;按 id+svg 缓存保证身份稳定,避免导航项重挂载)
|
||||
const pluginSettingsIconCache = new Map<string, IconType>()
|
||||
function pluginSettingsIcon(id: string, svg: string | undefined): IconType {
|
||||
const key = `${id}:${svg ?? ''}`
|
||||
function pluginSettingsIcon(id: string, icon: string | undefined): IconType {
|
||||
const key = `${id}:${icon ?? ''}`
|
||||
const cached = pluginSettingsIconCache.get(key)
|
||||
if (cached) return cached
|
||||
const Icon: IconType = (props) => (
|
||||
<PluginIcon svg={svg} size={typeof props.size === 'number' ? props.size : 16} className={props.className} />
|
||||
<PluginIconValue
|
||||
icon={icon}
|
||||
size={typeof props.size === 'number' ? props.size : 16}
|
||||
className={props.className}
|
||||
/>
|
||||
)
|
||||
pluginSettingsIconCache.set(key, Icon)
|
||||
return Icon
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
import { LuPanelLeft } from 'react-icons/lu'
|
||||
import { Button } from '@/components/atoms/Button'
|
||||
import { GlideMenu } from '@/components/GlideMenu'
|
||||
import { PluginIcon } from '@/plugins/PluginPageFrame'
|
||||
import { PluginIconValue } from '@/plugins/plugin-icons'
|
||||
import type { PluginInfo, SessionMeta, SessionState, WorkspaceMeta } from '@shared/types'
|
||||
|
||||
/** 可折叠工作区导航(beautifului.dev Sidebar Nav)。
|
||||
@@ -445,7 +445,7 @@ export function SidebarNav({
|
||||
className={`${navRowClass} ${active ? 'bg-hover-2 text-ink' : ''}`}
|
||||
>
|
||||
<span className="flex size-5 shrink-0 items-center justify-center">
|
||||
<PluginIcon svg={plugin.manifest.icon} size={18} />
|
||||
<PluginIconValue icon={plugin.manifest.icon} size={18} />
|
||||
</span>
|
||||
<span className="sidebar-copy ml-1.5 min-w-0 flex-1 truncate text-[13px] font-medium text-ink-3">
|
||||
{plugin.manifest.name}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import type { ReactNode } from 'react'
|
||||
import type { PluginStatusEntry } from '@shared/types'
|
||||
import { PluginIcon } from './PluginPageFrame'
|
||||
import { PluginIconValue } from './plugin-icons'
|
||||
|
||||
const TONE_CLASS: Record<string, string> = {
|
||||
default: 'border-line text-ink-2',
|
||||
@@ -39,7 +39,7 @@ function StatusPopover({ entry, onOpenView }: { entry: PluginStatusEntry; onOpen
|
||||
const clickable = Boolean(row.openView)
|
||||
const content = (
|
||||
<>
|
||||
{row.icon && <PluginIcon svg={row.icon} size={14} className="shrink-0 text-ink-2" />}
|
||||
{row.icon && <PluginIconValue icon={row.icon} size={14} className="shrink-0 text-ink-2" />}
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block truncate text-[12px] leading-4 text-ink">{row.label}</span>
|
||||
{row.detail && <span className="mt-0.5 block truncate text-[11px] leading-4 text-ink-3">{row.detail}</span>}
|
||||
@@ -130,7 +130,7 @@ export function PluginStatusCapsules({
|
||||
TONE_CLASS[tone] ?? TONE_CLASS.default
|
||||
} ${clickable ? 'hover:opacity-80 active:scale-[0.97]' : 'cursor-default'} ${open ? 'opacity-80' : ''}`}
|
||||
>
|
||||
{item.icon && <PluginIcon svg={item.icon} size={12} className="shrink-0" />}
|
||||
{item.icon && <PluginIconValue icon={item.icon} size={12} className="shrink-0" />}
|
||||
<span className="truncate">{item.text}</span>
|
||||
</button>
|
||||
{open && item.detail && <StatusPopover entry={{ pluginId, itemId, item }} onOpenView={onOpenView} />}
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
/**
|
||||
* 插件图标规范:manifest.icon / 状态胶囊 icon 统一填 **lucide 图标名**
|
||||
* (PascalCase 如 "Globe",kebab-case "layout-grid" 亦可),宿主经 react-icons/lu
|
||||
* 白名单渲染——线条风格、粗细、圆角与宿主完全一致(DESIGN.md 图标规范)。
|
||||
* 旧版内联 `<svg>` 字符串仍兼容(sanitizeSvg 防线继续生效);
|
||||
* 名单之外或缺失的名字一律回落拼图图标,杜绝来历不明的自绘图形。
|
||||
*/
|
||||
import type { ReactNode } from 'react'
|
||||
import type { IconType } from 'react-icons'
|
||||
import {
|
||||
LuActivity,
|
||||
LuAppWindow,
|
||||
LuArchive,
|
||||
LuAtom,
|
||||
LuAward,
|
||||
LuBadgeCheck,
|
||||
LuBan,
|
||||
LuBell,
|
||||
LuBlocks,
|
||||
LuBookOpen,
|
||||
LuBot,
|
||||
LuBoxes,
|
||||
LuBriefcase,
|
||||
LuCamera,
|
||||
LuCalendar,
|
||||
LuChartPie,
|
||||
LuChartBar,
|
||||
LuCheck,
|
||||
LuCircleCheck,
|
||||
LuCircle,
|
||||
LuCircleDot,
|
||||
LuClock,
|
||||
LuCloud,
|
||||
LuCode,
|
||||
LuCompass,
|
||||
LuCpu,
|
||||
LuDatabase,
|
||||
LuHardDrive,
|
||||
LuDownload,
|
||||
LuEye,
|
||||
LuEyeOff,
|
||||
LuFile,
|
||||
LuFileText,
|
||||
LuFilter,
|
||||
LuFlag,
|
||||
LuFolder,
|
||||
LuFolderOpen,
|
||||
LuGitBranch,
|
||||
LuGitCommitHorizontal,
|
||||
LuGithub,
|
||||
LuGitPullRequest,
|
||||
LuGlobe,
|
||||
LuHammer,
|
||||
LuHash,
|
||||
LuHeadphones,
|
||||
LuHouse,
|
||||
LuImage,
|
||||
LuInbox,
|
||||
LuKeyboard,
|
||||
LuLayers,
|
||||
LuLayoutGrid,
|
||||
LuLink,
|
||||
LuListChecks,
|
||||
LuListTodo,
|
||||
LuLock,
|
||||
LuMail,
|
||||
LuMap,
|
||||
LuMessageCircle,
|
||||
LuMic,
|
||||
LuMonitor,
|
||||
LuMoon,
|
||||
LuMusic,
|
||||
LuPackage,
|
||||
LuPalette,
|
||||
LuPaperclip,
|
||||
LuPenLine,
|
||||
LuPhone,
|
||||
LuPlay,
|
||||
LuPlug,
|
||||
LuPlugZap,
|
||||
LuPuzzle,
|
||||
LuRocket,
|
||||
LuSave,
|
||||
LuScan,
|
||||
LuSearch,
|
||||
LuServer,
|
||||
LuSettings,
|
||||
LuSettings2,
|
||||
LuShare2,
|
||||
LuShield,
|
||||
LuShoppingCart,
|
||||
LuSlidersHorizontal,
|
||||
LuSparkles,
|
||||
LuSquareStack,
|
||||
LuStar,
|
||||
LuSun,
|
||||
LuTable,
|
||||
LuTag,
|
||||
LuTarget,
|
||||
LuTerminal,
|
||||
LuThumbsUp,
|
||||
LuTimer,
|
||||
LuTrash2,
|
||||
LuTrendingUp,
|
||||
LuUpload,
|
||||
LuUser,
|
||||
LuUsers,
|
||||
LuVideo,
|
||||
LuWallet,
|
||||
LuWebhook,
|
||||
LuWrench,
|
||||
LuZap
|
||||
} from 'react-icons/lu'
|
||||
import { PluginIcon } from './PluginPageFrame'
|
||||
|
||||
/** 宿主核准的 lucide 图标白名单(manifest.icon / 胶囊 icon 可填的名字) */
|
||||
export const PLUGIN_LUCIDE_ICONS = {
|
||||
Activity: LuActivity,
|
||||
AppWindow: LuAppWindow,
|
||||
Archive: LuArchive,
|
||||
Atom: LuAtom,
|
||||
Award: LuAward,
|
||||
BadgeCheck: LuBadgeCheck,
|
||||
Ban: LuBan,
|
||||
Bell: LuBell,
|
||||
Blocks: LuBlocks,
|
||||
BookOpen: LuBookOpen,
|
||||
Bot: LuBot,
|
||||
Boxes: LuBoxes,
|
||||
Briefcase: LuBriefcase,
|
||||
Camera: LuCamera,
|
||||
Calendar: LuCalendar,
|
||||
ChartPie: LuChartPie,
|
||||
ChartBar: LuChartBar,
|
||||
Check: LuCheck,
|
||||
CircleCheck: LuCircleCheck,
|
||||
Circle: LuCircle,
|
||||
CircleDot: LuCircleDot,
|
||||
Clock: LuClock,
|
||||
Cloud: LuCloud,
|
||||
Code: LuCode,
|
||||
Compass: LuCompass,
|
||||
Cpu: LuCpu,
|
||||
Database: LuDatabase,
|
||||
HardDrive: LuHardDrive,
|
||||
Download: LuDownload,
|
||||
Eye: LuEye,
|
||||
EyeOff: LuEyeOff,
|
||||
File: LuFile,
|
||||
FileText: LuFileText,
|
||||
Filter: LuFilter,
|
||||
Flag: LuFlag,
|
||||
Folder: LuFolder,
|
||||
FolderOpen: LuFolderOpen,
|
||||
GitBranch: LuGitBranch,
|
||||
GitCommit: LuGitCommitHorizontal,
|
||||
Github: LuGithub,
|
||||
GitPullRequest: LuGitPullRequest,
|
||||
Globe: LuGlobe,
|
||||
Hammer: LuHammer,
|
||||
Hash: LuHash,
|
||||
Headphones: LuHeadphones,
|
||||
House: LuHouse,
|
||||
Image: LuImage,
|
||||
Inbox: LuInbox,
|
||||
Keyboard: LuKeyboard,
|
||||
Layers: LuLayers,
|
||||
LayoutGrid: LuLayoutGrid,
|
||||
Link: LuLink,
|
||||
ListChecks: LuListChecks,
|
||||
ListTodo: LuListTodo,
|
||||
Lock: LuLock,
|
||||
Mail: LuMail,
|
||||
Map: LuMap,
|
||||
MessageCircle: LuMessageCircle,
|
||||
Mic: LuMic,
|
||||
Monitor: LuMonitor,
|
||||
Moon: LuMoon,
|
||||
Music: LuMusic,
|
||||
Package: LuPackage,
|
||||
Palette: LuPalette,
|
||||
Paperclip: LuPaperclip,
|
||||
PenLine: LuPenLine,
|
||||
Phone: LuPhone,
|
||||
Play: LuPlay,
|
||||
Plug: LuPlug,
|
||||
PlugZap: LuPlugZap,
|
||||
Puzzle: LuPuzzle,
|
||||
Rocket: LuRocket,
|
||||
Save: LuSave,
|
||||
Scan: LuScan,
|
||||
Search: LuSearch,
|
||||
Server: LuServer,
|
||||
Settings: LuSettings,
|
||||
Settings2: LuSettings2,
|
||||
Share2: LuShare2,
|
||||
Shield: LuShield,
|
||||
ShoppingCart: LuShoppingCart,
|
||||
SlidersHorizontal: LuSlidersHorizontal,
|
||||
Sparkles: LuSparkles,
|
||||
SquareStack: LuSquareStack,
|
||||
Star: LuStar,
|
||||
Sun: LuSun,
|
||||
Table: LuTable,
|
||||
Tag: LuTag,
|
||||
Target: LuTarget,
|
||||
Terminal: LuTerminal,
|
||||
ThumbsUp: LuThumbsUp,
|
||||
Timer: LuTimer,
|
||||
Trash2: LuTrash2,
|
||||
TrendingUp: LuTrendingUp,
|
||||
Upload: LuUpload,
|
||||
User: LuUser,
|
||||
Users: LuUsers,
|
||||
Video: LuVideo,
|
||||
Wallet: LuWallet,
|
||||
Webhook: LuWebhook,
|
||||
Wrench: LuWrench,
|
||||
Zap: LuZap
|
||||
} satisfies Record<string, IconType>
|
||||
|
||||
/** kebab/snake/全小写 → PascalCase;已是 Pascal 的名字原样通过 */
|
||||
function toPascal(name: string): string {
|
||||
return name
|
||||
.trim()
|
||||
.replace(/[-_\s]+(.)/g, (_, c: string) => c.toUpperCase())
|
||||
.replace(/^./, (c) => c.toUpperCase())
|
||||
}
|
||||
|
||||
/** 按名字查白名单图标;未命中返回 null */
|
||||
export function lucideIconByName(name: string): IconType | null {
|
||||
const key = toPascal(name)
|
||||
return (PLUGIN_LUCIDE_ICONS as Record<string, IconType>)[key] ?? null
|
||||
}
|
||||
|
||||
/** 图标槽位统一渲染:lucide 名(规范用法)/ 内联 svg(旧兼容)/ 缺省与未知名回落拼图图标 */
|
||||
export function PluginIconValue({ icon, size = 16, className }: { icon?: string; size?: number; className?: string }): ReactNode {
|
||||
const v = icon?.trim()
|
||||
if (v && v.startsWith('<svg')) return <PluginIcon svg={v} size={size} className={className} />
|
||||
const Icon = (v ? lucideIconByName(v) : null) ?? LuPuzzle
|
||||
return <Icon size={size} className={className} />
|
||||
}
|
||||
Reference in New Issue
Block a user