Explorar el Código

Refine plugin inventory card details

ZiyaZhang hace 1 mes
padre
commit
2614a2df93

+ 6 - 4
apps/web/tests/snapshots/settings-chrome/plugins.expected.md

@@ -1,4 +1,6 @@
-- listitem "ui-settings, 存活, 已启用":
-  - strong: ui-settings
-  - img "存活"
-  - text: 已启用
+- listitem:
+  - button "ui-settings, 已挂载, 已启用":
+    - strong: ui-settings
+    - img "已挂载"
+    - text: 已启用
+    - img

+ 2 - 2
packages/client/ui-plugins/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/client/ui-plugins/README.md
-README.md: c3236935b2a568c6996fcac8469f061a813cd95c
-README.zh.md: 77810bccf95a55761518f89909fddd99818f686a
+README.md: bb487d5e2cbd34406d83867997ede4d70b190d70
+README.zh.md: 48a11911509ea260aa9727d55c0b4df6efbfb1c9

+ 1 - 1
packages/client/ui-plugins/README.md

@@ -4,7 +4,7 @@ English | [中文](README.zh.md)
 
 Read-only Plugins section for Web Settings. The browser plugin registers one localized `settings.section` contribution with id `plugin-inventory`, after Models, and lets the Settings shell supply its ordinary fallback icon. It performs no Remote read during plugin activation; mounting the section lazily calls `ctx.remote.pluginInventory.list()` through [`api-remotes`](../../api/remotes/README.md).
 
-The page renders a searchable two-column catalog of compact cards. Each card uses the local Loader id as its title, a colored root-Fiber status dot, and a small effective-enablement tag. Loading, empty, no-match, and generic failure states stay local to the mounted component, and a failed read can be retried without exposing transport details. The registration uses `ctx.slots.inject()`, so it follows late Settings declaration, redeclaration, locale changes, and teardown without owning another global store.
+The page renders a searchable two-column catalog of compact disclosure cards. Each collapsed card uses the local Loader id as its title, a colored root-Fiber status dot, and a small effective-enablement tag. Expanding one card reveals its Loader-tree entry value without a redundant field label, followed by the effective configuration and Cordis status. Loading, empty, no-match, and generic failure states stay local to the mounted component, and a failed read can be retried without exposing transport details. The registration uses `ctx.slots.inject()`, so it follows late Settings declaration, redeclaration, locale changes, and teardown without owning another global store.
 
 ## Model Experience
 

+ 1 - 1
packages/client/ui-plugins/README.zh.md

@@ -4,7 +4,7 @@
 
 Web 设置中的只读“插件”分区。浏览器插件在“模型”之后注册一个 id 为 `plugin-inventory` 的本地化 `settings.section` 贡献,并由 Settings shell 提供常规的回退图标。插件激活期间不会读取 Remote;挂载该分区时,组件才通过 [`api-remotes`](../../api/remotes/README.md) 懒调用 `ctx.remote.pluginInventory.list()`。
 
-页面以可搜索的双列紧凑卡片展示清单。每张卡片使用 Loader 本地 id 作为标题,以彩色圆点表示根 Fiber 状态,以小标签表示有效启停状态。加载、空结果、无匹配结果与通用失败状态只属于已挂载组件;读取失败后可以重试,且不会暴露传输细节。注册使用 `ctx.slots.inject()`,因此能跟随 Settings 的延迟声明、重新声明、本地化变化与 teardown,而不拥有另一份全局 store。
+页面以可搜索的双列紧凑折叠卡片展示清单。每张收起的卡片使用 Loader 本地 id 作为标题,以彩色圆点表示根 Fiber 状态,以小标签表示有效启停状态。展开卡片后会直接展示 Loader 树条目值,不附加重复的字段标题,并列出有效配置状态与 Cordis 状态。加载、空结果、无匹配结果与通用失败状态只属于已挂载组件;读取失败后可以重试,且不会暴露传输细节。注册使用 `ctx.slots.inject()`,因此能跟随 Settings 的延迟声明、重新声明、本地化变化与 teardown,而不拥有另一份全局 store。
 
 ## 模型体验
 

+ 74 - 0
packages/client/ui-plugins/src/client/PluginSettingsSection.module.css

@@ -124,11 +124,18 @@
   background: var(--dsw-alias-bg-layer-3);
 }
 
