Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
184 changes: 184 additions & 0 deletions .claude/docs/table/block-selection-core-migration-design-2026-03-12.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
# Block Selection Core Migration Design Draft

## Background

- The current block selection implementation lives in [BlockSelectionPlugin.tsx](/Users/felixfeng/Desktop/udecode/plate/packages/selection/src/react/BlockSelectionPlugin.tsx).
- The implementation is already on the right track: state is lightweight, the core data is `selectedIds: Set<string>`, and it provides per-id selectors.
- In contrast, table selection takes a heavier path:
- [useSelectedCells.ts](/Users/felixfeng/Desktop/udecode/plate/packages/table/src/react/components/TableElement/useSelectedCells.ts) recomputes the table/cell grid and writes `selectedCells` / `selectedTables`
- [useIsCellSelected.ts](/Users/felixfeng/Desktop/udecode/plate/packages/table/src/react/components/TableCellElement/useIsCellSelected.ts) makes every cell subscribe to the entire selection state
- The long-term direction: selection becomes a core editor capability, and block selection / table selection both build on top of it.
- Phase 1 does not address table selection. It only completes the block selection migration, but the design must leave room for table extension.

## Problem Definition

Although the current block selection works, it is still "a complete selection state and behavior set owned by a plugin", which causes several structural issues:

1. Selection is a feature-level capability, not a core-level capability
2. Other selection modes cannot reuse the same underlying model
3. Table selection will likely grow another parallel state set instead of reusing unified selection infrastructure

We need to take the first step: move block selection state ownership down to core, without immediately changing the base semantics of `editor.selection`.

## Goals

- Make block selection a core-backed capability
- Keep existing block selection interactions and external behavior unchanged
- Do not modify `editor.selection` in Phase 1
- Introduce a core model that can accommodate future non-text selection
- Keep Phase 1 within a scope that can be landed and verified independently

## Non-Goals

- Do not redo table selection in this phase
- Do not change Slate's `editor.selection` to `Range | Range[]`
- Do not require all transforms to understand multi-range discrete ranges in Phase 1
- Do not detail table merge / border implementation in this document

## Phase 1 Scope

Phase 1 does one thing: migrate block selection from "plugin-owned state" to "core-owned state".

Expected outcome after this phase:

- Block selection state is registered and managed by core
- Block selection UI can remain in the selection package
- Existing block selection commands continue to work, but delegate to core internally
- External callers no longer treat block plugin options as the ultimate source of truth

## Proposed Core State Model

The Phase 1 model should be minimal — do not prematurely include the full table shape.

```ts
type EditorSelectionState = {
primary: Range | null;
block: {
anchorId: string | null;
selectedIds: Set<string>;
isSelecting: boolean;
isSelectionAreaVisible: boolean;
};
};
```

Notes:

- `primary` continues to correspond to the current text selection
- `block` is a new core selection channel, not a plugin-private store
- `selectedIds` continues to use `Set<string>` because it is already the correct data shape: cheap per-id lookups, low-cost membership checks
- Phase 1 does not add a table descriptor, but the state boundary must not be hardcoded to "only block as an extra selection type"

## API Direction

Core should expose a thin selection API layer, and block selection adapts on top of it.

```ts
editor.api.selection.getPrimary()
editor.api.selection.setPrimary(range)

editor.api.selection.block.get()
editor.api.selection.block.clear()
editor.api.selection.block.set(ids)
editor.api.selection.block.add(ids)
editor.api.selection.block.delete(ids)
editor.api.selection.block.has(id)
editor.api.selection.block.isSelecting()
```

Existing block-facing helpers can be retained, but their semantics should change:

- `editor.getApi(BlockSelectionPlugin).blockSelection.add(...)`
- `editor.getApi(BlockSelectionPlugin).blockSelection.clear()`
- `editor.getApi(BlockSelectionPlugin).blockSelection.getNodes(...)`

These helpers should become compatibility wrappers rather than continuing to hold their own real state.

## Rendering Layer Direction

Phase 1 does not need to rewrite block selection visual interactions — only migrate state ownership.

- Block selection area UI can remain in the selection package
- [useBlockSelected.ts](/Users/felixfeng/Desktop/udecode/plate/packages/selection/src/react/hooks/useBlockSelected.ts) switches to reading a core-backed selector
- `BlockSelectionPlugin` shrinks to an adapter: event wiring, render integration, and compatibility layer API

