From 00d5590aa7b0b742a137ba7dbc71fd626de6eac5 Mon Sep 17 00:00:00 2001 From: wit_qq Date: Sun, 8 Mar 2026 17:09:48 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20planeta=20migration=20=E2=80=94=20new?= =?UTF-8?q?=20features,=20comprehensive=20documentation=20overhaul?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New engine features: - Auto row height: viewport-first sync measurement, idle async sweep, manual-always-wins priority, scroll compensation - Column stretch: 'all'/'last' modes, ResizeObserver, frozen column handling - Text wrapping: word-boundary + char fallback, multi-line canvas rendering - Date picker: calendar overlay, keyboard nav, ARIA dialog, scroll-aware - DateTime editor: calendar + hour/minute spinners, ISO commit format - Context menu submenus: recursive nesting, hover delays, keyboard nav - Locale system: EN/RU packs, runtime switching, resolveLocale() merge - Cell editor registry: priority-sorted resolution, CellEditor lifecycle - Benchmark suite: BenchmarkRunner with 6 metrics across 1K/10K/100K rows Documentation overhaul: - Internal: ARCHITECTURE.md (all engine subsystems), TESTING-GUIDE.md, DOCUMENTATION-STYLE-GUIDE.md, CHECKLIST.md, CLAUDE.md - Site audit: 47 existing pages verified against source code, 80+ factual errors fixed (wrong signatures, fabricated payloads, incorrect types, outdated class names) - 6 new guide pages: auto-row-height, text-wrapping, column-stretch, date-editors, cell-editor-registry, locale - Cross-references: features index, editing, configuration, cell-types - Context menu submenus section with positioning, timing, keyboard nav - Site: 48 → 54 pages, 0 build errors Demo: sidebar layout, feature toggles, badges, 1K/1M row toggle Tests: 1708 unit tests, 13 E2E Playwright tests --- CHANGELOG.md | 63 ++ CONTRIBUTING.md | 1 + README.md | 52 +- docs/ARCHITECTURE.md | 688 +++++++++++++++ docs/CHECKLIST.md | 45 + docs/DOCUMENTATION-STYLE-GUIDE.md | 111 +++ docs/SCREENSHOT-VALIDATION-GUIDE.md | 254 ++++++ docs/TESTING-GUIDE.md | 176 ++++ eslint.config.js | 6 +- package-lock.json | 333 ++++++++ package.json | 2 + packages/angular/src/index.ts | 6 +- packages/core/src/aria/aria-manager.ts | 27 +- .../auto-row-size/auto-row-size-manager.ts | 341 ++++++++ .../core/src/autofill/autofill-manager.ts | 2 - .../src/benchmark/performance-benchmark.ts | 143 +++- packages/core/src/clipboard/index.ts | 7 +- .../src/commands/batch-cell-edit-command.ts | 7 +- .../core/src/commands/cell-edit-command.ts | 7 +- packages/core/src/commands/merge-commands.ts | 2 +- packages/core/src/commands/row-commands.ts | 45 +- packages/core/src/constants.ts | 5 + .../src/context-menu/context-menu-manager.ts | 450 +++++++--- .../core/src/context-menu/default-items.ts | 97 ++- .../core/src/editing/cell-editor-registry.ts | 91 ++ packages/core/src/editing/cell-editor.ts | 119 +++ .../core/src/editing/date-picker-editor.ts | 93 ++ .../core/src/editing/date-picker-overlay.ts | 544 ++++++++++++ packages/core/src/editing/date-time-editor.ts | 797 ++++++++++++++++++ packages/core/src/editing/inline-editor.ts | 14 +- .../core/src/engine/spreadsheet-engine.ts | 551 +++++++++++- packages/core/src/events/event-types.ts | 19 +- packages/core/src/filter/filter-panel.ts | 37 +- .../core/src/grouping/row-group-manager.ts | 19 +- packages/core/src/index.ts | 71 +- packages/core/src/locale/en.ts | 98 +++ packages/core/src/locale/index.ts | 8 + packages/core/src/locale/locale-types.ts | 105 +++ packages/core/src/locale/resolve-locale.ts | 28 + packages/core/src/locale/ru.ts | 96 +++ packages/core/src/merge/merge-manager.ts | 10 +- packages/core/src/model/cell-store.ts | 9 +- packages/core/src/model/col-store.ts | 5 +- packages/core/src/model/row-store.ts | 128 ++- packages/core/src/model/style-pool.ts | 1 - packages/core/src/pivot/pivot-engine.ts | 29 +- packages/core/src/print/print-manager.ts | 10 +- packages/core/src/renderer/grid-renderer.ts | 48 +- .../src/renderer/layers/cell-text-layer.ts | 129 ++- .../src/renderer/layers/empty-state-layer.ts | 9 +- .../src/renderer/layers/fill-handle-layer.ts | 7 +- .../src/renderer/layers/grid-lines-layer.ts | 43 +- .../core/src/renderer/layers/header-layer.ts | 2 +- .../layers/selection-overlay-layer.ts | 41 +- packages/core/src/renderer/layout-engine.ts | 53 ++ packages/core/src/renderer/render-layer.ts | 7 + packages/core/src/renderer/render-pipeline.ts | 46 +- .../core/src/renderer/text-measure-cache.ts | 179 ++++ .../core/src/renderer/viewport-manager.ts | 25 +- .../core/src/resize/column-stretch-manager.ts | 181 ++++ packages/core/src/resize/index.ts | 6 + .../core/src/selection/selection-manager.ts | 34 +- .../core/src/streaming/streaming-adapter.ts | 2 +- packages/core/src/types/cell-type-registry.ts | 44 + packages/core/src/types/interfaces.ts | 7 +- .../core/tests/auto-row-size-manager.test.ts | 329 ++++++++ packages/core/tests/benchmark-suite.test.ts | 306 +++++++ .../core/tests/cell-editor-registry.test.ts | 148 ++++ .../core/tests/cell-type-registry.test.ts | 90 ++ .../core/tests/column-stretch-manager.test.ts | 274 ++++++ packages/core/tests/context-menu.test.ts | 512 +++++++++++ .../core/tests/date-picker-overlay.test.ts | 378 +++++++++ packages/core/tests/date-time-editor.test.ts | 366 ++++++++ packages/core/tests/layout-engine.test.ts | 184 ++++ packages/core/tests/locale.test.ts | 126 +++ .../core/tests/performance-benchmark.test.ts | 66 +- packages/core/tests/row-store.test.ts | 204 ++++- .../core/tests/text-measure-cache.test.ts | 207 ++++- packages/demo/src/App.tsx | 620 ++++++++++++-- packages/demo/src/visual-tests/index.ts | 102 ++- .../visual-tests/scenarios/cell-types-all.tsx | 27 +- .../scenarios/custom-row-height.tsx | 8 +- .../scenarios/demo-change-tracking.tsx | 9 +- .../visual-tests/scenarios/demo-clipboard.tsx | 4 +- .../visual-tests/scenarios/demo-formula.tsx | 14 +- .../scenarios/demo-frozen-panes.tsx | 9 +- .../src/visual-tests/scenarios/demo-hero.tsx | 40 +- .../scenarios/demo-row-grouping.tsx | 4 +- .../scenarios/demo-theme-switcher.tsx | 8 +- .../scenarios/demo-validation.tsx | 17 +- .../visual-tests/scenarios/frozen-panes.tsx | 8 +- .../src/visual-tests/scenarios/long-text.tsx | 31 +- .../visual-tests/scenarios/many-columns.tsx | 2 +- .../visual-tests/scenarios/no-row-numbers.tsx | 7 +- .../src/visual-tests/scenarios/shared.tsx | 37 +- .../scenarios/validation-errors.tsx | 4 +- .../src/conditional-format-plugin.ts | 2 +- packages/plugins/excel/src/excel-plugin.ts | 1 - .../plugins/formula/src/dependency-graph.ts | 17 - packages/plugins/formula/src/evaluator.ts | 2 +- .../src/collaboration/collaboration-plugin.ts | 13 +- .../plugins/src/collaboration/ot-engine.ts | 54 +- packages/plugins/tsconfig.json | 2 +- packages/react/src/components/Spreadsheet.tsx | 13 +- packages/react/src/index.ts | 6 +- packages/server/src/collab-server.ts | 20 +- packages/site/.astro/content-modules.mjs | 50 +- packages/site/astro.config.mjs | 6 + .../components/demos/AccessibilityDemo.tsx | 6 +- .../src/components/demos/AutofillDemo.tsx | 6 +- .../src/components/demos/BasicTableDemo.tsx | 5 +- .../src/components/demos/CellTypesDemo.tsx | 56 +- .../components/demos/ChangeTrackingDemo.tsx | 14 +- .../src/components/demos/ClipboardDemo.tsx | 27 +- .../demos/ConditionalFormatDemo.tsx | 55 +- .../src/components/demos/ContextMenuDemo.tsx | 20 +- .../site/src/components/demos/DemoButton.tsx | 14 +- .../site/src/components/demos/DemoWrapper.tsx | 80 +- .../site/src/components/demos/EditingDemo.tsx | 22 +- .../src/components/demos/EventBusDemo.tsx | 71 +- .../site/src/components/demos/ExcelDemo.tsx | 10 +- .../src/components/demos/FilteringDemo.tsx | 22 +- .../site/src/components/demos/FormulaDemo.tsx | 40 +- .../src/components/demos/FrozenPanesDemo.tsx | 6 +- .../site/src/components/demos/HeroDemo.tsx | 124 ++- .../src/components/demos/KeyboardNavDemo.tsx | 5 +- .../site/src/components/demos/MergingDemo.tsx | 6 +- .../src/components/demos/MillionRowsDemo.tsx | 163 +++- .../components/demos/PluginShowcaseDemo.tsx | 40 +- .../src/components/demos/PrintSupportDemo.tsx | 6 +- .../site/src/components/demos/ResizeDemo.tsx | 15 +- .../src/components/demos/RowGroupingDemo.tsx | 13 +- .../src/components/demos/SelectionDemo.tsx | 16 +- .../site/src/components/demos/SortingDemo.tsx | 22 +- .../components/demos/ThemeSwitcherDemo.tsx | 8 +- .../src/components/demos/UndoRedoDemo.tsx | 8 +- .../src/components/demos/ValidationDemo.tsx | 64 +- .../src/components/demos/generate-data.ts | 47 +- .../site/src/content/docs/api/cell-types.mdx | 10 +- packages/site/src/content/docs/api/types.mdx | 40 +- .../src/content/docs/concepts/data-model.mdx | 34 +- .../site/src/content/docs/concepts/events.mdx | 123 ++- .../src/content/docs/concepts/rendering.mdx | 73 +- .../site/src/content/docs/concepts/themes.mdx | 7 +- .../src/content/docs/frameworks/angular.mdx | 2 +- .../src/content/docs/frameworks/react.mdx | 7 +- .../site/src/content/docs/frameworks/vue.mdx | 2 +- .../src/content/docs/frameworks/widget.mdx | 20 +- .../docs/getting-started/configuration.mdx | 17 +- .../content/docs/getting-started/index.mdx | 21 +- .../docs/getting-started/installation.mdx | 11 +- .../docs/getting-started/performance.mdx | 9 +- .../docs/getting-started/quick-start.mdx | 4 +- .../docs/getting-started/typescript.mdx | 12 +- .../content/docs/guides/auto-row-height.mdx | 105 +++ .../site/src/content/docs/guides/autofill.mdx | 13 +- .../docs/guides/cell-editor-registry.mdx | 122 +++ .../src/content/docs/guides/clipboard.mdx | 45 +- .../content/docs/guides/column-stretch.mdx | 101 +++ .../src/content/docs/guides/context-menu.mdx | 93 +- .../src/content/docs/guides/date-editors.mdx | 99 +++ .../site/src/content/docs/guides/editing.mdx | 20 +- .../site/src/content/docs/guides/features.mdx | 16 +- .../src/content/docs/guides/filtering.mdx | 4 +- .../src/content/docs/guides/frozen-panes.mdx | 4 +- .../site/src/content/docs/guides/locale.mdx | 118 +++ .../guides/migration-from-handsontable.mdx | 38 +- .../site/src/content/docs/guides/print.mdx | 13 +- .../site/src/content/docs/guides/resize.mdx | 44 +- .../src/content/docs/guides/selection.mdx | 6 +- .../site/src/content/docs/guides/sorting.mdx | 8 +- .../src/content/docs/guides/text-wrapping.mdx | 96 +++ .../src/content/docs/guides/undo-redo.mdx | 35 +- .../src/content/docs/guides/validation.mdx | 14 +- .../content/docs/plugins/collaboration.mdx | 8 +- .../docs/plugins/conditional-format.mdx | 42 +- .../content/docs/plugins/custom-functions.mdx | 8 +- .../src/content/docs/plugins/formulas.mdx | 27 +- .../src/content/docs/plugins/overview.mdx | 15 +- .../docs/plugins/progressive-loader.mdx | 12 +- packages/vue/src/Spreadsheet.ts | 10 +- packages/vue/src/index.ts | 6 +- packages/widget/src/index.ts | 17 +- .../context-menu-submenu-verification.test.ts | 266 ++++++ tests/e2e/date-picker-screenshots.test.ts | 95 +++ tests/e2e/date-picker-verification.test.ts | 154 ++++ tests/e2e/date-picker.test.ts | 157 ++++ tests/e2e/date-time-verification.test.ts | 157 ++++ tests/e2e/word-wrap.test.ts | 145 ++++ 189 files changed, 13243 insertions(+), 1195 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 docs/ARCHITECTURE.md create mode 100644 docs/CHECKLIST.md create mode 100644 docs/DOCUMENTATION-STYLE-GUIDE.md create mode 100644 docs/SCREENSHOT-VALIDATION-GUIDE.md create mode 100644 docs/TESTING-GUIDE.md create mode 100644 packages/core/src/auto-row-size/auto-row-size-manager.ts create mode 100644 packages/core/src/constants.ts create mode 100644 packages/core/src/editing/cell-editor-registry.ts create mode 100644 packages/core/src/editing/cell-editor.ts create mode 100644 packages/core/src/editing/date-picker-editor.ts create mode 100644 packages/core/src/editing/date-picker-overlay.ts create mode 100644 packages/core/src/editing/date-time-editor.ts create mode 100644 packages/core/src/locale/en.ts create mode 100644 packages/core/src/locale/index.ts create mode 100644 packages/core/src/locale/locale-types.ts create mode 100644 packages/core/src/locale/resolve-locale.ts create mode 100644 packages/core/src/locale/ru.ts create mode 100644 packages/core/src/resize/column-stretch-manager.ts create mode 100644 packages/core/tests/auto-row-size-manager.test.ts create mode 100644 packages/core/tests/benchmark-suite.test.ts create mode 100644 packages/core/tests/cell-editor-registry.test.ts create mode 100644 packages/core/tests/column-stretch-manager.test.ts create mode 100644 packages/core/tests/date-picker-overlay.test.ts create mode 100644 packages/core/tests/date-time-editor.test.ts create mode 100644 packages/core/tests/locale.test.ts create mode 100644 packages/site/src/content/docs/guides/auto-row-height.mdx create mode 100644 packages/site/src/content/docs/guides/cell-editor-registry.mdx create mode 100644 packages/site/src/content/docs/guides/column-stretch.mdx create mode 100644 packages/site/src/content/docs/guides/date-editors.mdx create mode 100644 packages/site/src/content/docs/guides/locale.mdx create mode 100644 packages/site/src/content/docs/guides/text-wrapping.mdx create mode 100644 tests/e2e/context-menu-submenu-verification.test.ts create mode 100644 tests/e2e/date-picker-screenshots.test.ts create mode 100644 tests/e2e/date-picker-verification.test.ts create mode 100644 tests/e2e/date-picker.test.ts create mode 100644 tests/e2e/date-time-verification.test.ts create mode 100644 tests/e2e/word-wrap.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..d2f459c --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,63 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added +- Performance benchmark suite with `BenchmarkRunner`, `measureMultiRun`, `computeStats`, `measureThroughput` utilities +- `npm run benchmark` script for running performance benchmarks +- Baseline metrics for 6 categories across 1K/10K/100K row datasets +- `TextMeasureCache.measureEmHeight()` for font em-height measurement +- `TextMeasureCache.getWrappedLines()` for word-boundary text wrapping with character-level fallback +- `TextMeasureCache.countWrappedLines()` and `measureWrappedHeight()` for wrapped text height computation +- `ColumnDef.wrapText` option to enable word-wrap rendering per column +- Multi-line wrapped text rendering in `CellTextLayer` when `wrapText` is enabled +- E2E Playwright test for word-wrap visual verification (`tests/e2e/word-wrap.test.ts`) +- `LINE_HEIGHT_MULTIPLIER` shared constant (1.2) for consistent line-height across text rendering +- `RowStore` auto/manual height separation: `setAutoHeight()`, `setAutoHeightsBatch()`, `clearAutoHeight()`, `clearAllAutoHeights()`, `isManual()`, `isAuto()` +- `LayoutEngine.setRowHeightsBatch()` for O(n) batch row height updates (vs O(n²) for individual calls) +- `CellTypeRenderer.measureHeight()` optional method for custom cell type height measurement +- `RenderLayer.measureHeights()` optional method for bulk row height measurement by render layers +- `CellTextLayer.measureHeights()` implementation for measuring wrapped text row heights +- `SpreadsheetEngine.setAutoRowHeights()` for batch auto-measured height updates with manual-always-wins priority +- `AutoRowSizeManager` class: orchestrates automatic row height measurement with viewport-first sync strategy and off-screen async measurement via `requestIdleCallback` +- `SpreadsheetEngineConfig.autoRowHeight` option to enable auto row height (`boolean` or `AutoRowSizeConfig` with `batchSize`, `minRowHeight`, `cellPadding`) +- `SpreadsheetEngine.getAutoRowSizeManager()` accessor for the auto row size manager instance +- `AutoRowSizeManager` dirty tracking: `markDirtyRows()`, `markAllDirty()`, `clearDirty()`, `hasDirtyRows`, `isRowDirty()`, `isAllDirty`, `dirtyRowCount` +- `AutoRowSizeManager.startDirtyMeasurement()` for efficient re-measurement of only dirty rows +- Scroll compensation in `setAutoRowHeights()`: adjusts scroll position when row heights change above viewport to prevent visual jumping +- `SpreadsheetEngine.markAutoRowHeightDirty()` and `markAllAutoRowHeightDirty()` public API for triggering dirty re-measurement +- Auto row height integration with `setCell()`: cell value changes mark the row dirty for re-measurement +- Auto row height integration with column resize: resizing a wrap-enabled column triggers full re-measurement +- `ColumnStretchManager` class: distributes available container width across columns with two modes ('all' and 'last') +- `SpreadsheetEngineConfig.stretchColumns` option: `'all'` distributes extra space evenly among stretchable columns, `'last'` gives remaining space to the last visible column +- `LayoutEngine.setColumnWidthsBatch()` for efficient batch column width updates (single recomputation pass) +- Column stretch recalculation on container resize via existing `ResizeObserver` +- Manual column resize exclusion: manually resized columns are excluded from stretch distribution in 'all' mode +- Frozen column handling: frozen columns are excluded from stretch distribution +- `DatePickerOverlay` class: pure-DOM calendar widget for date-type cell editing, positioned below target cell +- `CellEditor` interface and `CellEditorRegistry` for extensible cell editing with type-based editor resolution +- `DatePickerEditor` adapter wrapping `DatePickerOverlay` as a `CellEditor` implementation +- `DateTimeEditor` class: combined date+time picker (calendar + hour/minute spin controls) for `datetime` columns, commits ISO `YYYY-MM-DDTHH:mm` format +- `'datetime'` added to `CellType` union type +- `dateTimePicker` section added to `SpreadsheetLocale` interface (hour, minute, now, ariaLabel) with EN and RU translations +- Date picker opens on double-click, F2, or type-to-edit for columns with `type: 'date'` +- Calendar month/year navigation, day grid with keyboard navigation (arrows, Enter, Escape, Tab) +- Date selection commits value in YYYY-MM-DD format via command system (undo/redo supported) +- Grid scroll and outside click close the date picker +- Today button for quick date selection +- ARIA attributes on date picker overlay (role=dialog, aria-label) +- `ContextMenuItem.submenu` optional field for recursive nested submenus +- Submenu chevron indicator (`▸`) on items with submenus +- Submenu opens on hover (200ms delay) or ArrowRight key, closes on ArrowLeft or Escape +- Nested submenus supported recursively to arbitrary depth +- Empty menu prevention: parent items with all invisible submenu children are hidden +- Keyboard navigation within submenus (ArrowUp/Down), Escape closes one level at a time +- Interactive demo page with sidebar layout showcasing all library features +- Demo sidebar sections: Display (theme, stretch, auto row height), Data (1M rows, progressive load, streaming), Views (grouping, pivot), Import/Export (Excel, print), Collaboration +- Demo feature badges showing active engine configuration +- Demo context menu items with Insert and Format submenus for submenu showcase diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 68da451..37485ae 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -18,6 +18,7 @@ npm run typecheck # TypeScript check npm run lint # ESLint npm run test # Unit tests (vitest) npm run test:e2e # E2E tests (playwright) +npm run benchmark # Performance benchmarks (6 metrics × 3 datasets) ``` ## Project Structure diff --git a/README.md b/README.md index 984ff3d..5c64275 100644 --- a/README.md +++ b/README.md @@ -27,11 +27,13 @@ Official wrappers for **React**, **Vue 3**, and **Angular**. Embeddable widget b | Category | Features | |----------|----------| -| **Rendering** | Canvas 2D multi-layer pipeline, 100K+ rows at 60fps, progressive loading | -| **Editing** | Inline editor, undo/redo (100 steps), clipboard (TSV + HTML), autofill | -| **Data** | Sort (multi-column), filter (14 operators), frozen panes, merged cells | -| **Plugins** | Formulas, conditional formatting, collaboration (OT), Excel I/O, context menu | +| **Rendering** | Canvas 2D multi-layer pipeline, 100K+ rows at 60fps, progressive loading, auto row height with text wrapping | +| **Editing** | Inline editor, date picker, datetime picker, custom cell editors via CellEditorRegistry, undo/redo (100 steps), clipboard (TSV + HTML), autofill | +| **Layout** | Column stretch (`'all'` / `'last'`), frozen panes, auto row sizing, container resize observation | +| **Data** | Sort (multi-column), filter (14 operators), merged cells | +| **Plugins** | Formulas, conditional formatting, collaboration (OT), Excel I/O, context menu with submenus | | **Theming** | Light/dark built-in, fully customizable via `WitTheme` | +| **Localization** | Built-in English and Russian locales, custom locale packs, runtime switching | | **Accessibility** | WCAG 2.1 AA: role=grid, aria-live, keyboard-only, print support | | **Frameworks** | React, Vue 3, Angular, vanilla JS widget (<36KB gzip) | @@ -96,6 +98,48 @@ Full documentation with interactive demos at **[spreadsheet.witqq.dev](https://s - [API Reference](https://spreadsheet.witqq.dev/api/wit-engine/) — WitEngine, types, cell types - [Migration from Handsontable](https://spreadsheet.witqq.dev/guides/migration-from-handsontable/) — Side-by-side API mapping +## 🌐 Localization + +Built-in English and Russian locale packs. Create custom locales for any language: + +```tsx +import { WitTable } from '@witqq/spreadsheet-react'; +import { ruLocale } from '@witqq/spreadsheet'; + + +``` + +Custom partial locales are merged over English defaults: + +```ts +import { resolveLocale } from '@witqq/spreadsheet'; + +const myLocale = resolveLocale({ + contextMenu: { cut: 'Cortar', copy: 'Copiar', paste: 'Pegar' }, + formatLocale: 'es-ES', +}); +``` + +Locale covers: context menu labels, date picker, filter panel, accessibility announcements, aggregate labels, print notices, and number/date formatting. + +## 🔌 Custom Cell Editors + +Register custom overlay editors for specific column types via `CellEditorRegistry`: + +```ts +import type { CellEditor } from '@witqq/spreadsheet'; + +class ColorPickerEditor implements CellEditor { + readonly id = 'color-picker'; + // ... implement open(), close(), setTheme(), setLocale(), destroy() +} + +const engine = new SpreadsheetEngine({ columns, data }); +engine.registerCellEditor(new ColorPickerEditor(), 'color'); +``` + +Built-in editors: `DatePickerEditor` (registered for `type: 'date'`), `DateTimeEditor` (registered for `type: 'datetime'` — calendar + hour/minute controls, commits ISO `YYYY-MM-DDTHH:mm`), `InlineEditor` (textarea fallback). The registry resolves editors by priority — higher priority wins when multiple editors match. + ## 🛠 Development ```bash diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..73db203 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,688 @@ +# Architecture + +## Overview + +`@witqq/spreadsheet` is a canvas-based spreadsheet engine. It renders everything on a single `` element using a layered rendering pipeline. The engine is framework-agnostic — framework wrappers (React, Vue, Angular) are thin adapters. + +## Monorepo Packages + +| Package | npm name | Purpose | +|---------|----------|---------| +| `packages/core` | `@witqq/spreadsheet` | Engine: rendering, data, editing, selection, commands. Zero external deps. | +| `packages/react` | `@witqq/spreadsheet-react` | React wrapper (`` component) | +| `packages/vue` | `@witqq/spreadsheet-vue` | Vue wrapper | +| `packages/angular` | `@witqq/spreadsheet-angular` | Angular wrapper | +| `packages/widget` | `@witqq/spreadsheet-widget` | Single-file IIFE/UMD bundle for embedding | +| `packages/plugins` | `@witqq/spreadsheet-plugins` | Official plugins (formulas, collaboration, context menu, etc.) | +| `packages/demo` | `@witqq/spreadsheet-demo` | Demo app (React + Vite, private) | +| `packages/server` | — | Collaboration WebSocket server | +| `packages/site` | — | Documentation site (Astro + Starlight) | + +## Core Engine Architecture + +### Entry Point + +`SpreadsheetEngine` (`packages/core/src/engine/spreadsheet-engine.ts`) is the main class. It orchestrates ~30 subsystems and uses two-phase initialization: + +- **Constructor** (headless) — creates CellStore, ColStore, RowStore, EventBus, CommandManager, LayoutEngine, and other non-DOM subsystems +- **mount()** — creates DOM-dependent subsystems: CanvasManager, ScrollManager, RenderPipeline, InlineEditor, SelectionManager + +--- + +## Rendering Pipeline + +*Source: `packages/core/src/renderer/`* + +### RenderScheduler + +*Source: `render-scheduler.ts`* + +Classic RAF coalescing with a boolean dirty flag. When `requestRender()` is called: + +1. If `dirty` is already `true`, return immediately (coalescing — any number of calls per frame collapse to one) +2. Set `dirty = true`, schedule a single `requestAnimationFrame` +3. Inside the RAF callback: reset `dirty = false`, invoke `renderCallback()` + +The coalescing is O(1) — the first `requestRender()` in a frame wins the RAF slot, all subsequent calls are no-ops. No queue, no debounce timer, no priority system. + +### RenderPipeline + +*Source: `render-pipeline.ts`* + +Orchestrates layer rendering. Layers are stored in an ordered array. + +**Non-frozen render cycle:** +1. `ctx.clearRect(0, 0, canvasWidth, canvasHeight)` — wipe entire canvas +2. Iterate layers in order, call `layer.render(renderContext)` with viewport/scroll/theme/geometry + +**Default layer order** (from GridRenderer, `grid-renderer.ts`): +1. BackgroundLayer — cell backgrounds +2. CellTextLayer — cell values, formatted text +3. CellStatusLayer — validation icons, status indicators +4. EmptyStateLayer — placeholder when no data +5. GridLinesLayer — grid borders (if enabled) +6. HeaderLayer — column/row headers +7. RowNumberLayer — row numbers (if enabled) +8. SelectionOverlayLayer — selection highlight, active cell (always last) + +Plugin layers are inserted before GridLinesLayer (or before SelectionOverlayLayer if no grid lines), ensuring plugins render above cell text but below the grid overlay. + +### Frozen Pane Rendering + +When frozen rows or columns are configured, the pipeline switches to a 4-region compositing model: + +| Region | Clip Area | Scrolls With | Cache Strategy | +|--------|-----------|-------------|----------------| +| **corner** | Top-left intersection | Neither axis | Rendered once, cached as ImageData | +| **frozenRow** | Top strip, scrollable columns | Horizontal scroll only | Cached, re-rendered when scrollX changes | +| **frozenCol** | Left strip, scrollable rows | Vertical scroll only | Cached, re-rendered when scrollY changes | +| **main** | Remaining area | Both axes | Re-rendered every frame | + +For each layer × each region: `ctx.save()` → clip to region rect → `layer.render()` → `ctx.restore()`. + +**ImageData caching:** After rendering, regions are captured via `ctx.getImageData()` at DPR-scaled coordinates. On subsequent frames, unchanged regions are restored via `ctx.putImageData()` instead of re-rendering all layers. Five caches are maintained: corner, frozenRow, frozenCol, frozenRowHeaders, frozenColRowNumbers. + +Cache invalidation occurs on theme change, data change, or structural change (column/row add/remove). + +### Key Layer Implementations + +**BackgroundLayer** (`background-layer.ts`) — fills entire canvas with `theme.colors.background`. Always the first layer. + +**CellTextLayer** (`cell-text-layer.ts`) — the most complex layer: +1. Clips to cell area (excluding headers and row number gutter) +2. Collects visible cells, handling merge anchors that may be off-screen +3. For each cell, resolves the cell type from column definition, cell data, or auto-detection +4. Custom renderer path: delegates to `renderer.render()` if the cell type provides one +5. Wrap text path: uses `TextMeasureCache.getWrappedLines()` for word breaking, draws each line with proper alignment and vertical centering +6. Single-line path: uses `TextMeasureCache.truncateText()` with ellipsis, draws via `ctx.fillText()` +7. Also provides `measureHeights()` for the auto row-size system + +**SelectionOverlayLayer** (`selection-overlay-layer.ts`) — draws selection fills (translucent rectangles) and borders for each range, plus the active cell border with merge-awareness. + +### CanvasManager + +*Source: `canvas-manager.ts`* + +Manages the `` element with DPI-aware scaling: +- Sets physical canvas size to `cssWidth × dpr` by `cssHeight × dpr` +- Sets CSS display size to match container +- Applies `ctx.setTransform(dpr, 0, 0, dpr, 0, 0)` so all drawing uses CSS pixel coordinates while rendering at native resolution + +Detects browser zoom (DPR changes) via `matchMedia(\`(resolution: ${dpr}dppx)\`)` and re-syncs canvas size on change. + +--- + +## Data Model + +*Source: `packages/core/src/model/`* + +### CellStore + +*Source: `cell-store.ts`* + +Sparse map storage: `Map` keyed by `"row:col"` string (e.g. `"5:3"`). Only non-empty cells are stored. + +**Version tracking:** `_version` counter incremented on every `set()`, `delete()`, `clear()`. Bulk methods (`bulkLoadChunk`, `bulkGenerate`) increment version once at the end for efficiency. + +**CellData interface** (`types/interfaces.ts`): +- `value: CellValue` — raw value (`string | number | boolean | Date | null`) +- `displayValue?: string` — formatted override +- `formula?: string` — e.g. `"=SUM(A1:A10)"` +- `style?: CellStyleRef` — `{ ref: string, style: CellStyle }` +- `type?: CellType` — rendering/editing behavior +- `metadata?: CellMetadata` — status, errors, links, comments + +All CellData fields are `readonly`. + +**CellType union:** `'string' | 'number' | 'boolean' | 'date' | 'datetime' | 'select' | 'dynamicSelect' | 'formula' | 'link' | 'image' | 'progressBar' | 'rating' | 'badge' | 'custom'` + +**Key methods:** `get/set/has/delete` (O(1)), `setValue()` (merges with existing data), `setMetadata()`, `iterateRange()` (generator over bounds), `bulkLoad()` / `bulkGenerate()` (progressive loading). + +CellStore has no row-shift logic — row insertion/deletion is handled externally by RowStore. + +### ColStore + +*Source: `col-store.ts`* + +Ordered array of `ColumnDef[]` plus `Set` for hidden columns. Each ColumnDef contains: `key`, `title`, `width`, `minWidth?`, `maxWidth?`, `type?: CellType`, `frozen?`, `sortable?`, `filterable?`, `editable?`, `resizable?`, `hidden?`, `wrapText?`, `validation?`. + +Version-tracked like CellStore. + +### RowStore + +*Source: `row-store.ts`* + +Three sparse collections: +- `manualHeightOverrides: Map` — user drag-resized heights +- `autoHeightOverrides: Map` — auto-measured heights +- `hiddenRows: Set` + +**Height resolution priority:** hidden → 0, manual override → auto override → defaultHeight. + +`setAutoHeightsBatch()` only updates rows without manual overrides and uses epsilon comparison (0.01) to avoid unnecessary version bumps. + +`shiftRowsUp(deletedRow)` rebuilds all three collections, shifting indices down by 1. + +### DataView + +*Source: `dataview/data-view.ts`* + +Provides logical (visible/sorted/filtered) ↔ physical (CellStore) row index mapping. + +**Passthrough mode:** When no sort or filter is active, mapping is `null` — all lookups return identity. Zero overhead. + +**Active mapping:** `_mapping: number[]` where `mapping[logicalRow] = physicalRow`, plus `_reverseMapping: Map` for physical → logical lookups. + +`recompute(physicalIndices[])` rebuilds mapping from sorted/filtered index array. `reset()` returns to passthrough. + +--- + +## Layout Engine + +*Source: `packages/core/src/renderer/layout-engine.ts`* + +All position data stored in `Float64Array` typed arrays for cache-friendly memory access and 64-bit precision: + +| Array | Size | Purpose | +|-------|------|---------| +| `colPositions` | visibleCols + 1 | Cumulative x-offsets (prefix sum) | +| `colWidths` | visibleCols | Per-column widths | +| `rowPositions` | rowCount + 1 | Cumulative y-offsets (prefix sum) | +| `rowHeights` | rowCount | Per-row heights | + +### Construction + +Filters hidden columns, then builds prefix-sum arrays in a single linear pass. The "+1" entry stores the total dimension (e.g. `colPositions[n] = totalWidth`). + +### getCellRect — O(1) + +Pure array index lookups: +``` +x = rowNumberWidth + colPositions[colIndex] +y = headerHeight + rowPositions[rowIndex] +width = colWidths[colIndex] +height = rowHeights[rowIndex] +``` + +No computation beyond array indexing. Returns `{0,0,0,0}` for out-of-bounds inputs. + +### Hit-testing — O(log n) binary search + +`getRowAtY(y)` and `getColAtX(x)` both use binary search on the cumulative positions array to find which cell a pixel coordinate falls within. Returns -1 if outside content bounds. + +### Mutation + +- `setRowHeight(row, h)` — updates height, recomputes cumulative positions from that row onward: O(n−row) +- `setRowHeightsBatch(Map)` — applies all changes, single recompute pass from minimum changed index: much faster than N individual calls +- `setRowCount(count)` — if exceeding current capacity, reallocates new Float64Array instances and copies old data via `.set(subarray)` (memcpy-speed) + +### Frozen pane helpers + +`getFrozenRowsHeight(count)` and `getFrozenColsWidth(count)` are O(1) prefix-sum lookups. + +### ViewportManager + +*Source: `viewport-manager.ts`* + +Determines visible cells by binary-searching scroll position against LayoutEngine's cumulative arrays: + +1. `layout.getRowAtY(scrollY)` → first visible row +2. `layout.getRowAtY(scrollY + viewportHeight)` → last visible row +3. Same for columns + +Applies render buffers: 10 extra rows and 5 extra columns beyond the visible area for smooth scrolling. + +For frozen panes, computes 4 separate viewport ranges (corner, frozenRow, frozenCol, main), each with appropriate scroll offsets. + +### ScrollManager + +*Source: `scroll-manager.ts`* + +Uses a transparent absolutely-positioned `
` with `overflow: auto` overlaying the canvas. A spacer div inside is sized to the total content dimensions, creating native scrollbars without rendering content. The scroll listener uses `{ passive: true }` for performance and syncs `scrollLeft`/`scrollTop` to the rendering pipeline. + +--- + +## Editing + +*Source: `packages/core/src/editing/`* + +### Two-Tier Architecture + +The editing system has a two-tier design: + +1. **InlineEditor** — built-in textarea fallback for free-text editing (text, number) +2. **CellEditorRegistry** + **CellEditor** interface — dispatches to specialized overlay editors (date pickers, etc.) for specific column types + +The engine method `openCellEditor()` is the central dispatch point: it queries the registry first, falls back to InlineEditor if no match. + +### InlineEditor + +*Source: `editing/inline-editor.ts`* + +A `