Sync specs & docs: style runs, JSX renderer, CLI expansion, tests
- Merge from master: 22 commits (rich text, JSX renderer, CLI, dedup, tests) - Update specs: text-editing (style runs, ⌘B/I/U, .fig roundtrip, double/triple-click, selectLine), canvas-rendering (mixed-style ParagraphBuilder), editor-ui (B/I/U/S buttons), cli (analyze, node, pages, variables), tooling (jscpd, kiwi-serialize, test:coverage), testing (.fig roundtrip, import perf, JSX tests), scene-graph (StyleRun model, JSX renderer) - Update docs: features (rich text formatting, JSX renderer, expanded CLI, code quality), figma-comparison (Text styles 🔲→🟡, 80/150), roadmap (Phase 4+5 delivered items) - Archive sync-style-runs-jsx-cli-tests change
This commit is contained in:
parent
37e1ba1caa
commit
5fbad7b092
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -29,3 +29,4 @@ test-results/
|
|||
# Git worktrees for parallel agent work
|
||||
.worktrees/
|
||||
.env
|
||||
docs/.vitepress/cache/
|
||||
|
|
|
|||
|
|
@ -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:**
|
||||
|
|
|
|||
|
|
@ -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: <kbd>⌥</kbd><kbd>←</kbd>/<kbd>→</kbd> for word movement, <kbd>⌘</kbd><kbd>←</kbd>/<kbd>→</kbd> for line start/end, <kbd>⌥</kbd><kbd>⌫</kbd> for word delete, <kbd>⌘</kbd><kbd>⌫</kbd> for line delete. Shift extends selection. <kbd>Esc</kbd> 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: <kbd>⌥</kbd><kbd>←</kbd>/<kbd>→</kbd> for word movement, <kbd>⌘</kbd><kbd>←</kbd>/<kbd>→</kbd> for line start/end, <kbd>⌥</kbd><kbd>⌫</kbd> for word delete, <kbd>⌘</kbd><kbd>⌫</kbd> for line delete. Shift extends selection. <kbd>Esc</kbd> 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 <kbd>⌘</kbd><kbd>B</kbd> for bold, <kbd>⌘</kbd><kbd>I</kbd> for italic, <kbd>⌘</kbd><kbd>U</kbd> 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. <kbd>⌘</kbd><kbd>Z</kbd> undoes, <kbd>⇧</kbd><kbd>⌘</kbd><kbd>Z</kbd> redoes.
|
||||
|
|
@ -163,5 +171,26 @@ The engine is extracted to `packages/core/` (@open-pencil/core) — scene-graph,
|
|||
- `open-pencil tree <file>` — visual node tree
|
||||
- `open-pencil find <file>` — search by name/type
|
||||
- `open-pencil export <file>` — render to PNG/JPG/WEBP at any scale
|
||||
- `open-pencil analyze colors <file>` — color palette usage with clustering
|
||||
- `open-pencil analyze typography <file>` — font/size/weight distribution
|
||||
- `open-pencil analyze spacing <file>` — gap/padding values with grid check
|
||||
- `open-pencil analyze clusters <file>` — repeated patterns (potential components)
|
||||
- `open-pencil node <file> <id>` — detailed properties of a node by ID
|
||||
- `open-pencil pages <file>` — list pages with node counts
|
||||
- `open-pencil variables <file>` — 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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
schema: spec-driven
|
||||
created: 2026-03-01
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
# cli Specification (delta)
|
||||
|
||||
## New Requirements
|
||||
|
||||
### Requirement: Analyze commands
|
||||
The CLI SHALL provide `open-pencil analyze <file>` 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 <file> <id>` 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 <file>` 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 <file>` 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
|
||||
|
|
@ -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
|
||||
|
|
@ -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('<Frame w={100}><Text>Hi</Text></Frame>') is called
|
||||
- **THEN** a scene graph with a FRAME parent and TEXT child is produced
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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/<domain>/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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -14,6 +14,13 @@ The CLI SHALL support the following commands:
|
|||
- `open-pencil tree <file>` — visual node tree with formatted output
|
||||
- `open-pencil find <file>` — search nodes by name or type
|
||||
- `open-pencil export <file>` — render to PNG/JPG/WEBP at any scale
|
||||
- `open-pencil analyze colors <file>` — color palette usage with clustering
|
||||
- `open-pencil analyze typography <file>` — font/size/weight distribution
|
||||
- `open-pencil analyze spacing <file>` — gap/padding values with grid alignment check
|
||||
- `open-pencil analyze clusters <file>` — repeated patterns (potential components)
|
||||
- `open-pencil node <file> <id>` — detailed properties of a specific node
|
||||
- `open-pencil pages <file>` — list pages with node counts
|
||||
- `open-pencil variables <file>` — 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 <file>` 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 <file> <id>` 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 <file>` 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 <file>` 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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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('<Frame w={100}><Text>Hi</Text></Frame>') is called
|
||||
- **THEN** a scene graph with a FRAME parent and TEXT child is produced
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue