From 0fdf63b0816866f88ce1ab4354579cbc6ebc17fe Mon Sep 17 00:00:00 2001 From: Atlas Zeng Date: Fri, 21 Aug 2026 21:39:59 +0800 Subject: [PATCH] =?UTF-8?q?feat(settings):=20=E4=BE=A7=E8=BE=B9=E5=8D=A1?= =?UTF-8?q?=E7=89=87=E8=AE=BE=E7=BD=AE=E9=A1=B5=20UI/UX=20=E7=8E=B0?= =?UTF-8?q?=E4=BB=A3=E5=8C=96=E2=80=94=E2=80=94=E5=8D=A1=E7=89=87=E5=BA=95?= =?UTF-8?q?=E9=83=A8=E8=AE=BE=E7=BD=AE=E6=9D=A1=E6=9B=BF=E4=BB=A3=E9=9A=90?= =?UTF-8?q?=E5=BD=A2=E9=BD=BF=E8=BD=AE=20(#300)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(settings): 侧边卡片设置页 UI/UX 现代化——卡片底部设置条替代隐形齿轮,协调双色启用态 - 二级设置入口:卡片右下 16px 幽灵齿轮(tertiary 色、像素钉位)改为 卡片底缘全宽设置条(hairline 分隔 + 齿轮图标 + 「功能设置」文字 标签),常态可发现;hover 换品牌淡底 + brand 字色引导。可见性条件 与 aria-label 契约不变(仅父级启用且声明 settings) - 常规行齿轮升级为带 border-l2 边框的 22px 芯片按钮,hover 同样 accent 引导 - 启用卡片:品牌描边柔化为 color-mix(45%),勾选徽标改用 state-success-primary(绿=已开启确认,brand=激活强调的协调双色) - 节奏:section gap 16 / group padding 20 / 网格 minmax(180px) gap 12; 计数徽章改为 accent-soft 淡底 pill;开关关态 thumb 降档 + track 底 layer-2 - 动效纪律:全部过渡 ≤160ms 且仅颜色属性;prefers-reduced-motion 扩展覆盖设置条/行齿轮/弹窗行 - 清理:合并 .pluginEntries 重复定义;文档同步(AGENTS.md §5 + 设计文档新增 §9 偏差记录) * feat(settings): 按反馈迭代——卡片开关滑块、二级弹窗长列表滚动、卡片等高 - 勾选徽标 → 紧凑品牌开关滑块(30×16 track + 10px thumb,开=品牌填充 反色拇指,关=中性且不渲染;纯视觉指示,卡片本身仍是 aria-pressed 开关,语义契约不变) - 二级弹窗长列表:popupRows 内部滚动 max-height min(52vh,440px) + 细滚动条(--dsw-alias-scrollbar-* 令牌),终端 6 行场景弹窗壳体与 Done 固定、仅行区滚动;行密度收紧(padding 12/14、gap 8) - 卡片等高:.card min-height 106px + .cardMain flex:1 + .cardTop min-height 28px——带设置条/不带设置条/无图标卡片全部等高 - prefers-reduced-motion 覆盖 cardSwitch 过渡;文档同步(AGENTS.md §5 + 设计文档 §9 迭代记录) * fix(settings): 二级设置弹窗行区取消横向滚动——box-sizing:border-box + overflow-x:hidden popupRows 的 width:100% 配 content-box 会在 padding-right:4px 时把 内容盒撑宽 4px,overflow-y:auto 隐式启用的 overflow-x 因此出现横向 滚动条;改为 border-box 并显式 overflow-x:hidden(minifier 输出 overflow:hidden auto),行区始终填满弹窗宽度、无左右滑动。add-plugin 弹窗的 pluginEntries 同步加 overflow-x:hidden。 --- AGENTS.md | 2 +- ...-11-declarative-sidebar-settings-design.md | 14 ++ src/client/SideCardSection.module.css | 206 ++++++++++++------ src/client/SideCardSection.tsx | 36 +-- 4 files changed, 177 insertions(+), 81 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index b328a78..a0f991b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -569,7 +569,7 @@ interface OpenTabSeed { } ``` -> **声明式设置(v0.4.1+)**:每个注册的 tab/viewer 自动出现在 DSH 设置页「侧边卡片」分区的清单里——响应式网格中的**小卡片**(图标 + 标题 + 类型 id + **高亮 = 启用**,勾选徽标钉在卡片最右端,viewer 卡片还显示扩展名),开关持久化到 `SidebarPrefs.tabsEnabled / viewersEnabled`(开放 map,缺省 = 启用)。关闭语义:tab 从 `+` 菜单消失、`openTab` 拒绝新开、子代理自动展开 / agent 终端自动补 tab 等派生流程停止,**已打开的 tab 保留**;viewer 被 `matchFileViewer` 跳过,文件落到下一个匹配。`settings.toggles` 声明的相关设置(如子代理的 `autoOpenSubagent`、终端的 `terminalFontFamily`/`terminalFontSize`)通过卡片右下角的齿轮按钮在**原生弹窗**中编辑——`type: 'switch'` 行是复选框,`type: 'text'`/`'number'` 行是输入框(v0.11.0+),`type: 'select'` 行是下拉选择(options 带 icon 时为大图标选项卡,`multi: true` 多选存 value 数组)——父级卡片关闭时齿轮隐藏;`settings.toggles` 的 **key 必须是宿主 PrefsSchema 的字段**(内置键:`autoOpenSubagent` / `agentTerminalTools` / `terminalFontFamily` / `terminalFontSize` / `editorExplorer` / `htmlViewerNoSandbox` / `htmlViewerDefaultUnsafe` / `browserNoSandbox` / `browserInterceptLinks` / `browserInterceptHttp` / `browserInterceptHttps`)。**v0.12.0 起设置 seam 已开放**:外部插件用 `settings.pluginToggles`(同款行控件,key 插件局部)或 `settings.render`(自定义面板)声明自己的设置,值持久化在 prefs 文档的 `pluginSettings[]`(开放 map,宿主 schema 已有字段,无需注册)——齿轮弹窗对 tab 与 viewer 都可用(viewer 卡片 v0.12.0 起也有齿轮)。 +> **声明式设置(v0.4.1+)**:每个注册的 tab/viewer 自动出现在 DSH 设置页「侧边卡片」分区的清单里——响应式网格中的**小卡片**(图标 + 标题 + 类型 id + **高亮 = 启用**,启用卡片最右端渲染紧凑品牌开关滑块,viewer 卡片还显示扩展名),开关持久化到 `SidebarPrefs.tabsEnabled / viewersEnabled`(开放 map,缺省 = 启用)。关闭语义:tab 从 `+` 菜单消失、`openTab` 拒绝新开、子代理自动展开 / agent 终端自动补 tab 等派生流程停止,**已打开的 tab 保留**;viewer 被 `matchFileViewer` 跳过,文件落到下一个匹配。`settings.toggles` 声明的相关设置(如子代理的 `autoOpenSubagent`、终端的 `terminalFontFamily`/`terminalFontSize`)通过卡片底部的「功能设置」设置条(齿轮图标 + 文字标签,hairline 分隔)在**原生弹窗**中编辑——`type: 'switch'` 行是复选框,`type: 'text'`/`'number'` 行是输入框(v0.11.0+),`type: 'select'` 行是下拉选择(options 带 icon 时为大图标选项卡,`multi: true` 多选存 value 数组)——父级卡片关闭时设置条隐藏;`settings.toggles` 的 **key 必须是宿主 PrefsSchema 的字段**(内置键:`autoOpenSubagent` / `agentTerminalTools` / `terminalFontFamily` / `terminalFontSize` / `editorExplorer` / `htmlViewerNoSandbox` / `htmlViewerDefaultUnsafe` / `browserNoSandbox` / `browserInterceptLinks` / `browserInterceptHttp` / `browserInterceptHttps`)。**v0.12.0 起设置 seam 已开放**:外部插件用 `settings.pluginToggles`(同款行控件,key 插件局部)或 `settings.render`(自定义面板)声明自己的设置,值持久化在 prefs 文档的 `pluginSettings[]`(开放 map,宿主 schema 已有字段,无需注册)——设置弹窗对 tab 与 viewer 都可用(viewer 卡片 v0.12.0 起也有设置条)。 --- diff --git a/docs/plans/2026-08-11-declarative-sidebar-settings-design.md b/docs/plans/2026-08-11-declarative-sidebar-settings-design.md index 5230783..ddea761 100644 --- a/docs/plans/2026-08-11-declarative-sidebar-settings-design.md +++ b/docs/plans/2026-08-11-declarative-sidebar-settings-design.md @@ -146,3 +146,17 @@ isViewerEnabled(id: string): boolean - **声明**:`SidebarSettingsDeclaration` 增加 `pluginToggles`(声明式行,控件同 toggles 的 switch/text/number,值须 JSON 可序列化)与 `render?: (props: SidebarSettingsRenderProps) => ReactNode`(自定义面板:store/service/prefs/pluginSettings/updatePluginSetting/close;抛错被吞并显示内联错误)。 - **渲染**:齿轮弹窗体抽为 `SettingsBody`(`render` 存在时优先,否则 toggles 行 + pluginToggles 行,后者把 pluginSettings 投影到 prefs 面复用 `FeatureSettingsRows`);`settingsFor` 状态从 `TabDescriptor | null` 扩为 `TabDescriptor | FileViewerDescriptor | null`——viewer 卡片 v0.12.0 起同样有齿轮设置弹窗(§4.6 原仅 tab)。 - **向后兼容**:旧设置文档无 `pluginSettings` 字段 → schema default `{}`;`toggles` 行为与 v0.11.0 完全一致。 + +--- + +## 9. 设置页 UI/UX 现代化(feat/settings-sidecard-uiux) + +纯视觉/交互入口改造,无 API 与持久化语义变化(`settings.toggles` / `pluginToggles` / `render` / Modal 条件挂载契约全部不变): + +- **二级设置入口:齿轮角标 → 卡片底部设置条**。原 `.cardGear`(16×16px 三级色透明幽灵按钮,`absolute; top:46px` 像素钉位 + `.cardWithGear` padding 耦合)可发现性不足;改为卡片底缘的**全宽设置条** `.cardSettings`(hairline 分隔、齿轮图标 12px + 「功能设置」文字标签 11px/500,hover 换 `--dsw-alias-interactive-bg-hover-accent` 品牌淡底 + brand 字色)。可见性条件不变(仅父级启用且声明 settings);aria-label 模式 `"${title} ${t('settingsPopup')}"` 不变(测试契约)。常规行齿轮 `.rowGear` 升级为 22×22 带 border-l2 边框的芯片按钮(hover 同样 accent 引导)。 +- **启用态指示(后续迭代:勾选 → 紧凑开关滑块)**:启用卡片描边从全强度品牌色柔化为 `color-mix(button-primary-fill 45%)`。第一版曾把勾选徽标改为 `--dsw-alias-state-success-primary` 实心绿(绿 = 已开启确认,brand = 激活强调);按反馈再迭代为**紧凑品牌开关滑块** `.cardSwitch`(30×16 track + 10px thumb,开 = brand 填充 + thumb 反色,关 = 中性且不渲染滑块)——纯视觉指示,卡片本身仍是开关(`aria-pressed` 语义不变),不再使用 success 色。全部颜色仍为 `--dsw-alias-*` 令牌派生,零硬编码。 +- **卡片等高**:`.card` 统一 `min-height: 106px` + `.cardMain { flex: 1 }` + `.cardTop { min-height: 28px }`——带设置条的卡片(天然更高)与不带设置条的卡片、无图标卡片全部等高;`prefers-reduced-motion` 覆盖新增的 `.cardSwitchTrack/Thumb` 过渡。 +- **二级弹窗长列表**:`.popupRows` 增加 `max-height: min(56vh, 480px)` + 内部 `overflow-y: auto`(终端等声明 6 行设置的场景下弹窗壳体与 Done 按钮固定,仅行区滚动);行密度收紧(padding 12/14、gap 8)。 +- **节奏**:section gap 16 / group padding 20 / 网格 `minmax(180px,1fr)` gap 12;计数徽章改为 `accent-soft` 淡底 pill(versionBadgeTag 同款令牌配方);开关关态 thumb 降一档(label-tertiary)+ track 底 layer-2。 +- **动效纪律**:全部过渡 ≤160ms 且仅 background/border-color/color;`prefers-reduced-motion` 扩展覆盖 `.cardSettings` / `.rowGear` / `.popupRow`。 +- **清理**:合并 `.pluginEntries` 重复定义(原 L598 与 L795 两处,保留带 max-height/overflow 的一处)。 diff --git a/src/client/SideCardSection.module.css b/src/client/SideCardSection.module.css index f55fdef..97262bd 100644 --- a/src/client/SideCardSection.module.css +++ b/src/client/SideCardSection.module.css @@ -11,9 +11,13 @@ * - the feature inventories are responsive GRIDS of small toggle cards * (auto-fill columns — several cards per row, wrapping): icon chip + * title + type id, the card's on/off state IS its visual state - * (enabled = brand border + tinted fill + circular check badge, - * disabled = neutral hairline + dimmed); - * - features that declare related settings carry a gear corner button + * (enabled = softened brand border + tinted fill + a compact brand + * switch knob at the far right — the card itself is the toggle; + * disabled = neutral hairline + dimmed, no knob); every card shares a + * uniform min-height so rows stay equal whether or not they carry a + * settings strip; + * - features that declare related settings carry a labeled SETTINGS STRIP + * across the card's bottom edge (gear icon + text, hairline-separated) * opening a native Modal (wider than the primitive default) whose rows * follow the same row recipe with switch controls and a Done footer; * - the width input stays a native settings row (a value, not a switch). @@ -22,7 +26,7 @@ .section { display: flex; flex-direction: column; - gap: 14px; + gap: 16px; width: 100%; max-width: 760px; /* The DSH settings shell's `.options` area owns vertical scrolling. Keep @@ -47,7 +51,7 @@ display: flex; flex-direction: column; gap: 8px; - padding: 18px 20px 20px; + padding: 20px; box-sizing: border-box; border: 1px solid var(--dsw-alias-border-l2); border-radius: 16px; @@ -67,26 +71,36 @@ color: var(--dsw-alias-label-primary); } +/* The inventory count: a soft-accent pill (the versionBadgeTag token recipe) + so the count reads as metadata, not as part of the heading text. */ .count { - font-size: 12px; - line-height: 18px; - font-weight: 400; - color: var(--dsw-alias-label-tertiary); + padding: 1px 8px; + border-radius: 999px; + background: var(--dsw-alias-accent-soft, var(--dsw-alias-bg-layer-2)); + font-size: 11px; + line-height: 16px; + font-weight: 500; + color: var(--dsw-alias-label-secondary); font-variant-numeric: tabular-nums; } /* Responsive inventory: as many small cards per row as the width allows. */ .grid { display: grid; - grid-template-columns: repeat(auto-fill, minmax(168px, 1fr)); - gap: 10px; + grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); + gap: 12px; } -/* One small toggle card (visual chrome; the interactive part is .cardMain). */ +/* One small toggle card (visual chrome; the interactive part is .cardMain). + A uniform min-height keeps every inventory card the same height across + rows whether or not it carries a settings strip (strip cards are + naturally taller); overflow:hidden clips the strip to the card's radius. */ .card { position: relative; display: flex; flex-direction: column; + min-height: 106px; + overflow: hidden; border-radius: 12px; border: 1px solid var(--dsw-alias-border-l2); background: transparent; @@ -102,15 +116,19 @@ border-color: var(--dsw-alias-label-dimmed); } -/* Enabled = highlighted: brand border + tinted fill. */ +/* Enabled = highlighted: a SOFTENED brand border (color-mix, the icon-chip + precedent) + tinted fill — full-strength brand reads harsh at card scale. */ .cardOn { - border-color: var(--dsw-alias-button-primary-fill); + border-color: color-mix(in srgb, var(--dsw-alias-button-primary-fill) 45%, transparent); background: var(--dsw-alias-interactive-bg-active); } -/* The card's switch: the whole main area (top row + desc) flips the feature. */ +/* The card's switch: the whole main area (top row + desc) flips the feature. + flex:1 lets the main area absorb the extra height of the uniform card + (cards without a settings strip stretch to match the strip cards). */ .cardMain { display: flex; + flex: 1; flex-direction: column; gap: 6px; width: 100%; @@ -125,7 +143,7 @@ } .cardMain:focus-visible, -.cardGear:focus-visible, +.cardSettings:focus-visible, .rowGear:focus-visible { outline: 2px solid var(--dsw-alias-border-l4); outline-offset: 2px; @@ -136,6 +154,7 @@ align-items: center; gap: 8px; min-width: 0; + min-height: 28px; } /* The icon sits in a rounded chip: brand-tinted while the card is on, @@ -175,18 +194,46 @@ color: var(--dsw-alias-label-primary); } -/* The check badge: a circular brand-filled pin at the card's far right - (enabled only), the icon in the inverted surface token (the .save recipe). */ -.cardCheck { +/* The compact on/off switch knob at the card's far right (enabled only): + the card itself is the toggle (aria-pressed on .cardMain), so this is a + PURELY VISUAL switch — same track/thumb geometry as the row switches, + one size down. Off cards show no knob (neutral card = off). */ +.cardSwitch { display: inline-flex; align-items: center; - justify-content: center; flex: none; - width: 16px; +} + +.cardSwitchTrack { + display: inline-flex; + align-items: center; + width: 30px; height: 16px; + padding: 2px; + box-sizing: border-box; + border-radius: 8px; + background: var(--dsw-alias-bg-layer-2); + border: 1px solid var(--dsw-alias-border-l2); + transition: background 0.15s ease, border-color 0.15s ease; +} + +.cardSwitchThumb { + display: block; + width: 10px; + height: 10px; border-radius: 50%; + background: var(--dsw-alias-label-tertiary); + transition: transform 0.15s ease, background 0.15s ease; +} + +.cardOn .cardSwitchTrack { + border-color: var(--dsw-alias-button-primary-fill); background: var(--dsw-alias-button-primary-fill); - color: var(--dsw-alias-bg-layer-3); +} + +.cardOn .cardSwitchThumb { + transform: translateX(14px); + background: var(--dsw-alias-bg-layer-3); } .cardDesc { @@ -234,41 +281,42 @@ color: var(--dsw-alias-label-secondary); } -/* Cards with a settings gear keep the desc clear of it: the gear sits in - the desc row, sharing the check badge's right edge (a vertical column of - controls at the card's right side). */ -.cardWithGear .cardDesc { - padding-right: 30px; -} - -/* The gear corner button: directly UNDER the check badge on the card's - right edge — the two controls are vertically aligned (same right inset, - one row apart) and the SAME 16px size, not side by side. */ -.cardGear { - position: absolute; - right: 12px; - top: 46px; - display: inline-flex; +/* The settings strip: a labeled full-width footer across the card's bottom + edge (gear icon + "功能设置" text), hairline-separated from the toggle + area. This is the SECONDARY settings entry — it must be discoverable at + rest (not a hover-only ghost), so it keeps a steady secondary-ink label + and answers hover with the brand-accent tint + brand ink. */ +.cardSettings { + display: flex; align-items: center; - justify-content: center; - width: 16px; - height: 16px; - padding: 0; + gap: 6px; + width: 100%; + padding: 6px 12px; border: 0; - border-radius: 50%; + border-top: 1px solid var(--dsw-alias-border-l1); background: transparent; - color: var(--dsw-alias-label-tertiary); + color: var(--dsw-alias-label-secondary); + font: inherit; + font-size: 11px; + line-height: 16px; + font-weight: 500; + text-align: left; cursor: pointer; + transition: background 0.12s ease, color 0.12s ease; } -.cardGear:hover { - color: var(--dsw-alias-label-primary); - background: var(--dsw-alias-interactive-bg-hover); +.cardOn .cardSettings { + border-top-color: color-mix(in srgb, var(--dsw-alias-button-primary-fill) 18%, transparent); +} + +.cardSettings:hover { + background: var(--dsw-alias-interactive-bg-hover-accent); + color: var(--dsw-alias-brand-primary); } /* The gear icon button on a general settings ROW (the position-compat - row's "customize the strip height" entry): the same 16px icon-button - recipe as the card gear, sitting in the row's control area. */ + row's "customize the strip height" entry): a bordered chip (not a ghost) + so it reads as a control at rest; hover answers with the accent tint. */ .rowGear { display: inline-flex; align-items: center; @@ -276,17 +324,19 @@ width: 22px; height: 22px; padding: 0; - border: none; - border-radius: 50%; + border: 1px solid var(--dsw-alias-border-l2); + border-radius: 6px; background: transparent; - color: var(--dsw-alias-label-tertiary); + color: var(--dsw-alias-label-secondary); cursor: pointer; flex: none; + transition: background 0.12s ease, border-color 0.12s ease, color 0.12s ease; } .rowGear:hover { - color: var(--dsw-alias-label-primary); - background: var(--dsw-alias-interactive-bg-hover); + border-color: var(--dsw-alias-interactive-bg-hover-accent); + background: var(--dsw-alias-interactive-bg-hover-accent); + color: var(--dsw-alias-brand-primary); } /* Settings-row recipe (the DSH General section rows): title/desc left, @@ -349,7 +399,7 @@ box-sizing: border-box; border-radius: 10px; border: 1px solid var(--dsw-alias-border-l2); - background: var(--dsw-alias-bg-layer-1); + background: var(--dsw-alias-bg-layer-2); transition: background 0.15s ease, border-color 0.15s ease; } @@ -358,7 +408,7 @@ width: 14px; height: 14px; border-radius: 50%; - background: var(--dsw-alias-label-secondary); + background: var(--dsw-alias-label-tertiary); transition: transform 0.15s ease, background 0.15s ease; } @@ -493,21 +543,49 @@ /* Popup rows: each setting gets the same outer chrome as a plugin-list PluginCard (l2 hairline, 12px radius, layer-3 fill). The row's title, - description and control geometry stay unchanged inside that frame. */ + description and control geometry stay unchanged inside that frame. + The body scrolls INTERNALLY once a feature declares many rows (e.g. + Terminal's six) — the dialog shell and its Done footer stay put; a thin + token-driven scrollbar makes the scrollable region discoverable. + box-sizing:border-box keeps width:100% + the scrollbar gutter from + overflowing horizontally (a content-box width would add the 4px pad and + show a horizontal scrollbar); overflow-x:hidden is the belt-and-braces + guarantee that rows always FILL the width and never slide sideways. */ .popupRows { display: flex; flex-direction: column; - gap: 10px; + gap: 8px; width: 100%; + box-sizing: border-box; + max-height: min(52vh, 440px); + overflow-y: auto; + overflow-x: hidden; + padding-right: 4px; + scrollbar-width: thin; + scrollbar-color: var(--dsw-alias-scrollbar-bg-l2, transparent) transparent; +} + +.popupRows::-webkit-scrollbar { + width: 6px; +} + +.popupRows::-webkit-scrollbar-thumb { + border-radius: 3px; + background: var(--dsw-alias-scrollbar-bg-l2, var(--dsw-alias-border-l2)); +} + +.popupRows::-webkit-scrollbar-thumb:hover { + background: var(--dsw-alias-scrollbar-hover-l2, var(--dsw-alias-label-dimmed)); } .popupRow { + flex: none; display: flex; align-items: center; justify-content: space-between; gap: 16px; min-width: 0; - padding: 14px 16px; + padding: 12px 14px; border: 1px solid var(--dsw-alias-border-l2); border-radius: 12px; background: var(--dsw-alias-bg-layer-3); @@ -595,12 +673,6 @@ color: var(--dsw-alias-label-tertiary); } -.pluginEntries { - display: flex; - flex-direction: column; - gap: 10px; -} - /* One catalog entry: name + install button on the head row, description, then the install command in a mono block (the exact script that gets pre-typed into the terminal — review it here before installing). */ @@ -730,6 +802,11 @@ @media (prefers-reduced-motion: reduce) { .card, + .cardSettings, + .cardSwitchTrack, + .cardSwitchThumb, + .rowGear, + .popupRow, .switchTrack, .switchThumb { transition: none; @@ -798,6 +875,7 @@ gap: 10px; max-height: 46vh; overflow-y: auto; + overflow-x: hidden; padding-right: 2px; } diff --git a/src/client/SideCardSection.tsx b/src/client/SideCardSection.tsx index efbc0df..3ff1db6 100644 --- a/src/client/SideCardSection.tsx +++ b/src/client/SideCardSection.tsx @@ -21,13 +21,15 @@ * one-line intro (the DSH section heading+intro recipe). * * A card's on/off state is its VISUAL STATE: enabled = highlighted (brand - * border + tinted fill + a circular check badge pinned to the card's far - * right), disabled = neutral and dimmed. Features that declare - * `settings.toggles` carry a gear corner button that opens a native Modal - * (wider than the primitive default) with the related settings as - * title/desc + custom-switch rows and a Done footer. The toggles - * themselves are custom switches: a real checkbox (native semantics and - * focus) driving a styled track/thumb. + * border + tinted fill + a compact switch knob at the card's far right), + * disabled = neutral and dimmed. Features that declare + * `settings.toggles` carry a labeled settings strip at the card's bottom + * edge that opens a native Modal (wider than the primitive default) with + * the related settings as title/desc + custom-switch rows and a Done + * footer; the popup body scrolls internally when a feature declares many + * rows (e.g. Terminal's six). The toggles themselves are custom + * switches: a real checkbox (native semantics and focus) driving a styled + * track/thumb. * * 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 @@ -40,7 +42,6 @@ */ import { Fragment, useEffect, useMemo, useRef, useState, type ReactNode } from 'react' import { - IconCheckOutline16, IconChevronDownOutline14, IconPlusOutline16, IconSettingsOutline16, @@ -793,8 +794,9 @@ export function SideCardSection({ store, service }: SideCardSectionProps) { * One SMALL toggle card for the responsive inventory grid: the card's main * area is the switch (click to flips, visual state IS the state), the icon * sits in a rounded chip, the check badge pins to the far right, and a - * feature that declares related settings carries a gear corner button - * opening its settings popup. + * feature that declares related settings gets a labeled SETTINGS STRIP + * across the card's bottom edge (gear icon + text) opening its settings + * popup — discoverable at rest, not a hover-only ghost corner button. */ const renderCard = (props: { title: string @@ -802,13 +804,13 @@ export function SideCardSection({ store, service }: SideCardSectionProps) { icon?: ReactNode enabled: boolean onToggle: (next: boolean) => void - /** A feature with declared related settings shows the gear corner button. */ + /** A feature with declared related settings shows the settings strip. */ onOpenSettings?: () => void }) => { const hasSettings = props.onOpenSettings !== undefined return (
)}