feat: 胶囊详情面板复刻 ZCode Git 工具——头部标题+关闭、分组、行尾彩色数值(新增 success tone);撤下聊天区浮动圆钮(场景并入顶栏面板)
This commit is contained in:
@@ -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}
|
||||
/>
|
||||
<PluginFloatingCapsules entries={pluginsUi.status} onOpenView={handleOpenView} />
|
||||
<BottomSlot
|
||||
approvals={state.approvals}
|
||||
onResolve={resolveApproval}
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
/**
|
||||
* 聊天区浮动胶囊(B3 槽位):与 TitleBar 胶囊(B2)同源数据——主进程单一事实源,
|
||||
* 插件 setStatusItem 后两处同时出现。折叠/展开双态:
|
||||
* - 折叠:聊天列右上角单个圆钮(首条图标 + 计数徽标)
|
||||
* - 展开:纵向胶囊堆栈;条目点击行为与 B2 一致(detail → 向下弹层 / openView → 深链)
|
||||
* 无条目不渲染;点外部 / Esc 收起。
|
||||
*/
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import type { ReactNode } from 'react'
|
||||
import { LuPuzzle } from 'react-icons/lu'
|
||||
import type { PluginStatusEntry } from '@shared/types'
|
||||
import { PluginIconValue } from './plugin-icons'
|
||||
import { StatusPopover, TONE_DOT_CLASS } from './PluginStatusCapsules'
|
||||
|
||||
export function PluginFloatingCapsules({
|
||||
entries,
|
||||
onOpenView
|
||||
}: {
|
||||
entries: PluginStatusEntry[]
|
||||
onOpenView: (view: string, params?: unknown) => void
|
||||
}): ReactNode {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [detailKey, setDetailKey] = useState<string | null>(null)
|
||||
const rootRef = useRef<HTMLDivElement | null>(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 (
|
||||
<div ref={rootRef} className="absolute right-3 top-3 z-30 flex flex-col items-end gap-1.5 [-webkit-app-region:no-drag]">
|
||||
{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 (
|
||||
<div key={key} className="relative">
|
||||
<button
|
||||
type="button"
|
||||
title={item.text}
|
||||
onClick={() => {
|
||||
if (item.detail) setDetailKey(detailOpen ? null : key)
|
||||
else if (item.openView) {
|
||||
setOpen(false)
|
||||
setDetailKey(null)
|
||||
onOpenView(item.openView.view, item.openView.params)
|
||||
}
|
||||
}}
|
||||
className={`flex max-w-44 shrink-0 items-center gap-1.5 rounded-full border border-line bg-surface px-2 py-0.5 text-[11px] leading-[18px] text-ink-2 shadow-btn transition-colors ${
|
||||
clickable ? 'cursor-pointer hover:border-line-strong hover:text-ink' : 'cursor-default'
|
||||
} ${detailOpen ? 'border-line-strong text-ink' : ''}`}
|
||||
>
|
||||
<span aria-hidden className={`size-1.5 shrink-0 rounded-full ${TONE_DOT_CLASS[tone] ?? TONE_DOT_CLASS.default}`} />
|
||||
{item.icon ? <PluginIconValue icon={item.icon} size={12} className="shrink-0" /> : null}
|
||||
<span className="truncate">{item.text}</span>
|
||||
</button>
|
||||
{detailOpen && item.detail && <StatusPopover entry={{ pluginId, itemId, item }} onOpenView={onOpenView} />}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<button
|
||||
type="button"
|
||||
aria-expanded={open}
|
||||
aria-label={`插件状态(${entries.length} 条)`}
|
||||
title="插件状态"
|
||||
onClick={() => {
|
||||
setOpen((v) => !v)
|
||||
setDetailKey(null)
|
||||
}}
|
||||
className="relative flex size-8 items-center justify-center rounded-full border border-line bg-surface text-ink-2 shadow-btn transition-transform hover:text-ink active:scale-95"
|
||||
>
|
||||
{entries[0]?.item.icon ? (
|
||||
<PluginIconValue icon={entries[0].item.icon} size={14} />
|
||||
) : (
|
||||
<LuPuzzle size={14} className="text-ink-3" />
|
||||
)}
|
||||
<span className="absolute -right-1 -top-1 flex h-4 min-w-4 items-center justify-center rounded-full bg-accent px-1 text-[10px] font-medium leading-none text-white">
|
||||
{entries.length}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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<string, string> = {
|
||||
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<string, string> = {
|
||||
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<string, string> = {
|
||||
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 (
|
||||
<div
|
||||
className={`absolute right-0 z-50 w-72 overflow-hidden rounded-card border border-line bg-surface shadow-overlay ${
|
||||
className={`absolute right-0 z-50 w-80 overflow-hidden rounded-card border border-line bg-surface shadow-overlay ${
|
||||
direction === 'up' ? 'bottom-full mb-1.5' : 'top-full mt-1.5'
|
||||
}`}
|
||||
>
|
||||
<div className="px-2.5 pb-1 pt-2.5 text-[11.5px] font-medium text-ink">{item.detail?.title ?? item.text}</div>
|
||||
<div className="max-h-80 overflow-y-auto overscroll-contain px-1.5 pb-1.5">
|
||||
<div className="flex items-center justify-between gap-2 py-2 pl-3 pr-1.5">
|
||||
<div className="min-w-0 truncate text-[12.5px] font-medium text-ink">{item.detail?.title ?? item.text}</div>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="关闭"
|
||||
onClick={onClose}
|
||||
className="shrink-0 rounded-control p-1.5 text-ink-3 transition-colors hover:bg-hover hover:text-ink-2"
|
||||
>
|
||||
<LuX size={13} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="max-h-96 overflow-y-auto overscroll-contain border-t border-line px-1.5 py-1.5">
|
||||
{item.detail?.sections.map((section, si) => (
|
||||
<div key={si} className={si > 0 ? 'mt-1.5' : ''}>
|
||||
{section.title && <div className="px-2 pb-1 pt-1.5 text-[10px] text-ink-3">{section.title}</div>}
|
||||
<div key={si} className={si > 0 ? 'mt-1' : ''}>
|
||||
{section.title && (
|
||||
<div className="px-2 pb-1 pt-1.5 text-[11px] font-medium text-ink-2">{section.title}</div>
|
||||
)}
|
||||
{section.rows.map((row, ri) => {
|
||||
const clickable = Boolean(row.openView)
|
||||
const content = (
|
||||
@@ -50,7 +80,7 @@ export function StatusPopover({
|
||||
{row.detail && <span className="mt-0.5 block truncate text-[11px] leading-4 text-ink-3">{row.detail}</span>}
|
||||
</span>
|
||||
{row.trailing && (
|
||||
<span className={`shrink-0 rounded-control px-1.5 py-0.5 text-[10px] ${BADGE_TONE_CLASS[row.tone ?? 'default']}`}>
|
||||
<span className={`shrink-0 text-[11.5px] font-medium ${TRAILING_TONE_CLASS[row.tone ?? 'default']}`}>
|
||||
{row.trailing}
|
||||
</span>
|
||||
)}
|
||||
@@ -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}
|
||||
</button>
|
||||
) : (
|
||||
<div key={ri} className="flex w-full items-center gap-2 rounded-control px-2 py-1.5">
|
||||
<div key={ri} className="flex w-full items-center gap-2 rounded-control px-2 py-2">
|
||||
{content}
|
||||
</div>
|
||||
)
|
||||
@@ -80,14 +110,6 @@ export function StatusPopover({
|
||||
)
|
||||
}
|
||||
|
||||
/** 胶囊前导色点:tone 的安静表达(胶囊底保持中性,不整块染色) */
|
||||
export const TONE_DOT_CLASS: Record<string, string> = {
|
||||
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' : ''}`}
|
||||
>
|
||||
<span aria-hidden className={`size-1.5 shrink-0 rounded-full ${TONE_DOT_CLASS[tone] ?? TONE_DOT_CLASS.default}`} />
|
||||
{item.icon ? (
|
||||
<PluginIconValue icon={item.icon} size={12} className="shrink-0" />
|
||||
) : null}
|
||||
{item.icon ? <PluginIconValue icon={item.icon} size={12} className="shrink-0" /> : null}
|
||||
<span className="truncate">{item.text}</span>
|
||||
</button>
|
||||
{open && item.detail && <StatusPopover entry={{ pluginId, itemId, item }} onOpenView={onOpenView} />}
|
||||
{open && item.detail && (
|
||||
<StatusPopover entry={{ pluginId, itemId, item }} onOpenView={onOpenView} onClose={() => setOpenKey(null)} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
+1
-1
@@ -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`) | ⏳ |
|
||||
|
||||
## 交互基线
|
||||
|
||||
|
||||
+1
-1
@@ -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 子进程隔离模式 |
|
||||
|
||||
---
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user