This approach carries significantly lower risk than "rewriting the entire interaction model at once".

## Migration Steps

### Step 1: Introduce core block selection state

- Add block selection state structure in core
- Expose minimal selectors and mutators
- Keep `editor.selection` behavior unchanged

### Step 2: Redirect block selection API

- Redirect reads/writes of `selectedIds`, `anchorId`, `isSelecting`, etc. behind the core API
- Continue exposing the existing block selection command surface externally

### Step 3: Redirect hooks and render

- Hooks like [useBlockSelected.ts](/Users/felixfeng/Desktop/udecode/plate/packages/selection/src/react/hooks/useBlockSelected.ts) switch to consuming the core-backed selector
- UI behavior remains unchanged

### Step 4: Reduce plugin state ownership

- `BlockSelectionPlugin` retains:
- Event wiring
- Adapter APIs
- Rendering integration
- Core becomes the sole state owner

## Compatibility Strategy

To keep the blast radius under control, Phase 1 should adhere to these rules:

- `editor.selection` continues to be `Range | null`
- Not all editing commands are required to understand block selection immediately
- Block-specific operations continue to explicitly read block selection state
- Avoid introducing large-scale type modifications in Phase 1

This allows the migration to be incremental rather than affecting the entire Slate / Plate command surface at once.

## Why This Step First

The value of this phase:

- Reclaim selection ownership into core
- Remove a feature-level state owner
- Provide a unified foundation for other future selection modes

It is also low-risk because block selection's current data model is already relatively healthy — the main issue is "where the state lives", not "what the state looks like".

## Table Direction as Design Constraint Only

Table is not in Phase 1 scope, but Phase 1 design must avoid blocking future table work.

Phase 1 should explicitly avoid:

- Hardcoding core selection to serve only block ids
- Treating "flat id set" as the only non-text selection shape
- Letting future table selection still depend on materialized node arrays

Future table selection will likely need:

- A table-scoped descriptor instead of `selectedCells: TElement[]`
- Keyed selectors instead of each cell subscribing to the entire selection
- Expressive power for non-contiguous / grid-shaped selection semantics

No need to detail the table design here — just ensure Phase 1 state boundaries and API do not prevent Phase 2 from extending in this direction.

## Open Questions

- Should core selection expose a channeled model (`block` / `table` / `primary`) or a more generic descriptor registry?
- After migration, which `BlockSelectionPlugin` APIs are still worth keeping as public interfaces?
- Should block selection render logic stay in `packages/selection` long-term, or continue moving toward core?

## Phase 1 Acceptance Criteria

- Block selection state is owned by core
- Existing block selection interaction behavior remains consistent
- `useBlockSelected` and related selectors switch to reading core-backed state
- Existing block selection commands continue to work, delegating to core via compatibility wrappers
- Phase 1 does not require any changes to table selection behavior
50 changes: 50 additions & 0 deletions .claude/docs/table/dev-table-perf-performance-2026-03-10.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# dev/table-perf 性能快照

## 环境

- 测试时间:2026-03-10 23:36:30 CST
- 页面地址:`http://localhost:3002/dev/table-perf`
- 应用:`apps/www`,Next.js 16.1.6(Turbopack dev)
- 浏览器:`agent-browser` + Chromium 138.0.7204.15
- 机器:macOS 15.7.3
- 页面错误:`agent-browser errors` 未发现报错

## 采样方法

- 阅读 `apps/www/src/app/dev/table-perf/page.tsx`,确认页面内置了两套测试:
- benchmark:`5` 次 warmup + `20` 次 measured remount
- input latency:`10` 次 warmup + `50` 次 measured inserts
- 使用 `agent-browser` 打开 `/dev/table-perf`
- 读取页面 Metrics 面板和 console 输出
- 本次记录两组数据:
- 默认 `10 x 10`(100 cells)
- 压力 `60 x 60`(3600 cells)

## 结果

