feat(settings): 开关改为可点击卡片 — 高亮即启用
- 清单项(侧边栏内容/文件预览)、嵌套相关设置与「新会话默认打开」 全部从复选框行改为 <button aria-pressed> 卡片:整卡点击切换, 视觉状态即状态(启用 = 品牌描边 + 交互选中填充 + 勾选徽标; 禁用 = 中性 hairline + 文字降级),悬停仅在禁用卡上显示 - 嵌套相关设置渲染为更小的缩进卡片(仍仅父级启用时显示); 宽度输入保持原生设置行(数值非开关) - 设置导航齿轮为宿主 shell navIcon 硬编码(无插件注入点), 按用户选择不改宿主、保持现状 - 新增 settingsGeneralTitle 文案;规格测试改为断言 aria-pressed - 文档(README/AGENTS.md/设计文档偏差记录)同步
This commit is contained in:
@@ -370,7 +370,7 @@ interface BetterSidebarService {
|
||||
}
|
||||
```
|
||||
|
||||
> **声明式设置(v0.5.0+)**:每个注册的 tab/viewer 自动出现在 DSH 设置页「侧边卡片」分区的清单里(图标 + 标题 + 类型 id + 开关,viewer 行还显示扩展名),开关持久化到 `SidebarPrefs.tabsEnabled / viewersEnabled`(开放 map,缺省 = 启用)。关闭语义:tab 从 `+` 菜单消失、`openTab` 拒绝新开、子代理自动展开 / agent 终端自动补 tab 等派生流程停止,**已打开的 tab 保留**;viewer 被 `matchFileViewer` 跳过,文件落到下一个匹配。`settings.toggles` 声明的嵌套开关(如子代理的 `autoOpenSubagent`)仅父级启用时显示;**key 必须是宿主 PrefsSchema 的字段**(内置键:`autoOpenSubagent` / `agentTerminalTools`),外部插件的自定义键会被 settings seam 丢弃。
|
||||
> **声明式设置(v0.5.0+)**:每个注册的 tab/viewer 自动出现在 DSH 设置页「侧边卡片」分区的清单里(图标 + 标题 + 类型 id + **可点击卡片**,高亮 = 启用,viewer 卡片还显示扩展名),开关持久化到 `SidebarPrefs.tabsEnabled / viewersEnabled`(开放 map,缺省 = 启用)。关闭语义:tab 从 `+` 菜单消失、`openTab` 拒绝新开、子代理自动展开 / agent 终端自动补 tab 等派生流程停止,**已打开的 tab 保留**;viewer 被 `matchFileViewer` 跳过,文件落到下一个匹配。`settings.toggles` 声明的嵌套开关(如子代理的 `autoOpenSubagent`)仅父级启用时显示,同为小卡片样式;**key 必须是宿主 PrefsSchema 的字段**(内置键:`autoOpenSubagent` / `agentTerminalTools`),外部插件的自定义键会被 settings seam 丢弃。
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -154,9 +154,9 @@ dsh registry enable dsh-external/dsh-better-sidebar
|
||||
- 设置页新增 **Side card(侧边卡片)** 分区,持久化到 DSH 用户设置文档:
|
||||
- **新会话默认打开**:新建会话是否自动展开侧边卡片(已存在的会话保持各自布局)
|
||||
- **默认宽度占比**:新建会话时侧边卡片占窗口宽度的百分比(20–60,默认 30)
|
||||
- **声明式的功能清单**:分区按注册表实时渲染当前支持的侧边栏内容与文件预览器(内置与外部插件注册的 tab/viewer 一视同仁),每项显示**类型 id + 图标 + 开关**,可直接打开/关闭:
|
||||
- **侧边栏内容**:资源管理器 / 源代码管理 / 子代理 / 终端 / 编辑器 / 差异 等 tab 类型。关闭某类型后:`+` 菜单不再显示、无法新开(已打开的 tab 保留可继续使用,关闭后需重新开启才能再开);子代理的「检测到子代理时自动展开子代理页面」、终端的「为模型注入终端工具」作为**相关设置**嵌套在其行下(父级关闭时隐藏)
|
||||
- **文件预览**:图片 / PDF / Word / Excel / PPT / Markdown / 代码 / 二进制下载 等查看器,行内显示其覆盖的扩展名。关闭某查看器后文件预览落到下一个匹配的查看器(关闭兜底的代码查看器后无法预览的文件显示下载按钮)
|
||||
- **声明式的功能清单**:分区按注册表实时渲染当前支持的侧边栏内容与文件预览器(内置与外部插件注册的 tab/viewer 一视同仁),每项是一张**可点击的卡片**——图标 + 类型 id + 描述,**点击整卡切换开/关,卡片高亮(品牌描边 + 勾选徽标)即代表启用**,未启用呈灰色降级态:
|
||||
- **侧边栏内容**:资源管理器 / 源代码管理 / 子代理 / 终端 / 编辑器 / 差异 等 tab 类型。关闭某类型后:`+` 菜单不再显示、无法新开(已打开的 tab 保留可继续使用,关闭后需重新开启才能再开);子代理的「检测到子代理时自动展开子代理页面」、终端的「为模型注入终端工具」作为**相关设置**以小卡片嵌套在其行下(父级关闭时隐藏)
|
||||
- **文件预览**:图片 / PDF / Word / Excel / PPT / Markdown / 代码 / 二进制下载 等查看器,卡片上显示其覆盖的扩展名。关闭某查看器后文件预览落到下一个匹配的查看器(关闭兜底的代码查看器后无法预览的文件显示下载按钮)
|
||||
- **检测到子代理时自动展开子代理页面**(默认开启):当前会话产生新的子代理时,自动展开侧边栏并打开 Subagent 页面
|
||||
- **为模型注入终端工具**(默认关闭):开启后模型可通过 8 个 `terminal_*` 工具创建并操作侧边栏终端;关闭会注销工具并释放已创建的 agent 终端
|
||||
|
||||
|
||||
@@ -133,3 +133,4 @@ isViewerEnabled(id: string): boolean
|
||||
2. **宿主 `syncToolsGate` 不改**:`agentTerminalTools` 仅随自身开关走,不与 `tabsEnabled['terminal']` 联动(模型终端工具是模型面向能力,与侧边栏 UI 开关解耦)。
|
||||
3. **`renderRow` 统一行渲染**:tab/viewer/嵌套开关共用一行渲染器(`sub` 参数区分缩进),CSS 新增 `.sectionHeading` / `.rowIcon` / `.titleLine` / `.subRow`。
|
||||
4. **清单排序**:tabs 按 `hidden` 置后 + `order` 升序(editor/diff 殿后);viewers 按 priority 降序(code 兜底最后)。
|
||||
5. **开关改为卡片点击(v0.5.0 追加)**:清单项与嵌套相关设置、`openByDefault` 全部从"复选框行"改为**可点击卡片**——`<button aria-pressed>`,整卡即开关,视觉状态即状态(启用 = 品牌描边 + 交互选中填充 + `IconCheckOutline16` 勾选徽标;禁用 = 中性 hairline + 文字降级)。嵌套相关设置渲染为更小的缩进卡片;宽度输入保持原生设置行(它是数值不是开关)。设置导航齿轮图标为宿主 shell 硬编码(`navIcon` 按 section id 映射,无插件注入点),用户选择不改宿主,保持现状。
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
/**
|
||||
* Side card settings section chrome: the native settings-row recipe (figma
|
||||
* row 16/0 padding, hairline separator, 14px/22px title, 12px/17px desc) with
|
||||
* the native checkbox (16px, brand accent) and the Input atom + "%" suffix.
|
||||
* The shell strips the last row's separator via the trailing-child rule.
|
||||
* Side card settings section chrome: the toggle cards are the switch — a
|
||||
* clickable block whose visual state IS the state (enabled = brand border +
|
||||
* tinted fill + check badge; disabled = neutral hairline + dimmed text).
|
||||
* Nested declarative toggles render as smaller indented cards; the width
|
||||
* input stays a native settings row; the section heading groups the three
|
||||
* inventories. The shell strips the last row's separator via the
|
||||
* trailing-child rule.
|
||||
*/
|
||||
|
||||
.section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -15,30 +19,93 @@
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.row {
|
||||
/* One toggle card: whole-card click, `aria-pressed` announces the state. */
|
||||
.card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 16px 0;
|
||||
border-bottom: 1px solid var(--dsw-alias-border-l2);
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
padding: 12px 14px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--dsw-alias-border-l2);
|
||||
background: transparent;
|
||||
font: inherit;
|
||||
color: inherit;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition: background 0.12s ease, border-color 0.12s ease;
|
||||
}
|
||||
|
||||
/* Nested declarative toggle (a tab's related setting): indented child row. */
|
||||
.subRow {
|
||||
display: flex;
|
||||
/* Hover affordance on disabled cards only (enabled cards are already lit). */
|
||||
.card:not(.cardOn):hover {
|
||||
background: var(--dsw-alias-interactive-bg-hover);
|
||||
}
|
||||
|
||||
.card:focus-visible {
|
||||
outline: 2px solid var(--dsw-alias-border-l4);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Enabled = highlighted: brand border + tinted fill. */
|
||||
.cardOn {
|
||||
border-color: var(--dsw-alias-button-primary-fill);
|
||||
background: var(--dsw-alias-interactive-bg-active);
|
||||
}
|
||||
|
||||
/* Nested declarative toggle (a feature's related setting): smaller, indented. */
|
||||
.cardSub {
|
||||
margin-left: 28px;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.cardIcon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 10px 0 10px 28px;
|
||||
border-bottom: 1px solid var(--dsw-alias-border-l2);
|
||||
cursor: pointer;
|
||||
flex: none;
|
||||
color: var(--dsw-alias-label-tertiary);
|
||||
}
|
||||
|
||||
/* Inventory section heading (侧边栏内容 / 文件预览). */
|
||||
.cardOn .cardIcon {
|
||||
color: var(--dsw-alias-button-primary-fill);
|
||||
}
|
||||
|
||||
.cardText {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.cardTitle {
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
color: var(--dsw-alias-label-secondary);
|
||||
}
|
||||
|
||||
.cardOn .cardTitle {
|
||||
color: var(--dsw-alias-label-primary);
|
||||
}
|
||||
|
||||
.cardDesc {
|
||||
font-size: 12px;
|
||||
line-height: 17px;
|
||||
color: var(--dsw-alias-label-tertiary);
|
||||
}
|
||||
|
||||
.cardOn .cardDesc {
|
||||
color: var(--dsw-alias-label-secondary);
|
||||
}
|
||||
|
||||
.cardCheck {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex: none;
|
||||
color: var(--dsw-alias-button-primary-fill);
|
||||
}
|
||||
|
||||
/* Inventory section heading (常规 / 侧边栏内容 / 文件预览). */
|
||||
.sectionHeading {
|
||||
padding: 18px 0 6px;
|
||||
padding: 14px 0 0;
|
||||
font-size: 12px;
|
||||
line-height: 17px;
|
||||
font-weight: 600;
|
||||
@@ -46,20 +113,14 @@
|
||||
color: var(--dsw-alias-label-secondary);
|
||||
}
|
||||
|
||||
/* Icon of one inventory row, inline with the title. */
|
||||
.rowIcon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex: none;
|
||||
color: var(--dsw-alias-label-secondary);
|
||||
}
|
||||
|
||||
/* Title with the icon beside it. */
|
||||
.titleLine {
|
||||
/* The width input stays a native settings row (it is a value, not a switch). */
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid var(--dsw-alias-border-l2);
|
||||
}
|
||||
|
||||
.rowText {
|
||||
@@ -81,21 +142,6 @@
|
||||
color: var(--dsw-alias-label-secondary);
|
||||
}
|
||||
|
||||
/* Native checkbox: 16px, brand accent, no custom chrome (RiskConfirmation recipe). */
|
||||
.toggle {
|
||||
flex: none;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin: 0;
|
||||
accent-color: var(--dsw-alias-button-primary-fill);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.toggle:focus-visible {
|
||||
outline: 2px solid var(--dsw-alias-border-l4);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.control {
|
||||
flex: none;
|
||||
display: flex;
|
||||
@@ -121,7 +167,7 @@
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.row {
|
||||
.card {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,16 +4,18 @@
|
||||
*
|
||||
* The section is DECLARATIVE — it renders the enable/disable inventory from
|
||||
* the sidebar service's registries instead of hardcoding rows:
|
||||
* - 常规: new conversations open the panel by default (native checkbox),
|
||||
* and the default panel width as a percent of the window (number input).
|
||||
* - 侧边栏内容: one row per REGISTERED tab type (built-ins and external
|
||||
* plugins alike) — icon + title + type id + an on/off switch persisted in
|
||||
* `prefs.tabsEnabled[id]`. A tab's `settings.toggles` declaration renders
|
||||
* as nested switches under its row while the tab is enabled (e.g. the
|
||||
* Subagent page's "auto-open when a subagent appears").
|
||||
* - 文件预览: one row per REGISTERED file viewer — icon + title + the
|
||||
* extensions it covers + an on/off switch persisted in
|
||||
* `prefs.viewersEnabled[id]`.
|
||||
* - 常规: new conversations open the panel by default (a toggle card), and
|
||||
* the default panel width as a percent of the window (number input row).
|
||||
* - 侧边栏内容: one CARD per REGISTERED tab type (built-ins and external
|
||||
* plugins alike) — icon + title + type id, clicked to toggle the switch
|
||||
* persisted in `prefs.tabsEnabled[id]`. A card's `settings.toggles`
|
||||
* declaration renders as nested smaller cards under it while the tab is
|
||||
* enabled (e.g. the Subagent page's "auto-open when a subagent appears").
|
||||
* - 文件预览: one CARD per REGISTERED file viewer — icon + title + the
|
||||
* extensions it covers, clicked to toggle `prefs.viewersEnabled[id]`.
|
||||
*
|
||||
* A card's on/off state is its VISUAL STATE: enabled = highlighted (brand
|
||||
* border + tinted fill + check badge), disabled = neutral and dimmed.
|
||||
*
|
||||
* Writes ride the plugin's own fenced settings route (the host calls the
|
||||
* settings seam in-process — the DSH settings RPC domain does not serve
|
||||
@@ -25,7 +27,8 @@
|
||||
* shell.
|
||||
*/
|
||||
import { Fragment, useEffect, useRef, useState, type ReactNode } from 'react'
|
||||
import { Input } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import { IconCheckOutline16, IconPanelLeftOutline16, Input } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import clsx from 'clsx'
|
||||
// Type-only: pulls the settings shell's SlotMap merges ('settings.section').
|
||||
import type {} from '@deepseek-ai/dsh-client-ui-settings/client'
|
||||
import type { PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
@@ -210,49 +213,51 @@ export function SideCardSection({ store, service }: SideCardSectionProps) {
|
||||
void commit({ defaultWidthPercent: clamped }).then(outcome => applyOutcome(previous, outcome))
|
||||
}
|
||||
|
||||
/** One enable/disable row: icon + title + desc + checkbox. */
|
||||
const renderRow = (props: {
|
||||
/**
|
||||
* One toggle CARD: the whole card is the switch — clicking it flips the
|
||||
* feature, and the visual state IS the state (highlighted = enabled, with
|
||||
* a check badge; neutral + dimmed = disabled). Nested related settings
|
||||
* render as smaller indented cards (`sub`).
|
||||
*/
|
||||
const renderCard = (props: {
|
||||
title: string
|
||||
desc: string
|
||||
icon?: ReactNode
|
||||
checked: boolean
|
||||
label: string
|
||||
enabled: boolean
|
||||
onToggle: (next: boolean) => void
|
||||
sub?: boolean
|
||||
}) => (
|
||||
<label className={props.sub === true ? css.subRow : css.row}>
|
||||
<span className={css.rowText}>
|
||||
<span className={css.titleLine}>
|
||||
{props.icon !== null && props.icon !== undefined && <span className={css.rowIcon}>{props.icon}</span>}
|
||||
<span className={css.title}>{props.title}</span>
|
||||
</span>
|
||||
<span className={css.desc}>{props.desc}</span>
|
||||
<button
|
||||
type="button"
|
||||
className={clsx(css.card, props.sub === true && css.cardSub, props.enabled && css.cardOn)}
|
||||
aria-pressed={props.enabled}
|
||||
onClick={() => { props.onToggle(!props.enabled) }}
|
||||
>
|
||||
{props.icon !== null && props.icon !== undefined && (
|
||||
<span className={css.cardIcon}>{props.icon}</span>
|
||||
)}
|
||||
<span className={css.cardText}>
|
||||
<span className={css.cardTitle}>{props.title}</span>
|
||||
<span className={css.cardDesc}>{props.desc}</span>
|
||||
</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
className={css.toggle}
|
||||
checked={props.checked}
|
||||
aria-label={props.label}
|
||||
onChange={event => { props.onToggle(event.currentTarget.checked) }}
|
||||
/>
|
||||
</label>
|
||||
{props.enabled && (
|
||||
<span className={css.cardCheck}>
|
||||
<IconCheckOutline16 size={14} />
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
|
||||
return (
|
||||
<div className={css.section}>
|
||||
<label className={css.row}>
|
||||
<span className={css.rowText}>
|
||||
<span className={css.title}>{t('settingsOpenTitle')}</span>
|
||||
<span className={css.desc}>{t('settingsOpenDesc')}</span>
|
||||
</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
className={css.toggle}
|
||||
checked={prefs.openByDefault}
|
||||
aria-label={t('settingsOpenTitle')}
|
||||
onChange={event => { onToggle(event.currentTarget.checked) }}
|
||||
/>
|
||||
</label>
|
||||
<div className={css.sectionHeading}>{t('settingsGeneralTitle')}</div>
|
||||
{renderCard({
|
||||
title: t('settingsOpenTitle'),
|
||||
desc: t('settingsOpenDesc'),
|
||||
icon: <IconPanelLeftOutline16 size={16} />,
|
||||
enabled: prefs.openByDefault,
|
||||
onToggle,
|
||||
})}
|
||||
<div className={css.row}>
|
||||
<span className={css.rowText}>
|
||||
<span className={css.title}>{t('settingsWidthTitle')}</span>
|
||||
@@ -277,27 +282,25 @@ export function SideCardSection({ store, service }: SideCardSectionProps) {
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 侧边栏内容: one row per registered tab type, icon + type id + switch,
|
||||
plus each tab's declaratively-declared nested toggles (shown only
|
||||
while the tab itself is enabled). */}
|
||||
{/* 侧边栏内容: one card per registered tab type, icon + type id; the
|
||||
card's `settings.toggles` render as nested smaller cards while the
|
||||
tab itself is enabled. */}
|
||||
<div className={css.sectionHeading}>{t('settingsTabsTitle')}</div>
|
||||
{tabs.map(tab => (
|
||||
<Fragment key={tab.id}>
|
||||
{renderRow({
|
||||
{renderCard({
|
||||
title: textOf(tab.title),
|
||||
desc: tab.id,
|
||||
icon: iconOf(tab.icon, 16),
|
||||
checked: prefs.tabsEnabled[tab.id] !== false,
|
||||
label: textOf(tab.title),
|
||||
enabled: prefs.tabsEnabled[tab.id] !== false,
|
||||
onToggle: (next) => { onToggleTab(tab.id, next) },
|
||||
})}
|
||||
{prefs.tabsEnabled[tab.id] !== false && (tab.settings?.toggles ?? []).map(toggle => (
|
||||
<Fragment key={`${tab.id}:${toggle.key}`}>
|
||||
{renderRow({
|
||||
{renderCard({
|
||||
title: textOf(toggle.title),
|
||||
desc: textOf(toggle.desc),
|
||||
checked: prefBool(prefs, toggle.key),
|
||||
label: textOf(toggle.title),
|
||||
enabled: prefBool(prefs, toggle.key),
|
||||
onToggle: (next) => { onToggleSetting(toggle, next) },
|
||||
sub: true,
|
||||
})}
|
||||
@@ -306,16 +309,15 @@ export function SideCardSection({ store, service }: SideCardSectionProps) {
|
||||
</Fragment>
|
||||
))}
|
||||
|
||||
{/* 文件预览: one row per registered file viewer, icon + title + exts + switch. */}
|
||||
{/* 文件预览: one card per registered file viewer, icon + title + exts. */}
|
||||
<div className={css.sectionHeading}>{t('settingsViewersTitle')}</div>
|
||||
{viewers.map(viewer => (
|
||||
<Fragment key={viewer.id}>
|
||||
{renderRow({
|
||||
{renderCard({
|
||||
title: textOf(viewer.title) || viewer.id,
|
||||
desc: viewer.exts.length === 0 ? t('settingsViewerCatchAll') : viewer.exts.join(' · '),
|
||||
icon: iconOf(viewer.icon, 16),
|
||||
checked: prefs.viewersEnabled[viewer.id] !== false,
|
||||
label: textOf(viewer.title) || viewer.id,
|
||||
enabled: prefs.viewersEnabled[viewer.id] !== false,
|
||||
onToggle: (next) => { onToggleViewer(viewer.id, next) },
|
||||
})}
|
||||
</Fragment>
|
||||
|
||||
@@ -123,6 +123,7 @@ const zh = {
|
||||
settingsToolsDesc: '开启后,模型可通过 terminal_create 等 8 个工具创建并操作侧边栏终端(默认关闭)',
|
||||
settingsTabsTitle: '侧边栏内容',
|
||||
settingsViewersTitle: '文件预览',
|
||||
settingsGeneralTitle: '常规',
|
||||
settingsViewerCatchAll: '兜底:任意文件',
|
||||
viewerImage: '图片',
|
||||
viewerPdf: 'PDF',
|
||||
@@ -267,6 +268,7 @@ const en: Record<keyof typeof zh, string> = {
|
||||
settingsToolsDesc: 'When enabled, the model can create and drive sidebar terminals through the 8 terminal_* tools (off by default)',
|
||||
settingsTabsTitle: 'Sidebar content',
|
||||
settingsViewersTitle: 'File viewers',
|
||||
settingsGeneralTitle: 'General',
|
||||
settingsViewerCatchAll: 'Catch-all: any file',
|
||||
viewerImage: 'Image',
|
||||
viewerPdf: 'PDF',
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
/**
|
||||
* Side card settings section render tests: the section is DECLARATIVE —
|
||||
* every row (icon, title, type id, extensions, enable switch) derives from
|
||||
* every card (icon, title, type id, extensions, on/off state) derives from
|
||||
* the sidebar service's tab/viewer registries instead of hardcoded copy.
|
||||
* These specs pin that derivation with a fake store + a small registry:
|
||||
* The toggles are CARDS: the whole card is the switch, the visual state IS
|
||||
* the state (highlighted = enabled), announced via `aria-pressed`. These
|
||||
* specs pin that derivation with a fake store + a small registry:
|
||||
* registered tabs/viewers appear with their declared icon/title/settings,
|
||||
* disabled states render unchecked, and a disabled tab hides its declared
|
||||
* nested toggles.
|
||||
* disabled states render pressed=false, and a disabled tab hides its
|
||||
* declared nested cards.
|
||||
*
|
||||
* Rendered with renderToString (mount effects — the settings RPC sync — do
|
||||
* not run in SSR; the initial store prefs are the render input).
|
||||
@@ -60,58 +62,61 @@ function renderSection(store: SidebarStore, service: BetterSidebarService): stri
|
||||
))
|
||||
}
|
||||
|
||||
/** Count `aria-pressed` occurrences of one value in the rendered HTML. */
|
||||
function pressedCount(html: string, value: string): number {
|
||||
return html.match(new RegExp(`aria-pressed="${value}"`, 'g'))?.length ?? 0
|
||||
}
|
||||
|
||||
describe('SideCardSection declarative inventory', () => {
|
||||
it('renders one row per registered tab: icon + title + type id + switch', () => {
|
||||
it('renders one card per registered tab: icon + title + type id + pressed state', () => {
|
||||
const { store, service } = mount()
|
||||
const html = renderSection(store, service)
|
||||
expect(html).toContain('data-icon="explorer"')
|
||||
expect(html).toContain('>Explorer<')
|
||||
// The type id is the row's desc (the declarative "type" surface).
|
||||
// The type id is the card's desc (the declarative "type" surface).
|
||||
expect(html).toContain('>explorer<')
|
||||
expect(html).toContain('data-icon="subagent"')
|
||||
expect(html).toContain('>Subagents<')
|
||||
// Both tabs are enabled by default: their switches render checked.
|
||||
expect(html.match(/type="checkbox"/g)?.length).toBeGreaterThanOrEqual(4)
|
||||
// Default prefs: openByDefault + both tabs + the nested auto-open + the
|
||||
// image viewer are all enabled → 5 cards pressed, none pressed=false.
|
||||
expect(pressedCount(html, 'true')).toBe(5)
|
||||
expect(pressedCount(html, 'false')).toBe(0)
|
||||
})
|
||||
|
||||
it('renders the declared nested toggle under an enabled tab', () => {
|
||||
it('renders the declared nested card under an enabled tab', () => {
|
||||
const { store, service } = mount()
|
||||
const html = renderSection(store, service)
|
||||
// The subagent tab's declared related setting appears while enabled,
|
||||
// bound to its prefs key (default true → checked).
|
||||
// bound to its prefs key (default true → pressed).
|
||||
expect(html).toContain('Auto-open Subagents')
|
||||
expect(html).toContain('Expand on new subagent')
|
||||
})
|
||||
|
||||
it('hides the nested toggles while the parent tab is disabled', () => {
|
||||
it('hides the nested cards while the parent tab is disabled', () => {
|
||||
const { store, service } = mount()
|
||||
store.setPrefs({ ...store.getPrefs(), tabsEnabled: { subagent: false } })
|
||||
const html = renderSection(store, service)
|
||||
// The parent row stays (so the user can re-enable it) but unchecked.
|
||||
// The parent card stays (so the user can re-enable it) but pressed=false.
|
||||
expect(html).toContain('>Subagents<')
|
||||
expect(html).not.toContain('Auto-open Subagents')
|
||||
expect(pressedCount(html, 'false')).toBe(1)
|
||||
})
|
||||
|
||||
it('renders one row per registered viewer: icon + title + exts + switch', () => {
|
||||
it('renders one card per registered viewer: icon + title + exts', () => {
|
||||
const { store, service } = mount()
|
||||
const html = renderSection(store, service)
|
||||
expect(html).toContain('data-icon="image"')
|
||||
expect(html).toContain('>Image<')
|
||||
// The covered extensions are the row's desc.
|
||||
// The covered extensions are the card's desc.
|
||||
expect(html).toContain('png · jpg')
|
||||
})
|
||||
|
||||
it('a disabled viewer renders unchecked', () => {
|
||||
it('a disabled viewer renders pressed=false', () => {
|
||||
const { store, service } = mount()
|
||||
store.setPrefs({ ...store.getPrefs(), viewersEnabled: { image: false } })
|
||||
const html = renderSection(store, service)
|
||||
expect(html).toContain('>Image<')
|
||||
})
|
||||
|
||||
it('a disabled tab renders unchecked (its switch reflects the prefs map)', () => {
|
||||
const { store, service } = mount()
|
||||
store.setPrefs({ ...store.getPrefs(), tabsEnabled: { explorer: false } })
|
||||
const html = renderSection(store, service)
|
||||
expect(html).toContain('>explorer<')
|
||||
expect(pressedCount(html, 'false')).toBe(1)
|
||||
expect(pressedCount(html, 'true')).toBe(4)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user