Merge pull request #6669 from deepseek-harness/feat/narrow-settings-pages
fix(client): reflow settings and task fields for narrow screens
This commit is contained in:
commit
e98d49d094
56 files changed
+539
-96
No files matched your search
@@ -26,6 +26,10 @@ it('keeps settings and expanded plugin dialogs clear of the Windows caption acro
|
||||
await openSettings(page, 'zh')
|
||||
const settings = page.getByRole('dialog', { name: '设置', exact: true })
|
||||
const settingsMask = settings.locator('..').locator(':scope > [aria-hidden="true"]')
|
||||
const permissionText = settings.getByText('权限', { exact: true }).locator('..')
|
||||
expect(await permissionText.evaluate(el => getComputedStyle(el).paddingRight)).toBe('48px')
|
||||
const cubes = settings.getByRole('button', { name: /浅色|深色|跟随系统/ })
|
||||
expect(await cubes.first().evaluate(el => getComputedStyle(el).flexBasis)).toBe('180px')
|
||||
for (const fullscreen of [false, true, false]) {
|
||||
await page.evaluate(value => document.documentElement.toggleAttribute('data-fullscreen', value), fullscreen)
|
||||
const margin = fullscreen ? 24 : 60
|
||||
@@ -86,8 +90,10 @@ it('keeps settings and expanded plugin dialogs clear of the Windows caption acro
|
||||
}, platform)
|
||||
if (await reopenSidebar.isVisible()) await reopenSidebar.click()
|
||||
await openSettings(page, 'zh')
|
||||
await expect.poll(async () => (await settings.boundingBox())?.y).toBe(platform === 'darwin' ? 68 : 24)
|
||||
await expect.poll(async () => (await settings.boundingBox())?.y).toBe(platform === 'darwin' ? 68 : 0)
|
||||
expect((await settingsMask.boundingBox())?.y).toBe(0)
|
||||
expect(await permissionText.evaluate(el => getComputedStyle(el).paddingRight)).toBe(platform === 'darwin' ? '48px' : '16px')
|
||||
expect(await cubes.first().evaluate(el => getComputedStyle(el).flexBasis)).toBe(platform === 'darwin' ? '180px' : '0px')
|
||||
await page.keyboard.press('Escape')
|
||||
await settings.waitFor({ state: 'hidden' })
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
- dialog "设置":
|
||||
- text: 设置
|
||||
- navigation:
|
||||
- text: 设置
|
||||
- button "通用设置"
|
||||
- button "模型"
|
||||
- button "内置插件"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
- dialog "设置":
|
||||
- text: 设置
|
||||
- navigation:
|
||||
- text: 设置
|
||||
- button "通用设置"
|
||||
- button "模型"
|
||||
- button "内置插件"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
- dialog "设置":
|
||||
- text: 设置
|
||||
- navigation:
|
||||
- text: 设置
|
||||
- button "通用设置"
|
||||
- button "模型"
|
||||
- button "内置插件"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
- dialog "Settings":
|
||||
- text: Settings
|
||||
- navigation:
|
||||
- text: Settings
|
||||
- button "General"
|
||||
- button "Models"
|
||||
- button "Built-in plugins"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
- dialog "设置":
|
||||
- text: 设置
|
||||
- navigation:
|
||||
- text: 设置
|
||||
- button "通用设置"
|
||||
- button "模型"
|
||||
- button "内置插件"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
- dialog "设置":
|
||||
- text: 设置
|
||||
- navigation:
|
||||
- text: 设置
|
||||
- button "通用设置"
|
||||
- button "模型"
|
||||
- button "内置插件"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
- dialog "设置":
|
||||
- text: 设置
|
||||
- navigation:
|
||||
- text: 设置
|
||||
- button "通用设置"
|
||||
- button "模型"
|
||||
- button "内置插件"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
- dialog "设置":
|
||||
- text: 设置
|
||||
- navigation:
|
||||
- text: 设置
|
||||
- button "通用设置"
|
||||
- button "模型"
|
||||
- button "内置插件"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
- dialog "设置":
|
||||
- text: 设置
|
||||
- navigation:
|
||||
- text: 设置
|
||||
- button "通用设置"
|
||||
- button "模型"
|
||||
- button "内置插件"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
- dialog "设置":
|
||||
- text: 设置
|
||||
- navigation:
|
||||
- text: 设置
|
||||
- button "通用设置"
|
||||
- button "模型"
|
||||
- button "内置插件"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
- dialog "设置":
|
||||
- text: 设置
|
||||
- navigation:
|
||||
- text: 设置
|
||||
- button "通用设置"
|
||||
- button "模型"
|
||||
- button "内置插件"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
- dialog "设置":
|
||||
- text: 设置
|
||||
- navigation:
|
||||
- text: 设置
|
||||
- button "通用设置"
|
||||
- button "模型"
|
||||
- button "内置插件"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
- dialog "设置":
|
||||
- text: 设置
|
||||
- navigation:
|
||||
- text: 设置
|
||||
- button "通用设置"
|
||||
- button "模型"
|
||||
- button "内置插件"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
- dialog "设置":
|
||||
- text: 设置
|
||||
- navigation:
|
||||
- text: 设置
|
||||
- button "通用设置"
|
||||
- button "模型"
|
||||
- button "内置插件"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
- dialog "设置":
|
||||
- text: 设置
|
||||
- navigation:
|
||||
- text: 设置
|
||||
- button "通用设置"
|
||||
- button "模型"
|
||||
- button "内置插件"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
- dialog "设置":
|
||||
- text: 设置
|
||||
- navigation:
|
||||
- text: 设置
|
||||
- button "通用设置"
|
||||
- button "模型"
|
||||
- button "内置插件"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
- dialog "设置":
|
||||
- text: 设置
|
||||
- navigation:
|
||||
- text: 设置
|
||||
- button "通用设置"
|
||||
- button "模型"
|
||||
- button "内置插件"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
- dialog "Settings":
|
||||
- text: Settings
|
||||
- navigation:
|
||||
- text: Settings
|
||||
- button "General"
|
||||
- button "Models"
|
||||
- button "Built-in plugins"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
- dialog "Settings":
|
||||
- text: Settings
|
||||
- navigation:
|
||||
- text: Settings
|
||||
- button "General"
|
||||
- button "Models"
|
||||
- button "Built-in plugins"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
- dialog "设置":
|
||||
- text: 设置
|
||||
- navigation:
|
||||
- text: 设置
|
||||
- button "通用设置"
|
||||
- button "模型"
|
||||
- button "内置插件"
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
// UI-only phone geometry for the Automation tasks page: no text field it owns
|
||||
// sits under the 16px touch step, so the page never reads wider than the screen
|
||||
// — iOS Safari zooms the visual viewport for a focused field below it.
|
||||
import type { Browser, Locator, Page } from 'playwright'
|
||||
import { chromium } from 'playwright'
|
||||
import { expect, it } from 'vitest'
|
||||
import { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import type {} from '@deepseek-ai/dsh-schedule'
|
||||
import { launchWebScaffold, watchConsole } from './scaffold.ts'
|
||||
|
||||
const PHONE_TASK_SESSION = SessionId('schedule-phone-web-e2e')
|
||||
const PHONE_TASK_TITLE = 'Review overdue deployment'
|
||||
const PHONE_PAGE_WIDTH = 390
|
||||
|
||||
/** The page's box against the screen, plus the font steps of its text fields. */
|
||||
interface ViewFacts {
|
||||
width: number
|
||||
documentWidth: number
|
||||
smallestField: number
|
||||
name: string
|
||||
search: string
|
||||
instruction: string
|
||||
}
|
||||
|
||||
/** Open the Automation tasks page and return its panel. */
|
||||
async function openTaskManager(page: Page, url: string): Promise<Locator> {
|
||||
await page.goto(url, { waitUntil: 'load' })
|
||||
await page.getByRole('button', { name: '自动化任务', exact: true }).click()
|
||||
const panel = page.getByTestId('task-manager-page')
|
||||
await panel.getByRole('heading', { name: '自动化任务', exact: true }).waitFor({ timeout: 30_000 })
|
||||
await page.evaluate(() => document.fonts.ready)
|
||||
return panel
|
||||
}
|
||||
|
||||
/** Read the facts above from the open view. */
|
||||
async function viewFacts(page: Page): Promise<ViewFacts> {
|
||||
return page.evaluate(() => {
|
||||
const panel = document.querySelector('[data-testid="task-manager-page"]')
|
||||
const fields = Array.from(panel?.querySelectorAll('input, textarea, select') ?? [])
|
||||
const steps = fields.map(element => Number.parseFloat(getComputedStyle(element).fontSize))
|
||||
const step = (selector: string): string => {
|
||||
const element = document.querySelector(selector)
|
||||
return element === null ? 'absent' : getComputedStyle(element).fontSize
|
||||
}
|
||||
return {
|
||||
width: window.innerWidth,
|
||||
documentWidth: document.documentElement.scrollWidth,
|
||||
smallestField: steps.length === 0 ? -1 : Math.min(...steps),
|
||||
name: step('[class*="_editName"]'),
|
||||
search: step('[class*="_searchField"] input'),
|
||||
instruction: step('[class*="_instruction"]'),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
it('keeps the automation page on screen with its text fields at the touch step', async () => {
|
||||
const scaffold = await launchWebScaffold()
|
||||
const url = scaffold.authenticatedUrl
|
||||
try {
|
||||
await scaffold.ctx.schedule.create(PHONE_TASK_SESSION, {
|
||||
prompt: 'Check the deployment log', title: PHONE_TASK_TITLE, at: '2099-08-25T12:08:00.000Z',
|
||||
})
|
||||
const browser: Browser = await chromium.launch()
|
||||
try {
|
||||
// A coarse pointer is the touch device, so the phone context takes the
|
||||
// 16px step.
|
||||
const phone = await browser.newContext({
|
||||
viewport: { width: PHONE_PAGE_WIDTH, height: 844 }, deviceScaleFactor: 3, isMobile: true, hasTouch: true, locale: 'zh-CN',
|
||||
})
|
||||
const phonePage = await phone.newPage()
|
||||
const phoneConsole = watchConsole(phonePage)
|
||||
const phonePanel = await openTaskManager(phonePage, url)
|
||||
await expect.poll(() => viewFacts(phonePage)).toEqual({
|
||||
width: PHONE_PAGE_WIDTH,
|
||||
documentWidth: PHONE_PAGE_WIDTH,
|
||||
smallestField: 16,
|
||||
name: 'absent',
|
||||
search: '16px',
|
||||
instruction: 'absent',
|
||||
})
|
||||
// The detail keeps the floor too, and the name control keeps the 20px step
|
||||
// the floor must not flatten.
|
||||
await phonePanel.getByRole('button', { name: PHONE_TASK_TITLE, exact: true }).click()
|
||||
await phonePanel.locator('[class*="_detailScroll"]').waitFor({ timeout: 10_000 })
|
||||
await expect.poll(() => viewFacts(phonePage)).toEqual({
|
||||
width: PHONE_PAGE_WIDTH,
|
||||
documentWidth: PHONE_PAGE_WIDTH,
|
||||
smallestField: 16,
|
||||
name: '20px',
|
||||
search: '16px',
|
||||
instruction: '16px',
|
||||
})
|
||||
await phone.close()
|
||||
|
||||
// A fine pointer keeps the compact steps: the floor is pointer-scoped.
|
||||
const desktop = await browser.newContext({ viewport: { width: 1024, height: 800 }, locale: 'zh-CN' })
|
||||
const desktopPage = await desktop.newPage()
|
||||
const desktopConsole = watchConsole(desktopPage)
|
||||
const desktopPanel = await openTaskManager(desktopPage, url)
|
||||
await expect.poll(() => viewFacts(desktopPage)).toEqual({
|
||||
width: 1024,
|
||||
documentWidth: 1024,
|
||||
smallestField: 14,
|
||||
name: 'absent',
|
||||
search: '14px',
|
||||
instruction: 'absent',
|
||||
})
|
||||
await desktopPanel.getByRole('button', { name: PHONE_TASK_TITLE, exact: true }).click()
|
||||
await desktopPanel.locator('[class*="_detailScroll"]').waitFor({ timeout: 10_000 })
|
||||
await expect.poll(() => viewFacts(desktopPage)).toEqual({
|
||||
width: 1024,
|
||||
documentWidth: 1024,
|
||||
smallestField: 14,
|
||||
name: '20px',
|
||||
search: '14px',
|
||||
instruction: '14px',
|
||||
})
|
||||
await desktop.close()
|
||||
|
||||
expect(phoneConsole.pageErrors).toEqual([])
|
||||
expect(phoneConsole.warnings).toEqual([])
|
||||
expect(desktopConsole.pageErrors).toEqual([])
|
||||
expect(desktopConsole.warnings).toEqual([])
|
||||
} finally {
|
||||
await browser.close()
|
||||
}
|
||||
} finally {
|
||||
await scaffold.close()
|
||||
}
|
||||
}, 180_000)
|
||||
@@ -84,6 +84,17 @@ describe('web e2e: settings modal and General preferences', () => {
|
||||
const dialog = page.getByRole('dialog', { name: '设置' })
|
||||
await dialog.waitFor({ timeout: 10_000 })
|
||||
expect(await trigger.getAttribute('aria-expanded')).toBe('false')
|
||||
const panelBox = await dialog.boundingBox()
|
||||
const optionsBox = await dialog.locator('[class*="_options"]').boundingBox()
|
||||
const firstSectionBox = await dialog.getByRole('navigation').getByRole('button').first().boundingBox()
|
||||
expect(optionsBox!.y - panelBox!.y).toBe(54)
|
||||
expect(firstSectionBox!.y - panelBox!.y).toBe(64)
|
||||
const lastSection = dialog.getByRole('navigation').getByRole('button').last()
|
||||
await lastSection.focus()
|
||||
await page.keyboard.press('Tab')
|
||||
expect(await dialog.getByRole('button', { name: '打开配置文件' }).evaluate(el => el === document.activeElement)).toBe(true)
|
||||
await page.keyboard.press('Tab')
|
||||
expect(await dialog.getByRole('button', { name: '关闭', exact: true }).evaluate(el => el === document.activeElement)).toBe(true)
|
||||
// General is active by default; Permission, Language and Appearance are functional.
|
||||
expect(await dialog.getByRole('button', { name: '通用设置' }).getAttribute('aria-current')).toBe('true')
|
||||
await dialog.getByRole('button', { name: '工作区内修改' }).waitFor({ timeout: 10_000 })
|
||||
@@ -210,7 +221,7 @@ describe('web e2e: settings modal and General preferences', () => {
|
||||
await page.keyboard.press('Escape')
|
||||
await expect.poll(() => page.getByRole('dialog', { name: '设置' }).count(), { timeout: 5_000 }).toBe(0)
|
||||
expect(await trigger.getAttribute('aria-expanded')).toBe('false')
|
||||
// Close path 2: the header close button (focus lands there on open).
|
||||
// Close path 2: the header close button.
|
||||
await openSettings(page, 'zh')
|
||||
await page.getByRole('dialog', { name: '设置' }).getByRole('button', { name: '关闭' }).click()
|
||||
await expect.poll(() => page.getByRole('dialog', { name: '设置' }).count(), { timeout: 5_000 }).toBe(0)
|
||||
@@ -227,7 +238,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 +272,98 @@ 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)
|
||||
|
||||
// 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)
|
||||
|
||||
await cells.first().focus()
|
||||
for (let index = 1; index < await cells.count(); index += 1) {
|
||||
await phone.keyboard.press('Tab')
|
||||
}
|
||||
const last = cells.last()
|
||||
expect(await last.evaluate(el => el === document.activeElement)).toBe(true)
|
||||
const lastBox = await last.boundingBox()
|
||||
// Scroll offsets can round while glyph widths remain fractional.
|
||||
expect(Math.round(lastBox!.x)).toBeGreaterThanOrEqual(0)
|
||||
expect(Math.round(lastBox!.x + lastBox!.width)).toBeLessThanOrEqual(390)
|
||||
await last.click()
|
||||
await expect.poll(() => last.getAttribute('aria-current')).toBe('true')
|
||||
await phone.keyboard.press('Escape')
|
||||
await dialog.waitFor({ state: 'hidden' })
|
||||
await openSettings(phone, 'zh')
|
||||
const active = dialog.locator('[aria-current="true"]')
|
||||
expect(await active.textContent()).toBe('通用设置')
|
||||
expect(await active.evaluate(el => el === document.activeElement)).toBe(true)
|
||||
|
||||
const close = dialog.getByRole('button', { name: '关闭', exact: true })
|
||||
const closeBox = await close.boundingBox()
|
||||
expect(closeBox!.width).toBe(28)
|
||||
expect(closeBox!.x).toBeGreaterThanOrEqual(0)
|
||||
expect(closeBox!.y).toBeGreaterThanOrEqual(0)
|
||||
expect(closeBox!.x + closeBox!.width).toBeLessThanOrEqual(390)
|
||||
expect(closeBox!.y + closeBox!.height).toBeLessThanOrEqual(844)
|
||||
await close.click()
|
||||
await dialog.waitFor({ state: 'hidden' })
|
||||
const trigger = phone.getByRole('button', { name: '账号菜单', exact: true })
|
||||
await expect.poll(() => trigger.evaluate(el => el === document.activeElement)).toBe(true)
|
||||
|
||||
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'))
|
||||
|
||||
@@ -90,6 +90,7 @@
|
||||
"tests/cua-optin.e2e.ts",
|
||||
"tests/cua-permissions.e2e.ts",
|
||||
"tests/schedule-header.e2e.ts",
|
||||
"tests/schedule-phone.e2e.ts",
|
||||
"tests/worktree-opt-in.e2e.ts",
|
||||
"tests/session-inspector.e2e.ts",
|
||||
"tests/shared-inspector.e2e.ts",
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
en: 7fedfbcbc3ad8cea
|
||||
zh: e29ba5230e0b9ecb
|
||||
/deepseek-ai-dsh-client-locale/use-this-package/choosing-a-language:
|
||||
en: 8527d77583824eff
|
||||
zh: 2ac53ced0a32ba19
|
||||
en: 2b82458e185ba321
|
||||
zh: e55a7e709a3f0158
|
||||
/deepseek-ai-dsh-client-locale/use-this-package/registering-a-dictionary:
|
||||
en: 9b2fcfa8cae8dc4b
|
||||
zh: 293012a4d364ef59
|
||||
|
||||
@@ -29,6 +29,8 @@ Use it wherever the web GUI needs a language switch or translated copy: the ship
|
||||
|
||||
### Choosing a language
|
||||
|
||||
The Language row uses a smaller label gutter in narrow browser settings; Windows and macOS desktop windows retain the standard spacing.
|
||||
|
||||
Open Settings → General and select a registered language. The active locale is applied immediately: the UI copy switches, `<html lang>` points at the external id or built-in document tag, and the choice is written to the durable settings section. A browser without an explicit Host preference selects the first registered language that matches `navigator` by full tag and then primary subtag, falling back to English. A stored external locale waits for its definition to register instead of becoming active while unavailable.
|
||||
|
||||
Native shells may provide `__DSH_LOCALE__` with an asynchronous `read()` and an `onChange(locale)` callback. Initialization supplies the current Host preference and ordered OS languages before the Client mounts. Automatic selection stays provisional; only Settings selections write `locale.preference`. A fresh read on each page load prevents a stale preload preference after reload. Ordinary browsers keep navigator-based detection and their existing settings-scope policy.
|
||||
|
||||
@@ -29,6 +29,8 @@ kind: "package-reference"
|
||||
|
||||
### 选择语言
|
||||
|
||||
窄屏浏览器设置中,语言行缩小标签与控件的间距;Windows 和 macOS 桌面窗口保留标准间距。
|
||||
|
||||
打开“设置 → 常规”并选择一种已注册语言。生效中的 locale 会立即应用:UI 文案切换、`<html lang>` 指向外部 id 或内置语言的文档标签,选择写入持久设置分区。没有显式 Host 偏好的浏览器会按完整标签、再按主语言子标签选择 `navigator` 请求的第一个已注册语言,无法匹配时回退到英文。已存储的外部 locale 会等待其定义注册,不会在不可用时生效。
|
||||
|
||||
原生壳可以提供包含异步 `read()` 和 `onChange(locale)` 回调的 `__DSH_LOCALE__`。初始化在 Client 挂载前提供当前 Host 偏好和有序的系统语言列表。自动选择保持临时状态;只有设置中的选择会写入 `locale.preference`。每次加载页面都重新读取,避免重载后沿用过期的 preload 偏好。普通浏览器继续使用 navigator 检测和原有的设置作用域策略。
|
||||
|
||||
@@ -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) {
|
||||
:global(html:not([data-windows-titlebar]):not([data-platform='darwin'])) .rowText {
|
||||
padding-right: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
|
||||
@@ -24,8 +24,8 @@
|
||||
en: 41dcd70e14ebe9cc
|
||||
zh: ededead572b59037
|
||||
/deepseek-ai-dsh-client-ui-chat/turn-token-usage:
|
||||
en: bb20da372b144302
|
||||
zh: 75a8c5ab7ec8d665
|
||||
en: 059eef868161efe4
|
||||
zh: 52ecc8e682d4c630
|
||||
/deepseek-ai-dsh-client-ui-chat/completed-turn-footer:
|
||||
en: 382821ca3ffa60dd
|
||||
zh: 9840ffa07c13082a
|
||||
|
||||
@@ -72,6 +72,8 @@ The composer statistics are two `conversation.composer.dock` list entries: `acti
|
||||
|
||||
Explicit selections update every consumer immediately; accepted Host settings reconcile the live value, so a refused or pending write keeps the selected mode until the Host answers.
|
||||
|
||||
In narrow browser settings, preference labels use a smaller gutter before their controls. Windows and macOS desktop windows retain the standard spacing.
|
||||
|
||||
Preference menus restore focus to their trigger without scrolling before publishing a new selection.
|
||||
|
||||
<a id="completed-turn-footer"></a>
|
||||
|
||||
@@ -72,6 +72,8 @@ Assistant 尝试结束且没有可见消息时,Chat 隐藏已发布的 Node,
|
||||
|
||||
明确选择会立即更新所有使用方;Host 接受的设置会校正实时值,因此写入被拒绝或尚未应答时保持所选模式,直到 Host 应答。
|
||||
|
||||
窄屏浏览器设置中,偏好标签与控件的间距缩小;Windows 和 macOS 桌面窗口保留标准间距。
|
||||
|
||||
偏好菜单在发布新选择之前,先将焦点还给触发按钮,且不引起滚动。
|
||||
|
||||
<a id="completed-turn-footer"></a>
|
||||
|
||||
@@ -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) {
|
||||
:global(html:not([data-windows-titlebar]):not([data-platform='darwin'])) .rowText {
|
||||
padding-right: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
|
||||
@@ -21,8 +21,8 @@
|
||||
en: 7871f9fe6bcc4459
|
||||
zh: 0d51565c600b3958
|
||||
/deepseek-ai-dsh-client-ui-conversation/shell-and-standard-props:
|
||||
en: a7a0b235e1c5f2b0
|
||||
zh: ca24e6bf77ad7e1b
|
||||
en: 457e642073206f7d
|
||||
zh: 95ea47c204850314
|
||||
/deepseek-ai-dsh-client-ui-conversation/temporary-composer-entries:
|
||||
en: bb1244310f5c0382
|
||||
zh: 824b84077432b559
|
||||
|
||||
@@ -104,6 +104,8 @@ The width-handle indicator follows the pointer only during a captured drag; ordi
|
||||
|
||||
The resident composer survives no-Session and Session transitions. Whitespace hides its placeholder; a whitespace-only draft without attachments cannot be sent. The no-Session state keeps the same composer surface mounted but inert while the Workspace picker connects a blank Session. The surface is a shell-owned Lexical editor: reference chips are atomic decorator nodes carrying the owner's serialization identity (submission expands them through the owner codec), the claimed slash command is an atomic leading chip that carries its claim, folder text references carry the folder glyph as an icon prefix, and the draft's text and semantic references are persisted by the per-Session Conversation store. QueueDock reads `next-turn` from the Session `inbox` projection, including cold recovered messages, and excludes only requestIds still owned by local transcript submissions. Other queued rows retain their normal display and actions. Queue operations address exact queue occurrences through the scoped `ctx.conversation` service; queue previews use the [UserText Factory](#sent-user-text) (wire session forms fold to their label) and show local or durable images and files in original attachment order. Images use thumbnails; files use compact name-and-size cards. An edit exposes the literal sent text in the ui-primitives `InlineEditor`, so line breaks survive re-editing. Durable thumbnails resolve through the session image URL cache. Busy Enter behavior is stored in the Host-backed `ui-conversation` settings namespace. The composer keymap arbitrates the trigger menu's keys through the slash pipeline — Tab settles the highlighted completion (or drills a drillable one), Escape and Shift+Tab leave the menu without settling — and leaves every other key to the editor. An overlay that takes the keyboard hands it back through `SessionInput.focus()`, which rides Lexical's own focus so the caret returns where the draft left it rather than at the start.
|
||||
|
||||
In narrow browser settings, the busy-state Enter preference uses a smaller label gutter. Windows and macOS desktop windows retain the standard spacing.
|
||||
|
||||
Plain Enter uses the configured delivery mode, exactly Ctrl+Enter or Cmd+Enter uses its complement, and Shift+Enter inserts a line break. Enter with Alt, AltGraph, both Ctrl and Cmd, or Shift plus Ctrl/Cmd leaves the draft and command menu unchanged and keeps the DOM event available to application shortcuts. The Conversation plugin registers the fixed send, newline, complementary-delivery, command-menu, and reference-menu rows and reserves their bindings until it unloads.
|
||||
|
||||
Clicking unused space inside an editable composer preserves its current caret or selection when focused, or restores its previous caret or selection when unfocused. Text selection, scrollbars, controls, and embedded overlays keep their own pointer behavior.
|
||||
|
||||
@@ -104,6 +104,8 @@ Session 首次绑定或缓存的 Session 成为 current 时,shell 会在渲染
|
||||
|
||||
常驻 composer 在无 Session 与有 Session 之间保持挂载。输入空白字符会隐藏占位提示;没有附件的纯空白草稿无法发送。无 Session 时,同一个编辑器表面保持 inert,Workspace picker 连接 blank Session。该表面是 shell 所有的 Lexical 编辑器:引用 chip 是携带 owner 序列化身份的原子 decorator 节点(提交时经 owner codec 展开),已认领的 slash command 是携带认领的行首原子 chip,文件夹文本引用以图标前缀携带文件夹图形,草稿文字和引用语义字段由逐 Session Conversation store 持久化。QueueDock 从 Session 的 `inbox` 投影读取 `next-turn`,包含从冷状态恢复的消息,仅排除仍由本地 transcript 提交承接的 requestId。其他排队行保留正常展示和操作。Queue 操作通过 scoped `ctx.conversation` service 寻址准确的 queue occurrence;queue 预览使用 [UserText Factory](#sent-user-text)(wire 会话形式折叠为其标签),并按原始附件顺序展示本地或持久化的图片和文件。图片使用缩略图,文件使用紧凑的名称与大小卡片。编辑态在 ui-primitives 的 `InlineEditor` 中展示字面发送文本,因此重新编辑不会丢失换行。持久化缩略图通过会话图片 URL 缓存解析。繁忙时 Enter 行为保存在 Host-backed `ui-conversation` settings namespace。 composer 键盘映射经斜杠流水线裁决触发菜单的按键——Tab 确认高亮补全项(可下钻项则下钻),Escape 与 Shift+Tab 离开菜单且不选定——其余按键交给编辑器自身。 接管键盘的浮层通过 `SessionInput.focus()` 把键盘还回来,该路径走 Lexical 自己的 focus,因此光标回到草稿原来的位置而不是开头。
|
||||
|
||||
窄屏浏览器设置中,繁忙时 Enter 偏好的标签与控件间距缩小;Windows 和 macOS 桌面窗口保留标准间距。
|
||||
|
||||
普通 Enter 使用已配置的投递模式,严格的 Ctrl+Enter 或 Cmd+Enter 使用互补模式,Shift+Enter 插入换行。带 Alt、AltGraph、同时带 Ctrl 与 Cmd,或 Shift 与 Ctrl/Cmd 的 Enter 保持草稿和指令菜单不变,并将 DOM 事件留给应用快捷键。Conversation 插件注册发送、换行、互补投递、命令菜单和引用菜单的固定条目,并在卸载前保留这些键位。
|
||||
|
||||
点击可编辑输入框内的留白时,已聚焦的输入框保留当前光标或选区,未聚焦的输入框恢复上次的光标或选区。文本选择、滚动条、控件和内嵌浮层保持各自的指针交互行为。
|
||||
|
||||
@@ -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) {
|
||||
:global(html:not([data-windows-titlebar]):not([data-platform='darwin'])) .rowText {
|
||||
padding-right: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
en: d152484eb41ac6b4
|
||||
zh: 09388d293f9be9cb
|
||||
/deepseek-ai-dsh-client-ui-permission-presets/use-this-package:
|
||||
en: 91606bff7da02253
|
||||
zh: ef68e813d64c89be
|
||||
en: b4193258023cf8ae
|
||||
zh: e8e6e0ea0ccb8ef4
|
||||
/deepseek-ai-dsh-client-ui-permission-presets/use-this-package/the-picker:
|
||||
en: 4e5a070ad4321548
|
||||
zh: 010063336bd2b8df
|
||||
|
||||
@@ -27,6 +27,8 @@ Choose permission presets for the current Web session or future sessions. Genera
|
||||
|
||||
Mount this plugin alongside the settings, commands, and conversation packages. General settings receives the future-default row; the composer receives the `conversation.input.permission` control, and bare `/permission` opens the slash picker. Current-session controls require both the Session projection and a complete catalog snapshot. A permission-less composition exposes neither current-session controls nor the Settings row.
|
||||
|
||||
In narrow browser settings, the default-permission label uses a smaller gutter before its control. Windows and macOS desktop windows retain the standard spacing.
|
||||
|
||||
### The picker
|
||||
|
||||
A pick submits the `/permission <preset>` command line. The argument-bearing form (`/permission <preset>` typed directly) still switches directly; the decoration replaces only the bare invocation. The built-in labels are `Read Only`, `Workspace Write`, `Full access`, and `Auto review` in English and `仅可查看`, `工作区内修改`, `完全权限`, and `自动审查` in Chinese. Explicit host labels remain unchanged, unknown kebab-case names render in title case, and `auto` carries an `EXP` badge in menu options plus an experimental-risk confirmation. The selected composer control shows the mode without the badge. `custom` is display state, never a target.
|
||||
|
||||
@@ -27,6 +27,8 @@ kind: "package-reference"
|
||||
|
||||
将本插件与 settings、commands 和 conversation 包一同挂载。通用设置获得未来默认值行;composer 获得 `conversation.input.permission` 控件,裸 `/permission` 打开 slash 选择器。当前会话控件同时要求 Session 投影和完整目录快照。不提供权限功能的装配既不公开当前会话控件,也不公开设置行。
|
||||
|
||||
窄屏浏览器设置中,默认权限标签与控件的间距缩小;Windows 和 macOS 桌面窗口保留标准间距。
|
||||
|
||||
### 选择器
|
||||
|
||||
选中即提交 `/permission <preset>` 命令行。带参形式(直接键入 `/permission <preset>`)仍直接切换;装饰只替换裸调用。内置标签在英文界面中是 `Read Only`、`Workspace Write`、`Full access` 和 `Auto review`,在中文界面中是「仅可查看」「工作区内修改」「完全权限」和「自动审查」。显式 host 标签保持原样,未知 kebab-case 名称渲染为 Title Case;`auto` 在菜单选项中带有 `EXP` 标记,并在可见选择时要求实验风险确认。选中后的 composer 控件显示模式,不展示该标记。`custom` 只是显示状态,绝非目标。
|
||||
|
||||
@@ -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) {
|
||||
:global(html:not([data-windows-titlebar]):not([data-platform='darwin'])) .rowText {
|
||||
padding-right: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
en: d152484eb41ac6b4
|
||||
zh: 405b30feaf48c087
|
||||
/deepseek-ai-dsh-client-ui-schedule/use-this-package:
|
||||
en: 7d44cd1640bebfad
|
||||
zh: 1d6b6c28a3c27115
|
||||
en: 4c0dca55e4e9866d
|
||||
zh: 9ee6b565c7a81353
|
||||
/deepseek-ai-dsh-client-ui-schedule/use-this-package/time-zone-data-source:
|
||||
en: 6bd6882dbbbefcb0
|
||||
zh: 03aa15f1a65d13dc
|
||||
|
||||
@@ -27,7 +27,7 @@ Manage active and inactive tasks from the Automation tasks page and the right Si
|
||||
|
||||
The shipped Web composition mounts `ui-schedule` with the Host Schedule service. The clock context that supplies the model's current time is preset-owned: `standard`, `cordis`, and `ptc` declare it, and `minimal` declares neither it nor the reminder tools. The Automation tasks sidebar entry opens a global page without selecting or activating a Session. The page header's New action starts a new Session, where the reminder is created by asking the model; the page itself has no creation form. Its task list supports text search and one All, Enabled, and Inactive status filter row; search matches the stored task name, the instruction, and the internal Session id, not a resolved Session title, and the empty state shows a decorative document-and-clock illustration above its notice and New action, with colors that follow the light or dark theme. Selecting an active or inactive task opens a second detail level with Rules and Records tabs. Every list row shows the task's stored title with its status, frequency, and next run and never the original Session name; the detail's name control shows the stored title. Rules contains the complete reminder, status, next target for active tasks, original recurrence rule, and task id. Daily rule labels show the saved local time and exact stored IANA zone, preserving nonzero seconds and milliseconds rather than substituting the browser's zone. The Linked session entry in the detail tab strip names the linked Session by its catalog title, falling back to the Session id when no title is available. When the strip cannot fit the tab list beside the labelled entry, the entry becomes an icon button with the same button style as More task actions, with the Session chat icon and a Linked session tooltip that also shows while the entry is disabled; the entry keeps focus when it switches layouts; the tab list scrolls without a visible scrollbar. It opens an available, unarchived Session after metadata loads; pending Session or Workspace metadata, an archived Session, missing Session membership, or a Workspace query error keeps it disabled with an explanation. The click rechecks availability; browsing does not unarchive the Session. Only this action requests Session activation. Deletion is available from the More task actions menu in the detail's top strip, separate from navigation.
|
||||
|
||||
The page header keeps the title at its natural width and moves New to another line when the two no longer fit together. The action label stays on one line.
|
||||
The page header keeps the title at its natural width and moves New to another line when the two no longer fit together. The action label stays on one line. On a coarse pointer the list's search box and the detail's instruction and run-time fields take a 16px step — iOS Safari zooms the visual viewport for a focused field below that size — while the name control keeps its 20px step and a fine pointer keeps the compact steps.
|
||||
|
||||
Selecting a task from the list opens Rules; refreshing that task preserves the selected tab. Rules edits an active task's name, instruction, and run time; the edits stay in a local draft until saved, and inactive tasks remain read-only. Unmodified Left/Right arrows cycle the tabs, Home/End select the first/last tab, and keyboard focus follows the selection. The tabs do not consume these keys with Alt, Ctrl, Meta, or Shift. The linked-Session control in the detail tab strip stays available on the Rules tab, and confirmed deletion is available from either view.
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ kind: "package-reference"
|
||||
|
||||
随发行版交付的 Web 组合把 `ui-schedule` 与 Host Schedule 服务一起挂载。为模型提供当前时间的时钟上下文归 preset 所有:`standard`、`cordis` 与 `ptc` 声明它,`minimal` 既不声明它,也不声明提醒工具。侧栏的自动化任务入口打开全局页面,不选择或激活会话。页面页头的“新建”操作会打开一个新会话,提醒由模型在该会话中创建;页面本身没有创建表单。列表支持文本搜索和一行“全部/已开启/未运行”状态筛选;搜索匹配任务已存储的名称、指令和内部会话 id,不匹配解析出的会话标题,空状态在提示和“新建”操作上方显示装饰性的文档与时钟插画,配色跟随浅色或深色主题。选择活动或未运行任务后,进入含“规则”和“任务运行记录”页签的第二层详情。每条列表行显示任务已存储的标题及其状态、频率和下次运行,不显示原会话名称;详情中的名称输入框显示任务已存储的标题。“规则”显示完整提醒、状态、活动任务的下一目标时间、原始重复规则和任务 id。每天的规则标签显示已保存的本地时间与精确的存储 IANA 时区,保留非零秒与毫秒,而不替换为浏览器时区。详情页签条内的“关联会话”入口按会话标题显示,取不到标题时回退显示会话 id。页签条放不下页签列表和带文字的入口时,入口收为与“更多任务操作”同样式的图标按钮,使用会话对话图标,悬停提示“关联会话”,入口禁用时也会显示;切换形态时保留焦点;页签列表可滚动但不显示滚动条。元数据加载后,它可打开可用且未归档的会话;会话或 Workspace 元数据尚未就绪、会话已归档、会话不在列表中或 Workspace 查询失败时,入口保持禁用并显示原因。点击时会重新检查可用性;浏览不会取消会话归档。仅此操作请求激活会话。删除位于详情顶部条带的“更多任务操作”菜单。
|
||||
|
||||
页头按标题的自然宽度排布;标题和“新建”无法并排容纳时,将按钮移到下一行。操作文案保持单行。
|
||||
页头按标题的自然宽度排布;标题和“新建”无法并排容纳时,将按钮移到下一行。操作文案保持单行。粗指针设备上,列表的搜索框以及详情的指令和运行时间输入框采用 16px 字号——iOS Safari 会放大聚焦到更小字号输入框时的视觉视口——而名称输入框保留 20px、细指针设备保留紧凑字号。
|
||||
|
||||
从列表选择任务时默认打开“规则”;刷新同一任务会保留当前页签。活动任务的“规则”修改名称、指令和运行时间;这些修改保留在本地草稿中直到保存,未运行任务保持只读。不带修饰键的左右方向键循环切换页签,Home/End 选择第一个/最后一个页签,键盘焦点随选择移动。页签不会拦截这些键与 Alt、Ctrl、Meta 或 Shift 的组合。共用的会话入口位于详情页签条内,在“规则”页签可见;确认删除操作在两种页签下都可用。
|
||||
|
||||
|
||||
@@ -1048,6 +1048,10 @@
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@media (hover: none) {
|
||||
.ruleIntervalArrows { opacity: 1; }
|
||||
}
|
||||
|
||||
.ruleIntervalArrow {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -1519,3 +1523,16 @@
|
||||
gap: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Touch devices take a 16px step on this page's text fields: iOS Safari zooms
|
||||
the visual viewport for a focused field under that size and leaves the page
|
||||
pannable after the keyboard closes, so the compact 13-14px steps above read
|
||||
as a page wider than the screen. A fine pointer keeps those steps. */
|
||||
@media (pointer: coarse) {
|
||||
.searchField input,
|
||||
.instruction,
|
||||
.ruleInput,
|
||||
.zoneSearch {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
@@ -12,11 +12,11 @@
|
||||
en: cd94b4a8dae32670
|
||||
zh: 333b8603e77f92d7
|
||||
/deepseek-ai-dsh-client-ui-settings-general/table-of-contents:
|
||||
en: 2843d46e95311bce
|
||||
zh: ec11bdeb5d3ce818
|
||||
en: 83760d53b3ee5fb8
|
||||
zh: 30dd8802d4ffdcb7
|
||||
/deepseek-ai-dsh-client-ui-settings-general/use-this-package:
|
||||
en: 4285513efdb1be68
|
||||
zh: b28089509254e8d5
|
||||
en: d427d20b4dab6653
|
||||
zh: 84e80592f0a30a9f
|
||||
/deepseek-ai-dsh-client-ui-settings-general/use-this-package/the-general-section:
|
||||
en: de35e867b2a56d73
|
||||
zh: 6d4e5c912636f889
|
||||
@@ -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
|
||||
@@ -54,8 +54,8 @@
|
||||
en: ca75c51c89c2c9b0
|
||||
zh: de54f3467ef3b9be
|
||||
/deepseek-ai-dsh-client-ui-settings-general/known-limitations-and-deferred-work:
|
||||
en: 97dd637a320ac334
|
||||
zh: faa5c512c56a128e
|
||||
en: b04b2de0ec3624b9
|
||||
zh: d9b295d415937274
|
||||
/deepseek-ai-dsh-client-ui-settings-general/known-limitations-and-deferred-work/dev-note:
|
||||
en: e67f633dc6fe7773
|
||||
zh: 3bb0c20c93f16f98
|
||||
@@ -24,20 +24,24 @@ 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.
|
||||
|
||||
In browsers below 768px viewport width, the panel fills the viewport and 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.
|
||||
|
||||
<a id="use-this-package"></a>
|
||||
## 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.
|
||||
In a browser below 768px, the body-portaled panel fills the CSS layout viewport without additional safe-area padding and covers the mask. It keeps a fixed layout height rather than measuring the software keyboard; focused controls rely on the browser’s native viewport panning.
|
||||
|
||||
Windows and macOS desktop windows at every width, and wider browser windows, use the [frame’s shared overlay inset](../ui-layout/README.md#window-chrome-seat) and keep at least 24px of viewport margin. The mask leaves the Windows caption unpainted; the full-viewport layer still blocks background clicks. Linux windows follow the browser layout.
|
||||
|
||||
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; in browsers 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.
|
||||
|
||||
Settings visibility and section selection live in the shell owner store. The shell supplies the effective Settings binding to the contributed launcher for menu keycaps and `aria-keyshortcuts`; the fallback button uses the same binding for hover and keyboard-focus hints and `aria-keyshortcuts`. The Settings command (Desktop: `Mod+,`; Windows/macOS Web: `Mod+Alt+,`) toggles the dialog when Settings is in front or no modal is open; the sidebar control opens the same dialog. The command cannot open or close Settings while the shortcut reference or another modal is in front. Held-key repeats do nothing. Initial focus lands on the selected section in the navigation, or on the title when no sections are available, without drawing a focus outline. Tab and directional navigation retain their visible focus indicators. `Mod+/` can open the shortcut reference above Settings; `Escape` closes the top dialog and restores focus to its invoking control. Return focus omits outlines after shortcut, Escape, or pointer dismissal.
|
||||
Settings visibility and section selection live in the shell owner store. The shell supplies the effective Settings binding to the contributed launcher for menu keycaps and `aria-keyshortcuts`; the fallback button uses the same binding for hover and keyboard-focus hints and `aria-keyshortcuts`. The Settings command (Desktop: `Mod+,`; Windows/macOS Web: `Mod+Alt+,`) toggles the dialog when Settings is in front or no modal is open; the sidebar control opens the same dialog. The command cannot open or close Settings while the shortcut reference or another modal is in front. Held-key repeats do nothing. The account menu opens General settings; closing the dialog clears the section selection. Onboarding can open a specified section. Initial focus lands on the selected section in the navigation, or on the title when no sections are available, without drawing a focus outline. The section buttons precede header actions and Close in the Tab order. Tab and directional navigation retain their visible focus indicators. `Mod+/` can open the shortcut reference above Settings; `Escape` closes the top dialog and restores focus to its invoking control. Return focus omits outlines after shortcut, Escape, or pointer dismissal.
|
||||
|
||||
### The General section
|
||||
|
||||
@@ -65,7 +69,7 @@ The shell declares settings.launcher for an account-owned sidebar menu and retai
|
||||
<details>
|
||||
<summary>Implementation internals — click to expand</summary>
|
||||
|
||||
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
|
||||
|
||||
@@ -120,6 +124,7 @@ None; this package neither assembles nor sends a provider request.
|
||||
|
||||
These limits define what the shell itself provides versus what features must supply; they are current package constraints.
|
||||
|
||||
- **Mobile software keyboards require device validation** — browser regression tests cover layout and focus, but native Safari keyboard panning and installed-PWA behavior remain unverified.
|
||||
- **Additional General rows require their feature plugins** — the shell supplies Coding Tools and Current version; feature plugins supply the remaining preferences.
|
||||
- **The Windows caption badge keeps a side-opening bubble** — `DesktopUpdateBadge` occupies `sidebar.toggle.badge` in the caption and requests `side="right"`, so the Desktop-owned menu text can cover its bubble while the sidebar is collapsed on Windows; the sidebar toggle and New Session bubbles open below the caption instead (#4688).
|
||||
|
||||
|
||||
@@ -24,20 +24,24 @@ kind: "package-reference"
|
||||
|
||||
设置面板采用各页面共享的 800 × 800 布局,并受视口大小约束。较长的页面在内容栏内部滚动;账号入口使用账号图标。面板挂载在 `#root` 之外而不是其内部,这样在文档序上更靠后的 chrome 行所声明的 macOS 窗口拖拽区不会吞掉它的控件。
|
||||
|
||||
浏览器视口宽度低于 768px 时,面板铺满视口,导航变为横向条带:标题与头部操作、关闭入口同处第一行,注册的分区单元在第二行横向滚动,当前分区占满其余空间。单元会在富余宽度中伸展并保有一个最小宽度,条带滚动而不截断,因此注册方的文案长度或更多的分区都不会换行或裁掉名称;选中的单元承接初始焦点,从而被带入条带的可视范围。该宽度下分区内容保留 16px 的行内边距,分隔偏好标签与其控件的 48px 间距也随之收窄。
|
||||
|
||||
<a id="use-this-package"></a>
|
||||
## 使用本包
|
||||
|
||||
用户通过侧边栏底部的 Settings 控件进入外壳;功能插件通过本外壳所投影的 slot 账本贡献自己的页面与引导步骤。在展开侧边栏和收起轨道中,该控件都会把本地化的 Settings 文案作为其可访问名称。Settings 右侧浅黄色的**连接异常**操作表示浏览器离线暂停;其常驻重试图形与中文文案「连接异常,刷新重试」都指明重试动作。每次恢复尝试都显示共享 ongoing loading 加**重新连接中**,其后一至三个点每 500ms 前进一次,且每次尝试至少可见 800ms,短暂重试不会闪动。选中任一黄色状态都会立即发起重试;按压反馈留在黄色色阶内。恢复后该区域变为浅绿色的**连接成功**,从绿色药丸可见起驻留 2 秒再消失。药丸出现时淡入、移除时以 150ms 淡出,宽度随当前文案自适应。首次启动与未曾中断的健康连接保持静默。外壳渲染模态面板、由 `settings.section` 条目构建的导航,以及每次只挂载一个的引导步骤。
|
||||
|
||||
设置面板使用[框架共享的浮层起点](../ui-layout/README.zh.md#window-chrome-seat),视口边距至少为 24px。其遮罩不绘制 Windows 顶栏区域,覆盖整个视口的模态层仍阻挡背景点击。
|
||||
浏览器宽度低于 768px 时,挂载到 body 的面板铺满 CSS 布局视口并覆盖遮罩,不额外留出安全区边距。面板保持固定布局高度,不测量软键盘;获得焦点的控件依赖浏览器原生的视口平移来露出。
|
||||
|
||||
任意宽度的 Windows、macOS 桌面窗口及较宽的浏览器窗口中,设置面板使用[框架共享的浮层起点](../ui-layout/README.zh.md#window-chrome-seat),视口边距至少为 24px。遮罩不绘制 Windows 顶栏区域,覆盖整个视口的模态层仍阻挡背景点击。Linux 窗口沿用浏览器布局。
|
||||
|
||||
页面重连到运行另一个 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;错误提示与桌面升级弹窗保持一致,包括全屏切换超时后退出全屏并重试的指引。
|
||||
|
||||
设置的可见状态和分区选择由 shell owner store 保存。外壳将设置命令的有效绑定传给入口贡献,用于菜单键帽和 `aria-keyshortcuts`;回退按钮使用相同绑定提供悬停、键盘聚焦提示和 `aria-keyshortcuts`。设置位于最前方或没有模态弹窗时,设置命令(Desktop 为 `Mod+,`,Windows/macOS Web 为 `Mod+Alt+,`)切换设置弹窗的开关;侧栏控件打开同一弹窗。快捷键速查或其他模态弹窗位于前方时,该命令不能打开或关闭设置。长按产生的重复按键不执行操作。初始焦点落在导航中选中的分区;没有可用分区时落在标题上,均不绘制焦点外轮廓线。Tab 和方向键导航保留可见焦点提示。`Mod+/` 可在设置上方打开快捷键速查;`Escape` 关闭最上层弹窗,焦点返回触发该弹窗的控件。通过快捷键、Esc 或鼠标关闭后的回焦均不绘制外轮廓线。
|
||||
设置的可见状态和分区选择由 shell owner store 保存。外壳将设置命令的有效绑定传给入口贡献,用于菜单键帽和 `aria-keyshortcuts`;回退按钮使用相同绑定提供悬停、键盘聚焦提示和 `aria-keyshortcuts`。设置位于最前方或没有模态弹窗时,设置命令(Desktop 为 `Mod+,`,Windows/macOS Web 为 `Mod+Alt+,`)切换设置弹窗的开关;侧栏控件打开同一弹窗。快捷键速查或其他模态弹窗位于前方时,该命令不能打开或关闭设置。长按产生的重复按键不执行操作。账号菜单打开通用设置;关闭弹窗会清空分区选择。引导流程可以打开指定分区。初始焦点落在导航中选中的分区;没有可用分区时落在标题上,均不绘制焦点外轮廓线。Tab 顺序中,分区按钮先于头部操作和关闭按钮。Tab 和方向键导航保留可见焦点提示。`Mod+/` 可在设置上方打开快捷键速查;`Escape` 关闭最上层弹窗,焦点返回触发该弹窗的控件。通过快捷键、Esc 或鼠标关闭后的回焦均不绘制外轮廓线。
|
||||
|
||||
### 「通用」分区
|
||||
|
||||
@@ -65,7 +69,7 @@ Web 与桌面端的通用设置底部显示当前发布版本,使用构建注
|
||||
<details>
|
||||
<summary>实现细节——点击展开</summary>
|
||||
|
||||
外壳拥有界面框架与投影,并提供代码工作工具行与当前版本行;功能注册方拥有其余内容与文案。
|
||||
外壳拥有界面框架与投影,并提供代码工作工具行与当前版本行;功能注册方拥有其余内容与文案。面板的四个座位——标题、头部、导航、分区——是它的直接子节点并按网格落位,因此同一棵标记树同时服务桌面导航栏与移动端条带。
|
||||
|
||||
### 账本投影
|
||||
|
||||
@@ -120,6 +124,7 @@ Web 与桌面端的通用设置底部显示当前发布版本,使用构建注
|
||||
|
||||
这些限制说明外壳自身提供什么、功能包必须提供什么;它们是当前包约束。
|
||||
|
||||
- **移动端软键盘仍需设备验证** — 浏览器回归覆盖布局与焦点,但尚未验证原生 Safari 的键盘平移和安装态 PWA 行为。
|
||||
- **额外的通用设置行需要相应功能插件**:外壳提供代码工作工具与当前版本;功能插件提供其他偏好。
|
||||
- **Windows 顶栏徽标的气泡仍向右展开**:`DesktopUpdateBadge` 占用顶栏的 `sidebar.toggle.badge` 且请求 `side="right"`,侧栏收起时其气泡会被 Desktop 菜单文字遮挡;侧栏开关与新建会话的气泡则改在顶栏下方展开(#4688)。
|
||||
|
||||
|
||||
@@ -83,11 +83,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. Narrow browser windows use a full-viewport panel and
|
||||
rearrange these areas into a horizontal layout. */
|
||||
.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);
|
||||
@@ -108,21 +117,24 @@
|
||||
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: 10px 12px 0;
|
||||
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.
|
||||
Its 54px row aligns with the content header; the nav adds the remaining
|
||||
10px before the first cell. */
|
||||
.navTitle {
|
||||
padding: 0 12px;
|
||||
grid-area: title;
|
||||
padding: 22px 12px 8px 24px;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
font-weight: 500;
|
||||
@@ -134,6 +146,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
@@ -180,17 +193,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;
|
||||
@@ -211,6 +216,7 @@
|
||||
|
||||
/* Compact close control. */
|
||||
.close {
|
||||
flex: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -232,12 +238,89 @@
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
/* Browsers below 768px use the full-viewport panel and horizontal navigation.
|
||||
Windows custom captions and macOS native titlebars retain the desktop rail;
|
||||
their independent root markers exclude both window-chrome layouts. Linux
|
||||
follows the browser layout. The selected cell receives initial focus and
|
||||
scrolls into view when the panel opens. */
|
||||
@media (max-width: 767px) {
|
||||
:global(html:not([data-windows-titlebar]):not([data-platform='darwin'])) .overlay {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
background: var(--dsw-alias-bg-layer-2);
|
||||
}
|
||||
|
||||
:global(html:not([data-windows-titlebar]):not([data-platform='darwin'])) .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;
|
||||
}
|
||||
|
||||
:global(html:not([data-windows-titlebar]):not([data-platform='darwin'])) .navTitle {
|
||||
padding: 22px 0 0 16px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
:global(html:not([data-windows-titlebar]):not([data-platform='darwin'])) .header {
|
||||
height: auto;
|
||||
padding: 20px 16px 0 10px;
|
||||
}
|
||||
|
||||
:global(html:not([data-windows-titlebar]):not([data-platform='darwin'])) .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. */
|
||||
:global(html:not([data-windows-titlebar]):not([data-platform='darwin'])) .navList {
|
||||
flex-direction: row;
|
||||
gap: 4px;
|
||||
padding: 12px 16px 14px;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
overscroll-behavior: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
:global(html:not([data-windows-titlebar]):not([data-platform='darwin'])) .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. */
|
||||
:global(html:not([data-windows-titlebar]):not([data-platform='darwin'])) .navCell {
|
||||
flex: 1 0 auto;
|
||||
justify-content: center;
|
||||
min-width: 108px;
|
||||
padding: 0 14px;
|
||||
}
|
||||
|
||||
:global(html:not([data-windows-titlebar]):not([data-platform='darwin'])) .navLabel {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
:global(html:not([data-windows-titlebar]):not([data-platform='darwin'])) .options {
|
||||
padding: 0 16px 24px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Visually-hidden text seat (close button accessible name from slot content). */
|
||||
.hiddenLabel {
|
||||
position: absolute;
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
/**
|
||||
* 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 with the section nav rail. In browsers below 768px the panel fills
|
||||
* the viewport and the rail turns into a horizontal strip; Windows and macOS
|
||||
* desktop windows retain the rail
|
||||
* (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 +71,12 @@ function SettingsPanel({ rows, renderSlot, activeId, onSelect, onClose }: PanelP
|
||||
<div className={css.overlay} role="presentation">
|
||||
<div className={css.mask} aria-hidden="true" onClick={onClose} />
|
||||
<div ref={panel} tabIndex={-1} data-shortcut-modal="settings" className={css.panel} role="dialog" aria-modal="true" aria-labelledby={titleId}>
|
||||
{/* 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. */}
|
||||
<div className={css.navTitle} id={titleId} tabIndex={-1}
|
||||
data-modal-autofocus={active === undefined ? '' : undefined}>{renderSlot('settings.header', {})}</div>
|
||||
<nav className={css.nav}>
|
||||
<div className={css.navTitle} id={titleId} tabIndex={-1}
|
||||
data-modal-autofocus={active === undefined ? '' : undefined}>{renderSlot('settings.header', {})}</div>
|
||||
<div className={css.navList}>
|
||||
{rows.map(row => (
|
||||
<button
|
||||
@@ -79,6 +85,7 @@ function SettingsPanel({ rows, renderSlot, activeId, onSelect, onClose }: PanelP
|
||||
className={clsx(css.navCell, row.id === active && css.active)}
|
||||
aria-current={row.id === active ? 'true' : undefined}
|
||||
data-modal-autofocus={row.id === active ? '' : undefined}
|
||||
onFocus={(event) => { event.currentTarget.scrollIntoView({ block: 'nearest', inline: 'nearest' }) }}
|
||||
onClick={() => { onSelect(row.id) }}
|
||||
>
|
||||
{navIcon(row.id)}
|
||||
@@ -87,17 +94,15 @@ function SettingsPanel({ rows, renderSlot, activeId, onSelect, onClose }: PanelP
|
||||
))}
|
||||
</div>
|
||||
</nav>
|
||||
<div className={css.content}>
|
||||
<div className={css.header}>
|
||||
<div className={css.actions}>{renderSlot('settings.action', {})}</div>
|
||||
<button type="button" className={css.close} onClick={onClose}>
|
||||
<IconCloseOutlineRegular size={14} />
|
||||
<span className={css.hiddenLabel}>{renderSlot('settings.close', {})}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div className={css.options}>
|
||||
{active !== undefined && renderSlot('settings.section', { close: onClose }, { only: active })}
|
||||
</div>
|
||||
<div className={css.header}>
|
||||
<div className={css.actions}>{renderSlot('settings.action', {})}</div>
|
||||
<button type="button" className={css.close} onClick={onClose}>
|
||||
<IconCloseOutlineRegular size={14} />
|
||||
<span className={css.hiddenLabel}>{renderSlot('settings.close', {})}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div className={css.options}>
|
||||
{active !== undefined && renderSlot('settings.section', { close: onClose }, { only: active })}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// @vitest-environment jsdom
|
||||
import type { ShortcutCatalogEntry, ShortcutCommandId } from '@deepseek-ai/dsh-client-shortcuts/client'
|
||||
import type { GlobalStandardProps } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { act, cleanup, fireEvent, render, screen } from '@testing-library/react'
|
||||
import { createSettingsShellStore } from '../src/client/shell-store.ts'
|
||||
@@ -18,8 +18,19 @@ import { Modal } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
const useResource = (() => ({ status: 'none' as const, value: undefined, failure: undefined, reload: () => {} })) as GlobalStandardProps['useResource']
|
||||
const usePanelInfo: GlobalStandardProps['usePanelInfo'] = selector => selector({ activePanelId: null })
|
||||
|
||||
const scrollIntoView = vi.fn()
|
||||
let scrollIntoViewDescriptor: PropertyDescriptor | undefined
|
||||
|
||||
beforeEach(() => {
|
||||
scrollIntoViewDescriptor = Object.getOwnPropertyDescriptor(Element.prototype, 'scrollIntoView')
|
||||
Object.defineProperty(Element.prototype, 'scrollIntoView', { configurable: true, value: scrollIntoView })
|
||||
scrollIntoView.mockClear()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
if (scrollIntoViewDescriptor === undefined) Reflect.deleteProperty(Element.prototype, 'scrollIntoView')
|
||||
else Object.defineProperty(Element.prototype, 'scrollIntoView', scrollIntoViewDescriptor)
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
@@ -421,6 +432,8 @@ describe('SettingsPanel navigation', () => {
|
||||
expect(screen.getByRole('dialog')).toBeTruthy()
|
||||
expect(screen.getByTestId('section-models')).toBeTruthy()
|
||||
expect(document.activeElement).toBe(screen.getByRole('button', { name: 'Models' }))
|
||||
expect(scrollIntoView).toHaveBeenLastCalledWith({ block: 'nearest', inline: 'nearest' })
|
||||
expect(scrollIntoView.mock.instances.at(-1)).toBe(document.activeElement)
|
||||
|
||||
cleanup()
|
||||
const inactive = mount({ onboardingActive: false }).renderSlot.mock.calls
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
en: 5a279269e31713c1
|
||||
zh: dcd5c31fbdfd63e2
|
||||
/deepseek-ai-dsh-client-ui-theme/use-this-package/appearance-and-font-size:
|
||||
en: cbfc02f8b3b02e0d
|
||||
zh: 5a8f4d9d688d113c
|
||||
en: 7dc280d0e36a3e7d
|
||||
zh: f7a75164db34cf0a
|
||||
/deepseek-ai-dsh-client-ui-theme/use-this-package/fonts:
|
||||
en: b45d5b48ca799c3a
|
||||
zh: af65514b3759204f
|
||||
|
||||
@@ -29,6 +29,8 @@ Users switch the color scheme, font sizes, and fonts from rows in Settings (Gene
|
||||
|
||||
### Appearance and font size
|
||||
|
||||
In narrow browser settings, the three Appearance choices share one row and font-size labels use a smaller gutter. Windows and macOS desktop windows retain wrapping choices and standard spacing.
|
||||
|
||||
The plugin registers Appearance preference cubes and the text font-size stepper in the General section. A More font settings button after the stepper expands a region below it, collapsed on every settings open, with a font row for each of text, code, and the terminal, plus the code and terminal font-size steppers. The text stepper accepts integer values from 10 through 22 px and defaults to 14 px. It changes conversation headings and base text by the same increment, including the user bubble and composer draft; flow-row titles, summaries, and tables follow one step under the body size, while small text keeps a fixed size. The code stepper (10–16 px, default 11) sets the code-block size; inline code stays 1 px larger, and the small tool-output variant uses the same size. The terminal stepper (10–20 px, default 13) sets the sidebar terminal cell font size; changing it keeps the shell and output and refits the grid. Each accepted change writes through the Host settings API. Rapid changes serialize in gesture order with namespace revisions, and a rejected latest write reloads the durable values.
|
||||
|
||||
Desktop's fixed `Cmd`/`Ctrl` + `+` (or `=`) and `Cmd`/`Ctrl` + `-` shortcuts change the text, code, and terminal font sizes together by 1 px, each within its own range. Each physical key press saves the changed fields in one settings mutation; holding a key does not repeat the adjustment. The actions appear in the shortcut reference and work in the main document, including dialogs and text inputs, unless a local control consumes the input; terminal input retains its own key handling. Web retains the browser's zoom shortcuts.
|
||||
|
||||
@@ -29,6 +29,8 @@ kind: "package-reference"
|
||||
|
||||
### 外观与字号
|
||||
|
||||
窄屏浏览器设置中,三个外观选项同排显示,字号标签与控件的间距缩小;Windows 和 macOS 桌面窗口保留可换行选项和标准间距。
|
||||
|
||||
插件在「通用」分区注册外观偏好方块与正文字号步进器。步进器后的「更多字体设置」按钮在其下方展开一个区域,每次打开设置时均为折叠状态,其中包含正文、代码与终端各自的字体行,以及代码与终端的字号步进器。正文步进器接受 10 至 22 px 的整数,默认值为 14 px。它以相同增量调整会话标题与基础文本,包括用户气泡与 composer 草稿;流内行的标题、摘要与表格跟随比正文低一档的字号,小号文本保持固定字号。代码步进器(10–16 px,默认 11)设置代码块字号;行内代码保持大 1 px,工具输出的小号变体使用相同字号。终端步进器(10–20 px,默认 13)设置侧栏终端的单元格字号;修改后保留 shell 与输出,并重新适配网格。每次通过的变更都经 Host settings API 写入。连续快速变更按操作顺序携带命名空间 revision 串行写入,最新写入被拒时重新加载持久值。
|
||||
|
||||
Desktop 的固定快捷键 `Cmd`/`Ctrl` + `+`(或 `=`)和 `Cmd`/`Ctrl` + `-` 同时将正文、代码与终端字号增减 1 px,各自沿用其取值范围。每次实际按下按键,通过一次设置操作保存变化字段;长按不会连续调整字号。这些操作显示在快捷键参考中,可在主文档的对话框和文本输入框内使用,但会让已消费输入的本地控件先处理;终端输入保留自身的按键处理。Web 保留浏览器的缩放快捷键。
|
||||
|
||||
@@ -57,3 +57,13 @@
|
||||
background: var(--dsw-alias-bg-module-platform);
|
||||
border-color: var(--dsw-static-neutral-bluish-400);
|
||||
}
|
||||
|
||||
/* Narrow settings panel: all three cubes share one row instead of wrapping to
|
||||
two. The equal basis and the tighter horizontal padding keep a four-glyph
|
||||
label ("跟随系统") on one line at a 390px viewport. */
|
||||
@media (max-width: 767px) {
|
||||
:global(html:not([data-windows-titlebar]):not([data-platform='darwin'])) .themeCube {
|
||||
flex: 1 1 0;
|
||||
padding: 12px 6px;
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,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) {
|
||||
:global(html:not([data-windows-titlebar]):not([data-platform='darwin'])) .rowText {
|
||||
padding-right: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
@@ -89,6 +97,10 @@
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@media (hover: none) {
|
||||
.arrows { opacity: 1; }
|
||||
}
|
||||
|
||||
/* Arrow buttons: small chips carrying a translucent wash of the layer-1
|
||||
surface (white in light, the elevated layer in dark) over the module fill;
|
||||
hover raises the full layer-1 chip. */
|
||||
|
||||
@@ -81,6 +81,7 @@
|
||||
"apps/web/tests/cua-optin.e2e.ts",
|
||||
"apps/web/tests/cua-permissions.e2e.ts",
|
||||
"apps/web/tests/schedule-header.e2e.ts",
|
||||
"apps/web/tests/schedule-phone.e2e.ts",
|
||||
"apps/web/tests/worktree-opt-in.e2e.ts",
|
||||
"apps/web/tests/session-inspector.e2e.ts",
|
||||
"apps/web/tests/shared-inspector.e2e.ts",
|
||||
|
||||
Reference in new issue
Block a user