refactor(plugin-inventory): settings-row style group headers

Both scope groups drop their boxed chrome for the General-settings row
idiom: a title row (session-plugins title with the right-aligned preset
selector pill; global-plugins title), a grey subtitle line carrying the
count, a hairline divider between groups, and the cards grid below.
Counts ride the subtitle as text while the data attributes keep the raw
numbers for tests.
This commit is contained in:
Yichen Jiang
2026-08-29 17:13:45 +08:00
parent 5eb7195f9d
commit 0f06f973c4
4 changed files with 94 additions and 87 deletions
@@ -225,21 +225,29 @@
display: flex;
flex-direction: column;
gap: 10px;
border: 1px solid var(--dsw-alias-border-l2);
border-radius: 12px;
padding: 10px 12px 12px;
}
.groupHeader,
.groupToggle {
.groupTitleRow {
display: flex;
align-items: center;
gap: 8px;
min-height: 28px;
min-height: 32px;
}
.group + .group {
border-top: 1px solid var(--dsw-alias-border-l2);
padding-top: 14px;
}
.headerEnd {
margin-left: auto;
}
.groupToggle {
width: 100%;
display: flex;
flex: none;
align-items: center;
gap: 8px;
border: 0;
padding: 0;
background: transparent;
@@ -263,22 +271,17 @@
}
.groupTitle {
font-size: 13px;
line-height: 20px;
font-weight: 600;
font-size: 14px;
line-height: 22px;
font-weight: 500;
color: var(--dsw-alias-label-primary);
}
.groupSubtitle {
overflow: hidden;
color: var(--dsw-alias-label-tertiary);
font-size: 12px;
line-height: 18px;
text-overflow: ellipsis;
white-space: nowrap;
}
.groupCount {
margin-left: auto;
.groupSub {
display: flex;
flex-wrap: wrap;
gap: 4px 8px;
margin: -6px 0 0 20px;
color: var(--dsw-alias-label-tertiary);
font-size: 12px;
line-height: 18px;
@@ -286,10 +289,7 @@
}
.failedCount {
flex: none;
color: var(--dsw-alias-state-error-primary);
font-size: 12px;
line-height: 18px;
}
/* The preset switcher trigger mirrors the General-settings selector pill. */
@@ -331,12 +331,6 @@
text-overflow: ellipsis;
}
.iconToggle {
flex: none;
width: auto;
min-height: 0;
padding: 4px;
}
.groupBody {
display: flex;
@@ -365,46 +365,51 @@ export function PluginInventorySettingsTab({ list, t }: PluginInventorySettingsT
{selected !== undefined ? (
<section className={css.group} data-plugin-scope="preset" data-preset-id={selected.id}>
<div className={css.groupHeader}>
<div className={css.groupTitleRow}>
<button
type="button"
className={`${css.groupToggle} ${css.iconToggle}`}
className={css.groupToggle}
aria-expanded={presetEffectiveOpen}
aria-controls={`${sectionId}-preset`}
aria-label={t('presetSubtitle')}
onClick={() => { setPresetOpen(!presetEffectiveOpen) }}
>
<IconChevronDownOutline14 className={css.chevron} size={12} aria-hidden="true" />
<span className={css.groupTitle}>{t('presetTitle')}</span>
</button>
<Menu
open={switcherOpen}
onClose={() => { setSwitcherOpen(false) }}
items={presets.map(preset => ({ id: preset.id, label: presetLabel(preset, t) }))}
selectedId={selected.id}
onSelect={(id) => {
setSwitcherOpen(false)
setChosenPreset(id)
}}
portal
anchor={(
<button
type="button"
className={css.switcher}
aria-haspopup="menu"
aria-expanded={switcherOpen}
aria-label={t('switcherLabel')}
onClick={() => { setSwitcherOpen(value => !value) }}
>
<span className={css.switcherLabel}>{presetLabel(selected, t)}</span>
<IconChevronDownOutline14 className={css.chevron} size={12} aria-hidden="true" />
</button>
)}
/>
<span className={css.groupSubtitle}>{t('presetSubtitle')}</span>
<span className={css.groupCount} data-preset-plugin-count={selectedRows.length}>
{selectedRows.length}
</span>
<div className={css.headerEnd}>
<Menu
open={switcherOpen}
onClose={() => { setSwitcherOpen(false) }}
items={presets.map(preset => ({ id: preset.id, label: presetLabel(preset, t) }))}
selectedId={selected.id}
onSelect={(id) => {
setSwitcherOpen(false)
setChosenPreset(id)
}}
align="end"
portal
anchor={(
<button
type="button"
className={css.switcher}
aria-haspopup="menu"
aria-expanded={switcherOpen}
aria-label={t('switcherLabel')}
onClick={() => { setSwitcherOpen(value => !value) }}
>
<span className={css.switcherLabel}>{presetLabel(selected, t)}</span>
<IconChevronDownOutline14 className={css.chevron} size={12} aria-hidden="true" />
</button>
)}
/>
</div>
</div>
<p className={css.groupSub}>
{t('presetSubtitle')}
<span data-preset-plugin-count={selectedRows.length}>
{` · ${String(selectedRows.length)} ${t('countUnit')}`}
</span>
</p>
{presetEffectiveOpen ? (
<div id={`${sectionId}-preset`} className={css.groupBody}>
{selected.broken !== undefined ? (
@@ -437,21 +442,25 @@ export function PluginInventorySettingsTab({ list, t }: PluginInventorySettingsT
{entries.length > 0 ? (
<section className={css.group} data-plugin-scope="global">
<button
type="button"
className={css.groupToggle}
aria-expanded={globalEffectiveOpen}
aria-controls={`${sectionId}-global`}
onClick={() => { setGlobalOpen(!globalEffectiveOpen) }}
>
<IconChevronDownOutline14 className={css.chevron} size={12} aria-hidden="true" />
<span className={css.groupTitle}>{t('globalTitle')}</span>
<span className={css.groupSubtitle}>{t('globalSubtitle')}</span>
<span className={css.groupCount} data-plugin-count={globalCount}>{globalCount}</span>
<div className={css.groupTitleRow}>
<button
type="button"
className={css.groupToggle}
aria-expanded={globalEffectiveOpen}
aria-controls={`${sectionId}-global`}
onClick={() => { setGlobalOpen(!globalEffectiveOpen) }}
>
<IconChevronDownOutline14 className={css.chevron} size={12} aria-hidden="true" />
<span className={css.groupTitle}>{t('globalTitle')}</span>
</button>
</div>
<p className={css.groupSub}>
{t('globalSubtitle')}
<span data-plugin-count={globalCount}>{` · ${String(globalCount)} ${t('countUnit')}`}</span>
{filteredFailed.length > 0 ? (
<span className={css.failedCount}>{filteredFailed.length} {t('failedCountLabel')}</span>
) : null}
</button>
</p>
{globalEffectiveOpen && globalCount > 0 ? (
<ul className={css.cards} id={`${sectionId}-global`}>
{filteredFailed.map(entry => globalRowCard(entry))}
@@ -9,11 +9,13 @@ export const zh = {
search: '搜索插件',
empty: '暂无插件。',
emptySearch: '没有匹配的插件。',
presetSubtitle: '会话使用的插件',
presetTitle: '会话插件',
presetSubtitle: '由 Agent 预设按会话组成',
countUnit: '个',
switcherLabel: '选择要查看的 Agent 预设',
presetOptionDefault: '{name}(默认)',
presetOptionBroken: '{name}(加载失败)',
globalTitle: '全局',
globalTitle: '全局插件',
globalSubtitle: '系统与所有会话共用',
presetProvidedDetail: '全局已停用,由 Agent 预设按会话提供',
enabledIn: '启用于',
@@ -50,11 +52,13 @@ export const en = {
search: 'Search plugins',
empty: 'No plugins are available.',
emptySearch: 'No matching plugins.',
presetSubtitle: 'Plugins your sessions run',
presetTitle: 'Session plugins',
presetSubtitle: 'Composed per session by agent presets',
countUnit: 'plugins',
switcherLabel: 'Choose the agent preset to inspect',
presetOptionDefault: '{name} (default)',
presetOptionBroken: '{name} (failed to load)',
globalTitle: 'Global',
globalTitle: 'Global plugins',
globalSubtitle: 'Shared by the system and every session',
presetProvidedDetail: 'Disabled globally; agent presets provide it per session',
enabledIn: 'Enabled in',
@@ -92,7 +92,7 @@ describe('PluginInventorySettingsTab', () => {
fireEvent.keyDown(document, { key: 'Escape' })
expect(screen.queryAllByRole('menuitem')).toHaveLength(0)
expect(screen.getByText(en.presetSubtitle)).toBeTruthy()
expect(view.container.querySelector('[data-preset-plugin-count]')?.textContent).toBe('6')
expect(view.container.querySelector('[data-preset-plugin-count]')?.getAttribute('data-preset-plugin-count')).toBe('6')
// Only the preset group lists rows while the global plane stays collapsed.
expect(screen.getAllByRole('listitem')).toHaveLength(6)
@@ -105,7 +105,7 @@ describe('PluginInventorySettingsTab', () => {
expect(screen.queryByRole('img', { name: 'Not running' })).toBeNull()
expect(globalToggle().getAttribute('aria-expanded')).toBe('false')
expect(view.container.querySelector('[data-plugin-count]')?.textContent).toBe('7')
expect(view.container.querySelector('[data-plugin-count]')?.getAttribute('data-plugin-count')).toBe('7')
expect(screen.getByText(`1 ${en.failedCountLabel}`)).toBeTruthy()
// A preset row expands into its provenance facts.
@@ -172,25 +172,25 @@ describe('PluginInventorySettingsTab', () => {
}
pickPreset('ptc')
expect(view.container.querySelector('[data-preset-plugin-count]')?.textContent).toBe('3')
expect(view.container.querySelector('[data-preset-plugin-count]')?.getAttribute('data-preset-plugin-count')).toBe('3')
fireEvent.click(screen.getAllByRole('button', { name: 'tool-bash, Enabled' })[0]!)
// An unnamed preset labels provenance by its id.
expect(screen.getByText(en.fromPreset).nextElementSibling?.textContent).toBe('ptc')
pickPreset('坏预设 (failed to load)')
expect(screen.getByRole('alert').textContent).toBe('the composition file is missing')
expect(view.container.querySelector('[data-preset-plugin-count]')?.textContent).toBe('0')
expect(view.container.querySelector('[data-preset-plugin-count]')?.getAttribute('data-preset-plugin-count')).toBe('0')
})
it('collapses the preset group until a search forces it open', async () => {
const view = await renderReady()
const toggle = screen.getByRole('button', { name: en.presetSubtitle })
const toggle = screen.getByRole('button', { name: en.presetTitle })
expect(toggle.getAttribute('aria-expanded')).toBe('true')
fireEvent.click(toggle)
expect(toggle.getAttribute('aria-expanded')).toBe('false')
// The header keeps its count while the rows are folded away.
expect(view.container.querySelector('[data-preset-plugin-count]')?.textContent).toBe('6')
expect(view.container.querySelector('[data-preset-plugin-count]')?.getAttribute('data-preset-plugin-count')).toBe('6')
expect(view.container.querySelectorAll('[data-plugin-scope="preset"] li')).toHaveLength(0)
fireEvent.change(screen.getByRole('searchbox', { name: en.search }), { target: { value: 'pwsh' } })
@@ -221,8 +221,8 @@ describe('PluginInventorySettingsTab', () => {
fireEvent.change(search, { target: { value: 'tool-bash' } })
// Searching forces the collapsed global plane and drawer open.
expect(view.container.querySelector('[data-preset-plugin-count]')?.textContent).toBe('1')
expect(view.container.querySelector('[data-plugin-count]')?.textContent).toBe('1')
expect(view.container.querySelector('[data-preset-plugin-count]')?.getAttribute('data-preset-plugin-count')).toBe('1')
expect(view.container.querySelector('[data-plugin-count]')?.getAttribute('data-plugin-count')).toBe('1')
expect(screen.getByText(en.presetEnabledTag)).toBeTruthy()
expect(screen.queryByText(`1 ${en.failedCountLabel}`)).toBeNull()
const hint = screen.getByText((text: string) => text.startsWith('2 more matches'))
@@ -232,13 +232,13 @@ describe('PluginInventorySettingsTab', () => {
// A match visible only in another preset keeps the pointer without rows.
fireEvent.change(search, { target: { value: 'crashy' } })
expect(view.container.querySelector('[data-preset-plugin-count]')?.textContent).toBe('0')
expect(view.container.querySelector('[data-preset-plugin-count]')?.getAttribute('data-preset-plugin-count')).toBe('0')
expect(screen.getByText((text: string) => text.startsWith('1 more matches'))).toBeTruthy()
expect(screen.queryByText(en.emptySearch)).toBeNull()
// A match on a Loader entry id only reaches the global plane.
fireEvent.change(search, { target: { value: '8a1b2c3d' } })
expect(view.container.querySelector('[data-plugin-count]')?.textContent).toBe('1')
expect(view.container.querySelector('[data-plugin-count]')?.getAttribute('data-plugin-count')).toBe('1')
expect(screen.queryByText((text: string) => text.includes('more matches'))).toBeNull()
fireEvent.change(search, { target: { value: 'not-a-plugin' } })