ui: preview/edit toggle for markdown, empty-pane open cards, + flush to the rightmost tab

- markdown tabs open in rendered preview with a 预览/编辑 segmented toggle;
  edit mode uses the CodeMirror source editor (markdown highlighting), the
  preview renders the live unsaved draft, and the editor stays mounted while
  previewing so edits survive the toggle.
- an empty pane shows openable-type cards (explorer / git / terminal, the
  same options as the + menu, terminal dimmed at the cap) instead of the
  bare 'this pane is empty' message; clicking a card opens the tab.
- the + button sits immediately right of the last tab (sticky at the
  scrollport's right edge when the tab row overflows, so it stays
  reachable).
This commit is contained in:
Menghuan1918
2026-08-08 15:16:09 +08:00
parent 47ebffcf9b
commit 97e1f60e47
8 changed files with 190 additions and 38 deletions
+42 -2
View File
@@ -12,9 +12,11 @@
import { Fragment, useRef, useState } from 'react'
import type { ReactNode } from 'react'
import clsx from 'clsx'
import {
IconBranchOutline16, IconCodeOutline16, IconFolderOpen16,
} from '@deepseek-ai/dsh-client-ui-primitives'
import type { SidebarState, SidebarTab, SplitNode } from './state.ts'
import type { DropZone } from './state.ts'
import { t } from './locales.ts'
import { TabBar, type NewTabOption, parseDrag, type TabDragPayload } from './TabBar.tsx'
import css from './sidebar.module.css'
@@ -79,6 +81,44 @@ function zoneAt(event: React.DragEvent, pane: HTMLElement): DropZone {
return 'center'
}
/** The icon of one openable type card (mirror of the + menu options). */
function optionIcon(optionId: string): ReactNode {
switch (optionId) {
case 'explorer': return <IconFolderOpen16 size={16} />
case 'git': return <IconBranchOutline16 size={16} />
case 'terminal': return <IconCodeOutline16 size={16} />
default: return <IconCodeOutline16 size={16} />
}
}
/**
* An empty pane's welcome cards: the openable types as cards, clicked to
* open (instead of a bare "this pane is empty" message).
*/
function PaneEmptyCards(props: {
newTabOptions: NewTabOption[]
onNewTab: (optionId: string) => void
}) {
const { newTabOptions, onNewTab } = props
return (
<div className={css.paneEmptyCards}>
{newTabOptions.map(option => (
<button
key={option.id}
type="button"
className={css.paneCard}
disabled={option.disabled === true}
title={option.label}
onClick={() => { onNewTab(option.id) }}
>
{optionIcon(option.id)}
<span>{option.label}</span>
</button>
))}
</div>
)
}
/** A leaf: tab strip + active content + VSCode-style drop target for tabs. */
function LeafView(props: {
leaf: { id: string; tabs: SidebarTab[]; active: string | null }
@@ -135,7 +175,7 @@ function LeafView(props: {
{leaf.tabs.length > 0 ? (
<div className={css.paneContent}>{activeTab !== undefined ? renderTab(activeTab) : null}</div>
) : (
<div className={css.paneEmpty}>{t('emptyPane')}</div>
<PaneEmptyCards newTabOptions={newTabOptions} onNewTab={onNewTab} />
)}
</div>
)