diff --git a/apps/desktop/src/renderer/src/App.tsx b/apps/desktop/src/renderer/src/App.tsx index 63bc229..2fb1c14 100644 --- a/apps/desktop/src/renderer/src/App.tsx +++ b/apps/desktop/src/renderer/src/App.tsx @@ -22,7 +22,6 @@ import { } from './plugins/renderer-host' import { LoadingState, ErrorState } from '@aso/plugin-sdk/ui' import { PluginErrorBoundary } from './plugins/PluginErrorBoundary' -import { PluginFloatingCapsules } from './plugins/PluginFloatingCapsules' import { PluginStatusCapsules } from './plugins/PluginStatusCapsules' import { usePluginList, usePluginsUiState, sidebarPlugins } from './plugins/store' import { BUILTIN_PLUGIN_COMPONENTS } from './plugins/builtin-components' @@ -457,7 +456,6 @@ export default function App(): JSX.Element { onPickSuggestion={handlePickSuggestion} onQuoteSelection={handleQuoteSelection} /> - void -}): ReactNode { - const [open, setOpen] = useState(false) - const [detailKey, setDetailKey] = useState(null) - const rootRef = useRef(null) - - // 展开时:点外部 / Esc 收起 - useEffect(() => { - if (!open) return - const onDown = (e: MouseEvent): void => { - if (rootRef.current && !rootRef.current.contains(e.target as Node)) { - setOpen(false) - setDetailKey(null) - } - } - const onKey = (e: KeyboardEvent): void => { - if (e.key === 'Escape') { - setOpen(false) - setDetailKey(null) - } - } - document.addEventListener('mousedown', onDown) - document.addEventListener('keydown', onKey) - return () => { - document.removeEventListener('mousedown', onDown) - document.removeEventListener('keydown', onKey) - } - }, [open]) - - // 条目集合变化(停用/重载/删除)时收起;按 key 序列比较,避免引用变化误关 - const entryKeys = entries.map((e) => `${e.pluginId}:${e.itemId}`).join(',') - useEffect(() => { - setOpen(false) - setDetailKey(null) - }, [entryKeys]) - - if (entries.length === 0) return null - const shown = entries.slice(0, 4) - return ( -
- {open && - shown.map(({ pluginId, itemId, item }) => { - const tone = item.tone ?? 'default' - const key = `${pluginId}:${itemId}` - const expandable = Boolean(item.detail) - const clickable = expandable || Boolean(item.openView) - const detailOpen = detailKey === key - return ( -
- - {detailOpen && item.detail && } -
- ) - })} - -
- ) -} diff --git a/apps/desktop/src/renderer/src/plugins/PluginStatusCapsules.tsx b/apps/desktop/src/renderer/src/plugins/PluginStatusCapsules.tsx index 4e36cb8..82ac93d 100644 --- a/apps/desktop/src/renderer/src/plugins/PluginStatusCapsules.tsx +++ b/apps/desktop/src/renderer/src/plugins/PluginStatusCapsules.tsx @@ -1,45 +1,75 @@ /** * 插件状态胶囊(TitleBar B2 槽位):数据驱动渲染主进程广播的状态条目。 * 顶栏胶囊只是紧凑状态条;点击行为分三档: - * 1. 声明了 detail → 展开宿主渲染的详情弹层(分组列表,行内数据再负责深链) + * 1. 声明了 detail → 展开宿主渲染的详情面板(ZCode 式:头部标题+关闭、 + * 分组列表、行尾彩色数值;行内数据再负责深链) * 2. 声明了 openView → 执行深链 * 3. 都没有 → 纯展示 - * 聊天视图与插件视图下均可见。弹层点击外部/Esc 关闭。 + * 聊天视图与插件视图下均可见。面板点击外部/Esc 关闭。 */ import { useEffect, useRef, useState } from 'react' import type { ReactNode } from 'react' +import { LuX } from 'react-icons/lu' import type { PluginStatusEntry } from '@shared/types' import { PluginIconValue } from './plugin-icons' -export const BADGE_TONE_CLASS: Record = { - default: 'bg-hover text-ink-3', - accent: 'bg-accent-tint text-accent-ink', - warn: 'bg-orange-tint text-orange', - danger: 'bg-red-tint text-red' +/** 胶囊前导色点:tone 的安静表达(胶囊底保持中性,不整块染色) */ +export const TONE_DOT_CLASS: Record = { + default: 'bg-ink-3', + accent: 'bg-accent', + success: 'bg-green', + warn: 'bg-orange', + danger: 'bg-red' } -/** 详情弹层:分组列表,行数据自带深链(B2 顶栏向下弹、B3 浮动胶囊向上弹) */ +/** 行尾数值的着色(ZCode 式:+76 绿 / -40 红 的纯色文本,不用底色块) */ +const TRAILING_TONE_CLASS: Record = { + default: 'text-ink-3', + accent: 'text-accent', + success: 'text-green', + warn: 'text-orange', + danger: 'text-red' +} + +/** + * 详情面板(ZCode Git 工具式):头部标题 + 关闭,分组列表; + * 行 = 图标 + 标签(+次级说明)+ 行尾彩色数值。direction 控制从胶囊下方/上方弹出。 + */ export function StatusPopover({ entry, onOpenView, + onClose, direction = 'down' }: { entry: PluginStatusEntry onOpenView: (view: string, params?: unknown) => void + onClose: () => void direction?: 'down' | 'up' }): ReactNode { const { item } = entry return (
-
{item.detail?.title ?? item.text}
-
+
+
{item.detail?.title ?? item.text}
+ +
+
{item.detail?.sections.map((section, si) => ( -
0 ? 'mt-1.5' : ''}> - {section.title &&
{section.title}
} +
0 ? 'mt-1' : ''}> + {section.title && ( +
{section.title}
+ )} {section.rows.map((row, ri) => { const clickable = Boolean(row.openView) const content = ( @@ -50,7 +80,7 @@ export function StatusPopover({ {row.detail && {row.detail}} {row.trailing && ( - + {row.trailing} )} @@ -63,12 +93,12 @@ export function StatusPopover({ onClick={() => { if (row.openView) onOpenView(row.openView.view, row.openView.params) }} - className="flex w-full items-center gap-2 rounded-control px-2 py-1.5 text-left transition-colors hover:bg-hover" + className="flex w-full items-center gap-2 rounded-control px-2 py-2 text-left transition-colors hover:bg-hover" > {content} ) : ( -
+
{content}
) @@ -80,14 +110,6 @@ export function StatusPopover({ ) } -/** 胶囊前导色点:tone 的安静表达(胶囊底保持中性,不整块染色) */ -export const TONE_DOT_CLASS: Record = { - default: 'bg-ink-3', - accent: 'bg-accent', - warn: 'bg-orange', - danger: 'bg-red' -} - export function PluginStatusCapsules({ entries, onOpenView @@ -144,12 +166,12 @@ export function PluginStatusCapsules({ } ${open ? 'border-line-strong text-ink' : ''}`} > - {item.icon ? ( - - ) : null} + {item.icon ? : null} {item.text} - {open && item.detail && } + {open && item.detail && ( + setOpenKey(null)} /> + )}
) })} diff --git a/docs/plugins.md b/docs/plugins.md index 104ebaf..ef1745d 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -174,7 +174,7 @@ api.ui.onSettingsAction((key) => {}) // button 条目点击回调(key = 按钮 | 深链 | `openView(view, params)`(main 侧 IPC 广播 / 页侧桥直通) | ✅ | | 工具栏按钮(B5) | `registerToolbarButton` 数据已通,Composer 工具行渲染 | ⏳ 下版本 | | 聊天流富组件(B4) | ```类型``` 围栏块 → 宿主元组件(data-table/stat-grid/step-list/kv-card) | ⏳ 下版本 | -| 聊天区浮动胶囊(B3) | 与 B2 同源数据:聊天列右上角折叠圆钮(计数徽标),展开为纵向胶囊堆栈,样式/弹层/深链与 B2 一致 | ✅ | +| 聊天区浮动胶囊(B3) | 独立浮动态暂缓——顶栏胶囊的详情面板(ZCode 式)已覆盖同场景;数据同源(`setStatusItem`) | ⏳ | ## 交互基线 diff --git a/docs/roadmap.md b/docs/roadmap.md index 4e62f5d..83da6b8 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -12,7 +12,7 @@ | v0.4.0 | 工具与审批 | 工具调用卡片(参数/结果折叠展示)、tool_call 拦截 → Approval Card、defineTool 业务工具骨架(本地可跑的演示工具) | | v0.5.0 | 打磨 | 深浅主题、GSAP 动效细节、快捷键、错误态/空态、队列可视化、自动滚动策略 | | v0.8.0 | 动态插件系统(M0–M3) | 两层能力模型(内置组件级/外置 iframe)、pi 原生契约聚合 extension、@aso/plugin-sdk、plugin://peer:// 协议、视图切换+聊天保活+徽标、侧栏/面板/胶囊/深链槽位、浏览器面板插件化、插件重载 | -| v0.9.0 | 插件一等公民 + 可观测 | entry.renderer 宿主内 React 组件(Blob 装载+单实例 React)、.pi/extensions 一等插件源(build 开关门控)、声明式设置体系(13 控件+一级导航入口)、统一日志系统(滚动文件/api.log/IPC 追踪/渲染层错误归档)、渐进式教程 docs/plugins/ + DESIGN.md + plugin-template、性能/样式标准与检验器(激活 10s/装载 8s 护栏、@aso/plugin-sdk/ui 组件库+三态、withTimeout/withRetry、plugin:check)、状态胶囊详情弹层+聊天区浮动胶囊(B3,与 B2 同源)、图标统一 lucide 白名单、面板平铺标签条 | +| v0.9.0 | 插件一等公民 + 可观测 | entry.renderer 宿主内 React 组件(Blob 装载+单实例 React)、.pi/extensions 一等插件源(build 开关门控)、声明式设置体系(13 控件+一级导航入口)、统一日志系统(滚动文件/api.log/IPC 追踪/渲染层错误归档)、渐进式教程 docs/plugins/ + DESIGN.md + plugin-template、性能/样式标准与检验器(激活 10s/装载 8s 护栏、@aso/plugin-sdk/ui 组件库+三态、withTimeout/withRetry、plugin:check)、状态胶囊详情面板(ZCode Git 工具式:头部+分组+行尾彩色数值,B3 浮动态并入顶栏胶囊)、图标统一 lucide 白名单、面板平铺标签条 | | 未来 | | 插件管理与 zip 导入(M4)、ed25519 签名在线更新(M5)、外置插件模板与开发工作流(M6)、pi 原生扩展目录管控(M7)、B3/B4/B5 槽位、electron-builder 打包与自动更新、i18n(中/英)、sqlite 持久化与迁移、本地备份恢复、热点词/生成/模板业务插件、手动模式编排 UI、RPC 子进程隔离模式 | --- diff --git a/packages/plugin-sdk/src/status.ts b/packages/plugin-sdk/src/status.ts index 6cf6d18..3523640 100644 --- a/packages/plugin-sdk/src/status.ts +++ b/packages/plugin-sdk/src/status.ts @@ -11,7 +11,7 @@ export interface OpenViewAction { } /** 徽标/胶囊共用色调语义 */ -export type StatusTone = 'default' | 'accent' | 'warn' | 'danger' +export type StatusTone = 'default' | 'accent' | 'success' | 'warn' | 'danger' /** 详情弹层里的一行(数据驱动;openView 缺省为纯展示行) */ export interface StatusDetailRow {