- 钩子调用讲成状态、引用、记忆值、记忆函数、上下文值、过渡、副作用、钩子返回值、钩子调用几种卡片,行照签名表 reactHooks.ts 排,去掉 react.hook - 解构接住时名称一行写出解构,展开是「第几项 | 名称 | 说明」表,每项一个调用树操作;单个名字的按钮在名称一行最右边 - 属性表的行、子行可以带操作(RowAction),表格多一列「操作」,宿主按块记号加序号起键 - 调用树服务按键数次数,新增查所有引用(references):按所在函数分组,去掉定义处和依赖数组里的 - 钩子收的函数参数都收成小框,函数体是卡片子块,直接返回的表达式由 functionBody 排成「返回 …」卡片;小框和卡片折叠箭头同一个开关 - 规则卡片可指定语言种类图标(languageKind)、种类图标悬停字和颜色(KindBadge.tooltip / iconColor)
523 lines
27 KiB
TypeScript
523 lines
27 KiB
TypeScript
import type { CSSProperties, ReactElement, ReactNode } from 'react'
|
||
import type { BlockView, ChainSlot, ChainStep, Inline, ItemDiff, ItemsDiff, KindBadge, PreviewText, PropertyItem, PropertyRow, Removed, RowAction, RowDiff, RowsDiff, TextDiff } from './types'
|
||
import { ChevronDown, ChevronRight } from 'lucide-react'
|
||
import { Fragment, useContext } from 'react'
|
||
import { cn } from '@/lib/utils'
|
||
import { CallTreeButton } from './BlockTools'
|
||
import { ChainFoldContext } from './chainFold'
|
||
import { DiffInline, DiffText } from './DiffText'
|
||
import { RowActionsContext } from './rowActions'
|
||
import { inlineText } from './segments'
|
||
import { SegmentText } from './SegmentText'
|
||
import { subRowsLayout, tableCells, tableTemplate } from './subRows'
|
||
import { TextPreview } from './TextPreview'
|
||
import { chainStepId, propertyItemId, propertyRowId, tableId, textKey } from './viewState'
|
||
|
||
/** 列表子行里除最后一列以外各列的宽度(字典的键),最后一列占满剩下的;编辑区窄时按比例收,不把最后一列挤没 */
|
||
const CELL_WIDTHS = ['min(150px, 28%)', 'min(220px, 38%)']
|
||
|
||
function gridTemplate(count: number): string {
|
||
const fixed = Array.from({ length: count - 1 }, (_, i) => CELL_WIDTHS[i] ?? CELL_WIDTHS[CELL_WIDTHS.length - 1])
|
||
return [...fixed, 'minmax(0, 1fr)'].join(' ')
|
||
}
|
||
|
||
/**
|
||
* 库规则挂在卡片子行上的子块(BlockSlot):blocks 是卡片的这些子块,render 把一行下面展开着的几块画成一层块(同块树),
|
||
* onToggle 是点子行第一格时展开 / 收起讲它的那一块。
|
||
*/
|
||
export interface GridSlots {
|
||
blocks: BlockView[]
|
||
render: (blocks: BlockView[]) => ReactNode
|
||
onToggle: (block: BlockView) => void
|
||
}
|
||
|
||
/**
|
||
* 挂在卡片链式调用某一步下面的子块(ChainSlot):blocks 是卡片的这些子块,render 把一步下面的几块画成一层块(同块树),
|
||
* open 是那一步眼下展开着没有,onToggle 是点那一步收起来的闭包时展开 / 收起它。
|
||
*/
|
||
export interface ChainSlots {
|
||
blocks: BlockView[]
|
||
render: (blocks: BlockView[]) => ReactNode
|
||
open: (slot: ChainSlot) => boolean
|
||
onToggle: (slot: ChainSlot) => void
|
||
}
|
||
|
||
/** 表格里一个子行讲的子块:展开着没有,它的种类(展开时这一行淡淡垫上它的颜色),点第一格时怎么切换。 */
|
||
interface ItemToggle {
|
||
open: boolean
|
||
badge?: KindBadge
|
||
onToggle: () => void
|
||
}
|
||
|
||
/** 眼下折叠着哪些属性行、子行,点折叠箭头时切换哪个。 */
|
||
export interface Folding {
|
||
collapsed: ReadonlySet<string>
|
||
onToggleFold: (id: string) => void
|
||
}
|
||
|
||
/** 删掉的行下面的子行:总是展开,点了也不收(旧内容只是看看)。 */
|
||
const FIXED: Folding = { collapsed: new Set(), onToggleFold: () => {} }
|
||
|
||
/** 表格行第一格前的记号:加的 +、删的 −,没变的空一格,列照样对齐。 */
|
||
const SIGNS = { add: '+', del: '−', none: ' ' } as const
|
||
|
||
/** 删掉的行(子行)里排在新的第 after 行后面的那些,带着它在删掉的行里的序号(页面上认它用)。 */
|
||
function removedAt<T>(removed: readonly Removed<T>[] | undefined, after: number): { value: T, index: number }[] {
|
||
return (removed ?? []).flatMap((entry, index) => entry.after === after ? [{ value: entry.value, index }] : [])
|
||
}
|
||
|
||
/** 一格是不是成块排的 Markdown 说明(参数的说明、异常的触发条件):它不能放进 .cr-code 的段落里。 */
|
||
function isBlock(cell: PreviewText, parts: TextDiff | undefined): boolean {
|
||
return 'markdown' in cell || (parts !== undefined && 'markdown' in parts)
|
||
}
|
||
|
||
/** 一格的字:着色的词照常,改了的逐截标(DiffInline);Markdown 说明照常排版,改了的逐词标(DiffText)。 */
|
||
function CellText({ cell, parts }: { cell: PreviewText, parts: TextDiff | undefined }): ReactElement {
|
||
if (parts)
|
||
return 'inline' in parts ? <DiffInline parts={parts.inline} /> : <DiffText diff={parts} className="min-w-0" />
|
||
return 'markdown' in cell ? <TextPreview text={cell} className="min-w-0" /> : <SegmentText segments={cell} />
|
||
}
|
||
|
||
/** 一格连外面那层:着色的词排在 .cr-code 的段落里,Markdown 说明是块,lead(差异记号、展开按钮)排在它左边。`find` 是这一格的记号(见 [textKey]),记在外面那层上。 */
|
||
function CellBox({ cell, parts, className, lead, find, ...attrs }: { cell: PreviewText, parts: TextDiff | undefined, className?: string, lead?: ReactNode, find?: string, role?: string }): ReactElement {
|
||
if (isBlock(cell, parts)) {
|
||
return (
|
||
<div className={cn('flex min-w-0 items-start gap-1', className)} data-find={find} {...attrs}>
|
||
{lead}
|
||
<CellText cell={cell} parts={parts} />
|
||
</div>
|
||
)
|
||
}
|
||
return (
|
||
<p className={cn('cr-code min-w-0', className)} data-find={find} {...attrs}>
|
||
{lead}
|
||
<CellText cell={cell} parts={parts} />
|
||
</p>
|
||
)
|
||
}
|
||
|
||
/** 格子写成一串字(读屏念的展开 / 折叠按钮名):着色的词拼起来,Markdown 取原文。 */
|
||
function cellLabel(cell: PreviewText): string {
|
||
return 'markdown' in cell ? cell.markdown : inlineText(cell)
|
||
}
|
||
|
||
/**
|
||
* 子行的第一列(字典的键):带下一层的子行前面是折叠箭头(图标、大小、颜色同属性行的),点箭头或这一列切换;
|
||
* 同一层里不带下一层的子行在同样的位置留空,第一列才对得齐。
|
||
*/
|
||
function ItemHead({ id, cell, foldable, folding, find }: { id: string, cell: PreviewText, foldable: boolean, folding: Folding, find?: string }): ReactElement {
|
||
if (!foldable) {
|
||
return (
|
||
<div className="flex min-w-0 gap-1">
|
||
<span className="w-3.5 shrink-0" />
|
||
<CellBox cell={cell} parts={undefined} find={find} />
|
||
</div>
|
||
)
|
||
}
|
||
const folded = folding.collapsed.has(id)
|
||
const Chevron = folded ? ChevronRight : ChevronDown
|
||
const toggle = (): void => folding.onToggleFold(id)
|
||
// 只有箭头是按钮:这一列里还可能有别的按钮(渲染大纲的「样式 N 个类」),按钮里不能套按钮;点这一列的字照样切换
|
||
return (
|
||
<div className="group flex min-w-0 items-start gap-1">
|
||
<button
|
||
type="button"
|
||
className="flex h-5 w-3.5 shrink-0 items-center rounded-sm"
|
||
aria-label={`${folded ? '展开' : '折叠'}${cellLabel(cell)}`}
|
||
aria-expanded={!folded}
|
||
onClick={toggle}
|
||
>
|
||
<Chevron size={14} strokeWidth={1.8} className="text-[var(--vscode-descriptionForeground)] group-hover:text-[var(--vscode-foreground)]" />
|
||
</button>
|
||
<div className="min-w-0 cursor-pointer" onClick={toggle}>
|
||
<CellBox cell={cell} parts={undefined} find={find} />
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
/**
|
||
* 一行能做的操作([RowAction]),按钮从左往右排:现在都是看调用树([CallTreeButton]),悬停写操作自己的说法,次数、开着没有照卡片给的(见 [RowActionsContext])。
|
||
* 卡片没给(定义卡片浮窗、删掉的块的旧内容)、操作没有键时不画。
|
||
*/
|
||
function RowActionButtons({ actions }: { actions: readonly RowAction[] | undefined }): ReactElement | null {
|
||
const state = useContext(RowActionsContext)
|
||
const keyed = state ? (actions ?? []).flatMap(action => action.key === undefined ? [] : [{ action, key: action.key }]) : []
|
||
if (!state || keyed.length === 0)
|
||
return null
|
||
return (
|
||
<span className="cr-row-actions" role="group" aria-label="操作">
|
||
{keyed.map(({ action, key }) => (
|
||
<CallTreeButton key={key} callKey={key} label={action.label} count={state.counts.get(key) ?? 0} open={state.openKey === key} onOpen={() => state.onOpen(key)} />
|
||
))}
|
||
</span>
|
||
)
|
||
}
|
||
|
||
/** 往右缩进一层、左边一条竖线:一行或一个子行下面的一层子行,列表的一项下面挂的实参表 */
|
||
function Nested({ children }: { children: ReactNode }): ReactElement {
|
||
return (
|
||
<div className="flex gap-2.5 pl-[5px]">
|
||
<div className="cr-guide w-px shrink-0" />
|
||
<div className="flex min-w-0 flex-1 flex-col gap-0.5">{children}</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
/**
|
||
* 展开在一行或一个子行(parentId)下面的一层子行:左边一条竖线,每个子行几列对齐。一行译文下面拆开的各项也用它。
|
||
* 带下一层的子行(字典 / 对象、跨几行写的列表的各项),下一层展开在它下面、再往右缩进一层,默认展开,能折叠;
|
||
* 带实参表的子行(列表里整项是调用),表排在它下面、同样往右缩进一层,总是展开。
|
||
* 标差异时(marks)新加的子行淡绿底、左边 +,删掉的放回原来的位置、淡红底划掉、左边 −,改了的格子逐截标。
|
||
*/
|
||
export function PropertyItems({ parentId, items, folding, marks }: { parentId: string, items: PropertyItem[], folding: Folding, marks?: ItemsDiff }): ReactElement {
|
||
const nested = items.some(item => (item.items?.length ?? 0) > 0)
|
||
/** 一个子行:删掉的(change 是 del)不带 id、下面的子行总是展开 */
|
||
const itemRow = (item: PropertyItem, id: string, change: 'add' | 'del' | undefined, mark: ItemDiff | undefined): ReactElement => {
|
||
const children = item.items ?? []
|
||
const folds = change === 'del' ? FIXED : folding
|
||
const open = children.length > 0 && !folds.collapsed.has(id)
|
||
const below = (
|
||
<>
|
||
{open && <PropertyItems parentId={id} items={children} folding={folds} marks={mark?.items} />}
|
||
{item.table && (
|
||
<Nested>
|
||
<PropertyTable parentId={tableId(id)} columns={item.table.columns} items={item.table.items} marks={mark?.table} folding={folds} />
|
||
</Nested>
|
||
)}
|
||
</>
|
||
)
|
||
return (
|
||
<>
|
||
<div
|
||
className="cr-prop-item grid gap-x-3"
|
||
data-id={change === 'del' ? undefined : id}
|
||
data-diff={change}
|
||
style={{ gridTemplateColumns: gridTemplate(item.cells.length) }}
|
||
>
|
||
{item.cells.map((cell, i) => {
|
||
const find = change === 'del' ? undefined : textKey(id, String(i))
|
||
return i === 0 && nested
|
||
? <ItemHead key={i} id={id} cell={cell} foldable={children.length > 0 && change !== 'del'} folding={folds} find={find} />
|
||
: <CellBox key={i} cell={cell} parts={mark?.cells?.[i]} find={find} />
|
||
})}
|
||
</div>
|
||
{(open || item.table) && (change === 'del' ? <div className="cr-diff-gone">{below}</div> : below)}
|
||
</>
|
||
)
|
||
}
|
||
const removed = (after: number): ReactElement[] => removedAt(marks?.removed, after)
|
||
.map(({ value, index }) => <Fragment key={`removed-${index}`}>{itemRow(value, `${parentId}/x${index}`, 'del', undefined)}</Fragment>)
|
||
return (
|
||
<Nested>
|
||
{removed(-1)}
|
||
{items.map((item, index) => {
|
||
const mark = marks?.changed[index]
|
||
return (
|
||
<Fragment key={index}>
|
||
{itemRow(item, propertyItemId(parentId, index), mark?.added && 'add', mark)}
|
||
{removed(index)}
|
||
</Fragment>
|
||
)
|
||
})}
|
||
</Nested>
|
||
)
|
||
}
|
||
|
||
/**
|
||
* 给了列名的子行(输入参数、数据表的字段)排成表格:第一行是表头(列名),下面每个子行一行;外框圆角,格子之间、行之间一条分隔线。
|
||
* 列宽按整列的内容定(见 tableTemplate),最后一列分剩下的,长了在格子里换行。一行译文下面摊开实参的表也用它;
|
||
* 行带 items(实参的值是跨几行写的列表)时各项排在最后一格里、文字下面,同属性行下面的子行(项下面的下一层按 folding 折叠);
|
||
* 行带 table(实参的值里跨几行写的调用)时那张表排在它们后面,往右缩一点。一层层往下套。
|
||
* 标差异时(marks)有加行、删行的表每行第一格前多一个记号位(+ / −,没变的空着),加的行淡绿底,删的行放回原来的位置、淡红底划掉。
|
||
* 讲某个子块的行(toggles,Rust 实现块的方法表)第一格是按钮,点它在表下面展开那一块,展开时这一行淡淡垫上那一块的种类色。
|
||
* 有行带操作([PropertyItem.actions])、卡片又给了操作的状态([RowActionsContext])时,最后多一列「操作」,宽度按按钮最多的那一行,
|
||
* 各行的操作按钮在这一格里从左往右排,没有操作的行空着。
|
||
*/
|
||
export function PropertyTable({ parentId, columns, items, marks, folding, toggles }: {
|
||
parentId: string
|
||
columns: string[]
|
||
items: PropertyItem[]
|
||
marks?: ItemsDiff
|
||
/** 格子里拆开的子行下面的下一层眼下折叠着没有 */
|
||
folding: Folding
|
||
/** 按子行下标:讲它的子块(点第一格展开,见 GridSlots) */
|
||
toggles?: ReadonlyMap<number, ItemToggle>
|
||
}): ReactElement {
|
||
// 有加行、删行的表每行第一格前留一个记号位
|
||
const signed = marks !== undefined && (marks.removed.length > 0 || Object.values(marks.changed).some(mark => mark.added))
|
||
const operable = useContext(RowActionsContext) !== undefined && items.some(item => (item.actions?.length ?? 0) > 0)
|
||
/** 一行:删掉的(change 是 del)不带 id,套着的表不再标;toggle 是这一行讲的子块,第一格成了展开它的按钮 */
|
||
const tableRow = (item: PropertyItem, id: string, change: 'add' | 'del' | undefined, mark: ItemDiff | undefined, toggle?: ItemToggle): ReactElement => {
|
||
const cells = tableCells(item, columns)
|
||
const listed = (item.items?.length ?? 0) > 0
|
||
const sign = (i: number): ReactNode => signed && i === 0 && <span className="cr-diff-sign" data-change={change} aria-hidden>{SIGNS[change ?? 'none']}</span>
|
||
const find = (i: number): string | undefined => change === 'del' ? undefined : textKey(id, String(i))
|
||
/** 一格:讲子块的那一格是展开它的按钮(只放着色的词),别的照常(见 CellBox) */
|
||
const cellBox = (cell: PreviewText, i: number, className?: string): ReactElement => toggle && i === 0
|
||
? (
|
||
<p className={cn('cr-code min-w-0', className)} role="cell" data-find={find(i)}>
|
||
{sign(i)}
|
||
<button type="button" className="cr-item-toggle" aria-expanded={toggle.open} aria-label={`${toggle.open ? '收起' : '展开'}${cellLabel(cell)}`} onClick={toggle.onToggle}>
|
||
<CellText cell={cell} parts={mark?.cells?.[i]} />
|
||
</button>
|
||
</p>
|
||
)
|
||
: <CellBox cell={cell} parts={mark?.cells?.[i]} className={className} lead={sign(i)} find={find(i)} role="cell" />
|
||
const tint = toggle?.open && toggle.badge
|
||
? { 'data-badge': '', 'style': { '--kind-dark': toggle.badge.color.dark, '--kind-light': toggle.badge.color.light } as CSSProperties }
|
||
: {}
|
||
return (
|
||
<div className="cr-table-row" role="row" data-id={change === 'del' ? undefined : id} data-diff={change} data-open={toggle?.open ? '' : undefined} {...tint}>
|
||
{cells.map((cell, i) => (listed || item.table) && i === cells.length - 1
|
||
? (
|
||
<div key={i} className="cr-table-cell flex min-w-0 flex-col gap-[3px]" role="cell">
|
||
<CellBox cell={cell} parts={mark?.cells?.[i]} lead={sign(i)} find={find(i)} />
|
||
{listed && <PropertyItems parentId={id} items={item.items ?? []} folding={change === 'del' ? FIXED : folding} marks={mark?.items} />}
|
||
{item.table && <PropertyTable parentId={tableId(id)} columns={item.table.columns} items={item.table.items} marks={mark?.table} folding={change === 'del' ? FIXED : folding} />}
|
||
</div>
|
||
)
|
||
: <Fragment key={i}>{cellBox(cell, i, 'cr-table-cell')}</Fragment>)}
|
||
{operable && (
|
||
<div className="cr-table-cell cr-table-ops" role="cell">
|
||
{change !== 'del' && <RowActionButtons actions={item.actions} />}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|
||
const removed = (after: number): ReactElement[] => removedAt(marks?.removed, after)
|
||
.map(({ value, index }) => <Fragment key={`removed-${index}`}>{tableRow(value, `${parentId}/x${index}`, 'del', undefined)}</Fragment>)
|
||
return (
|
||
<div className="pl-[5px]">
|
||
<div className="cr-table" role="table" style={{ gridTemplateColumns: operable ? `${tableTemplate(columns.length)} max-content` : tableTemplate(columns.length) }}>
|
||
<div className="cr-table-head" role="row">
|
||
{columns.map((name, i) => <span key={i} className="cr-table-cell" role="columnheader" data-find={textKey(parentId, `h${i}`)}>{name}</span>)}
|
||
{operable && <span className="cr-table-cell" role="columnheader">操作</span>}
|
||
</div>
|
||
{removed(-1)}
|
||
{items.map((item, index) => {
|
||
const mark = marks?.changed[index]
|
||
return (
|
||
<Fragment key={index}>
|
||
{tableRow(item, propertyItemId(parentId, index), mark?.added && 'add', mark, toggles?.get(index))}
|
||
{removed(index)}
|
||
</Fragment>
|
||
)
|
||
})}
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
/**
|
||
* 函数、类、枚举、导入、常量、变量块和库规则的卡片的预览:属性名一列对齐,值按词性着色;输入参数、导入项、字典 / 对象的值下面展开子行,
|
||
* 给了列名的(输入参数、数据表的字段)排成表格,没给的排成列表。一行带操作([PropertyRow.actions],React 卡片只接一个名字时的「名称」一行)时,
|
||
* 按钮排在这一行最右边(见 [RowActionButtons])。
|
||
* 标差异时(marks)新加的行淡绿底、左边 +,删掉的行放回原来的位置、淡红底划掉、左边 −,值改了的按旧值 → 新值或逐词标。
|
||
*/
|
||
export function PropertyGrid({ blockId, rows, collapsed, onToggleFold, marks, slots, chain }: {
|
||
blockId: string
|
||
rows: PropertyRow[]
|
||
collapsed: ReadonlySet<string>
|
||
onToggleFold: (id: string) => void
|
||
/** 标差异时这张属性表改了哪些行 */
|
||
marks?: RowsDiff
|
||
/** 挂在这张属性表子行上的子块(库规则的卡片),见 GridSlots */
|
||
slots?: GridSlots
|
||
/** 挂在某一行链式调用某一步下面的子块,见 ChainSlots */
|
||
chain?: ChainSlots
|
||
}): ReactElement {
|
||
const folding: Folding = { collapsed, onToggleFold }
|
||
const removed = (after: number): ReactElement[] => removedAt(marks?.removed, after)
|
||
.map(({ value, index }) => <RemovedRow key={`removed-${index}`} id={`${blockId}/x${index}`} row={value} />)
|
||
return (
|
||
<div className="grid grid-cols-[minmax(76px,max-content)_minmax(0,1fr)] gap-x-3 gap-y-[3px] *:col-span-full">
|
||
{removed(-1)}
|
||
{rows.map((row, index) => {
|
||
const layout = subRowsLayout(row)
|
||
const id = propertyRowId(blockId, index)
|
||
const folded = collapsed.has(id)
|
||
const Chevron = folded ? ChevronRight : ChevronDown
|
||
const mark = marks?.changed[index]
|
||
const placed = slots?.blocks.filter(block => block.slot?.row === index) ?? []
|
||
// 挂在子行上的子块只画展开着的
|
||
const shownBlocks = placed.filter(block => !collapsed.has(block.id))
|
||
const toggles = new Map(placed.flatMap((block): [number, ItemToggle][] => block.slot
|
||
? [[block.slot.item, { open: !collapsed.has(block.id), badge: block.badge, onToggle: () => slots?.onToggle(block) }]]
|
||
: []))
|
||
return (
|
||
<Fragment key={index}>
|
||
<div className="cr-prop-row grid grid-cols-subgrid" data-id={id} data-diff={mark?.added && 'add'}>
|
||
{layout
|
||
? (
|
||
<button
|
||
type="button"
|
||
className="cr-prop-label group flex h-5 items-center gap-1 rounded-sm"
|
||
aria-label={`${folded ? '展开' : '折叠'}${row.label}`}
|
||
aria-expanded={!folded}
|
||
data-find={textKey(id, 'label')}
|
||
onClick={() => onToggleFold(id)}
|
||
>
|
||
<Chevron size={14} strokeWidth={1.8} className="shrink-0 text-[var(--vscode-descriptionForeground)] group-hover:text-[var(--vscode-foreground)]" />
|
||
{row.label}
|
||
</button>
|
||
)
|
||
: <span className="cr-prop-label flex h-5 items-center" data-find={textKey(id, 'label')}>{row.label}</span>}
|
||
<WithActions actions={row.actions}>
|
||
{row.chain
|
||
? (
|
||
<div className="min-w-0">
|
||
<RowValue row={row} mark={mark} id={id} />
|
||
<ChainRail blockId={blockId} row={index} steps={row.chain} folding={folding} slots={chain} />
|
||
</div>
|
||
)
|
||
: <RowValue row={row} mark={mark} id={id} />}
|
||
</WithActions>
|
||
</div>
|
||
{!folded && layout === 'table' && <PropertyTable parentId={id} columns={row.columns ?? []} items={row.items ?? []} marks={mark?.items} folding={folding} toggles={toggles} />}
|
||
{!folded && layout === 'list' && <PropertyItems parentId={id} items={row.items ?? []} folding={folding} marks={mark?.items} />}
|
||
{!folded && slots && shownBlocks.length > 0 && <div className="cr-slot">{slots.render(shownBlocks)}</div>}
|
||
{removed(index)}
|
||
</Fragment>
|
||
)
|
||
})}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
/** 属性行的值,这一行带操作、卡片又给了操作的状态时,操作按钮排在值的最右边;不然只有值。 */
|
||
function WithActions({ actions, children }: { actions: readonly RowAction[] | undefined, children: ReactElement }): ReactElement {
|
||
const state = useContext(RowActionsContext)
|
||
if (!state || !actions?.some(action => action.key !== undefined))
|
||
return children
|
||
return (
|
||
<div className="flex min-w-0 items-start gap-2">
|
||
<div className="min-w-0 flex-1">{children}</div>
|
||
<RowActionButtons actions={actions} />
|
||
</div>
|
||
)
|
||
}
|
||
|
||
/**
|
||
* 一行的值:着色的词或 Markdown(改了的逐截 / 逐词标);跟着说明(PropertyRow.note,输出参数的 `@return`)时说明接在值后面,
|
||
* 同一行放得下就并排,剩的地方不到 12 个字宽时换到下一行。`id` 是这一行的 id,值和说明按它记上记号(见 [textKey]);删掉的行没有。
|
||
*/
|
||
function RowValue({ row, mark, id }: { row: PropertyRow, mark: RowDiff | undefined, id?: string }): ReactElement {
|
||
const find = (part: string): string | undefined => id === undefined ? undefined : textKey(id, part)
|
||
const value = mark?.value ? <DiffText diff={mark.value} className="min-w-0" find={find('value')} /> : <TextPreview text={row.value} className="min-w-0" find={find('value')} />
|
||
if (!row.note)
|
||
return value
|
||
return (
|
||
<div className="flex min-w-0 flex-wrap items-baseline gap-x-2">
|
||
{value}
|
||
{mark?.note ? <DiffText diff={mark.note} className="min-w-0 flex-[1_1_12em]" find={find('note')} /> : <TextPreview text={row.note} className="min-w-0 flex-[1_1_12em]" find={find('note')} />}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
/** 属性表里删掉的一行:整行淡红底、划掉,左边 −;它下面的子行照样摊开、划掉。 */
|
||
function RemovedRow({ id, row }: { id: string, row: PropertyRow }): ReactElement {
|
||
const layout = subRowsLayout(row)
|
||
return (
|
||
<>
|
||
<div className="cr-prop-row grid grid-cols-subgrid" data-diff="del">
|
||
<span className="cr-prop-label flex h-5 items-center">{row.label}</span>
|
||
<div className="min-w-0">
|
||
<RowValue row={row} mark={undefined} />
|
||
{row.chain && <ChainRail blockId={id} row={0} steps={row.chain} folding={FIXED} />}
|
||
</div>
|
||
</div>
|
||
{layout && (
|
||
<div className="cr-diff-gone">
|
||
{layout === 'table'
|
||
? <PropertyTable parentId={id} columns={row.columns ?? []} items={row.items ?? []} folding={FIXED} />
|
||
: <PropertyItems parentId={id} items={row.items ?? []} folding={FIXED} />}
|
||
</div>
|
||
)}
|
||
</>
|
||
)
|
||
}
|
||
|
||
/** 一列着色的词大约占几个等宽字宽:汉字这类全角字算两个。 */
|
||
function textWidth(pieces: Inline[]): number {
|
||
let width = 0
|
||
for (const char of inlineText(pieces))
|
||
width += /[\u2E80-\uFFEF]/.test(char) ? 2 : 1
|
||
return width
|
||
}
|
||
|
||
/**
|
||
* 链式调用的各步排成的竖轨道,接在属性行的值(链的起点)下面:一步一个实心点,点与点之间一条函数色的线;方法那一列按最长的那个对齐,
|
||
* 值那一列照抄实参,收起来的闭包是能点的小框,点开时这一步挂着的子块(slots)画在这一步下面。
|
||
* 一步下面依次是小链(空心点,一节一行)、几行属性(写在上面的注释,属性名在方法那一列)、表(合成的一步各次的实参)、展开着的子块。
|
||
* 删掉的行里画的旧链没有 slots,闭包的小框不能点。
|
||
*
|
||
* 
|
||
*/
|
||
export function ChainRail({ blockId, row, steps, folding, slots }: {
|
||
blockId: string
|
||
/** 链在属性表的第几行 */
|
||
row: number
|
||
steps: ChainStep[]
|
||
folding: Folding
|
||
slots?: ChainSlots
|
||
}): ReactElement {
|
||
const width = Math.max(4, ...steps.map(step => textWidth(step.name)))
|
||
return (
|
||
<div className="cr-chain" style={{ '--cr-chain-name': `${width}ch` } as CSSProperties}>
|
||
{steps.map((step, index) => {
|
||
const slot = { row, step: index }
|
||
const id = chainStepId(blockId, slot)
|
||
const blocks = slots?.blocks.filter(block => block.step?.row === row && block.step.step === index) ?? []
|
||
const fold = slots && blocks.length > 0 ? { open: slots.open(slot), onToggle: () => slots.onToggle(slot) } : undefined
|
||
return (
|
||
<div key={index} className="cr-chain-step" data-id={id}>
|
||
<ChainDot first={index === 0} last={index === steps.length - 1} />
|
||
<div className="cr-chain-body">
|
||
<ChainFoldContext value={fold}>
|
||
<div className="cr-chain-line">
|
||
<p className="cr-code cr-chain-name" data-find={textKey(id, 'name')}><SegmentText segments={step.name} /></p>
|
||
<p className="cr-code min-w-0" data-find={textKey(id, 'value')}><SegmentText segments={step.value} /></p>
|
||
</div>
|
||
{step.lines && (
|
||
<div className="cr-chain-line">
|
||
<span />
|
||
<div className="cr-chain-mini">
|
||
{step.lines.map((line, at) => (
|
||
<div key={at} className="cr-chain-step">
|
||
<ChainDot first={at === 0} last={at === (step.lines?.length ?? 0) - 1} hollow />
|
||
<p className="cr-code min-w-0" data-find={textKey(id, `l${at}`)}><SegmentText segments={line} /></p>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</ChainFoldContext>
|
||
{step.rows?.map((item, at) => (
|
||
<div key={at} className="cr-chain-line">
|
||
<span className="cr-chain-label" data-find={textKey(id, `r${at}-label`)}>{item.label}</span>
|
||
<TextPreview text={item.value} className="min-w-0" find={textKey(id, `r${at}`)} />
|
||
</div>
|
||
))}
|
||
{step.table && <PropertyTable parentId={tableId(id)} columns={step.table.columns} items={step.table.items} folding={folding} />}
|
||
{fold?.open && <div className="cr-chain-blocks">{slots?.render(blocks)}</div>}
|
||
</div>
|
||
</div>
|
||
)
|
||
})}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
/** 轨道上的一个点,上下各一截线连着前后两步(第一步上面、最后一步下面不画);小链的点是空心的。 */
|
||
function ChainDot({ first, last, hollow = false }: { first: boolean, last: boolean, hollow?: boolean }): ReactElement {
|
||
return (
|
||
<span className="cr-chain-rail" data-first={first ? '' : undefined} data-last={last ? '' : undefined}>
|
||
<span className="cr-chain-up" />
|
||
<span className="cr-chain-dot" data-hollow={hollow ? '' : undefined} />
|
||
<span className="cr-chain-down" />
|
||
</span>
|
||
)
|
||
}
|