feat(host): fixed-label show-hidden toggle; even divider clearance; 680x500 card

This commit is contained in:
creatixchu
2026-07-29 13:29:17 +08:00
parent cdcdd2221e
commit c747f721ba
5 changed files with 39 additions and 26 deletions

View File

@@ -1,6 +1,7 @@
/* Directory-browser dialog (figma 813-23126 family). The shared Modal renders
* headless here — mask, card, Escape only — and this module owns the figma
* frame: 600×420 card (viewport-clamped), header (title + crumbs, l3 separator),
* frame: 680×500 card (viewport-clamped; upsized from the figma 600×420),
* header (title + crumbs, l3 separator),
* the one-or-two-column Miller content, and the bordered footer. */
/* Doubled class beats Modal's own .dialog regardless of stylesheet order. */
@@ -8,19 +9,19 @@
* columns scroll, so shrinking the height keeps Open/Cancel reachable
* instead of clipping them below a fixed overlay. */
.dialog.dialog {
width: min(600px, 100%);
height: min(420px, calc(100dvh - 32px));
width: min(680px, 100%);
height: min(500px, calc(100dvh - 32px));
padding: 0;
gap: 0;
}
/* Header block: pl24 pr14 pt22 pb12, 8px between title row and crumb row. */
/* Header block: pl24 pr14 pt16 pb8, 8px between title row and crumb row. */
.header {
display: flex;
flex-direction: column;
gap: 8px;
flex: none;
padding: 22px 14px 12px 24px;
padding: 16px 14px 8px 24px;
border-bottom: 1px solid var(--dsw-alias-border-l3);
}
@@ -56,6 +57,7 @@
min-height: 0;
gap: 20px;
overflow-x: auto;
scrollbar-width: none;
}
.crumbTrail {
@@ -126,28 +128,31 @@
color: var(--dsw-alias-label-primary);
}
/* Miller content: pt16 px24; columns are 256 wide (or full width solo) with
* the hairline divider centered between them; each column scrolls alone. */
/* Miller content: symmetric 16px vertical padding so the divider clears the
* header and footer rules evenly; columns are 256 wide (or full width solo)
* with the hairline divider centered between them; each column scrolls alone. */
.content {
display: flex;
flex-direction: column;
flex: 1 1 0;
min-height: 0;
padding: 16px 24px 0;
padding: 16px 24px;
}
/* Two-pane columns split the row evenly around the divider; 256px is the
* floor below which the row scrolls (scrollbar hidden, the effect pins the
* child pane into view) instead of squeezing the panes. */
.column {
display: flex;
flex-direction: column;
gap: 2px;
width: 256px;
flex: none;
flex: 1 1 0;
min-width: 256px;
overflow-y: auto;
}
.columnWide {
width: 100%;
flex: 1 1 0;
}
.divider {
@@ -228,8 +233,8 @@
color: var(--dsw-alias-state-error-primary);
}
/* Footer: l3 separator on top, pt12 px24, New-folder pinned left; the fixed
* card leaves the figma 28px below the 36px buttons. */
/* Footer: l3 separator on top, symmetric padding so the row sits vertically
* centered in the bar; New-folder and the show-hidden toggle pin left. */
.footerBar {
display: flex;
align-items: center;
@@ -238,12 +243,17 @@
flex-wrap: wrap;
gap: 8px;
flex: none;
padding: 12px 24px 28px;
padding: 16px 24px;
border-top: 1px solid var(--dsw-alias-border-l3);
}
/* Show-hidden toggle: a subtle text button in the footer, left of the gap. */
/* Show-hidden toggle: a subtle fixed-label text button left of the gap;
* the pressed state seats a check glyph before the label (Menu's selected
* vocabulary) instead of flipping the wording. */
.showHiddenToggle {
display: inline-flex;
align-items: center;
gap: 4px;
border: none;
background: transparent;
padding: 0;

View File

@@ -10,13 +10,14 @@
* selects the created folder. Open adopts the selected folder, falling back
* to the listed level. Pure consumer of the injected browse calls — the
* owning flow decides what "Open" means and owns the workspace-creation
* error surface. Hidden entries are host-flagged and hidden by default;
* a "Show hidden files" toggle in the footer reveals them (client-side only).
* error surface. Hidden entries are host-flagged and hidden by default; the
* footer's fixed-label "Show hidden files" toggle (aria-pressed, check when
* on) reveals them (client-side only).
*/
import { useCallback, useEffect, useRef, useState } from 'react'
import clsx from 'clsx'
import {
Button, IconChevronRightOutline14, IconFolderClose16, IconFolderOpen16, IconPlusOutline16, Modal,
Button, IconCheckOutline16, IconChevronRightOutline14, IconFolderClose16, IconFolderOpen16, IconPlusOutline16, Modal,
} from '@deepseek-ai/dsh-client-ui-primitives'
import type { DirectoryEntry, DirectoryListing } from '@deepseek-ai/dsh-client-runtime/client'
import { DirectoryBrowseError } from '@deepseek-ai/dsh-client-runtime/client'
@@ -451,10 +452,12 @@ export function DirectoryBrowser({ open, listDirectory, createDirectory, onOpen,
<button
type="button"
className={clsx(css.showHiddenToggle, showHidden && css.showHiddenToggleActive)}
aria-pressed={showHidden}
disabled={parentInert}
onClick={() => { setShowHidden(prev => !prev) }}
>
{t(showHidden ? 'browser.hideHidden' : 'browser.showHidden')}
{showHidden && <IconCheckOutline16 size={14} />}
{t('browser.showHidden')}
</button>
<span className={css.footerGap} />
<Button variant="outline" className={clsx(css.footerAction)} disabled={parentInert} onClick={onClose}>{t('browser.cancel')}</Button>

View File

@@ -47,7 +47,6 @@ export function apply(ctx: ClientContext): void {
'browser.loading': '加载中…',
'browser.truncated': '文件夹过多,仅显示开头部分。',
'browser.showHidden': '显示隐藏文件',
'browser.hideHidden': '隐藏隐藏文件',
}],
['en', {
'browser.title': 'Select Workspace Directory',
@@ -63,7 +62,6 @@ export function apply(ctx: ClientContext): void {
'browser.loading': 'Loading…',
'browser.truncated': 'Too many folders to list; only the beginning is shown.',
'browser.showHidden': 'Show hidden files',
'browser.hideHidden': 'Hide hidden files',
}],
]
try {

View File

@@ -163,7 +163,6 @@ describe('directory-picker-browse client half', () => {
expect(injected.t('browser.title')).toBe('选择工作区目录')
expect(injected.t('browser.newFolder')).toBe('新建文件夹')
expect(injected.t('browser.showHidden')).toBe('显示隐藏文件')
expect(injected.t('browser.hideHidden')).toBe('隐藏隐藏文件')
})
it('drives the injected browse calls through the hole entry', async () => {

View File

@@ -107,11 +107,14 @@ describe('DirectoryBrowser', () => {
const b = mount()
await waitFor(() => { expect(screen.getByRole('listitem')).toBeTruthy() })
expect(screen.queryByText('.config')).toBeNull()
// Toggle hidden files on.
fireEvent.click(screen.getByRole('button', { name: 'browser.showHidden' }))
// The fixed-label toggle reports its state through aria-pressed.
const toggle = screen.getByRole('button', { name: 'browser.showHidden' })
expect(toggle.getAttribute('aria-pressed')).toBe('false')
fireEvent.click(toggle)
expect(toggle.getAttribute('aria-pressed')).toBe('true')
expect(screen.getByText('.config')).toBeTruthy()
// Toggle hidden files off.
fireEvent.click(screen.getByRole('button', { name: 'browser.hideHidden' }))
fireEvent.click(toggle)
expect(toggle.getAttribute('aria-pressed')).toBe('false')
expect(screen.queryByText('.config')).toBeNull()
// Close resets the toggle.
b.view.rerender(<DirectoryBrowser {...b.props} open={false} />)