fix: docs
This commit is contained in:
imccyu
2026-07-30 15:56:41 +08:00
parent 085383b145
commit 1e10966ef6
160 changed files with 2521 additions and 1135 deletions

View File

@@ -27,28 +27,27 @@ import css from './WorkspaceBrowser.module.css'
*/
const EXPAND_SLIDE_MS = 300
const GROUP_BY_ITEMS = [
{ type: 'label' as const, id: 'group-by', text: 'Group by' },
{ id: 'workspace', label: 'WorkSpace' },
{ id: 'flat', label: 'In one list' },
]
/** Immutable membership toggle for the local expansion arrays. */
function toggled(list: readonly string[], key: string): string[] {
return list.includes(key) ? list.filter(k => k !== key) : [...list, key]
}
/** Group-by strategy menu; own open state so it resets with the wide chrome. */
function GroupByMenu({ groupBy, onPick }: {
function GroupByMenu({ groupBy, onPick, t }: {
groupBy: 'workspace' | 'flat'
onPick: (mode: 'workspace' | 'flat') => void
t: WorkspaceBrowserProps['t']
}) {
const [open, setOpen] = useState(false)
return (
<Menu
open={open}
onClose={() => { setOpen(false) }}
items={GROUP_BY_ITEMS}
items={[
{ type: 'label' as const, id: 'group-by', text: t('groupBy.label') },
{ id: 'workspace', label: t('groupBy.workspace') },
{ id: 'flat', label: t('groupBy.flat') },
]}
selectedId={groupBy}
onSelect={(id) => {
/* v8 ignore next -- narrowing guard: the heading label is not selectable, so the only arriving ids are the two modes. */
@@ -63,7 +62,7 @@ function GroupByMenu({ groupBy, onPick }: {
<button
type="button"
className={clsx(css.iconButton, css.wide)}
aria-label="Group by"
aria-label={t('groupBy.label')}
onClick={() => { setOpen(v => !v) }}
>
<IconPersonalizationOutline16 />
@@ -83,7 +82,7 @@ interface DragState {
type SessionTreeProps = Pick<
WorkspaceBrowserProps,
'useSessions' | 'startSession' | 'open' | 'forkSession' | 'insertSessionBefore'
'useSessions' | 'startSession' | 'open' | 'forkSession' | 'insertSessionBefore' | 't'
> & {
workspaces: readonly WorkspaceView[]
/** Live search filter owned by the browser root (the query outlives the tree). */
@@ -99,7 +98,7 @@ type SessionTreeProps = Pick<
/** The scrolling session tree; unmounting at collapse settle drops the sessions subscription and expansion state. */
function SessionTree({
useSessions, startSession, open, forkSession, workspaces, query,
onRenameRequest, onDeleteRequest, onSessionRename, insertSessionBefore,
onRenameRequest, onDeleteRequest, onSessionRename, insertSessionBefore, t,
}: SessionTreeProps) {
const list = useSessions(s => s)
const current = list.current
@@ -122,9 +121,9 @@ function SessionTree({
return (
<div className={clsx(css.treeBody, css.wide)}>
<div className={css.list} role="tree" aria-label="Sessions">
<div className={css.list} role="tree" aria-label={t('section.sessions')}>
{groups.length === 0 && (
<div className={css.empty}>{query === '' ? 'No sessions yet' : 'No matches'}</div>
<div className={css.empty}>{query === '' ? t('empty.none') : t('empty.noMatches')}</div>
)}
{groups.map(group => (
// Group section: header row + expanded top-level session rows. The
@@ -133,6 +132,7 @@ function SessionTree({
<div key={group.key} className={css.groupSection}>
<ProjectRowItem
group={group}
t={t}
onToggle={() => { setExpandedProjects(l => toggled(l, group.key)) }}
onCreate={() => {
if (group.workspaceId !== undefined) startSession(group.workspaceId)
@@ -195,6 +195,7 @@ function SessionTree({
onRename={onSessionRename}
onFork={forkSession}
drag={dragProps}
t={t}
/>
)
})}
@@ -207,15 +208,15 @@ function SessionTree({
}
/** The flat "In one list" body: every session a top-level row, newest-first. */
function FlatList({ useSessions, open, forkSession, onSessionRename, query }: Pick<SessionTreeProps, 'useSessions' | 'open' | 'forkSession' | 'onSessionRename' | 'query'>) {
function FlatList({ useSessions, open, forkSession, onSessionRename, query, t }: Pick<SessionTreeProps, 'useSessions' | 'open' | 'forkSession' | 'onSessionRename' | 'query' | 't'>) {
const list = useSessions(s => s)
const rows = useMemo(() => deriveFlat(list, { query }), [list, query])
const now = Date.now()
return (
<div className={clsx(css.treeBody, css.wide)}>
<div className={css.list} role="tree" aria-label="Sessions">
<div className={css.list} role="tree" aria-label={t('section.sessions')}>
{rows.length === 0 && (
<div className={css.empty}>{query === '' ? 'No sessions yet' : 'No matches'}</div>
<div className={css.empty}>{query === '' ? t('empty.none') : t('empty.noMatches')}</div>
)}
{rows.map(node => (
<SessionNodeItem
@@ -226,6 +227,7 @@ function FlatList({ useSessions, open, forkSession, onSessionRename, query }: Pi
onOpen={open}
onRename={onSessionRename}
onFork={forkSession}
t={t}
/>
))}
</div>
@@ -256,6 +258,7 @@ export function WorkspaceBrowser({
createWorkspace,
useDirectoryFlow,
renderSlot,
t,
}: WorkspaceBrowserProps) {
const workspaces = useWorkspaces(state => state.items)
const groupBy = useStore(s => s.groupBy)
@@ -383,16 +386,16 @@ export function WorkspaceBrowser({
<div className={css.sectionHeader}>
{wide && (
<span className={clsx(css.sectionLabel, css.wide)}>
{groupBy === 'flat' ? 'Sessions' : 'Workspaces'}
{groupBy === 'flat' ? t('section.sessions') : t('section.workspaces')}
</span>
)}
{wide && <GroupByMenu groupBy={groupBy} onPick={(mode) => { actions.setGroupBy(mode) }} />}
<Tooltip label="New Workspace" disabled={wide}>
{wide && <GroupByMenu groupBy={groupBy} onPick={(mode) => { actions.setGroupBy(mode) }} t={t} />}
<Tooltip label={t('workspace.new')} disabled={wide}>
<button
ref={wsPlusRef}
type="button"
className={css.iconButton}
aria-label="Create workspace"
aria-label={t('create.confirm')}
onClick={() => {
setWsPickerOpen(v => !v)
}}
@@ -402,6 +405,7 @@ export function WorkspaceBrowser({
</Tooltip>
{/* Picker menu + create dialogs (same package — direct composition). */}
<WorkspaceCreateFlow
t={t}
open={wsPickerOpen}
anchorRef={wsPlusRef}
useWorkspaces={useWorkspaces}
@@ -421,11 +425,11 @@ export function WorkspaceBrowser({
{/* Expanded: the row is a click-to-focus field (the leading icon is
decorative). Rail: the icon is the region's search control. */}
<div className={css.search} onClick={() => { if (wide) searchInput.current?.focus() }}>
<Tooltip label="Search" disabled={wide}>
<Tooltip label={t('search')} disabled={wide}>
<button
type="button"
className={css.searchButton}
aria-label="Search sessions"
aria-label={t('search.sessions.aria')}
tabIndex={wide ? -1 : 0}
onClick={() => { if (!wide) { setSearchOnExpand(true); expandSidebar() } }}
>
@@ -437,7 +441,7 @@ export function WorkspaceBrowser({
ref={searchInput}
className={clsx(css.searchInput, css.wide)}
type="text"
placeholder="Search name, keywords..."
placeholder={t('search.placeholder')}
value={query}
onChange={(e) => { setQuery(e.target.value) }}
/>
@@ -446,7 +450,7 @@ export function WorkspaceBrowser({
<button
type="button"
className={clsx(css.clearButton, css.wide)}
aria-label="Clear search"
aria-label={t('search.clear')}
onClick={() => { setQuery('') }}
>
<IconCloseFill14 />
@@ -458,7 +462,12 @@ export function WorkspaceBrowser({
itself is wide-only. */}
<div className={css.listArea}>
{wide && (groupBy === 'flat'
? <FlatList useSessions={useSessions} open={open} forkSession={forkSession} onSessionRename={onSessionRename} query={query} />
? (
<FlatList
useSessions={useSessions} open={open} forkSession={forkSession}
onSessionRename={onSessionRename} query={query} t={t}
/>
)
: (
<SessionTree
useSessions={useSessions}
@@ -469,6 +478,7 @@ export function WorkspaceBrowser({
open={open}
query={query}
insertSessionBefore={insertSessionBefore}
t={t}
onRenameRequest={(workspaceId, currentTitle) => {
setRenameTarget({ workspaceId, currentTitle })
setRenameDraft(currentTitle)
@@ -485,18 +495,19 @@ export function WorkspaceBrowser({
<Modal
open={renameTarget !== null}
onClose={closeRename}
title="Rename workspace"
closeLabel={t('close')}
title={t('rename.workspace.title')}
footer={(
<>
<Button variant="outline" disabled={renaming} onClick={closeRename}>Cancel</Button>
<Button variant="primary" disabled={renameBlocked} onClick={confirmRename}>Rename</Button>
<Button variant="outline" disabled={renaming} onClick={closeRename}>{t('cancel')}</Button>
<Button variant="primary" disabled={renameBlocked} onClick={confirmRename}>{t('rename')}</Button>
</>
)}
>
<input
className={css.renameInput}
value={renameDraft}
aria-label="Workspace name"
aria-label={t('field.workspaceName')}
autoFocus
disabled={renaming}
onFocus={(e) => { e.target.select() }}
@@ -511,7 +522,7 @@ export function WorkspaceBrowser({
}}
/>
{renameDuplicate && (
<div className={css.renameError} role="alert">A workspace named “{renameTrimmed}” already exists.</div>
<div className={css.renameError} role="alert">{t('conflict.named', { name: renameTrimmed })}</div>
)}
{renameError !== null && <div className={css.renameError} role="alert">{renameError}</div>}
</Modal>
@@ -519,18 +530,19 @@ export function WorkspaceBrowser({
<Modal
open={sessionRenameTarget !== null}
onClose={closeSessionRename}
title="Rename session"
closeLabel={t('close')}
title={t('rename.session.title')}
footer={(
<>
<Button variant="outline" disabled={sessionRenaming} onClick={closeSessionRename}>Cancel</Button>
<Button variant="primary" disabled={sessionRenameBlocked} onClick={confirmSessionRename}>Rename</Button>
<Button variant="outline" disabled={sessionRenaming} onClick={closeSessionRename}>{t('cancel')}</Button>
<Button variant="primary" disabled={sessionRenameBlocked} onClick={confirmSessionRename}>{t('rename')}</Button>
</>
)}
>
<input
className={css.renameInput}
value={sessionRenameDraft}
aria-label="Session name"
aria-label={t('field.sessionName')}
autoFocus
disabled={sessionRenaming}
onFocus={(e) => { e.target.select() }}
@@ -549,25 +561,26 @@ export function WorkspaceBrowser({
<Modal
open={deleteTarget !== null}
onClose={closeDelete}
title="Delete workspace"
closeLabel={t('close')}
title={t('delete.workspace')}
{...deleteTarget === null
? {}
: { description: `This removes “${deleteTarget.title}” from the workspace list. The folder and session logs will be kept. Its sessions will appear under Ungrouped.` }}
: { description: t('delete.desc', { name: deleteTarget.title }) }}
footer={(
<>
<Button variant="outline" disabled={deleting} onClick={closeDelete}>Cancel</Button>
<Button variant="outline" disabled={deleting} onClick={closeDelete}>{t('cancel')}</Button>
<Button
variant="outline"
className={css.deleteAction}
disabled={deleting}
onClick={confirmDelete}
>
Delete workspace
{t('delete.workspace')}
</Button>
</>
)}
>
{deleting && <div className={css.deleteStatus} role="status">Deleting workspace…</div>}
{deleting && <div className={css.deleteStatus} role="status">{t('delete.pending')}</div>}
{deleteError !== null && <div className={css.renameError} role="alert">{deleteError}</div>}
</Modal>
</div>