diff --git a/.gitignore b/.gitignore index 77895d518..738067b7b 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,4 @@ test-results/ # Git worktrees for parallel agent work .worktrees/ .env +docs/.vitepress/cache/ diff --git a/docs/development/roadmap.md b/docs/development/roadmap.md index d242aeafa..84cb4aba3 100644 --- a/docs/development/roadmap.md +++ b/docs/development/roadmap.md @@ -86,6 +86,9 @@ Components, instances, overrides, variables, collections, modes, image export. - ColorInput component extraction, ColorPicker alpha slider checkerboard fix - App identity: pencil icon, Cargo crate open_pencil, macOS Dock "OpenPencil" - Splash loader during WASM initialization +- Rich text style runs: per-selection ⌘B/I/U, StyleRun model, ParagraphBuilder pushStyle/pop, .fig roundtrip +- B/I/U/S toggle buttons in TypographySection +- Double-click (word), triple-click (select all) text selection **Remaining (deferred to Phase 6):** - Variant switching @@ -98,7 +101,12 @@ Core extraction, CLI, MCP server, design guidelines, screenshot verification loo **Delivered:** - @open-pencil/core extracted to packages/core/ (zero DOM deps, Bun workspace) -- @open-pencil/cli with headless .fig operations (info, tree, find, export), CanvasKit CPU rasterization, --json output +- @open-pencil/cli with headless .fig operations (info, tree, find, export, analyze, node, pages, variables), CanvasKit CPU rasterization, --json output +- JSX renderer: TreeNode builders (Frame, Text, Rectangle, etc.), renderTreeNode/renderJsx, Tailwind-like shorthand props, 27 tests +- jscpd copy-paste detection (15.6% → 0.62%), kiwi-serialize.ts consolidation +- .fig roundtrip tests with LFS fixtures (material3.fig 87K nodes, nuxtui.fig 314K nodes) +- .fig import O(n²) → O(n) fix (37s → 535ms on 87K nodes), ByteBuffer optimization +- test:coverage script - npm publishing preparation for core and cli packages **Planned:** diff --git a/docs/guide/features.md b/docs/guide/features.md index 632b60d8a..6cab6d426 100644 --- a/docs/guide/features.md +++ b/docs/guide/features.md @@ -33,10 +33,18 @@ Shift+A toggles auto-layout on a frame or wraps selected nodes. Canvas-native text editing — no DOM textarea overlay on screen. A `TextEditor` class in `@open-pencil/core` handles cursor positioning, text selection, word boundary detection, and line navigation using the CanvasKit Paragraph API (`getGlyphPositionAtCoordinate`, `getRectsForRange`, `getLineMetrics`). A hidden phantom textarea captures keyboard input, IME composition, and clipboard events. -Double-click a text node to enter edit mode. The canvas renders a blinking caret, translucent blue selection rectangles, and a blue outline around the node. Click and drag to select text, double-click a word to select it. Keyboard navigation with modifier support: ⌥←/→ for word movement, ⌘←/→ for line start/end, ⌥⌫ for word delete, ⌘⌫ for line delete. Shift extends selection. Esc or clicking outside commits the edit. +Double-click a text node to enter edit mode. The canvas renders a blinking caret, translucent blue selection rectangles, and a blue outline around the node. Click and drag to select text, double-click a word to select it, triple-click to select all. Keyboard navigation with modifier support: ⌥←/→ for word movement, ⌘←/→ for line start/end, ⌥⌫ for word delete, ⌘⌫ for line delete. Shift extends selection. Esc or clicking outside commits the edit. **Font picker** with virtual scroll (reka-ui ListboxVirtualizer), search filter, and CSS font preview — each font name renders in its own typeface. In Tauri, system fonts are enumerated via Rust `font-kit` crate (`list_system_fonts`/`load_system_font` commands) with OnceLock caching for instant picker access. In browser, the Local Font Access API is used when available. +## Rich Text Formatting + +Per-character formatting within a single text node. Select text and press ⌘B for bold, ⌘I for italic, ⌘U for underline, or use the B/I/U/S buttons in the Typography section. With no selection, the shortcut toggles the whole-node style. + +Implemented via a StyleRun model — an array of `{start, length, style}` segments where style includes fontWeight, italic, and textDecoration. The renderer uses CanvasKit ParagraphBuilder.pushStyle/pop to render mixed formatting in a single paragraph. Style runs adjust automatically on insert and delete to preserve formatting boundaries. + +Rich text formatting is preserved during .fig import/export — `characterStyleIDs` and `styleOverrideTable` from Figma's TextData are imported as StyleRun arrays and exported back with a deduped style table. + ## Undo/Redo Every operation is undoable — node creation/deletion, moves, resizes, property changes, reparenting, layout changes, and all variable operations (create/delete/rename variables, create/rename collections, color and value changes). The system uses an inverse-command pattern — before applying any change, it snapshots affected fields. The snapshot becomes the inverse. ⌘Z undoes, ⇧⌘Z redoes. @@ -163,5 +171,26 @@ The engine is extracted to `packages/core/` (@open-pencil/core) — scene-graph, - `open-pencil tree ` — visual node tree - `open-pencil find ` — search by name/type - `open-pencil export ` — render to PNG/JPG/WEBP at any scale +- `open-pencil analyze colors ` — color palette usage with clustering +- `open-pencil analyze typography ` — font/size/weight distribution +- `open-pencil analyze spacing ` — gap/padding values with grid check +- `open-pencil analyze clusters ` — repeated patterns (potential components) +- `open-pencil node ` — detailed properties of a node by ID +- `open-pencil pages ` — list pages with node counts +- `open-pencil variables ` — list design variables and collections All commands support `--json` for machine-readable output. Runnable via `bun open-pencil` in the workspace. See [Project Structure](/development/contributing#project-structure) for the full monorepo layout. + +## JSX Renderer + +Programmatic design creation via TreeNode builder functions exported from `@open-pencil/core`: Frame, Text, Rectangle, Ellipse, and others. Supports Tailwind-like shorthand props — `w`, `h`, `bg`, `rounded`, `flex`, `gap`, `p`/`px`/`py`, `justify`, `items`, `shadow`, `blur`. + +Two rendering paths: +- `renderTreeNode()` — tree → scene graph (any runtime, no external deps) +- `renderJsx()` — JSX string → esbuild → tree → scene graph (CLI/headless) + +Covered by 27 tests for all node types, layout props, effects, and nesting. + +## Code Quality + +Copy-paste detection via jscpd — reduced project-wide duplication from 15.6% to 0.62%. Kiwi serialization consolidated into `kiwi-serialize.ts` (shared by clipboard, fig-export, and the CLI). The .fig import pipeline was optimized from O(n²) to O(n) by building a children index upfront — material3.fig (87K nodes) went from 37s to 535ms. ByteBuffer optimized with inline readVarUint and TextDecoder for strings. diff --git a/docs/guide/figma-comparison.md b/docs/guide/figma-comparison.md index 6da6a538f..f665ab75d 100644 --- a/docs/guide/figma-comparison.md +++ b/docs/guide/figma-comparison.md @@ -6,7 +6,7 @@ Feature-by-feature comparison of Figma Design capabilities with Open Pencil's cu ✅ Supported — feature works end-to-end · 🟡 Partial — core behavior exists, some sub-features missing · 🔲 Not yet implemented ::: -**Coverage:** 79 of 150 Figma feature items addressed — 64 ✅ fully supported, 15 🟡 partial, 71 🔲 not yet. Last updated: 2026-03-01 (notes updated for text editing, fonts, variables). +**Coverage:** 80 of 150 Figma feature items addressed — 64 ✅ fully supported, 16 🟡 partial, 70 🔲 not yet. Last updated: 2026-03-01. ## Interface & Navigation @@ -78,13 +78,13 @@ Feature-by-feature comparison of Figma Design capabilities with Open Pencil's cu | Feature | Status | Notes | |---------|--------|-------| -| Text tool & inline editing | ✅ | T to create, double-click to edit, canvas-native with phantom textarea, cursor/selection/word select, drag to select | +| Text tool & inline editing | ✅ | Canvas-native editing, phantom textarea, cursor/selection/word select, drag to select, double/triple-click, rich text style runs (⌘B/I/U, S button) | | Text rendering (Paragraph API) | ✅ | CanvasKit Paragraph for shaping, line-breaking, metrics | | Font loading (system fonts) | ✅ | Inter default, font-kit in Tauri with OnceLock cache + preloading, queryLocalFonts in browser | | Font family & weight | ✅ | FontPicker with virtual scroll, search, CSS preview; weight selection in properties panel | | Font size & line height | ✅ | Editable in typography section | | Text alignment | 🟡 | Basic alignment; Figma has vertical alignment and auto-width/height modes | -| Text styles | 🔲 | Reusable named text style presets | +| Text styles | 🟡 | Per-selection bold/italic/underline/strikethrough (⌘B/I/U, S button); not yet reusable named text style presets | | Text resizing modes (auto, fixed, hug) | 🔲 | Figma's auto-width, auto-height, fixed-size text modes | | Bulleted & numbered lists | 🔲 | List formatting in text | | Links in text | 🔲 | Hyperlinks within text content | @@ -209,7 +209,7 @@ Feature-by-feature comparison of Figma Design capabilities with Open Pencil's cu | Code snippets | 🔲 | Auto-generated code from layers | | Figma for VS Code | 🔲 | Editor plugin integration | | MCP server | 🔲 | Planned for Phase 5; 117-tool MCP server from figma-use | -| CLI tools | 🟡 | Headless CLI: info, tree, find, export commands; MCP server not yet | +| CLI tools | 🟡 | Headless CLI: info, tree, find, export, analyze (colors/typography/spacing/clusters), node, pages, variables; MCP server not yet | ## Figma Draw diff --git a/openspec/changes/archive/2026-03-01-sync-style-runs-jsx-cli-tests/.openspec.yaml b/openspec/changes/archive/2026-03-01-sync-style-runs-jsx-cli-tests/.openspec.yaml new file mode 100644 index 000000000..0b4defe0e --- /dev/null +++ b/openspec/changes/archive/2026-03-01-sync-style-runs-jsx-cli-tests/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-03-01 diff --git a/openspec/changes/archive/2026-03-01-sync-style-runs-jsx-cli-tests/design.md b/openspec/changes/archive/2026-03-01-sync-style-runs-jsx-cli-tests/design.md new file mode 100644 index 000000000..263628489 --- /dev/null +++ b/openspec/changes/archive/2026-03-01-sync-style-runs-jsx-cli-tests/design.md @@ -0,0 +1,22 @@ +# Design: Sync style runs, JSX renderer, CLI expansion, tests + +## Approach +Documentation-only change. Update delta-specs for affected domains, then propagate to VitePress docs and comparison matrix. + +## Affected spec domains + +1. **text-editing** — rich text style runs (per-selection bold/italic/underline), double/triple-click, selectLine +2. **canvas-rendering** — mixed-style text rendering with ParagraphBuilder.pushStyle/pop +3. **editor-ui** — B/I/U/S toggle buttons in TypographySection +4. **cli** — new commands: analyze (colors/typography/spacing/clusters), node, pages, variables +5. **tooling** — jscpd, kiwi deduplication, kiwi-serialize.ts, test:coverage +6. **testing** — .fig roundtrip tests, LFS fixtures, import performance fix, 27 JSX renderer tests +7. **scene-graph** — JSX renderer (TreeNode builders, renderTreeNode, renderJsx), StyleRun model + +## VitePress docs updates +- features.md: add rich text formatting (⌘B/I/U), JSX renderer, expanded CLI commands, double/triple-click +- figma-comparison.md: update text editing notes (style runs, mixed formatting), mark new CLI capabilities +- roadmap.md: add new items to Phase 4/5 delivered + +## Risks +- None — documentation only diff --git a/openspec/changes/archive/2026-03-01-sync-style-runs-jsx-cli-tests/proposal.md b/openspec/changes/archive/2026-03-01-sync-style-runs-jsx-cli-tests/proposal.md new file mode 100644 index 000000000..95373f972 --- /dev/null +++ b/openspec/changes/archive/2026-03-01-sync-style-runs-jsx-cli-tests/proposal.md @@ -0,0 +1,50 @@ +# Proposal: Sync style runs, JSX renderer, CLI expansion, tests + +## Why +22 commits merged from master (33d3def..6268d8b) introduce rich text formatting, a JSX renderer for programmatic design creation, expanded CLI analysis commands, .fig roundtrip tests, and major deduplication. + +## What changed + +### Rich text style runs (88bfacd) +- StyleRun model: `{start, length, style}` with CharacterStyleOverride (fontWeight, italic, textDecoration) +- Renderer: ParagraphBuilder.pushStyle/pop for mixed-style text within a single text node +- ⌘B/I/U apply to selection range (toggle per-selection) or whole-node style +- B/I/U/S toggle buttons in TypographySection +- .fig roundtrip: import characterStyleIDs + styleOverrideTable from TextData, export with deduped style table + +### Text editing improvements (6268d8b) +- Double-click: select word at position, Triple-click: select all +- selectLine/selectLineAt added to TextEditor + +### JSX renderer in @open-pencil/core (13f562a, a32d80f) +- TreeNode builder functions (Frame, Text, Rectangle, Ellipse, etc.) +- renderTreeNode(): tree → scene graph (browser) +- renderJsx(): JSX string → esbuild → tree → scene graph (CLI/headless) +- Tailwind-like shorthand props: w/h, bg, rounded, flex, gap, p/px/py, justify, items, shadow, blur +- 27 tests covering all node types, layout props, effects, nesting + +### CLI expansion (36e3f14, 7dcc582) +- analyze colors: color palette usage with clustering +- analyze typography: font/size/weight distribution +- analyze spacing: gap/padding values with grid check +- analyze clusters: repeated patterns (potential components) +- node: detailed properties by ID +- pages: list pages with node counts +- variables: list design variables and collections + +### Deduplication (f92a883, e3ae61e, 20827a2, 46bb923) +- kiwi-serialize.ts: shared sceneNodeToKiwi, buildFigKiwi, parseFigKiwiChunks +- Vendored kiwi-schema removed from src/kiwi/ (re-exports from core) +- jscpd added: 15.6% → 0.62% duplication +- Shared font weight mapping, layout merge, style-runs expandRuns helper + +### .fig testing (f24f6e0, ca42214, f1806f6) +- Git LFS fixtures: material3.fig (87K nodes), nuxtui.fig (314K nodes) +- O(n²) getChildren() fix: 37s → 535ms on material3 +- Roundtrip tests: property invariants, encode/decode cycle +- test:coverage script +- ByteBuffer optimization: inline readVarUint, TextDecoder for strings + +### AI chat design (4fb9290, af6a98c, 017b0aa, cc5dda2) +- Planning documents only (PLAN.md), no implementation +- JSX-first rendering, OpenRouter from browser, Stronghold key storage diff --git a/openspec/changes/archive/2026-03-01-sync-style-runs-jsx-cli-tests/specs/canvas-rendering/spec.md b/openspec/changes/archive/2026-03-01-sync-style-runs-jsx-cli-tests/specs/canvas-rendering/spec.md new file mode 100644 index 000000000..9ca11d5af --- /dev/null +++ b/openspec/changes/archive/2026-03-01-sync-style-runs-jsx-cli-tests/specs/canvas-rendering/spec.md @@ -0,0 +1,10 @@ +# canvas-rendering Specification (delta) + +## New Requirements + +### Requirement: Mixed-style text rendering +The renderer SHALL render text nodes with per-character formatting using CanvasKit ParagraphBuilder.pushStyle/pop. Each StyleRun segment pushes its own TextStyle (weight, slant, decoration) before adding text, then pops to restore the default style. + +#### Scenario: Render mixed bold and regular text +- **WHEN** a text node has "Hello " (regular) + "world" (bold) +- **THEN** the renderer outputs a single Paragraph with two style segments, "world" visually bold diff --git a/openspec/changes/archive/2026-03-01-sync-style-runs-jsx-cli-tests/specs/cli/spec.md b/openspec/changes/archive/2026-03-01-sync-style-runs-jsx-cli-tests/specs/cli/spec.md new file mode 100644 index 000000000..8be870fb2 --- /dev/null +++ b/openspec/changes/archive/2026-03-01-sync-style-runs-jsx-cli-tests/specs/cli/spec.md @@ -0,0 +1,41 @@ +# cli Specification (delta) + +## New Requirements + +### Requirement: Analyze commands +The CLI SHALL provide `open-pencil analyze ` subcommands for design file analysis: +- `analyze colors` — color palette usage with clustering +- `analyze typography` — font/size/weight distribution +- `analyze spacing` — gap/padding values with grid alignment check +- `analyze clusters` — repeated patterns that could be components + +All subcommands SHALL support `--json` for machine-readable output. + +#### Scenario: Analyze colors +- **WHEN** `bun open-pencil analyze colors design.fig` is run +- **THEN** a color palette summary with usage counts and clusters is printed + +#### Scenario: Analyze clusters +- **WHEN** `bun open-pencil analyze clusters design.fig` is run +- **THEN** repeated node patterns that could be components are listed + +### Requirement: Node command +The CLI SHALL provide `open-pencil node ` to display detailed properties of a specific node by ID. + +#### Scenario: Node details +- **WHEN** `bun open-pencil node design.fig abc123` is run +- **THEN** the node's type, properties, children, and parent are displayed + +### Requirement: Pages command +The CLI SHALL provide `open-pencil pages ` to list all pages with node counts. + +#### Scenario: List pages +- **WHEN** `bun open-pencil pages design.fig` is run +- **THEN** each page name and its node count are listed + +### Requirement: Variables command +The CLI SHALL provide `open-pencil variables ` to list design variables and collections. + +#### Scenario: List variables +- **WHEN** `bun open-pencil variables design.fig` is run +- **THEN** all variable collections, modes, and variable values are listed diff --git a/openspec/changes/archive/2026-03-01-sync-style-runs-jsx-cli-tests/specs/editor-ui/spec.md b/openspec/changes/archive/2026-03-01-sync-style-runs-jsx-cli-tests/specs/editor-ui/spec.md new file mode 100644 index 000000000..d873464ff --- /dev/null +++ b/openspec/changes/archive/2026-03-01-sync-style-runs-jsx-cli-tests/specs/editor-ui/spec.md @@ -0,0 +1,14 @@ +# editor-ui Specification (delta) + +## New Requirements + +### Requirement: Rich text formatting buttons +The TypographySection SHALL include B/I/U/S toggle buttons for Bold, Italic, Underline, and Strikethrough. When text is selected during editing, the buttons apply formatting to the selection. When editing with no selection, they toggle the whole-node style. The active state reflects current selection formatting. + +#### Scenario: Bold button with selection +- **WHEN** user selects text and clicks the Bold button (or presses ⌘B) +- **THEN** the selection becomes bold and the B button shows active state + +#### Scenario: Underline button with no selection +- **WHEN** user clicks the Underline button during editing with no selection +- **THEN** the entire text node's underline style toggles diff --git a/openspec/changes/archive/2026-03-01-sync-style-runs-jsx-cli-tests/specs/scene-graph/spec.md b/openspec/changes/archive/2026-03-01-sync-style-runs-jsx-cli-tests/specs/scene-graph/spec.md new file mode 100644 index 000000000..a15d45d11 --- /dev/null +++ b/openspec/changes/archive/2026-03-01-sync-style-runs-jsx-cli-tests/specs/scene-graph/spec.md @@ -0,0 +1,21 @@ +# scene-graph Specification (delta) + +## New Requirements + +### Requirement: StyleRun model +SceneNode SHALL support a `styleRuns` array of `{start, length, style}` where style is CharacterStyleOverride (fontWeight, italic, textDecoration). The scene graph SHALL provide helpers to apply/toggle styles on ranges and adjust runs on insert/delete. + +#### Scenario: Apply bold to range +- **WHEN** applyStyleToRange is called with fontWeight 700 on characters 6–11 +- **THEN** the styleRuns array contains a run for that range with fontWeight 700 + +### Requirement: JSX renderer +@open-pencil/core SHALL export TreeNode builder functions (Frame, Text, Rectangle, Ellipse, etc.) and two rendering paths: renderTreeNode() (tree → scene graph, no deps) and renderJsx() (JSX string → esbuild → tree → scene graph, for CLI/headless). Builder functions SHALL accept Tailwind-like shorthand props: w, h, bg, rounded, flex, gap, p/px/py, justify, items, shadow, blur. + +#### Scenario: Build tree from functions +- **WHEN** Frame({w: 200, h: 100, bg: '#f00'}, [Text({children: 'Hello'})]) is called +- **THEN** a TreeNode with type FRAME, width 200, height 100, and red fill is created with a TEXT child + +#### Scenario: Render JSX string +- **WHEN** renderJsx('Hi') is called +- **THEN** a scene graph with a FRAME parent and TEXT child is produced diff --git a/openspec/changes/archive/2026-03-01-sync-style-runs-jsx-cli-tests/specs/testing/spec.md b/openspec/changes/archive/2026-03-01-sync-style-runs-jsx-cli-tests/specs/testing/spec.md new file mode 100644 index 000000000..bb2fec182 --- /dev/null +++ b/openspec/changes/archive/2026-03-01-sync-style-runs-jsx-cli-tests/specs/testing/spec.md @@ -0,0 +1,28 @@ +# testing Specification (delta) + +## New Requirements + +### Requirement: .fig roundtrip tests +The test suite SHALL include roundtrip tests for real .fig files: parsing property invariants, encode/decode cycle fidelity. Test fixtures (material3.fig, nuxtui.fig) SHALL be tracked via Git LFS. + +#### Scenario: Roundtrip encode/decode +- **WHEN** a .fig file is parsed and re-encoded +- **THEN** the re-encoded data decodes to the same node properties + +#### Scenario: Property invariants +- **WHEN** a real .fig file is parsed +- **THEN** all nodes have valid types, dimensions ≥ 0, and required fields present + +### Requirement: .fig import performance +The .fig import pipeline SHALL avoid O(n²) child resolution. A children index SHALL be built upfront for linear-time lookups. + +#### Scenario: Large file import speed +- **WHEN** material3.fig (87K nodes) is imported +- **THEN** parsing completes in under 2 seconds (was 37s before fix) + +### Requirement: JSX renderer tests +The test suite SHALL include tests for the JSX renderer covering all node types (Frame, Text, Rectangle, Ellipse, etc.), layout props, effects, and nesting. + +#### Scenario: Render Frame with children +- **WHEN** a JSX tree with Frame containing Rectangle and Text is rendered +- **THEN** the scene graph contains correct parent-child relationships and properties diff --git a/openspec/changes/archive/2026-03-01-sync-style-runs-jsx-cli-tests/specs/text-editing/spec.md b/openspec/changes/archive/2026-03-01-sync-style-runs-jsx-cli-tests/specs/text-editing/spec.md new file mode 100644 index 000000000..331428435 --- /dev/null +++ b/openspec/changes/archive/2026-03-01-sync-style-runs-jsx-cli-tests/specs/text-editing/spec.md @@ -0,0 +1,45 @@ +# text-editing Specification (delta) + +## New Requirements + +### Requirement: Rich text style runs +Text nodes SHALL support per-character formatting via StyleRun arrays: `{start, length, style}` where style includes fontWeight, italic (boolean), and textDecoration (UNDERLINE, STRIKETHROUGH). ⌘B toggles bold, ⌘I toggles italic, ⌘U toggles underline on the current selection. With no selection, the shortcut toggles the whole-node style. Style runs adjust on insert/delete to preserve formatting. + +#### Scenario: Bold selection +- **WHEN** user selects "world" in "Hello world" and presses ⌘B +- **THEN** the word "world" renders bold while "Hello " stays regular + +#### Scenario: Toggle italic on whole node +- **WHEN** user presses ⌘I with no text selected in a text node +- **THEN** the entire text node toggles italic + +#### Scenario: Style preservation on insert +- **WHEN** user types "X" between a bold and regular segment +- **THEN** the inserted character inherits the style of the preceding segment and run boundaries adjust + +### Requirement: .fig roundtrip for style runs +The .fig import SHALL parse characterStyleIDs and styleOverrideTable from TextData into StyleRun arrays. The .fig export SHALL write back with a deduped style table. + +#### Scenario: Import rich text from .fig +- **WHEN** a .fig file with mixed bold/italic text is imported +- **THEN** the style runs are correctly reconstructed + +## Modified Requirements + +### Requirement: Text selection via mouse drag (MODIFIED) +Double-click SHALL select the word at click position. Triple-click SHALL select the entire text. Single click positions cursor. Drag extends selection. Click outside commits. + +#### Scenario: Double-click selects word +- **WHEN** user double-clicks on "world" in "Hello world" during editing +- **THEN** "world" is selected + +#### Scenario: Triple-click selects all +- **WHEN** user triple-clicks inside a text node during editing +- **THEN** all text is selected + +### Requirement: TextEditor class in core (MODIFIED) +TextEditor SHALL additionally support selectLine(pos) and selectLineAt(x, y) methods for line-level selection. + +#### Scenario: Select line +- **WHEN** selectLineAt is called at a position within line 2 +- **THEN** the entire second line is selected diff --git a/openspec/changes/archive/2026-03-01-sync-style-runs-jsx-cli-tests/specs/tooling/spec.md b/openspec/changes/archive/2026-03-01-sync-style-runs-jsx-cli-tests/specs/tooling/spec.md new file mode 100644 index 000000000..9ed2ea06b --- /dev/null +++ b/openspec/changes/archive/2026-03-01-sync-style-runs-jsx-cli-tests/specs/tooling/spec.md @@ -0,0 +1,24 @@ +# tooling Specification (delta) + +## New Requirements + +### Requirement: Copy-paste detection +The project SHALL use jscpd for copy-paste detection. The `bun run jscpd` command SHALL scan for duplicated code blocks. + +#### Scenario: Detect duplicates +- **WHEN** `bun run jscpd` is run +- **THEN** duplicated code blocks are reported with locations and percentages + +### Requirement: Kiwi serialization consolidation +Shared kiwi serialization logic (sceneNodeToKiwi, buildFigKiwi, parseFigKiwiChunks, decompressFigKiwiDataAsync) SHALL be extracted to packages/core/src/kiwi-serialize.ts. The app's src/kiwi/ SHALL re-export from core, eliminating the vendored kiwi-schema copy. + +#### Scenario: Clipboard and fig-export share serialization +- **WHEN** clipboard.ts and fig-export.ts serialize nodes to kiwi +- **THEN** both use the shared kiwi-serialize.ts functions + +### Requirement: Test coverage script +The project SHALL include a `test:coverage` script for measuring code coverage. + +#### Scenario: Run coverage +- **WHEN** `bun run test:coverage` is run +- **THEN** test coverage metrics are reported diff --git a/openspec/changes/archive/2026-03-01-sync-style-runs-jsx-cli-tests/tasks.md b/openspec/changes/archive/2026-03-01-sync-style-runs-jsx-cli-tests/tasks.md new file mode 100644 index 000000000..ffb307c72 --- /dev/null +++ b/openspec/changes/archive/2026-03-01-sync-style-runs-jsx-cli-tests/tasks.md @@ -0,0 +1,42 @@ +# Tasks: Sync style runs, JSX renderer, CLI expansion, tests + +Reference: 22 commits from master (33d3def..6268d8b). +Delta-specs in `openspec/changes/sync-style-runs-jsx-cli-tests/specs/` (text-editing, canvas-rendering, editor-ui, cli, tooling, testing, scene-graph). +Proposal: `proposal.md`, Design: `design.md` — validated and aligned with delta-specs. + +Section 1 (specs) MUST complete before Section 2 (docs) — docs reference merged spec content. + +## 1. Merge Delta-Specs into Main Specs + +Each task merges ADDED and MODIFIED requirements from the delta-spec into the baseline at `openspec/specs//spec.md`. Untouched baseline requirements are preserved. No conflicts between domains — each delta-spec targets a distinct section of its baseline. + +- [x] 1.1 Update `openspec/specs/text-editing/spec.md` — ADDED: "Rich text style runs" (⌘B/I/U per-selection, style preservation on insert/delete), ".fig roundtrip for style runs" (import characterStyleIDs, export deduped table). MODIFIED: "Text selection via mouse drag" (add double-click word, triple-click all), "TextEditor class in core" (add selectLine/selectLineAt) +- [x] 1.2 Update `openspec/specs/canvas-rendering/spec.md` — ADDED: "Mixed-style text rendering" (ParagraphBuilder pushStyle/pop per StyleRun segment) +- [x] 1.3 Update `openspec/specs/editor-ui/spec.md` — ADDED: "Rich text formatting buttons" (B/I/U/S toggles in TypographySection, selection-aware) +- [x] 1.4 Update `openspec/specs/cli/spec.md` — ADDED: "Analyze commands" (colors/typography/spacing/clusters with --json), "Node command" (properties by ID), "Pages command" (list with counts), "Variables command" (collections and values) +- [x] 1.5 Update `openspec/specs/tooling/spec.md` — ADDED: "Copy-paste detection" (jscpd, 15.6%→0.62%), "Kiwi serialization consolidation" (kiwi-serialize.ts shared by clipboard + fig-export), "Test coverage script" (test:coverage) +- [x] 1.6 Update `openspec/specs/testing/spec.md` — ADDED: ".fig roundtrip tests" (LFS fixtures material3/nuxtui, property invariants, encode/decode cycle), ".fig import performance" (O(n²)→O(n), 37s→535ms), "JSX renderer tests" (27 tests) +- [x] 1.7 Update `openspec/specs/scene-graph/spec.md` — ADDED: "StyleRun model" (per-character formatting array on SceneNode), "JSX renderer" (TreeNode builders + renderTreeNode + renderJsx, Tailwind-like shorthand props). These are two independent additions to scene-graph — StyleRun extends the text data model, JSX renderer adds a programmatic creation API + +## 2. Update VitePress Docs + +All three target files already exist — no sidebar/nav changes needed. + +- [x] 2.1 Update `docs/guide/features.md` — modify existing sections and add new ones: + - **Modify "Inline Text Editing"**: add double-click (word), triple-click (all) to existing paragraph + - **Add "Rich Text Formatting" section** (after "Inline Text Editing"): ⌘B/I/U toggle per-selection bold/italic/underline, ⌘U for underline, strikethrough via S button in TypographySection, StyleRun model, .fig roundtrip preserves formatting + - **Add "JSX Renderer" section** (after "@open-pencil/core & CLI"): TreeNode builders (Frame, Text, Rectangle, etc.), Tailwind-like shorthand props (w/h/bg/rounded/flex/gap/p), renderTreeNode() for browser, renderJsx() for CLI/headless via esbuild, 27 tests + - **Expand CLI commands** in "@open-pencil/core & CLI" section: add analyze (colors/typography/spacing/clusters), node, pages, variables to the list + - **Add "Code Quality"** section (after "CI/CD Builds"): jscpd copy-paste detection (15.6%→0.62%), kiwi-serialize.ts consolidation, .fig import O(n²) fix (37s→535ms on 87K nodes) +- [x] 2.2 Update `docs/guide/figma-comparison.md` — specific row changes: + - "Text tool & inline editing" ✅: add "rich text style runs (⌘B/I/U), double/triple-click" to notes + - "Text styles" 🔲→🟡: "Per-selection bold/italic/underline/strikethrough via ⌘B/I/U and S button; not yet reusable named text styles" + - "CLI tools" 🟡: add "analyze colors/typography/spacing/clusters, node, pages, variables" to notes + - No other rows change status. After: 64✅, 16🟡, 70🔲 = 150 total. Update coverage line +- [x] 2.3 Update `docs/development/roadmap.md` — specific phase assignments: + - **Phase 4 Delivered** (text/rendering): rich text style runs (⌘B/I/U per-selection, StyleRun model, .fig roundtrip), B/I/U/S buttons in TypographySection, double/triple-click text selection + - **Phase 5 Delivered** (tooling/infra): JSX renderer (TreeNode builders, renderTreeNode/renderJsx, 27 tests), expanded CLI (analyze/node/pages/variables), jscpd (15.6%→0.62%), kiwi-serialize.ts consolidation, .fig roundtrip tests (LFS fixtures), import O(n²) fix, test:coverage script + +## 3. Verify + +- [x] 3.1 Run `bun run docs:build` — verify build passes. Manually confirm: coverage count in figma-comparison.md matches actual ✅/🟡/🔲 tallies, no broken internal anchor links in output diff --git a/openspec/specs/canvas-rendering/spec.md b/openspec/specs/canvas-rendering/spec.md index 002e33330..0b3975d17 100644 --- a/openspec/specs/canvas-rendering/spec.md +++ b/openspec/specs/canvas-rendering/spec.md @@ -206,3 +206,10 @@ During text editing, the renderer SHALL draw on the canvas: a blue outline aroun #### Scenario: Blue outline during editing - **WHEN** user is editing a text node - **THEN** a blue outline is drawn around the text node bounds (at selection color) + +### Requirement: Mixed-style text rendering +The renderer SHALL render text nodes with per-character formatting using CanvasKit ParagraphBuilder.pushStyle/pop. Each StyleRun segment pushes its own TextStyle (weight, slant, decoration) before adding text, then pops to restore the default style. + +#### Scenario: Render mixed bold and regular text +- **WHEN** a text node has "Hello " (regular) + "world" (bold) +- **THEN** the renderer outputs a single Paragraph with two style segments, "world" visually bold diff --git a/openspec/specs/cli/spec.md b/openspec/specs/cli/spec.md index 31553fbbe..238c8c1c2 100644 --- a/openspec/specs/cli/spec.md +++ b/openspec/specs/cli/spec.md @@ -14,6 +14,13 @@ The CLI SHALL support the following commands: - `open-pencil tree ` — visual node tree with formatted output - `open-pencil find ` — search nodes by name or type - `open-pencil export ` — render to PNG/JPG/WEBP at any scale +- `open-pencil analyze colors ` — color palette usage with clustering +- `open-pencil analyze typography ` — font/size/weight distribution +- `open-pencil analyze spacing ` — gap/padding values with grid alignment check +- `open-pencil analyze clusters ` — repeated patterns (potential components) +- `open-pencil node ` — detailed properties of a specific node +- `open-pencil pages ` — list pages with node counts +- `open-pencil variables ` — list design variables and collections All commands SHALL support `--json` for machine-readable output. @@ -35,3 +42,35 @@ The CLI SHALL be runnable via `bun open-pencil` within the Bun workspace, withou #### Scenario: Run from workspace root - **WHEN** `bun open-pencil info design.fig` is run from the project root - **THEN** the CLI executes using the workspace-linked binary + +### Requirement: Analyze commands +The CLI SHALL provide `open-pencil analyze ` subcommands for design file analysis: colors (palette usage with clustering), typography (font/size/weight distribution), spacing (gap/padding values with grid check), clusters (repeated patterns that could be components). + +#### Scenario: Analyze colors +- **WHEN** `bun open-pencil analyze colors design.fig` is run +- **THEN** a color palette summary with usage counts and clusters is printed + +#### Scenario: Analyze clusters +- **WHEN** `bun open-pencil analyze clusters design.fig` is run +- **THEN** repeated node patterns that could be components are listed + +### Requirement: Node command +The CLI SHALL provide `open-pencil node ` to display detailed properties of a specific node by ID. + +#### Scenario: Node details +- **WHEN** `bun open-pencil node design.fig abc123` is run +- **THEN** the node's type, properties, children, and parent are displayed + +### Requirement: Pages command +The CLI SHALL provide `open-pencil pages ` to list all pages with node counts. + +#### Scenario: List pages +- **WHEN** `bun open-pencil pages design.fig` is run +- **THEN** each page name and its node count are listed + +### Requirement: Variables command +The CLI SHALL provide `open-pencil variables ` to list design variables and collections. + +#### Scenario: List variables +- **WHEN** `bun open-pencil variables design.fig` is run +- **THEN** all variable collections, modes, and variable values are listed diff --git a/openspec/specs/editor-ui/spec.md b/openspec/specs/editor-ui/spec.md index e67d19190..56cc2a7d1 100644 --- a/openspec/specs/editor-ui/spec.md +++ b/openspec/specs/editor-ui/spec.md @@ -281,3 +281,14 @@ The demo document SHALL include three variable collections demonstrating multi-m #### Scenario: Demo loads with variables - **WHEN** the demo document is created - **THEN** three collections with variables and bindings are present in the scene graph + +### Requirement: Rich text formatting buttons +The TypographySection SHALL include B/I/U/S toggle buttons for Bold, Italic, Underline, and Strikethrough. When text is selected during editing, the buttons apply formatting to the selection. When editing with no selection, they toggle the whole-node style. The active state reflects current selection formatting. + +#### Scenario: Bold button with selection +- **WHEN** user selects text and clicks the Bold button (or presses ⌘B) +- **THEN** the selection becomes bold and the B button shows active state + +#### Scenario: Underline button with no selection +- **WHEN** user clicks the Underline button during editing with no selection +- **THEN** the entire text node's underline style toggles diff --git a/openspec/specs/scene-graph/spec.md b/openspec/specs/scene-graph/spec.md index fdf6968e1..c93338958 100644 --- a/openspec/specs/scene-graph/spec.md +++ b/openspec/specs/scene-graph/spec.md @@ -245,3 +245,21 @@ Removing a variable SHALL clean up all bindings referencing it across all nodes. #### Scenario: Remove variable with bindings - **WHEN** a variable bound to three nodes is removed - **THEN** all three nodes' variableBindings no longer reference that variable + +### Requirement: StyleRun model +SceneNode SHALL support a `styleRuns` array of `{start, length, style}` where style is CharacterStyleOverride (fontWeight, italic, textDecoration). The scene graph SHALL provide helpers to apply/toggle styles on ranges and adjust runs on insert/delete. + +#### Scenario: Apply bold to range +- **WHEN** applyStyleToRange is called with fontWeight 700 on characters 6–11 +- **THEN** the styleRuns array contains a run for that range with fontWeight 700 + +### Requirement: JSX renderer +@open-pencil/core SHALL export TreeNode builder functions (Frame, Text, Rectangle, Ellipse, etc.) and two rendering paths: renderTreeNode() (tree → scene graph, no deps) and renderJsx() (JSX string → esbuild → tree → scene graph, for CLI/headless). Builder functions SHALL accept Tailwind-like shorthand props: w, h, bg, rounded, flex, gap, p/px/py, justify, items, shadow, blur. + +#### Scenario: Build tree from functions +- **WHEN** Frame({w: 200, h: 100, bg: '#f00'}, [Text({children: 'Hello'})]) is called +- **THEN** a TreeNode with type FRAME, width 200, height 100, and red fill is created with a TEXT child + +#### Scenario: Render JSX string +- **WHEN** renderJsx('Hi') is called +- **THEN** a scene graph with a FRAME parent and TEXT child is produced diff --git a/openspec/specs/testing/spec.md b/openspec/specs/testing/spec.md index 78d772b7b..530fae253 100644 --- a/openspec/specs/testing/spec.md +++ b/openspec/specs/testing/spec.md @@ -96,3 +96,28 @@ Unit tests SHALL cover the variables system: add and resolve color/number variab - **WHEN** `bun test ./tests/engine` is run - **THEN** all variable tests pass (add/resolve color, resolve number, alias chain, circular alias, mode switching, bind/unbind, removal cleanup) + +### Requirement: .fig roundtrip tests +The test suite SHALL include roundtrip tests for real .fig files: parsing property invariants, encode/decode cycle fidelity. Test fixtures (material3.fig, nuxtui.fig) SHALL be tracked via Git LFS. + +#### Scenario: Roundtrip encode/decode +- **WHEN** a .fig file is parsed and re-encoded +- **THEN** the re-encoded data decodes to the same node properties + +#### Scenario: Property invariants +- **WHEN** a real .fig file is parsed +- **THEN** all nodes have valid types, dimensions ≥ 0, and required fields present + +### Requirement: .fig import performance +The .fig import pipeline SHALL avoid O(n²) child resolution. A children index SHALL be built upfront for linear-time lookups. + +#### Scenario: Large file import speed +- **WHEN** material3.fig (87K nodes) is imported +- **THEN** parsing completes in under 2 seconds (was 37s before fix) + +### Requirement: JSX renderer tests +The test suite SHALL include tests for the JSX renderer covering all node types (Frame, Text, Rectangle, Ellipse, etc.), layout props, effects, and nesting. Currently 27 tests. + +#### Scenario: Render Frame with children +- **WHEN** a JSX tree with Frame containing Rectangle and Text is rendered +- **THEN** the scene graph contains correct parent-child relationships and properties diff --git a/openspec/specs/text-editing/spec.md b/openspec/specs/text-editing/spec.md index 0d38d9bf1..a8033c5bc 100644 --- a/openspec/specs/text-editing/spec.md +++ b/openspec/specs/text-editing/spec.md @@ -24,7 +24,7 @@ Double-clicking a text node SHALL enter canvas-native inline editing mode. A hid - **THEN** the phantom textarea handles compositionstart/compositionend events and the composed text is inserted into the TextEditor ### Requirement: TextEditor class in core -A TextEditor class SHALL live in packages/core/src/text-editor.ts providing cursor positioning, text selection, word boundary detection, and line navigation using CanvasKit Paragraph API (getGlyphPositionAtCoordinate, getRectsForRange, getLineMetrics). It SHALL support: insert, backspace, delete, select all, select word, move left/right/up/down, move to line start/end, move word left/right, and extend selection with Shift. +A TextEditor class SHALL live in packages/core/src/text-editor.ts providing cursor positioning, text selection, word boundary detection, and line navigation using CanvasKit Paragraph API (getGlyphPositionAtCoordinate, getRectsForRange, getLineMetrics). It SHALL support: insert, backspace, delete, select all, select word, selectLine, selectLineAt, move left/right/up/down, move to line start/end, move word left/right, and extend selection with Shift. #### Scenario: Cursor positioning - **WHEN** user clicks inside a text node during editing @@ -34,6 +34,10 @@ A TextEditor class SHALL live in packages/core/src/text-editor.ts providing curs - **WHEN** user double-clicks a word during text editing - **THEN** the word is selected (from word boundary to word boundary) +#### Scenario: Line selection +- **WHEN** selectLineAt is called at a position within line 2 +- **THEN** the entire second line is selected + #### Scenario: Line navigation - **WHEN** user presses ⌘← during text editing - **THEN** the cursor moves to the start of the current line @@ -50,12 +54,20 @@ The phantom textarea SHALL handle keyboard navigation with modifier support: ⌥ - **THEN** the selected text is copied to the clipboard ### Requirement: Text selection via mouse drag -Clicking inside a text node during editing SHALL position the cursor. Dragging SHALL extend the selection. Clicking outside the text node bounds SHALL commit the edit and exit editing mode. +Clicking inside a text node during editing SHALL position the cursor. Dragging SHALL extend the selection. Double-click SHALL select the word at click position. Triple-click SHALL select all text. Clicking outside the text node bounds SHALL commit the edit and exit editing mode. #### Scenario: Drag to select - **WHEN** user clicks and drags inside a text node during editing - **THEN** the text between click start and current position is selected +#### Scenario: Double-click selects word +- **WHEN** user double-clicks on "world" in "Hello world" during editing +- **THEN** "world" is selected + +#### Scenario: Triple-click selects all +- **WHEN** user triple-clicks inside a text node during editing +- **THEN** all text is selected + #### Scenario: Click outside commits - **WHEN** user clicks outside the editing text node - **THEN** the text edit is committed and editing mode exits @@ -110,3 +122,25 @@ A blue outline SHALL appear around a text node during editing to indicate edit m #### Scenario: Edit mode indicator - **WHEN** user is editing a text node inline - **THEN** a blue outline is visible around the text node bounds + +### Requirement: Rich text style runs +Text nodes SHALL support per-character formatting via StyleRun arrays: `{start, length, style}` where style includes fontWeight, italic (boolean), and textDecoration (UNDERLINE, STRIKETHROUGH). ⌘B toggles bold, ⌘I toggles italic, ⌘U toggles underline on the current selection. With no selection, the shortcut toggles the whole-node style. Style runs adjust on insert/delete to preserve formatting. + +#### Scenario: Bold selection +- **WHEN** user selects "world" in "Hello world" and presses ⌘B +- **THEN** the word "world" renders bold while "Hello " stays regular + +#### Scenario: Toggle italic on whole node +- **WHEN** user presses ⌘I with no text selected in a text node +- **THEN** the entire text node toggles italic + +#### Scenario: Style preservation on insert +- **WHEN** user types "X" between a bold and regular segment +- **THEN** the inserted character inherits the style of the preceding segment and run boundaries adjust + +### Requirement: .fig roundtrip for style runs +The .fig import SHALL parse characterStyleIDs and styleOverrideTable from TextData into StyleRun arrays. The .fig export SHALL write back with a deduped style table. + +#### Scenario: Import rich text from .fig +- **WHEN** a .fig file with mixed bold/italic text is imported +- **THEN** the style runs are correctly reconstructed diff --git a/openspec/specs/tooling/spec.md b/openspec/specs/tooling/spec.md index 1330fa3be..7e2d83c75 100644 --- a/openspec/specs/tooling/spec.md +++ b/openspec/specs/tooling/spec.md @@ -88,3 +88,24 @@ The project SHALL use Bun workspaces with packages: root (app), packages/core (@ ### Requirement: npm publishing preparation @open-pencil/core and @open-pencil/cli SHALL have proper package.json fields for npm publishing: name, version, description, exports, main, types, files, license, repository. + +### Requirement: Copy-paste detection +The project SHALL use jscpd for copy-paste detection. The `bun run jscpd` command SHALL scan for duplicated code blocks. + +#### Scenario: Detect duplicates +- **WHEN** `bun run jscpd` is run +- **THEN** duplicated code blocks are reported with locations and percentages + +### Requirement: Kiwi serialization consolidation +Shared kiwi serialization logic (sceneNodeToKiwi, buildFigKiwi, parseFigKiwiChunks, decompressFigKiwiDataAsync) SHALL be extracted to packages/core/src/kiwi-serialize.ts. The app's src/kiwi/ SHALL re-export from core, eliminating the vendored kiwi-schema copy. + +#### Scenario: Clipboard and fig-export share serialization +- **WHEN** clipboard.ts and fig-export.ts serialize nodes to kiwi +- **THEN** both use the shared kiwi-serialize.ts functions + +### Requirement: Test coverage script +The project SHALL include a `test:coverage` script for measuring code coverage. + +#### Scenario: Run coverage +- **WHEN** `bun run test:coverage` is run +- **THEN** test coverage metrics are reported