145 lines
6.3 KiB
TypeScript
145 lines
6.3 KiB
TypeScript
import type { CSSProperties, ReactElement } from 'react'
|
||
import type { BlockView } from './types'
|
||
import { ChevronDown, ChevronRight, Eye, EyeOff, Pencil } from 'lucide-react'
|
||
import { Fragment } from 'react'
|
||
import { cn } from '@/lib/utils'
|
||
import { CodeEditor } from './CodeEditor'
|
||
import { PropertyGrid } from './PropertyGrid'
|
||
import { TextPreview } from './TextPreview'
|
||
|
||
export interface BlockTreeState {
|
||
version: number
|
||
editing: ReadonlySet<string>
|
||
/** 眼下折叠着的块 id、属性行 id 和子行 id(propertyRowId、propertyItemId):用户的选择叠在默认上,见 collapsedIds */
|
||
collapsed: ReadonlySet<string>
|
||
/** 按筛选不画的块(连同子孙),见 applyFilter */
|
||
hidden: ReadonlySet<string>
|
||
/** 本来被筛掉、跳到定义时临时显示的块 */
|
||
temporary: ReadonlySet<string>
|
||
/** 种类标签上写什么:翻译插件给种类起的名字(库规则的种类是规则给的标签),没给就是 id */
|
||
kindName: (kind: string) => string
|
||
/** 刚点了「编辑」、要把光标放进去的块 */
|
||
focusId: string | null
|
||
onFocused: () => void
|
||
onToggleEdit: (id: string) => void
|
||
onToggleFold: (id: string) => void
|
||
}
|
||
|
||
/** 块下面有没有画出来的子块:子块都被筛掉时不留空的一层、不给折叠箭头。 */
|
||
function hasShownChildren(block: BlockView, tree: BlockTreeState): boolean {
|
||
return block.children.some(child => !tree.hidden.has(child.id))
|
||
}
|
||
|
||
/**
|
||
* 块的种类色写在哪:内置种类按 data-kind 取 CSS 里的配色;库规则认领的块(带 badge)按规则给的颜色,
|
||
* 暗色、亮色两个都写在 style 上,CSS 按主题挑一个(见 [data-badge])。
|
||
*/
|
||
function kindProps(block: BlockView | undefined): { 'data-kind'?: string, 'data-badge'?: '', 'style'?: CSSProperties } {
|
||
if (!block)
|
||
return {}
|
||
if (!block.badge)
|
||
return { 'data-kind': block.kind }
|
||
const { dark, light } = block.badge.color
|
||
return { 'data-kind': block.kind, 'data-badge': '', 'style': { '--kind-dark': dark, '--kind-light': light } as CSSProperties }
|
||
}
|
||
|
||
/**
|
||
* 一层代码块:左边一条竖线标出这一层,每块的子块紧跟在它下面、再往右缩进一层。被筛掉的块不画,不留空位,竖线跟着变短。
|
||
* owner 是这一层所属的块(顶层没有),悬停它时竖线换成它的种类色。
|
||
*/
|
||
export function BlockLevel({ blocks, owner, tree }: { blocks: BlockView[], owner?: BlockView, tree: BlockTreeState }): ReactElement {
|
||
return (
|
||
<div className="cr-level flex gap-2" {...kindProps(owner)}>
|
||
<div className="flex w-3 shrink-0 justify-center">
|
||
<div className="cr-guide w-px" />
|
||
</div>
|
||
<div className="flex min-w-0 flex-1 flex-col gap-2">
|
||
{blocks.filter(block => !tree.hidden.has(block.id)).map(block => (
|
||
<Fragment key={block.id}>
|
||
<BlockCard block={block} tree={tree} />
|
||
{hasShownChildren(block, tree) && !tree.collapsed.has(block.id) && (
|
||
<BlockLevel blocks={block.children} owner={block} tree={tree} />
|
||
)}
|
||
</Fragment>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
/** 预览状态的内容:函数、类、导入、常量、变量块和库规则认领的块是属性表,其余是一行译文,带说明的译文下面接着说明。 */
|
||
function BlockPreview({ block, tree }: { block: BlockView, tree: BlockTreeState }): ReactElement {
|
||
if ('properties' in block)
|
||
return <PropertyGrid blockId={block.id} rows={block.properties} collapsed={tree.collapsed} onToggleFold={tree.onToggleFold} />
|
||
return (
|
||
<>
|
||
<TextPreview text={block.translation} />
|
||
{block.doc && <TextPreview text={block.doc} className="mt-0.5" />}
|
||
</>
|
||
)
|
||
}
|
||
|
||
/** 一块:折叠箭头、种类标签、预览内容或源码、右边的「编辑 / 预览」切换。 */
|
||
function BlockCard({ block, tree }: { block: BlockView, tree: BlockTreeState }): ReactElement {
|
||
const editing = tree.editing.has(block.id)
|
||
const collapsed = tree.collapsed.has(block.id)
|
||
const hasChildren = hasShownChildren(block, tree)
|
||
const Chevron = collapsed ? ChevronRight : ChevronDown
|
||
return (
|
||
<div
|
||
className="cr-block flex items-start gap-2.5 rounded-lg py-2 pr-2.5 pl-2"
|
||
{...kindProps(block)}
|
||
data-id={block.id}
|
||
data-line={block.startLine}
|
||
data-editing={editing ? '' : undefined}
|
||
>
|
||
{hasChildren
|
||
? (
|
||
<button
|
||
type="button"
|
||
className="flex h-5 w-3.5 shrink-0 items-center justify-center rounded-sm text-[var(--vscode-descriptionForeground)] hover:text-[var(--vscode-foreground)]"
|
||
aria-label={collapsed ? '展开子块' : '折叠子块'}
|
||
aria-expanded={!collapsed}
|
||
onClick={() => tree.onToggleFold(block.id)}
|
||
>
|
||
<Chevron size={14} strokeWidth={1.8} />
|
||
</button>
|
||
)
|
||
: <span className="w-3.5 shrink-0" />}
|
||
<span className="cr-badge flex h-5 min-w-11 shrink-0 items-center justify-center rounded px-1 text-[11px] leading-[14px] font-medium whitespace-nowrap">
|
||
{tree.kindName(block.kind)}
|
||
</span>
|
||
<div className="min-w-0 flex-1">
|
||
{editing
|
||
? (
|
||
<CodeEditor
|
||
block={block}
|
||
version={tree.version}
|
||
autoFocus={tree.focusId === block.id}
|
||
onAutoFocused={tree.onFocused}
|
||
onExit={() => tree.onToggleEdit(block.id)}
|
||
/>
|
||
)
|
||
: <BlockPreview block={block} tree={tree} />}
|
||
</div>
|
||
{tree.temporary.has(block.id) && (
|
||
<span className="cr-temporary flex h-5 shrink-0 items-center gap-1 rounded px-1.5 text-[11px] whitespace-nowrap">
|
||
<EyeOff size={12} strokeWidth={1.8} aria-hidden />
|
||
已被筛选隐藏,临时显示
|
||
</span>
|
||
)}
|
||
<button
|
||
type="button"
|
||
className={cn(
|
||
'flex shrink-0 items-center gap-1 rounded px-2 py-0.5 text-xs leading-4 text-[var(--vscode-descriptionForeground)] hover:bg-[var(--vscode-toolbar-hoverBackground)]',
|
||
editing && 'bg-[var(--vscode-toolbar-activeBackground)]',
|
||
)}
|
||
onClick={() => tree.onToggleEdit(block.id)}
|
||
>
|
||
{editing ? <Eye size={13} strokeWidth={1.8} /> : <Pencil size={13} strokeWidth={1.8} />}
|
||
{editing ? '预览' : '编辑'}
|
||
</button>
|
||
</div>
|
||
)
|
||
}
|