+.card[data-open='true'] {
+  border-color: var(--dsw-alias-border-l1);
+  box-shadow: var(--dsw-shadow-lv1);
+}
+
 .cardContent {
+  box-sizing: border-box;
   display: flex;
   align-items: center;
   justify-content: space-between;
   gap: 12px;
+  width: 100%;
   min-height: 52px;
   border: 0;
   padding: 12px 14px;
@@ -136,6 +143,17 @@
   color: inherit;
   font: inherit;
   text-align: left;
+  cursor: pointer;
+}
+
+.cardContent:hover,
+.card[data-open='true'] > .cardContent {
+  background: var(--dsw-alias-interactive-bg-hover);
+}
+
+.cardContent:focus-visible {
+  outline: 2px solid var(--dsw-alias-state-business-primary);
+  outline-offset: -2px;
 }
 
 .cardTitle {
@@ -195,6 +213,56 @@
   color: var(--dsw-alias-state-success-primary);
 }
 
+.chevron {
+  flex: none;
+  color: var(--dsw-alias-label-tertiary);
+}
+
+.card[data-open='true'] .chevron {
+  transform: rotate(180deg);
+}
+
+.cardDetails {
+  border-top: 1px solid var(--dsw-alias-border-l2);
+  padding: 10px 14px 12px;
+  background: var(--dsw-alias-bg-module-platform);
+}
+
+.entryValue {
+  display: block;
+  overflow-wrap: anywhere;
+  color: var(--dsw-alias-label-primary);
+  font-family: var(--ds-font-family-code);
+  font-size: 12px;
+  line-height: 18px;
+}
+
+.details {
+  display: grid;
+  grid-template-columns: 76px minmax(0, 1fr);
+  gap: 6px 10px;
+  margin: 8px 0 0;
+}
+
+.details div {
+  display: contents;
+}
+
+.details dt {
+  color: var(--dsw-alias-label-tertiary);
+  font-size: 11px;
+  line-height: 17px;
+}
+
+.details dd {
+  min-width: 0;
+  margin: 0;
+  overflow-wrap: anywhere;
+  color: var(--dsw-alias-label-secondary);
+  font-size: 12px;
+  line-height: 17px;
+}
+
 .visuallyHidden {
   position: absolute;
   width: 1px;
@@ -205,6 +273,12 @@
   white-space: nowrap;
 }
 
