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} />
|
||||
}
|
||||
+1
-1
@@ -27,7 +27,7 @@
|
||||
"version": "1.2.0", // semver(x.y.z)
|
||||
"minContract": "0.1.0",
|
||||
"description": "…",
|
||||
"icon": "<svg…/>", // 侧栏/面板图标(svg 字符串;宿主清除事件属性与 script)
|
||||
"icon": "Globe", // 侧栏/面板/胶囊图标:lucide 图标名(白名单见 DESIGN.md;kebab 亦可);兼容旧 svg 字符串;未知名回落拼图图标
|
||||
"order": 10, // 侧栏排序(只在插件间;核心入口恒在前)
|
||||
"panel": true, // 在右侧 SidePanel 出现一个 tab
|
||||
"entry": { "page": "index.html", "main": "main.mjs", "renderer": "renderer.mjs" }, // 内置插件省略 entry
|
||||
|
||||
@@ -64,7 +64,7 @@
|
||||
|
||||
- 字号阶:`10px`(徽标)/ `11px`(辅助按钮)/ `11.5px`(次要按钮、说明)/ `12px`(正文说明) / `12.5px`(行标题/正文)/ `16px`(页面标题)。**不使用其他字号**;行高 `leading-4/5`。
|
||||
- 间距基数为 4px:卡片内边距 `px-3 py-2.5`,区块间 `space-y-1.5`,小节间 `pt-3`。
|
||||
- 图标 12–16px,线条风格,`currentColor`。
|
||||
- 图标一律 **lucide**(宿主白名单 `plugin-icons.tsx`,约 100 个常用名):`manifest.icon` / 状态胶囊 `icon` 填 lucide 图标名(如 `"Globe"`,kebab-case 亦可),12–16px、`currentColor` 由宿主保证;**禁止自绘 svg**(旧内联 svg 仅对存量插件兼容),未知名回落拼图图标
|
||||
|
||||
## 4. 标准模式(复制即用)
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ export interface PluginManifest {
|
||||
/** 最低 harness 契约版本(semver),不满足拒载 */
|
||||
minContract: string
|
||||
description?: string
|
||||
/** 侧栏入口图标:svg 字符串;缺省用宿主默认字形 */
|
||||
/** 侧栏入口图标:lucide 图标名(如 "Globe",白名单见 docs/plugins/DESIGN.md);兼容旧内联 svg 字符串;缺省/未知名回落拼图图标 */
|
||||
icon?: string
|
||||
/** 侧栏入口排序(升序,只在插件之间排序;核心入口恒在前) */
|
||||
order?: number
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"description": "TODO:一句话描述插件做什么",
|
||||
"order": 50,
|
||||
"panel": true,
|
||||
"icon": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"2\"/></svg>",
|
||||
"icon": "Puzzle",
|
||||
"entry": {
|
||||
"main": "main.mjs",
|
||||
"renderer": "renderer.mjs"
|
||||
|
||||
Reference in New Issue
Block a user