| 配置 | Cells | Initial render | Benchmark mean | Benchmark median | Benchmark p95 | Input mean | Input median | Input p95 |
| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
| 10 x 10 | 100 | 209.30 ms | 99.32 ms | 83.80 ms | 147.50 ms | 28.07 ms | 28.40 ms | 30.50 ms |
| 60 x 60 | 3600 | 2663.30 ms | 2499.24 ms | 2453.30 ms | 2848.70 ms | 390.48 ms | 379.40 ms | 443.70 ms |

## 对比

- `60 x 60` 相比 `10 x 10`
- Initial render:`12.72x`
- Benchmark mean:`25.16x`
- Input mean:`13.91x`

## 结论

- `10 x 10` 基线可接受。input latency 均值约 `28 ms`,交互感觉应当是顺的。
- `60 x 60` 仍可跑完,但已经明显进入高延迟区间:
- 初始挂载约 `2.66 s`
- benchmark 均值约 `2.50 s`
- 单次输入延迟均值约 `390 ms`
- 以当前页面表现看,大表场景下主要问题不是偶发尖峰,而是整体延迟已经稳定抬高到肉眼可感知的程度。

## 解读注意点

- 切换 preset 后点击 `Generate Table`,页面会刷新当前表格和基础 metrics。
- `Benchmark Results` 会被清空后重新计算。
- `Input Latency` 结果不会在 `Generate Table` 时自动清空;如果切到新 preset,必须重新跑一次 input latency 才能读到当前配置的数据。
59 changes: 59 additions & 0 deletions .claude/docs/table/dev-table-perf-performance-2026-03-12.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# dev/table-perf Performance Snapshot

## Environment

- Test time: 2026-03-12 21:46:54 CST
- Page URL: `http://localhost:3000/dev/table-perf`
- App: `apps/www`
- Commit: `98ae3116f`
- Browser: Playwright + Chromium `138.0.7204.15`
- Page accessibility check: confirmed `/dev/table-perf` loads via `agent-browser`

## Sampling Method

- Read `apps/www/src/app/dev/table-perf/page.tsx`, confirmed the page has two built-in tests:
- Benchmark: `5` warmup + `20` measured remount iterations
- Input latency: `10` warmup + `50` measured inserts
- Used `Large (1600)` preset, corresponding to `40 x 40`
- Click sequence:
- `Generate Table`
- `Run Benchmark (20 iter)`
- `Test Input Latency (50 samples)`
- Metrics read from the page Metrics panel

## Results

### 40 x 40 (1600 cells)

| Category | Metric | Value |
| --- | --- | ---: |
| Metrics | Initial render | 1002.00 ms |
| Metrics | Re-render count | 3 |
| Metrics | Last render | 0.40 ms |
| Metrics | Avg render | 351.27 ms |
| Metrics | Render median | 51.40 ms |
| Metrics | Render p95 | 1002.00 ms |
| Benchmark Results | Mean | 841.44 ms |
| Benchmark Results | Median | 827.10 ms |
| Benchmark Results | P95 | 959.30 ms |
| Benchmark Results | P99 | 959.30 ms |
| Benchmark Results | Min | 804.00 ms |
| Benchmark Results | Max | 959.30 ms |
| Benchmark Results | Std Dev | 31.64 ms |
| Input Latency | Mean | 40.74 ms |
| Input Latency | Median | 38.70 ms |
| Input Latency | P95 | 52.70 ms |
| Input Latency | Min | 28.00 ms |
| Input Latency | Max | 61.60 ms |

## Conclusion

- The `40 x 40` remount benchmark is still in the high-cost range, with a mean of ~`841 ms`.
- The `40 x 40` input latency mean is ~`41 ms`, median ~`39 ms`, well below the `100+ ms` threshold where noticeable lag occurs.
- This snapshot is better suited for tracking large-table input performance; if the focus shifts to resize/hover interactions, a separate drag/hover profiling session is recommended.

## Interpretation Notes

- `Initial render`, `Re-render count`, `Last render`, and `Avg render / Median / P95` come from the left-side Metrics panel.
- `Benchmark Results` are the remount benchmark statistics.
- `Input Latency` results do not auto-clear when switching presets or clicking `Generate Table`; re-run after changing configuration.
2 changes: 1 addition & 1 deletion apps/www/public/r/components-changelog-docs.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion apps/www/public/r/table-node.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion apps/www/public/tailwind.css

Large diffs are not rendered by default.

Loading