+
{t('settingsGeneralTitle')}
+
+
+ {t('settingsOpenTitle')}
+ {t('settingsOpenDesc')}
+
+
- {t('settingsWidthSuffix')}
-
+
+
+
+ {t('settingsWidthTitle')}
+ {t('settingsWidthDesc')}
+
+
+ { setWidthDraft(event.currentTarget.value) }}
+ onBlur={commitWidth}
+ onKeyDown={event => {
+ if (event.key === 'Enter') event.currentTarget.blur()
+ }}
+ />
+ {t('settingsWidthSuffix')}
+
+
+
+
+ {t('settingsOpenPathTitle')}
+ {t('settingsOpenPathDesc')}
+
+ { togglePref({ interceptOpenPath: next }) }}
+ />
+
- {renderCard({
- title: t('settingsOpenPathTitle'),
- desc: t('settingsOpenPathDesc'),
- icon:
- {tabs.map(tab => (
-
- {renderCard({
- title: textOf(tab.title),
- desc: tab.id,
- icon: iconOf(tab.icon, 16),
- enabled: prefs.tabsEnabled[tab.id] !== false,
- onToggle: (next) => { onToggleTab(tab.id, next) },
- // The settings gear only while the feature is enabled: its
- // related settings are dormant while the feature is off.
- onOpenSettings: prefs.tabsEnabled[tab.id] !== false
- && (tab.settings?.toggles?.length ?? 0) > 0
- ? () => { setSettingsFor(tab) }
- : undefined,
- })}
-
- ))}
+
+
+ {t('settingsTabsTitle')}
+ {tabs.length}
+
+
+ {tabs.map(tab => (
+
+ {renderCard({
+ title: textOf(tab.title),
+ desc: tab.id,
+ icon: iconOf(tab.icon, 16),
+ enabled: prefs.tabsEnabled[tab.id] !== false,
+ onToggle: (next) => { onToggleTab(tab.id, next) },
+ // The settings gear only while the feature is enabled: its
+ // related settings are dormant while the feature is off.
+ onOpenSettings: prefs.tabsEnabled[tab.id] !== false
+ && (tab.settings?.toggles?.length ?? 0) > 0
+ ? () => { setSettingsFor(tab) }
+ : undefined,
+ })}
+
+ ))}
+
{/* 文件预览: one small card per registered file viewer. */}
-
{t('settingsViewersTitle')}
-
- {viewers.map(viewer => (
-
- {renderCard({
- title: textOf(viewer.title) || viewer.id,
- desc: viewer.exts.length === 0 ? t('settingsViewerCatchAll') : viewer.exts.join(' · '),
- icon: iconOf(viewer.icon, 16),
- enabled: prefs.viewersEnabled[viewer.id] !== false,
- onToggle: (next) => { onToggleViewer(viewer.id, next) },
- })}
-
- ))}
+
+
+ {t('settingsViewersTitle')}
+ {viewers.length}
+
+
+ {viewers.map(viewer => (
+
+ {renderCard({
+ title: textOf(viewer.title) || viewer.id,
+ desc: viewer.exts.length === 0 ? t('settingsViewerCatchAll') : viewer.exts.join(' · '),
+ icon: iconOf(viewer.icon, 16),
+ enabled: prefs.viewersEnabled[viewer.id] !== false,
+ onToggle: (next) => { onToggleViewer(viewer.id, next) },
+ })}
+
+ ))}
+
{/* The secondary settings popup: a feature's declared related settings
- as native checkbox rows (Modal chrome is the app's own). Mounted
- only while a feature is open — the Modal primitive runs hooks
- unconditionally, so a closed-but-mounted Modal would break SSR
- (and the renderToString spec) under the test dual-react split. */}
+ as title/desc + switch rows in a wider-than-default Modal with a
+ Done footer (Modal chrome is the app's own). Mounted only while a
+ feature is open — the Modal primitive runs hooks unconditionally,
+ so a closed-but-mounted Modal would break SSR (and the
+ renderToString spec) under the test dual-react split. */}
{settingsFor !== null && (
{ setSettingsFor(null) }}
title={textOf(settingsFor.title)}
+ description={t('settingsPopupDesc', { feature: textOf(settingsFor.title) })}
closeLabel={t('close')}
+ className={css.popupDialog}
+ footer={(
+
+ )}
>
= {
copyAbsolute: 'Copy absolute path',
download: 'Download',
settingsNav: 'Side card',
+ settingsIntro: 'Manage what the side card shows and how it behaves',
+ settingsPopupDesc: 'Configure related options for {feature}',
+ settingsDone: 'Done',
settingsOpenTitle: 'Open by default for new conversations',
settingsOpenDesc: 'Expand the side card automatically for brand-new conversations; existing conversations keep their own layouts',
settingsWidthTitle: 'Default width share',
diff --git a/tests/side-card-section.spec.tsx b/tests/side-card-section.spec.tsx
index faffe9e..1014914 100644
--- a/tests/side-card-section.spec.tsx
+++ b/tests/side-card-section.spec.tsx
@@ -5,9 +5,11 @@
* copy. The toggles are CARDS in a responsive grid: the card's main area is
* the switch, the visual state IS the state (highlighted = enabled),
* announced via `aria-pressed`, and the check badge sits at the far right.
- * Features that declare related settings carry a gear corner button whose
- * popup rows (native checkboxes) are tested through the extracted
- * FeatureSettingsRows component (the Modal portal renders only while open).
+ * The general rows follow the DSH settings-row recipe with custom SWITCHES
+ * (real checkboxes driving a styled track). Features that declare related
+ * settings carry a gear corner button whose popup rows (switch controls)
+ * are tested through the extracted FeatureSettingsRows component (the Modal
+ * portal renders only while open).
*
* Rendered with renderToString (mount effects — the settings RPC sync — do
* not run in SSR; the initial store prefs are the render input).
@@ -78,14 +80,27 @@ describe('SideCardSection declarative inventory', () => {
expect(html).toContain('>explorer<')
expect(html).toContain('data-icon="subagent"')
expect(html).toContain('>Subagents<')
- // Default prefs: openByDefault + interceptOpenPath + both tabs + the
- // image viewer are all enabled → 5 cards pressed, none pressed=false.
+ // Default prefs: openByDefault + interceptOpenPath switches checked, and
+ // both tabs + the image viewer cards pressed (3 aria-pressed cards).
// The nested auto-open toggle is NOT an inline card (it lives in the popup).
- expect(pressedCount(html, 'true')).toBe(5)
+ expect(pressedCount(html, 'true')).toBe(3)
expect(pressedCount(html, 'false')).toBe(0)
+ // The general toggles are custom switches (real checkboxes, checked).
+ expect(html.match(/checked=""/g)?.length).toBe(2)
expect(html).not.toContain('Auto-open Subagents')
})
+ it('renders the section intro and group headings with inventory counts', () => {
+ const { store, service } = mount()
+ const html = renderSection(store, service)
+ expect(html).toContain('Manage what the side card shows and how it behaves')
+ // Group headings carry the inventory count badge (2 tabs, 1 viewer).
+ expect(html).toContain('>Sidebar contentFile viewers2')
+ expect(html).toContain('>1')
+ })
+
it('renders one small card per registered viewer: icon + title + exts', () => {
const { store, service } = mount()
const html = renderSection(store, service)
@@ -111,8 +126,9 @@ describe('SideCardSection declarative inventory', () => {
expect(html).toContain('>Subagents<')
expect(html).toContain('>Image<')
expect(pressedCount(html, 'false')).toBe(2)
- // openByDefault + interceptOpenPath + the explorer stay pressed.
- expect(pressedCount(html, 'true')).toBe(3)
+ // The explorer card stays pressed; the general switches stay checked.
+ expect(pressedCount(html, 'true')).toBe(1)
+ expect(html.match(/checked=""/g)?.length).toBe(2)
})
it('hides the gear of a disabled feature (its related settings are dormant)', () => {
@@ -134,7 +150,7 @@ describe('FeatureSettingsRows (the secondary settings popup body)', () => {
desc: () => 'Expand on new subagent',
}]
- it('renders one native checkbox row per declared toggle with its current value', () => {
+ it('renders one switch row per declared toggle with its current value', () => {
const html = renderToString(createElement(FeatureSettingsRows, {
toggles,
prefs,
@@ -142,7 +158,9 @@ describe('FeatureSettingsRows (the secondary settings popup body)', () => {
}))
expect(html).toContain('Auto-open Subagents')
expect(html).toContain('Expand on new subagent')
- // The checkbox reflects the prefs value (false here → unchecked).
+ // The row's switch is a real checkbox (aria-label = the toggle title)
+ // reflecting the prefs value (false here → unchecked).
+ expect(html).toContain('aria-label="Auto-open Subagents"')
expect(html).not.toContain('checked=""')
})