+@media (prefers-reduced-motion: no-preference) {
+  .chevron {
+    transition: transform 140ms var(--ds-ease-in-out);
+  }
+}
+
 @media (max-width: 680px) {
   .cards {
     grid-template-columns: minmax(0, 1fr);

+ 41 - 4
packages/client/ui-plugins/src/client/PluginSettingsSection.tsx

@@ -1,6 +1,9 @@
 import { useEffect, useId, useMemo, useState, type ReactNode } from 'react'
 import type { ClientRemote } from '@deepseek-ai/dsh-api-remotes/client'
-import { IconSearchOutline16 } from '@deepseek-ai/dsh-client-ui-primitives'
+import {
+  IconChevronDownOutline14,
+  IconSearchOutline16,
+} from '@deepseek-ai/dsh-client-ui-primitives'
 import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
 import type { PluginsKey } from './locales.ts'
 import css from './PluginSettingsSection.module.css'
@@ -54,6 +57,7 @@ export function PluginSettingsSection({ list, t }: PluginSettingsSectionProps):
   const titleId = useId()
   const [request, setRequest] = useState(0)
   const [query, setQuery] = useState('')
+  const [expanded, setExpanded] = useState<PluginInventoryEntry['entryId'] | null>(null)
   const [state, setState] = useState<ViewState>({ status: 'loading' })
 
   useEffect(() => {
@@ -73,6 +77,12 @@ export function PluginSettingsSection({ list, t }: PluginSettingsSectionProps):
     [normalizedQuery, state],
   )
 
+  useEffect(() => {
+    if (expanded !== null && !filteredEntries.some(entry => entry.entryId === expanded)) {
+      setExpanded(null)
+    }
+  }, [expanded, filteredEntries])
+
   const retry = (): void => {
     setState({ status: 'loading' })
     setRequest(value => value + 1)
@@ -115,14 +125,25 @@ export function PluginSettingsSection({ list, t }: PluginSettingsSectionProps):
             <ul className={css.cards}>
               {filteredEntries.map((entry) => {
                 const status = phaseLabel(entry.fiberPhase, t)
+                const open = expanded === entry.entryId
+                const detailId = `${titleId}-details-${encodeURIComponent(entry.entryId)}`
                 return (
                   <li
                     className={css.card}
                     key={entry.entryId}
                     data-plugin-entry={entry.entryId}
-                    aria-label={`${entry.displayId}, ${status}, ${t(entry.enabled ? 'enabledTag' : 'disabledTag')}`}
+                    data-open={open ? 'true' : undefined}
                   >
-                    <div className={css.cardContent}>
+                    <button
+                      className={css.cardContent}
+                      type="button"
+                      aria-expanded={open}
+                      aria-controls={detailId}
+                      aria-label={`${entry.displayId}, ${status}, ${t(entry.enabled ? 'enabledTag' : 'disabledTag')}`}
+                      onClick={() => {
+                        setExpanded(current => current === entry.entryId ? null : entry.entryId)
+                      }}
+                    >
                       <strong className={css.cardTitle}>{entry.displayId}</strong>
                       <span className={css.cardTrailing}>
                         <span
@@ -135,8 +156,24 @@ export function PluginSettingsSection({ list, t }: PluginSettingsSectionProps):
                         <span className={css.configTag} data-enabled={entry.enabled ? 'true' : 'false'}>
                           {t(entry.enabled ? 'enabledTag' : 'disabledTag')}
                         </span>
+                        <IconChevronDownOutline14 className={css.chevron} size={12} aria-hidden="true" />
                       </span>
-                    </div>
+                    </button>
+                    {open ? (
+                      <div className={css.cardDetails} id={detailId}>
+                        <code className={css.entryValue} data-loader-entry>{entry.entryId}</code>
+                        <dl className={css.details}>
+                          <div>
+                            <dt>{t('configuration')}</dt>
+                            <dd>{t(entry.enabled ? 'enabledTag' : 'disabledTag')}</dd>
+                          </div>
+                          <div>
+                            <dt>{t('cordis')}</dt>
+                            <dd>{status}</dd>
+                          </div>
+                        </dl>
+                      </div>
+                    ) : null}
                   </li>
                 )
               })}

+ 11 - 7
packages/client/ui-plugins/src/client/locales.ts

@@ -13,11 +13,13 @@ export const zh = {
   emptySearch: '没有匹配的插件。',
   enabledTag: '已启用',
   disabledTag: '已停用',
-  unobserved: '无根 Fiber',
+  configuration: '配置状态',
+  cordis: 'Cordis 状态',
+  unobserved: '未挂载',
   pending: '等待依赖',
   loadingPhase: '加载中',
-  active: '存活',
-  failed: '失败',
+  active: '已挂载',
+  failed: '挂载失败',
   unloading: '卸载中',
 } satisfies Record<string, string>
 
@@ -37,10 +39,12 @@ export const en = {
   emptySearch: 'No matching plugins.',
   enabledTag: 'Enabled',
   disabledTag: 'Disabled',
-  unobserved: 'No root Fiber',
-  pending: 'Pending',
+  configuration: 'Configuration',
+  cordis: 'Cordis status',
+  unobserved: 'Not mounted',
+  pending: 'Waiting for dependencies',
   loadingPhase: 'Loading',
-  active: 'Active',
-  failed: 'Failed',
+  active: 'Mounted',
+  failed: 'Mount failed',
   unloading: 'Unloading',
 } satisfies Record<PluginsKey, string>

+ 17 - 2
packages/client/ui-plugins/tests/components.spec.tsx

@@ -50,10 +50,25 @@ describe('PluginSettingsSection', () => {
     expect(screen.getAllByRole('listitem')).toHaveLength(6)
     expect(screen.getAllByText(en.enabledTag)).toHaveLength(5)
     expect(screen.getByText(en.disabledTag)).toBeTruthy()
-    for (const value of ['Active', 'Pending', 'Loading', 'Failed', 'Unloading', 'No root Fiber']) {
+    for (const value of [
+      'Mounted',
+      'Waiting for dependencies',
+      'Loading',
+      'Mount failed',
+      'Unloading',
+      'Not mounted',
+    ]) {
       expect(screen.getByRole('img', { name: value })).toBeTruthy()
     }
-    expect(screen.getByRole('listitem', { name: 'active-name, Active, Enabled' })).toBeTruthy()
+    const active = screen.getByRole('button', { name: 'active-name, Mounted, Enabled' })
+    expect(active.getAttribute('aria-expanded')).toBe('false')
+    fireEvent.click(active)
+    expect(active.getAttribute('aria-expanded')).toBe('true')
+    expect(view.container.querySelector('[data-loader-entry]')?.textContent).toBe('active')
+    expect(screen.getByText(en.configuration)).toBeTruthy()
+    expect(screen.getByText(en.cordis)).toBeTruthy()
+    fireEvent.click(active)
+    expect(view.container.querySelector('[data-loader-entry]')).toBeNull()
   })
 
   it('filters by local id or Loader entry id', async () => {