From aeb71f106122defb6fd07641c483ba7f7d9edd60 Mon Sep 17 00:00:00 2001
From: Zrs000 <1900978743@qq.com>
Date: Wed, 14 Oct 2026 00:08:15 +0800
Subject: [PATCH 1/6] feat(web): reflow the Settings panel at narrow widths
Below 768px the panel fills the viewport and the rail becomes the horizontal
section strip the Web product uses: the title shares the first row with the
header actions and close control, the registered section cells scroll sideways
on the second, and the active section fills the rest. Cells keep their labels
whole and the strip scrolls instead of truncating, so a registrant's label
length or a longer section roster cannot wrap or clip a name.
The panel places its four seats (title, header, navigation, options) on one
grid, so the desktop rail keeps its 188px track and 64px first row. Preference
rows drop their 48px control gutter and the Appearance cubes share one row at
that width.
---
.../section.expected.md | 6 +-
.../cards.expected.md | 6 +-
.../desktop-locale/automatic.expected.md | 6 +-
.../desktop-locale/selected.expected.md | 6 +-
.../stored-error.expected.md | 6 +-
.../catalog-inputs.expected.md | 6 +-
.../models-settings/configured.expected.md | 6 +-
.../models-settings/declared-edit.expected.md | 6 +-
.../models-settings/declared.expected.md | 6 +-
.../models-settings/empty.expected.md | 6 +-
.../models-settings/image-only.expected.md | 6 +-
.../native-keyless.expected.md | 6 +-
.../models-settings/refreshing.expected.md | 6 +-
.../default-models.expected.md | 6 +-
.../models.expected.md | 6 +-
.../onboarding-native/models.expected.md | 6 +-
.../dismissed.expected.md | 6 +-
.../settings-chrome/dialog-en.expected.md | 6 +-
.../dialog-no-browser.expected.md | 6 +-
.../settings-chrome/dialog.expected.md | 6 +-
apps/web/tests/settings-chrome.e2e.ts | 70 +++++++++-
.../locale/src/client/LanguageRow.module.css | 8 ++
.../client/settings/PreferenceRow.module.css | 8 ++
.../settings/EnterBehaviorRow.module.css | 8 ++
.../src/client/PermissionRow.module.css | 8 ++
.../ui-settings-general/README.i18n.yaml | 12 +-
packages/client/ui-settings-general/README.md | 8 +-
.../client/ui-settings-general/README.zh.md | 8 +-
.../src/client/SettingsRoot.module.css | 121 +++++++++++++++---
.../src/client/SettingsRoot.tsx | 47 +++----
.../src/client/AppearanceRow.module.css | 10 ++
.../src/client/FontSizeRow.module.css | 8 ++
32 files changed, 321 insertions(+), 115 deletions(-)
diff --git a/apps/web/tests/expected/agent-preset-authoring/section.expected.md b/apps/web/tests/expected/agent-preset-authoring/section.expected.md
index dd8628cd57..3466d663b3 100644
--- a/apps/web/tests/expected/agent-preset-authoring/section.expected.md
+++ b/apps/web/tests/expected/agent-preset-authoring/section.expected.md
@@ -1,12 +1,12 @@
- dialog "设置":
+ - text: 设置
+ - button "打开配置文件"
+ - button "关闭"
- navigation:
- - text: 设置
- button "通用设置"
- button "模型"
- button "内置插件"
- button "Agent 预设"
- - button "打开配置文件"
- - button "关闭"
- heading "Agent 预设" [level=2]
- paragraph: 选择 Agent 的工具和工作方式。日常任务用「标准模式」,扩展 DSH 的能力用「创造模式」。
- heading "内置" [level=3]
diff --git a/apps/web/tests/expected/deepseek-messages-settings/cards.expected.md b/apps/web/tests/expected/deepseek-messages-settings/cards.expected.md
index abd17a0782..11e71eb1ce 100644
--- a/apps/web/tests/expected/deepseek-messages-settings/cards.expected.md
+++ b/apps/web/tests/expected/deepseek-messages-settings/cards.expected.md
@@ -1,12 +1,12 @@
- dialog "设置":
+ - text: 设置
+ - button "打开配置文件"
+ - button "关闭"
- navigation:
- - text: 设置
- button "通用设置"
- button "模型"
- button "内置插件"
- button "Agent 预设"
- - button "打开配置文件"
- - button "关闭"
- heading "模型" [level=2]
- paragraph: 填入各提供商的 API 密钥即可使用其模型。
- list:
diff --git a/apps/web/tests/expected/desktop-locale/automatic.expected.md b/apps/web/tests/expected/desktop-locale/automatic.expected.md
index 84a652e494..a8ab291d9b 100644
--- a/apps/web/tests/expected/desktop-locale/automatic.expected.md
+++ b/apps/web/tests/expected/desktop-locale/automatic.expected.md
@@ -1,12 +1,12 @@
- dialog "设置":
+ - text: 设置
+ - button "打开配置文件"
+ - button "关闭"
- navigation:
- - text: 设置
- button "通用设置"
- button "模型"
- button "内置插件"
- button "Agent 预设"
- - button "打开配置文件"
- - button "关闭"
- text: 权限 选择新会话的默认权限模式
- button "工作区内修改"
- text: 语言
diff --git a/apps/web/tests/expected/desktop-locale/selected.expected.md b/apps/web/tests/expected/desktop-locale/selected.expected.md
index 865390f104..d3582973c8 100644
--- a/apps/web/tests/expected/desktop-locale/selected.expected.md
+++ b/apps/web/tests/expected/desktop-locale/selected.expected.md
@@ -1,12 +1,12 @@
- dialog "Settings":
+ - text: Settings
+ - button "Open configuration file"
+ - button "Close"
- navigation:
- - text: Settings
- button "General"
- button "Models"
- button "Built-in plugins"
- button "Agent presets"
- - button "Open configuration file"
- - button "Close"
- text: Permission Choose the default permission mode for new sessions
- button "Workspace Write"
- text: Language
diff --git a/apps/web/tests/expected/models-settings-recovery/stored-error.expected.md b/apps/web/tests/expected/models-settings-recovery/stored-error.expected.md
index 84f93fc533..ea3e8250a8 100644
--- a/apps/web/tests/expected/models-settings-recovery/stored-error.expected.md
+++ b/apps/web/tests/expected/models-settings-recovery/stored-error.expected.md
@@ -1,12 +1,12 @@
- dialog "设置":
+ - text: 设置
+ - button "打开配置文件"
+ - button "关闭"
- navigation:
- - text: 设置
- button "通用设置"
- button "模型"
- button "内置插件"
- button "Agent 预设"
- - button "打开配置文件"
- - button "关闭"
- heading "模型" [level=2]
- paragraph: 填入各提供商的 API 密钥即可使用其模型。
- list:
diff --git a/apps/web/tests/expected/models-settings/catalog-inputs.expected.md b/apps/web/tests/expected/models-settings/catalog-inputs.expected.md
index de6a45d25c..1cde74acba 100644
--- a/apps/web/tests/expected/models-settings/catalog-inputs.expected.md
+++ b/apps/web/tests/expected/models-settings/catalog-inputs.expected.md
@@ -1,12 +1,12 @@
- dialog "设置":
+ - text: 设置
+ - button "打开配置文件"
+ - button "关闭"
- navigation:
- - text: 设置
- button "通用设置"
- button "模型"
- button "内置插件"
- button "Agent 预设"
- - button "打开配置文件"
- - button "关闭"
- heading "模型" [level=2]
- paragraph: 填入各提供商的 API 密钥即可使用其模型。
- list:
diff --git a/apps/web/tests/expected/models-settings/configured.expected.md b/apps/web/tests/expected/models-settings/configured.expected.md
index 0380871342..2f0b753442 100644
--- a/apps/web/tests/expected/models-settings/configured.expected.md
+++ b/apps/web/tests/expected/models-settings/configured.expected.md
@@ -1,12 +1,12 @@
- dialog "设置":
+ - text: 设置
+ - button "打开配置文件"
+ - button "关闭"
- navigation:
- - text: 设置
- button "通用设置"
- button "模型"
- button "内置插件"
- button "Agent 预设"
- - button "打开配置文件"
- - button "关闭"
- heading "模型" [level=2]
- paragraph: 填入各提供商的 API 密钥即可使用其模型。
- status: 已保存 minimax-cn。
diff --git a/apps/web/tests/expected/models-settings/declared-edit.expected.md b/apps/web/tests/expected/models-settings/declared-edit.expected.md
index 73e8fc9a47..568f4c4e28 100644
--- a/apps/web/tests/expected/models-settings/declared-edit.expected.md
+++ b/apps/web/tests/expected/models-settings/declared-edit.expected.md
@@ -1,12 +1,12 @@
- dialog "设置":
+ - text: 设置
+ - button "打开配置文件"
+ - button "关闭"
- navigation:
- - text: 设置
- button "通用设置"
- button "模型"
- button "内置插件"
- button "Agent 预设"
- - button "打开配置文件"
- - button "关闭"
- heading "模型" [level=2]
- paragraph: 填入各提供商的 API 密钥即可使用其模型。
- list:
diff --git a/apps/web/tests/expected/models-settings/declared.expected.md b/apps/web/tests/expected/models-settings/declared.expected.md
index 269d5ebd35..400df4adba 100644
--- a/apps/web/tests/expected/models-settings/declared.expected.md
+++ b/apps/web/tests/expected/models-settings/declared.expected.md
@@ -1,12 +1,12 @@
- dialog "设置":
+ - text: 设置
+ - button "打开配置文件"
+ - button "关闭"
- navigation:
- - text: 设置
- button "通用设置"
- button "模型"
- button "内置插件"
- button "Agent 预设"
- - button "打开配置文件"
- - button "关闭"
- heading "模型" [level=2]
- paragraph: 填入各提供商的 API 密钥即可使用其模型。
- list:
diff --git a/apps/web/tests/expected/models-settings/empty.expected.md b/apps/web/tests/expected/models-settings/empty.expected.md
index 0561d65614..f548ae2c41 100644
--- a/apps/web/tests/expected/models-settings/empty.expected.md
+++ b/apps/web/tests/expected/models-settings/empty.expected.md
@@ -1,12 +1,12 @@
- dialog "设置":
+ - text: 设置
+ - button "打开配置文件"
+ - button "关闭"
- navigation:
- - text: 设置
- button "通用设置"
- button "模型"
- button "内置插件"
- button "Agent 预设"
- - button "打开配置文件"
- - button "关闭"
- heading "模型" [level=2]
- paragraph: 填入各提供商的 API 密钥即可使用其模型。
- list
diff --git a/apps/web/tests/expected/models-settings/image-only.expected.md b/apps/web/tests/expected/models-settings/image-only.expected.md
index e0b4eb62cf..6a3536cea3 100644
--- a/apps/web/tests/expected/models-settings/image-only.expected.md
+++ b/apps/web/tests/expected/models-settings/image-only.expected.md
@@ -1,12 +1,12 @@
- dialog "设置":
+ - text: 设置
+ - button "打开配置文件"
+ - button "关闭"
- navigation:
- - text: 设置
- button "通用设置"
- button "模型"
- button "内置插件"
- button "Agent 预设"
- - button "打开配置文件"
- - button "关闭"
- heading "模型" [level=2]
- paragraph: 填入各提供商的 API 密钥即可使用其模型。
- list:
diff --git a/apps/web/tests/expected/models-settings/native-keyless.expected.md b/apps/web/tests/expected/models-settings/native-keyless.expected.md
index e8ae4de994..70569382a5 100644
--- a/apps/web/tests/expected/models-settings/native-keyless.expected.md
+++ b/apps/web/tests/expected/models-settings/native-keyless.expected.md
@@ -1,12 +1,12 @@
- dialog "设置":
+ - text: 设置
+ - button "打开配置文件"
+ - button "关闭"
- navigation:
- - text: 设置
- button "通用设置"
- button "模型"
- button "内置插件"
- button "Agent 预设"
- - button "打开配置文件"
- - button "关闭"
- heading "模型" [level=2]
- paragraph: 填入各提供商的 API 密钥即可使用其模型。
- list
diff --git a/apps/web/tests/expected/models-settings/refreshing.expected.md b/apps/web/tests/expected/models-settings/refreshing.expected.md
index ad9ac8cda1..99daa89cbf 100644
--- a/apps/web/tests/expected/models-settings/refreshing.expected.md
+++ b/apps/web/tests/expected/models-settings/refreshing.expected.md
@@ -1,12 +1,12 @@
- dialog "设置":
+ - text: 设置
+ - button "打开配置文件"
+ - button "关闭"
- navigation:
- - text: 设置
- button "通用设置"
- button "模型"
- button "内置插件"
- button "Agent 预设"
- - button "打开配置文件"
- - button "关闭"
- heading "模型" [level=2]
- paragraph: 填入各提供商的 API 密钥即可使用其模型。
- list:
diff --git a/apps/web/tests/expected/onboarding-deepseek-config/default-models.expected.md b/apps/web/tests/expected/onboarding-deepseek-config/default-models.expected.md
index cff2fd1b37..501aa615a7 100644
--- a/apps/web/tests/expected/onboarding-deepseek-config/default-models.expected.md
+++ b/apps/web/tests/expected/onboarding-deepseek-config/default-models.expected.md
@@ -1,12 +1,12 @@
- dialog "设置":
+ - text: 设置
+ - button "打开配置文件"
+ - button "关闭"
- navigation:
- - text: 设置
- button "通用设置"
- button "模型"
- button "内置插件"
- button "Agent 预设"
- - button "打开配置文件"
- - button "关闭"
- heading "模型" [level=2]
- paragraph: 填入各提供商的 API 密钥即可使用其模型。
- list:
diff --git a/apps/web/tests/expected/onboarding-deepseek-config/models.expected.md b/apps/web/tests/expected/onboarding-deepseek-config/models.expected.md
index 35771c887a..7a27d327c2 100644
--- a/apps/web/tests/expected/onboarding-deepseek-config/models.expected.md
+++ b/apps/web/tests/expected/onboarding-deepseek-config/models.expected.md
@@ -1,12 +1,12 @@
- dialog "设置":
+ - text: 设置
+ - button "打开配置文件"
+ - button "关闭"
- navigation:
- - text: 设置
- button "通用设置"
- button "模型"
- button "内置插件"
- button "Agent 预设"
- - button "打开配置文件"
- - button "关闭"
- heading "模型" [level=2]
- paragraph: 填入各提供商的 API 密钥即可使用其模型。
- list:
diff --git a/apps/web/tests/expected/onboarding-native/models.expected.md b/apps/web/tests/expected/onboarding-native/models.expected.md
index 317a184c37..69e104e3a0 100644
--- a/apps/web/tests/expected/onboarding-native/models.expected.md
+++ b/apps/web/tests/expected/onboarding-native/models.expected.md
@@ -1,12 +1,12 @@
- dialog "设置":
+ - text: 设置
+ - button "打开配置文件"
+ - button "关闭"
- navigation:
- - text: 设置
- button "通用设置"
- button "模型"
- button "内置插件"
- button "Agent 预设"
- - button "打开配置文件"
- - button "关闭"
- heading "模型" [level=2]
- paragraph: 填入各提供商的 API 密钥即可使用其模型。
- list:
diff --git a/apps/web/tests/expected/onboarding-usable-provider/dismissed.expected.md b/apps/web/tests/expected/onboarding-usable-provider/dismissed.expected.md
index ad970ad588..abab5e489e 100644
--- a/apps/web/tests/expected/onboarding-usable-provider/dismissed.expected.md
+++ b/apps/web/tests/expected/onboarding-usable-provider/dismissed.expected.md
@@ -1,12 +1,12 @@
- dialog "设置":
+ - text: 设置
+ - button "打开配置文件"
+ - button "关闭"
- navigation:
- - text: 设置
- button "通用设置"
- button "模型"
- button "内置插件"
- button "Agent 预设"
- - button "打开配置文件"
- - button "关闭"
- heading "模型" [level=2]
- paragraph: 填入各提供商的 API 密钥即可使用其模型。
- list:
diff --git a/apps/web/tests/expected/settings-chrome/dialog-en.expected.md b/apps/web/tests/expected/settings-chrome/dialog-en.expected.md
index e36458f98a..21986f75b9 100644
--- a/apps/web/tests/expected/settings-chrome/dialog-en.expected.md
+++ b/apps/web/tests/expected/settings-chrome/dialog-en.expected.md
@@ -1,12 +1,12 @@
- dialog "Settings":
+ - text: Settings
+ - button "Open configuration file"
+ - button "Close"
- navigation:
- - text: Settings
- button "General"
- button "Models"
- button "Built-in plugins"
- button "Agent presets"
- - button "Open configuration file"
- - button "Close"
- text: Permission Choose the default permission mode for new sessions
- button "Workspace Write"
- text: Language
diff --git a/apps/web/tests/expected/settings-chrome/dialog-no-browser.expected.md b/apps/web/tests/expected/settings-chrome/dialog-no-browser.expected.md
index e36458f98a..21986f75b9 100644
--- a/apps/web/tests/expected/settings-chrome/dialog-no-browser.expected.md
+++ b/apps/web/tests/expected/settings-chrome/dialog-no-browser.expected.md
@@ -1,12 +1,12 @@
- dialog "Settings":
+ - text: Settings
+ - button "Open configuration file"
+ - button "Close"
- navigation:
- - text: Settings
- button "General"
- button "Models"
- button "Built-in plugins"
- button "Agent presets"
- - button "Open configuration file"
- - button "Close"
- text: Permission Choose the default permission mode for new sessions
- button "Workspace Write"
- text: Language
diff --git a/apps/web/tests/expected/settings-chrome/dialog.expected.md b/apps/web/tests/expected/settings-chrome/dialog.expected.md
index 9f9c6ee85e..587677cdc0 100644
--- a/apps/web/tests/expected/settings-chrome/dialog.expected.md
+++ b/apps/web/tests/expected/settings-chrome/dialog.expected.md
@@ -1,12 +1,12 @@
- dialog "设置":
+ - text: 设置
+ - button "打开配置文件"
+ - button "关闭"
- navigation:
- - text: 设置
- button "通用设置"
- button "模型"
- button "内置插件"
- button "Agent 预设"
- - button "打开配置文件"
- - button "关闭"
- text: 权限 选择新会话的默认权限模式
- button "工作区内修改"
- text: 语言
diff --git a/apps/web/tests/settings-chrome.e2e.ts b/apps/web/tests/settings-chrome.e2e.ts
index 5155daf95c..cd7d63a798 100644
--- a/apps/web/tests/settings-chrome.e2e.ts
+++ b/apps/web/tests/settings-chrome.e2e.ts
@@ -227,7 +227,7 @@ describe('web e2e: settings modal and General preferences', () => {
const dialog = overflowPage.getByRole('dialog', { name: '设置', exact: true })
const navigation = dialog.getByRole('navigation')
const list = navigation.locator('[class*="_navList"]')
- const title = navigation.getByText('设置', { exact: true })
+ const title = dialog.getByText('设置', { exact: true })
const options = dialog.locator('[class*="_options"]')
const last = navigation.getByRole('button').last()
await last.waitFor()
@@ -261,6 +261,74 @@ describe('web e2e: settings modal and General preferences', () => {
expect(console.warnings).toEqual([])
})
+ it('fills a phone viewport and turns the rail into the horizontal section strip', async () => {
+ const phone = await browser.newPage({ viewport: { width: 390, height: 844 }, locale: ZH_BROWSER_LOCALE })
+ onTestFinished(() => phone.close())
+ onTestFailed(() => saveSettingsFailureShot(phone, 'web-e2e-settings-phone'))
+ const console = watchConsole(phone)
+ await phone.goto(scaffold.authenticatedUrl, { waitUntil: 'load' })
+ await openSettings(phone, 'zh')
+ const dialog = phone.getByRole('dialog', { name: '设置', exact: true })
+ await dialog.waitFor({ timeout: 10_000 })
+ await dialog.getByRole('button', { name: '通用设置', exact: true }).waitFor({ timeout: 10_000 })
+
+ // The panel is the viewport: no centered card, no side margins, no rail.
+ await expect.poll(() => dialog.boundingBox()).toEqual({ x: 0, y: 0, width: 390, height: 844 })
+
+ // The rail reads left to right on one line instead of stacking.
+ const navigation = dialog.getByRole('navigation')
+ const list = navigation.locator('[class*="_navList"]')
+ const cells = navigation.getByRole('button')
+ const first = await cells.first().boundingBox()
+ const second = await cells.nth(1).boundingBox()
+ expect(first).not.toBeNull()
+ expect(second).not.toBeNull()
+ expect(second!.x).toBeGreaterThanOrEqual(first!.x + first!.width - 1)
+ expect(Math.abs(second!.y - first!.y)).toBeLessThan(1)
+ expect(await list.evaluate(el => getComputedStyle(el).overflowX)).toBe('auto')
+
+ // The shipped sections overflow the strip, which scrolls sideways; the
+ // section column starts below it and keeps the panel's width.
+ const strip = await list.boundingBox()
+ const options = dialog.locator('[class*="_options"]')
+ const optionsBox = await options.boundingBox()
+ expect(strip).not.toBeNull()
+ expect(optionsBox).not.toBeNull()
+ expect(await list.evaluate(el => el.scrollWidth)).toBeGreaterThan(await list.evaluate(el => el.clientWidth))
+ expect(optionsBox!.y).toBeGreaterThanOrEqual(strip!.y + strip!.height - 1)
+ expect(Math.round(optionsBox!.width)).toBe(390)
+ expect(await phone.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(390)
+
+ // Opening the panel brings the active cell into the strip's viewport.
+ const active = dialog.locator('[aria-current="true"]')
+ const activeBox = await active.boundingBox()
+ expect(activeBox).not.toBeNull()
+ expect(activeBox!.x).toBeGreaterThanOrEqual(0)
+ expect(activeBox!.x + activeBox!.width).toBeLessThanOrEqual(390)
+
+ // Tapping a cell swaps the page, and a General row keeps its label and
+ // control on one line rather than stacking.
+ await cells.nth(1).click()
+ await expect.poll(() => cells.nth(1).getAttribute('aria-current')).toBe('true')
+ await expect.poll(() => dialog.getByText('权限', { exact: true }).count()).toBe(0)
+ await cells.first().click()
+ await expect.poll(() => dialog.getByText('权限', { exact: true }).count(), { timeout: 5_000 }).toBe(1)
+ const permissionRow = dialog.getByText('权限', { exact: true }).locator('..')
+ const permissionBox = await permissionRow.boundingBox()
+ const controlBox = await dialog.getByRole('button', { name: '工作区内修改' }).boundingBox()
+ expect(controlBox).not.toBeNull()
+ expect(permissionBox).not.toBeNull()
+ expect(controlBox!.y).toBeLessThan(permissionBox!.y + permissionBox!.height)
+
+ // The Appearance cubes share one row instead of wrapping to two.
+ const cubes = dialog.getByRole('button', { name: /浅色|深色|跟随系统/ })
+ const cubeBoxes = await cubes.evaluateAll(nodes => nodes.map(node => node.getBoundingClientRect().top))
+ expect(new Set(cubeBoxes.map(top => Math.round(top))).size).toBe(1)
+
+ expect(console.pageErrors).toEqual([])
+ expect(console.warnings).toEqual([])
+ })
+
it('stores Permission as the default for future sessions without changing an existing session', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-settings-permission'))
const existing = scaffold.ctx.sessions.create(SessionId('settings-permission-before'))
diff --git a/packages/client/locale/src/client/LanguageRow.module.css b/packages/client/locale/src/client/LanguageRow.module.css
index af02724b4f..1277c5b4e8 100644
--- a/packages/client/locale/src/client/LanguageRow.module.css
+++ b/packages/client/locale/src/client/LanguageRow.module.css
@@ -18,6 +18,14 @@
padding-right: 48px;
}
+/* Narrow settings panel: the 48px gutter separates the label from its control
+ across a wide row and only squeezes the label here. */
+@media (max-width: 767px) {
+ .rowText {
+ padding-right: 16px;
+ }
+}
+
.title {
font-size: 14px;
font-weight: 400;
diff --git a/packages/client/ui-chat/src/client/settings/PreferenceRow.module.css b/packages/client/ui-chat/src/client/settings/PreferenceRow.module.css
index 2042d54db3..bebe1971ed 100644
--- a/packages/client/ui-chat/src/client/settings/PreferenceRow.module.css
+++ b/packages/client/ui-chat/src/client/settings/PreferenceRow.module.css
@@ -17,6 +17,14 @@
padding-right: 48px;
}
+/* Narrow settings panel: the 48px gutter separates the label from its control
+ across a wide row and only squeezes the label here. */
+@media (max-width: 767px) {
+ .rowText {
+ padding-right: 16px;
+ }
+}
+
.title {
font-size: 14px;
font-weight: 400;
diff --git a/packages/client/ui-conversation/src/client/settings/EnterBehaviorRow.module.css b/packages/client/ui-conversation/src/client/settings/EnterBehaviorRow.module.css
index 0b5450cc95..cf9bb42469 100644
--- a/packages/client/ui-conversation/src/client/settings/EnterBehaviorRow.module.css
+++ b/packages/client/ui-conversation/src/client/settings/EnterBehaviorRow.module.css
@@ -17,6 +17,14 @@
padding-right: 48px;
}
+/* Narrow settings panel: the 48px gutter separates the label from its control
+ across a wide row and only squeezes the label here. */
+@media (max-width: 767px) {
+ .rowText {
+ padding-right: 16px;
+ }
+}
+
.title {
font-size: 14px;
font-weight: 400;
diff --git a/packages/client/ui-permission-presets/src/client/PermissionRow.module.css b/packages/client/ui-permission-presets/src/client/PermissionRow.module.css
index a40d0959cd..825ce5f372 100644
--- a/packages/client/ui-permission-presets/src/client/PermissionRow.module.css
+++ b/packages/client/ui-permission-presets/src/client/PermissionRow.module.css
@@ -17,6 +17,14 @@
padding-right: 48px;
}
+/* Narrow settings panel: the 48px gutter separates the label from its control
+ across a wide row and only squeezes the label here. */
+@media (max-width: 767px) {
+ .rowText {
+ padding-right: 16px;
+ }
+}
+
.title {
font-size: 14px;
font-weight: 400;
diff --git a/packages/client/ui-settings-general/README.i18n.yaml b/packages/client/ui-settings-general/README.i18n.yaml
index a265b7029e..8561f301f2 100644
--- a/packages/client/ui-settings-general/README.i18n.yaml
+++ b/packages/client/ui-settings-general/README.i18n.yaml
@@ -12,11 +12,11 @@
en: cd94b4a8dae32670
zh: 333b8603e77f92d7
/deepseek-ai-dsh-client-ui-settings-general/table-of-contents:
- en: 2843d46e95311bce
- zh: ec11bdeb5d3ce818
+ en: ae9928d13ce9a84c
+ zh: 20cd645ebd75934b
/deepseek-ai-dsh-client-ui-settings-general/use-this-package:
- en: 4285513efdb1be68
- zh: b28089509254e8d5
+ en: eb695d3dd1bc7155
+ zh: 671606eeeb6bd87a
/deepseek-ai-dsh-client-ui-settings-general/use-this-package/the-general-section:
en: 8b8a7c6287bb303b
zh: 19535112531aedd5
@@ -27,8 +27,8 @@
en: 27f0f6ec55f00228
zh: 69b47ff1efd81c17
/deepseek-ai-dsh-client-ui-settings-general/understand-the-implementation:
- en: 41726bcd4ae37c0a
- zh: d994b70ca7997da8
+ en: 1531b45780d0689f
+ zh: 844548208c08a9c9
/deepseek-ai-dsh-client-ui-settings-general/understand-the-implementation/ledger-projections:
en: 3e3c2c72f87e8b23
zh: 12b10ba2d37c9077
diff --git a/packages/client/ui-settings-general/README.md b/packages/client/ui-settings-general/README.md
index cf0c202f62..b6f43876bd 100644
--- a/packages/client/ui-settings-general/README.md
+++ b/packages/client/ui-settings-general/README.md
@@ -24,16 +24,18 @@ Use this package to give the dsh web client a Settings panel, connection-recover
The Settings panel uses a shared 800 × 800 layout, bounded by the viewport. Longer sections scroll inside the content column; the Account entry uses the account icon. The panel portals beside `#root` rather than inside it, so a macOS window drag region a chrome row declares later in document order cannot swallow its controls.
+Below 768px viewport width the panel fills the viewport and its navigation becomes a horizontal strip: the title shares the first row with the header's action and close seats, the registered section cells scroll sideways on the second, and the active section fills the rest. Cells grow into spare width and hold a minimum width, and the strip scrolls instead of truncating, so a registrant's label length or a longer section roster cannot wrap or clip a name; the selected cell carries initial focus, which brings it into the strip's view. Section content keeps a 16px inline padding at that width, and the 48px gutter separating a preference label from its control narrows with it.
+
## Use this package
Users reach the shell through the sidebar's bottom Settings control; feature plugins contribute their pages and onboarding steps through the slot ledgers this shell projects. In both the expanded sidebar and collapsed rail, the control exposes the localized Settings label as its accessible name. A pale-yellow **Disconnected** action beside Settings indicates browser offline suspension; its permanent retry glyph marks the retry action, which the Chinese outage copy also names (连接异常,刷新重试). Every recovery attempt shows the shared ongoing loader beside **Reconnecting** with one to three dots advancing every 500ms, and an attempt stays visible for at least 800ms so brief retries do not flicker. Selecting either yellow state starts an immediate retry; press feedback stays within the warning palette. Recovery changes the region to pale-green **Connected** for two seconds from the moment the green pill becomes visible. The pill fades in on appearance, fades out over 150ms on removal, and sizes to its current label. Initial startup and uninterrupted healthy operation remain silent. The shell renders the modal panel, the navigation built from `settings.section` entries, and exactly one mounted onboarding step at a time.
-Settings uses the [frame’s shared overlay inset](../ui-layout/README.md#window-chrome-seat) and keeps at least 24px of viewport margin. Its mask leaves the Windows caption unpainted; the full-viewport layer still blocks background clicks.
+Above the mobile width, Settings uses the [frame’s shared overlay inset](../ui-layout/README.md#window-chrome-seat) and keeps at least 24px of viewport margin. Its mask leaves the Windows caption unpainted; the full-viewport layer still blocks background clicks, and the full-viewport panel covers the mask.
When the page reconnects to a Host running another release, a **Service version changed** dialog draws two lines: **Service** from the Host's previous release (gray) to its current release, joined by a thin solid green arrow because the Host already switched, and **Page** from the release this page booted from to the Host's release (gray), joined by an equally thin dashed gray arrow because only a reload completes it. A line shows versions when its two versions differ and commits otherwise. The page applies no plugin update from that release. **Reload to load the new version** loads it; **Not now**, the close control, a mask click, and Escape keep the page at its own release and stop asking about that Host release. When the Host returns to the page's release, the dialog closes and live updates resume.
-When the section navigation exceeds the panel's available height, the list scrolls independently of the settings content and keeps the Settings title fixed.
+When the section navigation exceeds the panel's available height, the list scrolls independently of the settings content and keeps the Settings title fixed; below 768px the strip scrolls along its own axis the same way.
In Desktop, the account-row update control shows availability, progress, verification, readiness, and persistent retry feedback. It shares the connection indicator’s 28px height, 8px corners, 14px icon slot, 4px icon gap, and 12px medium text with an 18px line height; update-state border, fill, and text colors remain independently defined. Retry text and its dot use the same brand blue as other update labels; numeric download progress has no ellipsis. The preload carries semantic phase, version, progress, and classified failures; the component resolves every visible and accessible string from the active `settings` locale, including after an in-application language change. Selecting an available update starts downloading; installation requires a separate shell-owned confirmation. A collapsed sidebar shows the same status as a brand-blue dot on its top expand button, including failures. Connection feedback takes priority except during shell-reported installation, when the expected backend disconnect must not hide update status. Failure restores connection feedback. Both controls share one carrier subscription; browser code cannot choose packages or authorize installation. [Desktop updates](../../../apps/desktop/README.md) owns the release workflow. Checking, verifying, and preparing-to-restart tooltips show only the supplied version; without a version, they retain the state label. Desktop update tooltips preserve the complete version and prerelease suffix without adding v or V; their error summaries match the shell update dialogs, including guidance to exit fullscreen and retry after a fullscreen-transition timeout.
@@ -65,7 +67,7 @@ The shell declares settings.launcher for an account-owned sidebar menu and retai
Implementation internals — click to expand
-The shell owns the chrome and the projections; it contributes the Coding Tools and Current version rows, while feature registrants own their additional content and copy.
+The shell owns the chrome and the projections; it contributes the Coding Tools and Current version rows, while feature registrants own their additional content and copy. The panel's four seats — title, header, navigation, section — are its direct children and take their places from its grid, so one markup tree serves both the desktop rail and the mobile strip.
### Ledger projections
diff --git a/packages/client/ui-settings-general/README.zh.md b/packages/client/ui-settings-general/README.zh.md
index 616bfd3c84..94c5351d64 100644
--- a/packages/client/ui-settings-general/README.zh.md
+++ b/packages/client/ui-settings-general/README.zh.md
@@ -24,16 +24,18 @@ kind: "package-reference"
设置面板采用各页面共享的 800 × 800 布局,并受视口大小约束。较长的页面在内容栏内部滚动;账号入口使用账号图标。面板挂载在 `#root` 之外而不是其内部,这样在文档序上更靠后的 chrome 行所声明的 macOS 窗口拖拽区不会吞掉它的控件。
+视口宽度低于 768px 时,面板铺满视口,导航变为横向条带:标题与头部操作、关闭入口同处第一行,注册的分区单元在第二行横向滚动,当前分区占满其余空间。单元会在富余宽度中伸展并保有一个最小宽度,条带滚动而不截断,因此注册方的文案长度或更多的分区都不会换行或裁掉名称;选中的单元承接初始焦点,从而被带入条带的可视范围。该宽度下分区内容保留 16px 的行内边距,分隔偏好标签与其控件的 48px 间距也随之收窄。
+
## 使用本包
用户通过侧边栏底部的 Settings 控件进入外壳;功能插件通过本外壳所投影的 slot 账本贡献自己的页面与引导步骤。在展开侧边栏和收起轨道中,该控件都会把本地化的 Settings 文案作为其可访问名称。Settings 右侧浅黄色的**连接异常**操作表示浏览器离线暂停;其常驻重试图形与中文文案「连接异常,刷新重试」都指明重试动作。每次恢复尝试都显示共享 ongoing loading 加**重新连接中**,其后一至三个点每 500ms 前进一次,且每次尝试至少可见 800ms,短暂重试不会闪动。选中任一黄色状态都会立即发起重试;按压反馈留在黄色色阶内。恢复后该区域变为浅绿色的**连接成功**,从绿色药丸可见起驻留 2 秒再消失。药丸出现时淡入、移除时以 150ms 淡出,宽度随当前文案自适应。首次启动与未曾中断的健康连接保持静默。外壳渲染模态面板、由 `settings.section` 条目构建的导航,以及每次只挂载一个的引导步骤。
-设置面板使用[框架共享的浮层起点](../ui-layout/README.zh.md#window-chrome-seat),视口边距至少为 24px。其遮罩不绘制 Windows 顶栏区域,覆盖整个视口的模态层仍阻挡背景点击。
+在移动端宽度以上,设置面板使用[框架共享的浮层起点](../ui-layout/README.zh.md#window-chrome-seat),视口边距至少为 24px。其遮罩不绘制 Windows 顶栏区域,覆盖整个视口的模态层仍阻挡背景点击;移动端铺满视口的面板覆盖该遮罩。
页面重连到运行另一个 release 的 Host 时,会弹出**服务版本变化**对话框,画出两条线:**服务**从 Host 之前的 release(灰色)指向它当前的 release,用细的绿色实线箭头连接,表示 Host 已经切换;**页面**从页面启动时的 release 指向 Host 的 release(灰色),用同样细的灰色虚线箭头连接,表示要刷新后才完成。每行两端版本号不同时显示版本号,否则显示 commit。页面不应用该 release 的任何插件更新。**刷新以加载新版本**加载它;**暂不刷新**、关闭按钮、点击遮罩和 Escape 都让页面保持自己的 release,并不再为该 Host release 提示。Host 回到页面的 release 时,对话框关闭,热更新恢复。
-当分区导航超出面板可用高度时,列表独立于设置内容滚动,并保持「设置」标题固定。
+当分区导航超出面板可用高度时,列表独立于设置内容滚动,并保持「设置」标题固定;低于 768px 时条带以同样方式沿自身轴滚动。
在 Desktop 中,账户行更新控件显示可用版本、进度、验证、就绪和持久重试反馈。它与连接指示器共用 28px 高度、8px 圆角、14px 图标占位、4px 图文间距,以及 12px 中等字重文字和 18px 行高;更新状态的边框、填充和文字颜色独立定义。重试文字及其圆点与其他更新文案使用相同的品牌蓝色;数字下载进度不带省略号。预加载仅传递语义化阶段、版本、进度和失败类别;组件从当前 `settings` locale 解析全部可见与无障碍文案,并在应用内语言变化后同步更新。选择可用更新即开始下载;安装需要独立的壳拥有的确认。侧栏收起时,其顶部展开按钮以品牌蓝色圆点显示相同状态,包括失败状态。连接反馈通常优先展示,但壳报告正在安装时,预期的后端断开不能遮盖更新状态;失败后恢复连接反馈。两个控件共用一次载体订阅;浏览器代码不能选择安装包或授权安装。[Desktop 更新](../../../apps/desktop/README.zh.md)负责发布流程。检查中、校验中和准备重启状态的悬浮提示只显示已提供的版本号;没有版本号时保留状态文案。桌面更新提示完整保留版本号及预发布后缀,不额外添加 v 或 V;错误提示与桌面升级弹窗保持一致,包括全屏切换超时后退出全屏并重试的指引。
@@ -65,7 +67,7 @@ Web 与桌面端的通用设置底部显示当前发布版本,使用构建注
实现细节——点击展开
-外壳拥有界面框架与投影,并提供代码工作工具行与当前版本行;功能注册方拥有其余内容与文案。
+外壳拥有界面框架与投影,并提供代码工作工具行与当前版本行;功能注册方拥有其余内容与文案。面板的四个座位——标题、头部、导航、分区——是它的直接子节点并按网格落位,因此同一棵标记树同时服务桌面导航栏与移动端条带。
### 账本投影
diff --git a/packages/client/ui-settings-general/src/client/SettingsRoot.module.css b/packages/client/ui-settings-general/src/client/SettingsRoot.module.css
index 0f695de58a..ac0c275dac 100644
--- a/packages/client/ui-settings-general/src/client/SettingsRoot.module.css
+++ b/packages/client/ui-settings-general/src/client/SettingsRoot.module.css
@@ -81,11 +81,20 @@
content-sized panel would resize under the pointer on every nav click.
The viewport height leaves 24px of air above and below, widened to the
frame's published overlay inset on both sides, so the panel sits centered
- and clear of the strip. */
+ and clear of the strip.
+ The four shell seats are direct children placed by area: the rail's title
+ and the content header share the first row, the rail's cells and the active
+ section the second. The mobile block at the end of this file rearranges the
+ same areas into the horizontal layout, so only the areas move. */
.panel {
position: relative;
z-index: 1;
- display: flex;
+ display: grid;
+ grid-template-columns: 188px minmax(0, 1fr);
+ grid-template-rows: auto minmax(0, 1fr);
+ grid-template-areas:
+ 'title header'
+ 'nav options';
width: 800px;
height: min(800px, calc(100vh - 2 * max(24px, var(--dsh-frame-overlay-top, 24px))));
max-width: calc(100vw - 48px);
@@ -106,21 +115,25 @@
outline: none;
}
-/* Nav rail (figma .Setting-nav 501:29958): 188 wide, pad (12,22,12,0),
- gap 18, no own fill — the panel white shows through. */
+/* Nav rail (figma .Setting-nav 501:29958): the 188px track's cells, pad
+ (12,0), no own fill — the panel white shows through. */
.nav {
- flex: none;
+ grid-area: nav;
+ min-width: 0;
+ min-height: 0;
display: flex;
flex-direction: column;
- gap: 18px;
- width: 188px;
- padding: 22px 12px 0;
+ padding: 0 12px;
box-sizing: border-box;
}
-/* Title row (figma 501:29959): 16/500 lh24, 12px side padding. */
+/* Title row (figma 501:29959): 16/500 lh24, at the rail's 24px text inset.
+ The rail's own 22px top padding and 18px gap below the title become this
+ seat's padding, so the first grid row measures the 64px the title and gap
+ occupied rather than the 54px content header beside it. */
.navTitle {
- padding: 0 12px;
+ grid-area: title;
+ padding: 22px 12px 18px 24px;
font-size: 16px;
line-height: 24px;
font-weight: 500;
@@ -132,6 +145,7 @@
display: flex;
flex-direction: column;
gap: 4px;
+ min-height: 0;
overflow-y: auto;
}
@@ -176,17 +190,9 @@
text-overflow: ellipsis;
}
-/* Content column (figma Content 501:29980): header + options. */
-.content {
- flex: 1;
- min-width: 0;
- display: flex;
- flex-direction: column;
-}
-
/* Header (figma .Header 501:29981): h54, pad (10,20,14,8), close right. */
.header {
- flex: none;
+ grid-area: header;
display: flex;
align-items: flex-start;
justify-content: space-between;
@@ -226,12 +232,87 @@
/* Options area (figma Options 501:29983): pad (24,0,24,24), scrolls. */
.options {
- flex: 1;
+ grid-area: options;
+ min-width: 0;
min-height: 0;
padding: 0 24px 24px;
overflow-y: auto;
}
+/* Phones and narrow windows, under the 768px boundary ui-sidebar-right's
+ fullscreen rule and PlanPreview already use: the panel takes the viewport and
+ the rail becomes the horizontal tab strip the Web product uses. The title
+ shares the first row with the header's action and close seats, the registered
+ section cells scroll sideways on the second, and the active section fills the
+ rest. Cells keep their labels whole and the strip scrolls instead of
+ truncating, so a registrant's label length or section count cannot wrap or
+ clip a name; the active cell carries data-modal-autofocus, so opening the
+ panel scrolls it into view. */
+@media (max-width: 767px) {
+ .panel {
+ grid-template-columns: minmax(0, 1fr) auto;
+ grid-template-rows: auto auto minmax(0, 1fr);
+ grid-template-areas:
+ 'title header'
+ 'nav nav'
+ 'options options';
+ width: 100%;
+ max-width: none;
+ height: 100%;
+ border-radius: 0;
+ }
+
+ .navTitle {
+ padding: 22px 0 0 16px;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ }
+
+ .header {
+ height: auto;
+ padding: 20px 16px 0 10px;
+ }
+
+ .nav {
+ padding: 0;
+ }
+
+ /* The strip runs edge to edge and pads its own ends, so the first cell lines
+ up with the title and the last one stays clear of the panel edge. */
+ .navList {
+ flex-direction: row;
+ gap: 4px;
+ padding: 12px 16px 14px;
+ overflow-x: auto;
+ overflow-y: hidden;
+ overscroll-behavior: none;
+ scrollbar-width: none;
+ }
+
+ .navList::-webkit-scrollbar {
+ display: none;
+ }
+
+ /* Cells grow into spare width and hold a 108px floor. The four shipped
+ sections already measure more than a 390px viewport, so the next cell shows
+ past the edge and the strip reads as scrollable. */
+ .navCell {
+ flex: 1 0 auto;
+ justify-content: center;
+ min-width: 108px;
+ padding: 0 14px;
+ }
+
+ .navLabel {
+ flex: none;
+ }
+
+ .options {
+ padding: 0 16px 24px;
+ }
+}
+
/* Visually-hidden text seat (close button accessible name from slot content). */
.hiddenLabel {
position: absolute;
diff --git a/packages/client/ui-settings-general/src/client/SettingsRoot.tsx b/packages/client/ui-settings-general/src/client/SettingsRoot.tsx
index 761ae2dc0f..3066d9530d 100644
--- a/packages/client/ui-settings-general/src/client/SettingsRoot.tsx
+++ b/packages/client/ui-settings-general/src/client/SettingsRoot.tsx
@@ -1,14 +1,16 @@
/**
* Settings shell root: the sidebar-foot trigger row plus the centered modal
- * panel (figma 2552:26025, 760x500) with the section nav rail. The shell is
- * a pure composition face — slot-owned text (trigger label, panel title,
- * close label, sections) arrives from registrants through slots; accessible
- * names resolve from localized content (trigger: shell locale; dialog:
- * aria-labelledby the title node; close: visually-hidden slot text). Modal
- * open state and the active section id belong to the declared owner store;
- * the onboarding coordinator mounts exactly one ordered registrant while the
- * sessions-derived empty-Hero fact is active. Visible dialog chrome belongs
- * to the step, so a mounted-but-deciding step paints nothing here.
+ * panel (figma 2552:26025, 760x500) with the section nav rail; below 768px
+ * the panel takes the viewport and the rail turns into a horizontal strip
+ * (SettingsRoot.module.css). The shell is a pure composition face —
+ * slot-owned text (trigger label, panel title, close label, sections) arrives
+ * from registrants through slots; accessible names resolve from localized
+ * content (trigger: shell locale; dialog: aria-labelledby the title node;
+ * close: visually-hidden slot text). Modal open state and the active section
+ * id belong to the declared owner store; the onboarding coordinator mounts
+ * exactly one ordered registrant while the sessions-derived empty-Hero fact
+ * is active. Visible dialog chrome belongs to the step, so a
+ * mounted-but-deciding step paints nothing here.
*/
import { useCallback, useEffect, useId, useLayoutEffect, useRef, useState } from 'react'
import { createPortal } from 'react-dom'
@@ -68,9 +70,19 @@ function SettingsPanel({ rows, renderSlot, activeId, onSelect, onClose }: PanelP
+ {/* The four seats stay direct children of the panel and take their
+ places from its grid: title and header share the first row, the
+ navigation and the active section the second. */}
+