feat(host): fixed-label show-hidden toggle; even divider clearance; 680x500 card
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
/* Directory-browser dialog (figma 813-23126 family). The shared Modal renders
|
/* Directory-browser dialog (figma 813-23126 family). The shared Modal renders
|
||||||
* headless here — mask, card, Escape only — and this module owns the figma
|
* 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. */
|
* the one-or-two-column Miller content, and the bordered footer. */
|
||||||
|
|
||||||
/* Doubled class beats Modal's own .dialog regardless of stylesheet order. */
|
/* 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
|
* columns scroll, so shrinking the height keeps Open/Cancel reachable
|
||||||
* instead of clipping them below a fixed overlay. */
|
* instead of clipping them below a fixed overlay. */
|
||||||
.dialog.dialog {
|
.dialog.dialog {
|
||||||
width: min(600px, 100%);
|
width: min(680px, 100%);
|
||||||
height: min(420px, calc(100dvh - 32px));
|
height: min(500px, calc(100dvh - 32px));
|
||||||
padding: 0;
|
padding: 0;
|
||||||
gap: 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 {
|
.header {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
flex: none;
|
flex: none;
|
||||||
padding: 22px 14px 12px 24px;
|
padding: 16px 14px 8px 24px;
|
||||||
border-bottom: 1px solid var(--dsw-alias-border-l3);
|
border-bottom: 1px solid var(--dsw-alias-border-l3);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,6 +57,7 @@
|
|||||||
min-height: 0;
|
min-height: 0;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
|
scrollbar-width: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.crumbTrail {
|
.crumbTrail {
|
||||||
@@ -126,28 +128,31 @@
|
|||||||
color: var(--dsw-alias-label-primary);
|
color: var(--dsw-alias-label-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Miller content: pt16 px24; columns are 256 wide (or full width solo) with
|
/* Miller content: symmetric 16px vertical padding so the divider clears the
|
||||||
* the hairline divider centered between them; each column scrolls alone. */
|
* 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 {
|
.content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex: 1 1 0;
|
flex: 1 1 0;
|
||||||
min-height: 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 {
|
.column {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 2px;
|
gap: 2px;
|
||||||
width: 256px;
|
flex: 1 1 0;
|
||||||
flex: none;
|
min-width: 256px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.columnWide {
|
.columnWide {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
flex: 1 1 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.divider {
|
.divider {
|
||||||
@@ -228,8 +233,8 @@
|
|||||||
color: var(--dsw-alias-state-error-primary);
|
color: var(--dsw-alias-state-error-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Footer: l3 separator on top, pt12 px24, New-folder pinned left; the fixed
|
/* Footer: l3 separator on top, symmetric padding so the row sits vertically
|
||||||
* card leaves the figma 28px below the 36px buttons. */
|
* centered in the bar; New-folder and the show-hidden toggle pin left. */
|
||||||
.footerBar {
|
.footerBar {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -238,12 +243,17 @@
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
flex: none;
|
flex: none;
|
||||||
padding: 12px 24px 28px;
|
padding: 16px 24px;
|
||||||
border-top: 1px solid var(--dsw-alias-border-l3);
|
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 {
|
.showHiddenToggle {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
border: none;
|
border: none;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|||||||
@@ -10,13 +10,14 @@
|
|||||||
* selects the created folder. Open adopts the selected folder, falling back
|
* selects the created folder. Open adopts the selected folder, falling back
|
||||||
* to the listed level. Pure consumer of the injected browse calls — the
|
* to the listed level. Pure consumer of the injected browse calls — the
|
||||||
* owning flow decides what "Open" means and owns the workspace-creation
|
* owning flow decides what "Open" means and owns the workspace-creation
|
||||||
* error surface. Hidden entries are host-flagged and hidden by default;
|
* error surface. Hidden entries are host-flagged and hidden by default; the
|
||||||
* a "Show hidden files" toggle in the footer reveals them (client-side only).
|
* 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 { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import {
|
import {
|
||||||
Button, IconChevronRightOutline14, IconFolderClose16, IconFolderOpen16, IconPlusOutline16, Modal,
|
Button, IconCheckOutline16, IconChevronRightOutline14, IconFolderClose16, IconFolderOpen16, IconPlusOutline16, Modal,
|
||||||
} from '@deepseek-ai/dsh-client-ui-primitives'
|
} from '@deepseek-ai/dsh-client-ui-primitives'
|
||||||
import type { DirectoryEntry, DirectoryListing } from '@deepseek-ai/dsh-client-runtime/client'
|
import type { DirectoryEntry, DirectoryListing } from '@deepseek-ai/dsh-client-runtime/client'
|
||||||
import { DirectoryBrowseError } 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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={clsx(css.showHiddenToggle, showHidden && css.showHiddenToggleActive)}
|
className={clsx(css.showHiddenToggle, showHidden && css.showHiddenToggleActive)}
|
||||||
|
aria-pressed={showHidden}
|
||||||
disabled={parentInert}
|
disabled={parentInert}
|
||||||
onClick={() => { setShowHidden(prev => !prev) }}
|
onClick={() => { setShowHidden(prev => !prev) }}
|
||||||
>
|
>
|
||||||
{t(showHidden ? 'browser.hideHidden' : 'browser.showHidden')}
|
{showHidden && <IconCheckOutline16 size={14} />}
|
||||||
|
{t('browser.showHidden')}
|
||||||
</button>
|
</button>
|
||||||
<span className={css.footerGap} />
|
<span className={css.footerGap} />
|
||||||
<Button variant="outline" className={clsx(css.footerAction)} disabled={parentInert} onClick={onClose}>{t('browser.cancel')}</Button>
|
<Button variant="outline" className={clsx(css.footerAction)} disabled={parentInert} onClick={onClose}>{t('browser.cancel')}</Button>
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ export function apply(ctx: ClientContext): void {
|
|||||||
'browser.loading': '加载中…',
|
'browser.loading': '加载中…',
|
||||||
'browser.truncated': '文件夹过多,仅显示开头部分。',
|
'browser.truncated': '文件夹过多,仅显示开头部分。',
|
||||||
'browser.showHidden': '显示隐藏文件',
|
'browser.showHidden': '显示隐藏文件',
|
||||||
'browser.hideHidden': '隐藏隐藏文件',
|
|
||||||
}],
|
}],
|
||||||
['en', {
|
['en', {
|
||||||
'browser.title': 'Select Workspace Directory',
|
'browser.title': 'Select Workspace Directory',
|
||||||
@@ -63,7 +62,6 @@ export function apply(ctx: ClientContext): void {
|
|||||||
'browser.loading': 'Loading…',
|
'browser.loading': 'Loading…',
|
||||||
'browser.truncated': 'Too many folders to list; only the beginning is shown.',
|
'browser.truncated': 'Too many folders to list; only the beginning is shown.',
|
||||||
'browser.showHidden': 'Show hidden files',
|
'browser.showHidden': 'Show hidden files',
|
||||||
'browser.hideHidden': 'Hide hidden files',
|
|
||||||
}],
|
}],
|
||||||
]
|
]
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -163,7 +163,6 @@ describe('directory-picker-browse client half', () => {
|
|||||||
expect(injected.t('browser.title')).toBe('选择工作区目录')
|
expect(injected.t('browser.title')).toBe('选择工作区目录')
|
||||||
expect(injected.t('browser.newFolder')).toBe('新建文件夹')
|
expect(injected.t('browser.newFolder')).toBe('新建文件夹')
|
||||||
expect(injected.t('browser.showHidden')).toBe('显示隐藏文件')
|
expect(injected.t('browser.showHidden')).toBe('显示隐藏文件')
|
||||||
expect(injected.t('browser.hideHidden')).toBe('隐藏隐藏文件')
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('drives the injected browse calls through the hole entry', async () => {
|
it('drives the injected browse calls through the hole entry', async () => {
|
||||||
|
|||||||
@@ -107,11 +107,14 @@ describe('DirectoryBrowser', () => {
|
|||||||
const b = mount()
|
const b = mount()
|
||||||
await waitFor(() => { expect(screen.getByRole('listitem')).toBeTruthy() })
|
await waitFor(() => { expect(screen.getByRole('listitem')).toBeTruthy() })
|
||||||
expect(screen.queryByText('.config')).toBeNull()
|
expect(screen.queryByText('.config')).toBeNull()
|
||||||
// Toggle hidden files on.
|
// The fixed-label toggle reports its state through aria-pressed.
|
||||||
fireEvent.click(screen.getByRole('button', { name: 'browser.showHidden' }))
|
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()
|
expect(screen.getByText('.config')).toBeTruthy()
|
||||||
// Toggle hidden files off.
|
fireEvent.click(toggle)
|
||||||
fireEvent.click(screen.getByRole('button', { name: 'browser.hideHidden' }))
|
expect(toggle.getAttribute('aria-pressed')).toBe('false')
|
||||||
expect(screen.queryByText('.config')).toBeNull()
|
expect(screen.queryByText('.config')).toBeNull()
|
||||||
// Close resets the toggle.
|
// Close resets the toggle.
|
||||||
b.view.rerender(<DirectoryBrowser {...b.props} open={false} />)
|
b.view.rerender(<DirectoryBrowser {...b.props} open={false} />)
|
||||||
|
|||||||
Reference in New Issue
Block a user