fix(plugins): 悬浮胶囊三点修正——位置/面板头/分组手风琴
This commit is contained in:
@@ -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
@@ -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) | ⏳ 下版本 |
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user