fix(plugins): 悬浮胶囊三点修正——位置/面板头/分组手风琴

This commit is contained in:
Rick Ye
2026-09-11 16:00:48 +08:00
parent ad16ed7239
commit 781a6c8beb
6 changed files with 201 additions and 145 deletions
+4 -1
View File
@@ -424,8 +424,11 @@ export default function App(): JSX.Element {
title={activeView.view === 'chat' ? sessionTitle : activePlugin?.manifest.name} title={activeView.view === 'chat' ? sessionTitle : activePlugin?.manifest.name}
panelOpen={sideOpen} panelOpen={sideOpen}
onTogglePanel={toggleSide} onTogglePanel={toggleSide}
extra={<PluginFloatingCapsule entries={pluginsUi.status} plugins={plugins} onOpenView={handleOpenView} />}
/> />
{/* 悬浮状态胶囊:main 区右上角(侧面板打开时左移让位) */}
<div className="absolute top-12 z-40" style={{ right: sideOpen ? sideWidth + 12 : 12 }}>
<PluginFloatingCapsule entries={pluginsUi.status} plugins={plugins} onOpenView={handleOpenView} />
</div>
<div className="flex min-h-0 flex-1"> <div className="flex min-h-0 flex-1">
{/* 聊天列:切到插件视图时 display:none 保活(草稿/流式/滚动全保留) */} {/* 聊天列:切到插件视图时 display:none 保活(草稿/流式/滚动全保留) */}
<div <div
@@ -1,23 +1,21 @@
import { useEffect, useState, type JSX, type ReactNode } from 'react' import { useEffect, useState, type JSX } from 'react'
import { LuPanelRight } from 'react-icons/lu' import { LuPanelRight } from 'react-icons/lu'
/** /**
* 自绘标题栏(配合主进程 titleBarStyle:'hidden')。 * 自绘标题栏(配合主进程 titleBarStyle:'hidden')。
* 整条是拖拽区;左上角展示当前会话/插件名称(无名称时显示「新会话」); * 整条是拖拽区;左上角展示当前会话/插件名称(无名称时显示「新会话」);
* title 之后可挂 extra 槽(插件状态胶囊);右侧自绘窗口控制按钮(最小化/ * 右侧自绘窗口控制按钮(最小化/最大化还原/关闭),高度 40px。
* 最大化还原/关闭),高度 40px。不使用系统 WCO overlay —— 其原生背景会盖住标题栏分隔线。 * 不使用系统 WCO overlay —— 其原生背景会盖住标题栏分隔线。
* (插件悬浮状态胶囊不再挂这里:改悬浮于 main 区域右上角。)
*/ */
export function TitleBar({ export function TitleBar({
title, title,
panelOpen, panelOpen,
onTogglePanel, onTogglePanel
extra
}: { }: {
title?: string title?: string
panelOpen: boolean panelOpen: boolean
onTogglePanel: () => void onTogglePanel: () => void
/** 标题右侧插槽(插件状态胶囊等;聊天/插件视图下均可见) */
extra?: ReactNode
}): JSX.Element { }): JSX.Element {
const name = title?.trim() const name = title?.trim()
const [maximized, setMaximized] = useState(false) const [maximized, setMaximized] = useState(false)
@@ -27,7 +25,6 @@ export function TitleBar({
return ( return (
<div className="relative z-20 flex h-10 shrink-0 select-none items-center border-b border-line bg-surface pl-4 [-webkit-app-region:drag]"> <div className="relative z-20 flex h-10 shrink-0 select-none items-center border-b border-line bg-surface pl-4 [-webkit-app-region:drag]">
<div className="min-w-0 truncate text-[12.5px] font-medium text-ink">{name || '新会话'}</div> <div className="min-w-0 truncate text-[12.5px] font-medium text-ink">{name || '新会话'}</div>
{extra && <div className="ml-3 flex shrink-0 items-center [-webkit-app-region:no-drag]">{extra}</div>}
<div className="ml-auto flex h-full shrink-0 items-center gap-1 pr-1.5 [-webkit-app-region:no-drag]"> <div className="ml-auto flex h-full shrink-0 items-center gap-1 pr-1.5 [-webkit-app-region:no-drag]">
<button <button
type="button" type="button"
@@ -1,18 +1,19 @@
/** /**
* 悬浮状态胶囊(B3 槽位,复刻 ZCode「最小化药丸 ⇄ 展开面板」二态): * 悬浮状态胶囊(B3 槽位,复刻 ZCode「最小化药丸 ⇄ 展开面板」二态):
* - 所有插件共用一个胶囊,数据同源 plugins:uiState(api.ui.setStatusItem) * - 所有插件共用一个胶囊,数据同源 plugins:uiState(api.ui.setStatusItem);
* 挂在 main 区域右上角悬浮(侧面板打开时自动左移让位,由挂载点控制)
* - 宿主为每条状态记录 updatedAt:最小化药丸展示「插件最新的消息」 * - 宿主为每条状态记录 updatedAt:最小化药丸展示「插件最新的消息」
* (tone 点 + 插件图标 + 文本),点击展开面板 * (tone 点 + 插件图标 + 文本 + 总数角标),点击展开面板
* - 展开面板 = 每个插件一个专属区域:区域头(图标+名称)可收起/展开 * - 展开面板:插件区域头 = 插件名(第一个区域的右侧是收回药丸的 ⤡);
* (记忆在 localStorage),区域内是该插件的状态条目与详情行 * 区域内 detail 的**分组是手风琴**——默认收起只显示「标题 + 行尾徽标」,
* (行 = 图标 + 标签 + 次级说明 + 行尾彩色数值,行内可深链) * 点击展开 rows(ZCode 式:计划 ›);无标题分组直接平铺 rows
* - 面板高度固定,内容溢出滚动;点击面板外部 / Esc 收回最小化 * - 面板高度固定,内容溢出滚动;点击面板外部 / Esc 收回最小化
*/ */
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import type { ReactNode } from 'react' import type { ReactNode } from 'react'
import { LuChevronDown, LuMinimize2 } from 'react-icons/lu' import { LuChevronDown, LuChevronRight, LuMinimize2 } from 'react-icons/lu'
import type { PluginInfo, PluginStatusEntry } from '@shared/types' import type { PluginInfo, PluginStatusEntry } from '@shared/types'
import type { StatusDetail } from '@aso/plugin-sdk' import type { StatusDetailRow } from '@aso/plugin-sdk'
import { PluginIconValue } from './plugin-icons' import { PluginIconValue } from './plugin-icons'
/** 胶囊前导色点:tone 的安静表达(底保持中性,不整块染色) */ /** 胶囊前导色点:tone 的安静表达(底保持中性,不整块染色) */
@@ -45,56 +46,49 @@ function loadCollapsedSections(): string[] {
} }
} }
/** 详情分组渲染(标题层由容器决定:面板里分区头是插件名,这里只渲染 sections/rows) */ /** 明细行(数据驱动:图标 + 标签 + 次级说明 + 行尾彩色数值,行内可深链) */
function StatusDetailBody({ function StatusRows({
detail, rows,
onOpenView onOpenView
}: { }: {
detail: StatusDetail rows: StatusDetailRow[]
onOpenView: (view: string, params?: unknown) => void onOpenView: (view: string, params?: unknown) => void
}): ReactNode { }): ReactNode {
return ( return (
<> <>
{detail.sections.map((section, si) => ( {rows.map((row, ri) => {
<div key={si} className={si > 0 ? 'mt-0.5' : ''}> const clickable = Boolean(row.openView)
{section.title && ( const content = (
<div className="px-2 pb-0.5 pt-1.5 text-[11px] font-medium text-ink-2">{section.title}</div> <>
)} {row.icon && <PluginIconValue icon={row.icon} size={14} className="shrink-0 text-ink-2" />}
{section.rows.map((row, ri) => { <span className="min-w-0 flex-1">
const clickable = Boolean(row.openView) <span className="block truncate text-[12px] leading-4 text-ink">{row.label}</span>
const content = ( {row.detail && <span className="mt-0.5 block truncate text-[11px] leading-4 text-ink-3">{row.detail}</span>}
<> </span>
{row.icon && <PluginIconValue icon={row.icon} size={14} className="shrink-0 text-ink-2" />} {row.trailing && (
<span className="min-w-0 flex-1"> <span className={`shrink-0 text-[11.5px] font-medium ${TRAILING_TONE_CLASS[row.tone ?? 'default']}`}>
<span className="block truncate text-[12px] leading-4 text-ink">{row.label}</span> {row.trailing}
{row.detail && <span className="mt-0.5 block truncate text-[11px] leading-4 text-ink-3">{row.detail}</span>} </span>
</span> )}
{row.trailing && ( </>
<span className={`shrink-0 text-[11.5px] font-medium ${TRAILING_TONE_CLASS[row.tone ?? 'default']}`}> )
{row.trailing} return clickable ? (
</span> <button
)} key={ri}
</> type="button"
) onClick={() => {
return clickable ? ( if (row.openView) onOpenView(row.openView.view, row.openView.params)
<button }}
key={ri} className="flex w-full items-center gap-2 rounded-control px-2 py-1.5 text-left transition-colors hover:bg-hover"
type="button" >
onClick={() => { {content}
if (row.openView) onOpenView(row.openView.view, row.openView.params) </button>
}} ) : (
className="flex w-full items-center gap-2 rounded-control px-2 py-1.5 text-left transition-colors hover:bg-hover" <div key={ri} className="flex w-full items-center gap-2 rounded-control px-2 py-1.5">
> {content}
{content} </div>
</button> )
) : ( })}
<div key={ri} className="flex w-full items-center gap-2 rounded-control px-2 py-1.5">
{content}
</div>
)
})}
</div>
))}
</> </>
) )
} }
@@ -109,12 +103,14 @@ export function PluginFloatingCapsule({
onOpenView: (view: string, params?: unknown) => void onOpenView: (view: string, params?: unknown) => void
}): ReactNode { }): ReactNode {
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const [collapsedSections, setCollapsedSections] = useState<string[]>(loadCollapsedSections) const [collapsedAreas, setCollapsedAreas] = useState<string[]>(loadCollapsedSections)
/** 分组手风琴(展开的 section 键 pluginId:itemId:idx;默认收起,会话内记忆) */
const [openSections, setOpenSections] = useState<Set<string>>(() => new Set())
const rootRef = useRef<HTMLDivElement | null>(null) const rootRef = useRef<HTMLDivElement | null>(null)
useEffect(() => { useEffect(() => {
localStorage.setItem(SECTION_STORAGE_KEY, JSON.stringify(collapsedSections)) localStorage.setItem(SECTION_STORAGE_KEY, JSON.stringify(collapsedAreas))
}, [collapsedSections]) }, [collapsedAreas])
// 面板开着时:点击外部 / Esc 收回最小化 // 面板开着时:点击外部 / Esc 收回最小化
useEffect(() => { useEffect(() => {
@@ -149,11 +145,19 @@ export function PluginFloatingCapsule({
byPlugin.set(entry.pluginId, arr) byPlugin.set(entry.pluginId, arr)
} }
const toggleSection = (pluginId: string): void => { const toggleArea = (pluginId: string): void => {
setCollapsedSections((prev) => setCollapsedAreas((prev) =>
prev.includes(pluginId) ? prev.filter((id) => id !== pluginId) : [...prev, pluginId] prev.includes(pluginId) ? prev.filter((id) => id !== pluginId) : [...prev, pluginId]
) )
} }
const toggleSection = (key: string): void => {
setOpenSections((prev) => {
const next = new Set(prev)
if (next.has(key)) next.delete(key)
else next.add(key)
return next
})
}
if (!open) { if (!open) {
return ( return (
@@ -162,7 +166,7 @@ export function PluginFloatingCapsule({
type="button" type="button"
title="插件状态(点击展开)" title="插件状态(点击展开)"
onClick={() => setOpen(true)} onClick={() => setOpen(true)}
className="flex max-w-48 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 transition-colors hover:border-line-strong hover:text-ink" className="flex max-w-48 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-card transition-colors hover:border-line-strong hover:text-ink"
> >
<span <span
aria-hidden aria-hidden
@@ -180,87 +184,135 @@ export function PluginFloatingCapsule({
return ( return (
<div ref={rootRef} className="relative shrink-0"> <div ref={rootRef} className="relative shrink-0">
<div className="absolute right-0 top-full z-50 mt-1.5 flex w-80 flex-col overflow-hidden rounded-card border border-line bg-surface text-left shadow-overlay [-webkit-app-region:no-drag]"> <div className="absolute right-0 top-full z-50 mt-1.5 flex w-80 flex-col overflow-hidden rounded-card border border-line bg-surface text-left shadow-overlay">
{/* 面板头:总量 + 收回最小化 */} {/* 固定高度内容区:分组手风琴 + 溢出滚动 */}
<div className="flex shrink-0 items-center justify-between gap-2 border-b border-line py-1.5 pl-3 pr-1">
<div className="min-w-0 truncate text-[12.5px] font-medium text-ink">
插件状态
<span className="ml-1.5 text-[11px] font-normal text-ink-3">{byPlugin.size} 个插件</span>
</div>
<button
type="button"
aria-label="收起到悬浮胶囊"
title="收起"
onClick={() => setOpen(false)}
className="shrink-0 rounded-control p-1.5 text-ink-3 transition-colors hover:bg-hover hover:text-ink-2"
>
<LuMinimize2 size={13} />
</button>
</div>
{/* 固定高度内容区:插件太多/内容过长向下滚动 */}
<div className="h-[min(480px,58vh)] overflow-y-auto overscroll-contain px-1.5 py-1.5"> <div className="h-[min(480px,58vh)] overflow-y-auto overscroll-contain px-1.5 py-1.5">
{[...byPlugin.entries()].map(([pluginId, pluginEntries]) => { {[...byPlugin.entries()].map(([pluginId, pluginEntries], areaIdx) => {
const plugin = plugins.find((p) => p.manifest.id === pluginId) const plugin = plugins.find((p) => p.manifest.id === pluginId)
const collapsed = collapsedSections.includes(pluginId) const areaCollapsed = collapsedAreas.includes(pluginId)
return ( return (
<div key={pluginId} className="mb-0.5 last:mb-0"> <div key={pluginId} className={areaIdx > 0 ? 'mt-1 border-t border-line pt-1' : ''}>
{/* 区域头:插件专属,可收起/展开 */} {/* 区域头 = 插件名;第一个区域右侧是收回药丸的 ⤡ */}
<button <div className="flex w-full items-center gap-1 rounded-control pr-0.5">
type="button" <button
aria-expanded={!collapsed} type="button"
onClick={() => toggleSection(pluginId)} aria-expanded={!areaCollapsed}
className="flex w-full items-center gap-1.5 rounded-control px-2 py-1.5 text-left transition-colors hover:bg-hover" onClick={() => toggleArea(pluginId)}
> className="flex min-w-0 flex-1 items-center gap-1.5 rounded-control px-2 py-1.5 text-left transition-colors hover:bg-hover"
{plugin ? ( >
<PluginIconValue icon={plugin.manifest.icon} size={14} className="shrink-0 text-ink-2" /> {plugin ? (
) : null} <PluginIconValue icon={plugin.manifest.icon} size={14} className="shrink-0 text-ink-2" />
<span className="min-w-0 flex-1 truncate text-[12px] font-medium text-ink-2"> ) : null}
{plugin?.manifest.name ?? pluginId} <span className="min-w-0 flex-1 truncate text-[12.5px] font-medium text-ink">
</span> {plugin?.manifest.name ?? pluginId}
<span className="shrink-0 text-[10.5px] text-ink-3">{pluginEntries.length}</span> </span>
<LuChevronDown <LuChevronDown
size={12} size={12}
aria-hidden aria-hidden
className={`shrink-0 text-ink-3 transition-transform duration-150 ${collapsed ? '-rotate-90' : ''}`} className={`shrink-0 text-ink-3 transition-transform duration-150 ${areaCollapsed ? '-rotate-90' : ''}`}
/> />
</button> </button>
{!collapsed && ( {areaIdx === 0 && (
<div className="pb-1 pl-1"> <button
type="button"
aria-label="收起到悬浮胶囊"
title="收起"
onClick={() => setOpen(false)}
className="shrink-0 rounded-control p-1.5 text-ink-3 transition-colors hover:bg-hover hover:text-ink-2"
>
<LuMinimize2 size={13} />
</button>
)}
</div>
{!areaCollapsed && (
<div className="pb-1">
{pluginEntries.map((entry) => { {pluginEntries.map((entry) => {
const clickable = Boolean(entry.item.openView) const clickable = Boolean(entry.item.openView)
const body = (
<>
<span
aria-hidden
className={`size-1.5 shrink-0 rounded-full ${
TONE_DOT_CLASS[entry.item.tone ?? 'default'] ?? TONE_DOT_CLASS.default
}`}
/>
{entry.item.icon ? (
<PluginIconValue icon={entry.item.icon} size={13} className="shrink-0 text-ink-2" />
) : null}
<span className="min-w-0 flex-1 truncate text-[12px] leading-4 text-ink">{entry.item.text}</span>
</>
)
return ( return (
<div key={entry.itemId}> <div key={entry.itemId}>
{clickable ? ( {entry.item.detail ? (
<button // 有详情:分组手风琴(默认收起,标题行 + 行尾徽标;无标题分组平铺)
type="button" entry.item.detail.sections.map((section, si) => {
onClick={() => { if (!section.title) {
if (entry.item.openView) onOpenView(entry.item.openView.view, entry.item.openView.params) return <StatusRows key={si} rows={section.rows} onOpenView={onOpenView} />
}} }
className="flex w-full items-center gap-2 rounded-control px-2 py-1.5 text-left transition-colors hover:bg-hover" const key = `${pluginId}:${entry.itemId}:${si}`
> const expanded = openSections.has(key)
{body} return (
</button> <div key={key}>
<button
type="button"
aria-expanded={expanded}
onClick={() => toggleSection(key)}
className="flex w-full items-center gap-2 rounded-control px-2 py-1.5 text-left transition-colors hover:bg-hover"
>
<span className="min-w-0 flex-1 truncate text-[12px] leading-4 text-ink">
{section.title}
</span>
{section.trailing && (
<span
className={`shrink-0 text-[11.5px] font-medium ${
TRAILING_TONE_CLASS[section.trailingTone ?? 'default']
}`}
>
{section.trailing}
</span>
)}
<LuChevronRight
size={12}
aria-hidden
className={`shrink-0 text-ink-3 transition-transform duration-150 ${
expanded ? 'rotate-90' : ''
}`}
/>
</button>
{expanded && (
<div className="pb-1 pl-1">
<StatusRows rows={section.rows} onOpenView={onOpenView} />
</div>
)}
</div>
)
})
) : ( ) : (
<div className="flex w-full items-center gap-2 rounded-control px-2 py-1.5">{body}</div> // 纯文本条目行
)} clickable ? (
{entry.item.detail && ( <button
<div className="pb-1"> type="button"
<StatusDetailBody detail={entry.item.detail} onOpenView={onOpenView} /> onClick={() => {
</div> if (entry.item.openView) onOpenView(entry.item.openView.view, entry.item.openView.params)
}}
className="flex w-full items-center gap-2 rounded-control px-2 py-1.5 text-left transition-colors hover:bg-hover"
>
<span
aria-hidden
className={`size-1.5 shrink-0 rounded-full ${
TONE_DOT_CLASS[entry.item.tone ?? 'default'] ?? TONE_DOT_CLASS.default
}`}
/>
{entry.item.icon ? (
<PluginIconValue icon={entry.item.icon} size={13} className="shrink-0 text-ink-2" />
) : null}
<span className="min-w-0 flex-1 truncate text-[12px] leading-4 text-ink">
{entry.item.text}
</span>
</button>
) : (
<div className="flex w-full items-center gap-2 rounded-control px-2 py-1.5">
<span
aria-hidden
className={`size-1.5 shrink-0 rounded-full ${
TONE_DOT_CLASS[entry.item.tone ?? 'default'] ?? TONE_DOT_CLASS.default
}`}
/>
{entry.item.icon ? (
<PluginIconValue icon={entry.item.icon} size={13} className="shrink-0 text-ink-2" />
) : null}
<span className="min-w-0 flex-1 truncate text-[12px] leading-4 text-ink">
{entry.item.text}
</span>
</div>
)
)} )}
</div> </div>
) )
+1 -1
View File
@@ -181,7 +181,7 @@ api.ui.onSettingsAction((key) => {}) // button 条目点击回调(key = 按钮
| 侧栏入口(B1) | manifest icon/name/order;点进插件整页视图 | ✅ | | 侧栏入口(B1) | manifest icon/name/order;点进插件整页视图 | ✅ |
| 插件整页 Main | 内置组件级 / 外置 renderer 组件 / 外置 iframe;聊天列 display:none 保活 | ✅ | | 插件整页 Main | 内置组件级 / 外置 renderer 组件 / 外置 iframe;聊天列 display:none 保活 | ✅ |
| 面板 tab(B6) | `panel: true`;SidePanel 零改动承载(内置组件 / 外置 renderer 组件 / iframe) | ✅ | | 面板 tab(B6) | `panel: true`;SidePanel 零改动承载(内置组件 / 外置 renderer 组件 / iframe) | ✅ |
| 状态胶囊(B2+B3) | `setStatusItem` 数据驱动,标题栏**悬浮胶囊**统一承载(ZCode 复刻):最小化药丸展示最新一条消息(宿主记录 updatedAt),点击展开面板——每插件一个专属区域、区域可收起/展开(记忆跨启动)、固定高度溢出滚动、行尾彩色数值与行内深链 | ✅ | | 状态胶囊(B2+B3) | `setStatusItem` 数据驱动,**悬浮胶囊**挂 main 区右上角(ZCode 复刻):最小化药丸展示最新一条消息(宿主记录 updatedAt),展开面板头即插件名、`detail` 分组为手风琴(默认收起显示 标题+trailing 徽标,点击展开行)、固定高度滚动、行尾彩色数值与行内深链 | ✅ |
| 深链 | `openView(view, params)`(main 侧 IPC 广播 / 页侧桥直通) | ✅ | | 深链 | `openView(view, params)`(main 侧 IPC 广播 / 页侧桥直通) | ✅ |
| 工具栏按钮(B5) | `registerToolbarButton` 数据已通,Composer 工具行渲染 | ⏳ 下版本 | | 工具栏按钮(B5) | `registerToolbarButton` 数据已通,Composer 工具行渲染 | ⏳ 下版本 |
| 聊天流富组件(B4) | ```类型``` 围栏块 → 宿主元组件(data-table/stat-grid/step-list/kv-card) | ⏳ 下版本 | | 聊天流富组件(B4) | ```类型``` 围栏块 → 宿主元组件(data-table/stat-grid/step-list/kv-card) | ⏳ 下版本 |
+3 -3
View File
@@ -4,10 +4,10 @@
## 1. 状态胶囊(悬浮胶囊:最小化药丸 ⇄ 展开面板) ## 1. 状态胶囊(悬浮胶囊:最小化药丸 ⇄ 展开面板)
所有插件**共用一个悬浮胶囊**(标题栏右侧,ZCode 复刻)。插件照常 `setStatusItem`,宿主记录每次写入时间,两种形态: 所有插件**共用一个悬浮胶囊**,悬浮于 main 区域右上角(侧面板打开时自动左移让位;ZCode 复刻)。插件照常 `setStatusItem`,宿主记录每次写入时间,两种形态:
- **最小化药丸**:展示「最新的消息」(全部插件中 `updatedAt` 最新的一条:tone 点 + 插件图标 + 文本;多于一条时带总数角标),点击展开面板; - **最小化药丸**:展示「最新的消息」(全部插件中 `updatedAt` 最新的一条:tone 点 + 插件图标 + 文本;多于一条时带总数角标),点击展开面板;
- **展开面板**:每个插件一个**专属区域**——区域头(插件图标 + 名称 + 条目数)可收起/展开(记忆跨启动),区域内是该插件的条目与详情行;面板高度固定,内容太多向下滚动;点击面板外部 / Esc 收回药丸。 - **展开面板**:面板头就是插件名(第一个区域的右侧是收回药丸的 ⤡);多个插件时各占一个区域、区域头可收起/展开(记忆跨启动)。区域内 `detail` 的**分组是手风琴**——默认收起只显示「分组标题 + 行尾徽标」,点击展开 rows(ZCode 式「计划 ›」);无标题分组直接平铺。面板高度固定,内容太多向下滚动;点击面板外部 / Esc 收回药丸。
```js ```js
// ① 纯展示(只出现在药丸/区域行) // ① 纯展示(只出现在药丸/区域行)
@@ -21,7 +21,7 @@ api.ui.setStatusItem('conn', {
openView: { view: 'my-plugin' } // 点击跳到插件整页视图 openView: { view: 'my-plugin' } // 点击跳到插件整页视图
}) })
// ③ 携详情(声明 detail 时区域行下展开分组明细) // ③ 携详情(分组 = 手风琴:收起显示 标题+trailing 徽标,点击展开 rows)
api.ui.setStatusItem('tasks', { api.ui.setStatusItem('tasks', {
text: '任务 2/5', text: '任务 2/5',
tone: 'accent', tone: 'accent',
+6 -2
View File
@@ -28,10 +28,14 @@ export interface StatusDetailRow {
openView?: OpenViewAction openView?: OpenViewAction
} }
/** 详情弹层分组 */ /** 详情弹层分组(悬浮面板中为手风琴:默认收起只显示标题行,点击展开 rows) */
export interface StatusDetailSection { export interface StatusDetailSection {
/** 分组标题(如「Git 工具」「计划」);缺省无标题 */ /** 分组标题(如「计划」「进程」);缺省无标题(rows 直接平铺、不可收起) */
title?: string title?: string
/** 标题行尾徽标(如「4/4」「35分7秒」;收起态也可见——ZCode 式摘要) */
trailing?: string
/** 行尾徽标色调,缺省 default */
trailingTone?: StatusTone
rows: StatusDetailRow[] rows: StatusDetailRow[]
} }