Sync specs & docs with master: docs→packages/docs, Safari save, .fig export fix, LOC stats
This commit is contained in:
parent
5fa8284766
commit
0e8866265d
|
|
@ -10,6 +10,7 @@ Bun workspace with two packages:
|
|||
|
||||
- `packages/core` — `@open-pencil/core`: scene graph, renderer, layout, codec, kiwi, clipboard, vector, snap, undo. Zero DOM deps, runs headless in Bun.
|
||||
- `packages/cli` — `@open-pencil/cli`: headless CLI for .fig inspection, export, linting. Uses `citty` + `agentfmt`.
|
||||
- `packages/docs` — `@open-pencil/docs`: VitePress documentation site. Run with `cd packages/docs && bun run dev`.
|
||||
|
||||
The root app (`src/`) is the Tauri/Vite desktop editor. Its `src/engine/` files are thin re-export shims from `@open-pencil/core`.
|
||||
|
||||
|
|
@ -117,6 +118,7 @@ The root app (`src/`) is the Tauri/Vite desktop editor. Its `src/engine/` files
|
|||
- `NodeChange` is the central type for Kiwi encode/decode
|
||||
- Vector data uses reverse-engineered `vectorNetworkBlob` binary format — encoder/decoder in `packages/core/src/vector.ts`
|
||||
- showOpenFilePicker/showSaveFilePicker are File System Access API (Chrome/Edge), not Tauri-only — code has fallbacks
|
||||
- Safari save: no File System Access API → uses `<a>` download link with deferred `revokeObjectURL`. SafariBanner warns users about limitations.
|
||||
- Tauri detection: `IS_TAURI` constant from `packages/core/src/constants.ts` — don't use `'__TAURI_INTERNALS__' in window` inline
|
||||
- .fig export: compression with fflate (browser) or Tauri Rust commands
|
||||
- Test .fig round-trip by exporting and reimporting in Figma
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
schema: spec-driven
|
||||
created: 2026-03-01
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
## Context
|
||||
|
||||
Retrospective doc sync — code is merged and tested. Key structural change: `docs/` → `packages/docs/` as `@open-pencil/docs` workspace package. Repo org changed: dannote/open-pencil → open-pencil/open-pencil.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:** Update specs and docs to match current codebase state. Fix stale `docs/` paths.
|
||||
|
||||
**Non-Goals:** Code changes. New features. Restructuring specs.
|
||||
|
||||
## Decisions
|
||||
|
||||
1. **Bulk path replacement** `docs/` → `packages/docs/` in specs — mechanical, low risk.
|
||||
2. **Minimal delta specs** — only MODIFIED requirements for changed behavior, no padding.
|
||||
3. **AGENTS.md updates** — new commands paths, docs package mention.
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
## Why
|
||||
|
||||
Master branch advanced after our previous sync (commits 22f8599..7a2694b). Key changes: docs moved to `packages/docs/` as `@open-pencil/docs`, repo renamed to open-pencil/open-pencil, Safari compatibility fixes, .fig export bugfix, CHANGELOG.md, CI/CD for app.openpencil.dev, npm trusted publishing, download links and messaging updates. Specs and docs reference stale `docs/` paths and miss new capabilities.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Update all spec and doc path references from `docs/` to `packages/docs/`
|
||||
- Update tooling spec with docs package extraction and npm publishing improvements
|
||||
- Update vitepress-docs spec with new location and landing page changes (download links, "Open App" nav, "no subscription" messaging)
|
||||
- Add .fig export COMPONENT/COMPONENT_SET → SYMBOL fix to fig-import spec
|
||||
- Update desktop-app spec with Safari compatibility (save fallback, banner)
|
||||
- Add CHANGELOG.md reference to tooling spec
|
||||
- Update AGENTS.md with new paths and conventions
|
||||
- Update Figma comparison: Safari save support improved
|
||||
- Update Penpot comparison LOC stats if stale
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
(none — changes are incremental updates to existing capabilities)
|
||||
|
||||
### Modified Capabilities
|
||||
- `vitepress-docs`: docs moved to `packages/docs/`, new landing page features, download links, "Open App" nav link
|
||||
- `tooling`: docs extracted to separate package, npm trusted publishing, CHANGELOG, CI app deploy workflow
|
||||
- `fig-import`: .fig export fix for COMPONENT/COMPONENT_SET → SYMBOL mapping
|
||||
- `desktop-app`: Safari save improvements, banner for File System Access API limitation
|
||||
- `testing`: layers-panel test fixtures updated
|
||||
|
||||
## Impact
|
||||
|
||||
- **Specs:** 5 existing specs need path and content updates
|
||||
- **AGENTS.md:** docs paths, commands, repo URL
|
||||
- **No code changes** — all implementations already merged
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
## ADDED Requirements
|
||||
|
||||
### Requirement: Safari save fallback
|
||||
The editor SHALL support saving in Safari and browsers without File System Access API by creating a download link (`<a>` appended to DOM) with deferred `revokeObjectURL`.
|
||||
|
||||
#### Scenario: Save in Safari
|
||||
- **WHEN** user presses ⌘S in Safari (no showSaveFilePicker)
|
||||
- **THEN** system creates a blob URL, appends an anchor to DOM, triggers click, and defers revocation
|
||||
|
||||
### Requirement: Safari compatibility banner
|
||||
The editor SHALL display a dismissible banner when running in Safari (or browsers without File System Access API), explaining limitations and suggesting Chrome/Edge.
|
||||
|
||||
#### Scenario: Banner shown
|
||||
- **WHEN** user opens the app in Safari
|
||||
- **THEN** a banner appears explaining File System Access limitations
|
||||
|
||||
#### Scenario: Banner dismissal persisted
|
||||
- **WHEN** user dismisses the banner
|
||||
- **THEN** dismissal is saved to localStorage and banner does not reappear
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
## ADDED Requirements
|
||||
|
||||
### Requirement: Component type mapping in export
|
||||
The .fig export pipeline SHALL map COMPONENT and COMPONENT_SET node types to SYMBOL in Kiwi encoding (Figma uses SYMBOL enum value for component-like nodes).
|
||||
|
||||
#### Scenario: Exporting component
|
||||
- **WHEN** a COMPONENT node is serialized to Kiwi format
|
||||
- **THEN** the type field uses the SYMBOL enum value
|
||||
|
||||
#### Scenario: Exporting component set
|
||||
- **WHEN** a COMPONENT_SET node is serialized to Kiwi format
|
||||
- **THEN** the type field uses the SYMBOL enum value
|
||||
|
||||
#### Scenario: Round-trip fidelity
|
||||
- **WHEN** an exported .fig file is re-imported in Figma
|
||||
- **THEN** component nodes are recognized as components (not unknown types)
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Layers panel E2E tests
|
||||
E2E tests SHALL verify the layers panel: node visibility in tree, expand/collapse frames, selection sync between canvas and layers panel. Test fixtures match current demo shapes (Components, App Preview).
|
||||
|
||||
#### Scenario: Run layers panel E2E
|
||||
- **WHEN** the layers panel E2E tests run
|
||||
- **THEN** all tests pass verifying tree structure, visibility toggles, and selection sync with updated demo shape names
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Vite 7 build system
|
||||
The project SHALL use Vite 7 as the build tool with dev server at port 1420 and HMR support. Documentation lives in `packages/docs/` as `@open-pencil/docs` workspace package with its own dev/build/preview scripts.
|
||||
|
||||
#### Scenario: Dev server
|
||||
- **WHEN** `bun run dev` is executed
|
||||
- **THEN** Vite dev server starts at http://localhost:1420 with hot module replacement
|
||||
|
||||
#### Scenario: Docs dev
|
||||
- **WHEN** `cd packages/docs && bun run dev` is executed
|
||||
- **THEN** VitePress dev server starts for the documentation site
|
||||
|
||||
#### Scenario: Docs build
|
||||
- **WHEN** `cd packages/docs && bun run build` is executed
|
||||
- **THEN** VitePress builds the documentation site to `packages/docs/.vitepress/dist/`
|
||||
|
||||
### Requirement: Bun workspace monorepo
|
||||
The project SHALL use Bun workspaces with packages: root (app), packages/core (@open-pencil/core), packages/cli (@open-pencil/cli), packages/docs (@open-pencil/docs). The workspace is configured in the root package.json. CLI is runnable via `bun open-pencil` in the workspace.
|
||||
|
||||
#### Scenario: Workspace packages resolve
|
||||
- **WHEN** the app imports from @open-pencil/core
|
||||
- **THEN** Bun resolves it to packages/core/ via workspace linking
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: CHANGELOG
|
||||
The project SHALL maintain a CHANGELOG.md in the root following Keep a Changelog conventions.
|
||||
|
||||
#### Scenario: Changelog exists
|
||||
- **WHEN** user reads CHANGELOG.md
|
||||
- **THEN** version history with categorized changes (Editor, CLI, File Format, etc.) is listed
|
||||
|
||||
### Requirement: npm trusted publishing
|
||||
Package configs SHALL include `repository` field and `provenance: true` for npm trusted publishing via GitHub Actions.
|
||||
|
||||
#### Scenario: Publish with provenance
|
||||
- **WHEN** GitHub Actions release workflow runs
|
||||
- **THEN** packages are published to npm with provenance attestation
|
||||
|
||||
### Requirement: CI app deployment
|
||||
The project SHALL have a GitHub Actions workflow (`app.yml`) for deploying the web app to app.openpencil.dev.
|
||||
|
||||
#### Scenario: App deploys on push
|
||||
- **WHEN** code is pushed to main branch
|
||||
- **THEN** GitHub Actions builds and deploys the app
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: VitePress documentation site
|
||||
The project SHALL have a VitePress documentation site in the `packages/docs/` directory as `@open-pencil/docs` workspace package, with its own `.vitepress/config.ts` configuration and `package.json`.
|
||||
|
||||
#### Scenario: Docs dev server starts
|
||||
- **WHEN** `cd packages/docs && bun run dev` is executed
|
||||
- **THEN** VitePress dev server starts and serves the documentation site
|
||||
|
||||
#### Scenario: Docs build succeeds
|
||||
- **WHEN** `cd packages/docs && bun run build` is executed
|
||||
- **THEN** VitePress produces a static site in `packages/docs/.vitepress/dist/`
|
||||
|
||||
### Requirement: Landing page
|
||||
The docs site SHALL have an index.md landing page with project name, tagline "Open-source Figma alternative. Fully local, AI-native, programmable.", quick navigation to guide and reference sections, download links (GitHub Releases, app.openpencil.dev), and "no subscription, bring your own API key" messaging.
|
||||
|
||||
#### Scenario: Landing page loads
|
||||
- **WHEN** user opens the docs site root URL
|
||||
- **THEN** a landing page with "OpenPencil" title, tagline, download links, and feature highlights is displayed
|
||||
|
||||
#### Scenario: Open App link
|
||||
- **WHEN** user clicks "Open App" in the top nav
|
||||
- **THEN** browser navigates to https://app.openpencil.dev
|
||||
|
||||
### Requirement: Dark theme
|
||||
The docs site SHALL use dark appearance to match the editor's dark aesthetic.
|
||||
|
||||
#### Scenario: Dark mode
|
||||
- **WHEN** user opens the documentation site
|
||||
- **THEN** the site renders with dark theme by default
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
## Context
|
||||
|
||||
Sync with master commits 22f8599..7a2694b. Docs moved to `packages/docs/`. Repo renamed to open-pencil/open-pencil. All code already merged.
|
||||
|
||||
## 1. Fix stale docs paths in specs
|
||||
|
||||
- [x] 1.1 In `openspec/specs/vitepress-docs/spec.md`: replace all `docs/` references with `packages/docs/`, update Purpose line, update VitePress site requirement with new location, add "Open App" nav and download links to landing page requirement
|
||||
- [x] 1.2 In `openspec/specs/tooling/spec.md`: replace `docs/` → `packages/docs/` in build paths, update Bun workspace monorepo requirement to list 4 packages (add docs), add CHANGELOG, npm trusted publishing, and CI app deploy requirements
|
||||
- [x] 1.3 In `openspec/specs/fig-import/spec.md`: add COMPONENT/COMPONENT_SET → SYMBOL export mapping requirement
|
||||
- [x] 1.4 In `openspec/specs/desktop-app/spec.md`: add Safari save fallback and compatibility banner requirements
|
||||
- [x] 1.5 In `openspec/specs/testing/spec.md`: update layers panel test requirement with current demo shape names
|
||||
|
||||
## 2. Update AGENTS.md
|
||||
|
||||
- [x] 2.1 Update docs-related commands: `bun run docs:dev` → `cd packages/docs && bun run dev` (or equivalent workspace command)
|
||||
- [x] 2.2 Add `packages/docs` mention to Monorepo section
|
||||
- [x] 2.3 Add Safari save note to File format section (download fallback for browsers without File System Access)
|
||||
|
||||
## 3. Update docs content
|
||||
|
||||
- [x] 3.1 Verify `packages/docs/guide/figma-comparison.md` has correct Safari/save info (check Import & Export section, "Save / Save As" row notes)
|
||||
- [x] 3.2 Verify `packages/docs/guide/comparison.md` LOC stats — update if stale
|
||||
|
||||
## 4. Verification
|
||||
|
||||
- [x] 4.1 Run `cd packages/docs && bun run build` — verify no broken links
|
||||
- [x] 4.2 Verify no remaining `docs/` references in specs (should all be `packages/docs/`)
|
||||
|
|
@ -141,3 +141,21 @@ The Cargo crate SHALL be named `open_pencil` and the binary `OpenPencil`. The ma
|
|||
#### Scenario: macOS Dock name
|
||||
- **WHEN** the app is running on macOS
|
||||
- **THEN** the Dock displays "OpenPencil" (not "open-pencil-app")
|
||||
|
||||
### Requirement: Safari save fallback
|
||||
The editor SHALL support saving in Safari and browsers without File System Access API by creating a download link (`<a>` appended to DOM) with deferred `revokeObjectURL`.
|
||||
|
||||
#### Scenario: Save in Safari
|
||||
- **WHEN** user presses ⌘S in Safari (no showSaveFilePicker)
|
||||
- **THEN** system creates a blob URL, appends an anchor to DOM, triggers click, and defers revocation
|
||||
|
||||
### Requirement: Safari compatibility banner
|
||||
The editor SHALL display a dismissible banner when running in Safari (or browsers without File System Access API), explaining limitations and suggesting Chrome/Edge.
|
||||
|
||||
#### Scenario: Banner shown
|
||||
- **WHEN** user opens the app in Safari
|
||||
- **THEN** a banner appears explaining File System Access limitations
|
||||
|
||||
#### Scenario: Banner dismissal persisted
|
||||
- **WHEN** user dismisses the banner
|
||||
- **THEN** dismissal is saved to localStorage and banner does not reappear
|
||||
|
|
|
|||
|
|
@ -78,3 +78,18 @@ A .fig file imported and then exported SHALL produce a file that Figma can open
|
|||
- **WHEN** a .fig file is imported into OpenPencil and re-exported
|
||||
- **THEN** the exported file opens in Figma with matching visual output
|
||||
|
||||
|
||||
### Requirement: Component type mapping in export
|
||||
The .fig export pipeline SHALL map COMPONENT and COMPONENT_SET node types to SYMBOL in Kiwi encoding (Figma uses SYMBOL enum value for component-like nodes).
|
||||
|
||||
#### Scenario: Exporting component
|
||||
- **WHEN** a COMPONENT node is serialized to Kiwi format
|
||||
- **THEN** the type field uses the SYMBOL enum value
|
||||
|
||||
#### Scenario: Exporting component set
|
||||
- **WHEN** a COMPONENT_SET node is serialized to Kiwi format
|
||||
- **THEN** the type field uses the SYMBOL enum value
|
||||
|
||||
#### Scenario: Round-trip fidelity
|
||||
- **WHEN** an exported .fig file is re-imported in Figma
|
||||
- **THEN** component nodes are recognized as components (not unknown types)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
VitePress documentation page with feature-by-feature comparison tables mapping Figma Design features to Open Pencil's implementation status. Sourced from Figma help center articles (~90) and cross-referenced against Open Pencil's specs and features.
|
||||
## Requirements
|
||||
### Requirement: Feature comparison page exists
|
||||
The documentation site SHALL include a page at `docs/guide/figma-comparison.md` that provides feature-by-feature comparison tables mapping Figma Design features to Open Pencil status.
|
||||
The documentation site SHALL include a page at `packages/docs/guide/figma-comparison.md` that provides feature-by-feature comparison tables mapping Figma Design features to Open Pencil status.
|
||||
|
||||
#### Scenario: Page is accessible
|
||||
- **WHEN** user navigates to `/guide/figma-comparison` on the docs site
|
||||
|
|
|
|||
|
|
@ -60,11 +60,11 @@ Unit tests SHALL verify Yoga auto-layout computation: direction, gap, padding, j
|
|||
- **THEN** all layout computation tests pass
|
||||
|
||||
### Requirement: Layers panel E2E tests
|
||||
E2E tests SHALL verify the layers panel: node visibility in tree, expand/collapse frames, selection sync between canvas and layers panel.
|
||||
E2E tests SHALL verify the layers panel: node visibility in tree, expand/collapse frames, selection sync between canvas and layers panel. Test fixtures match current demo shapes (Components, App Preview).
|
||||
|
||||
#### Scenario: Run layers panel E2E
|
||||
- **WHEN** the layers panel E2E tests run
|
||||
- **THEN** all tests pass verifying tree structure, visibility toggles, and selection sync
|
||||
- **THEN** all tests pass verifying tree structure, visibility toggles, and selection sync with updated demo shape names
|
||||
|
||||
### Requirement: Component-instance sync unit tests
|
||||
Unit tests SHALL cover the component-instance sync lifecycle: instance creation with `componentId` mapping on children, sync propagation of property changes, override preservation during sync, new child addition to instances, and detach breaking the link.
|
||||
|
|
|
|||
|
|
@ -4,22 +4,22 @@
|
|||
Build and development tooling. Vite 7 build system, oxlint linting, oxfmt formatting, typescript-go type checking, and Tailwind CSS 4 integration.
|
||||
## Requirements
|
||||
### Requirement: Vite 7 build system
|
||||
The project SHALL use Vite 7 as the build tool with dev server at port 1420 and HMR support. VitePress SHALL be installed as a devDependency for the documentation site. The `docs:dev`, `docs:build`, and `docs:preview` scripts SHALL be added to package.json.
|
||||
The project SHALL use Vite 7 as the build tool with dev server at port 1420 and HMR support. Documentation lives in `packages/docs/` as `@open-pencil/docs` workspace package with its own dev/build/preview scripts.
|
||||
|
||||
#### Scenario: Dev server
|
||||
- **WHEN** `bun run dev` is executed
|
||||
- **THEN** Vite dev server starts at http://localhost:1420 with hot module replacement
|
||||
|
||||
#### Scenario: Docs scripts available
|
||||
- **WHEN** `bun run docs:dev` is executed
|
||||
#### Scenario: Docs dev
|
||||
- **WHEN** `cd packages/docs && bun run dev` is executed
|
||||
- **THEN** VitePress dev server starts for the documentation site
|
||||
|
||||
#### Scenario: Docs build
|
||||
- **WHEN** `bun run docs:build` is executed
|
||||
- **THEN** VitePress builds the documentation site to `docs/.vitepress/dist/`
|
||||
- **WHEN** `cd packages/docs && bun run build` is executed
|
||||
- **THEN** VitePress builds the documentation site to `packages/docs/.vitepress/dist/`
|
||||
|
||||
#### Scenario: Docs preview
|
||||
- **WHEN** `bun run docs:preview` is executed
|
||||
- **WHEN** `cd packages/docs && bun run preview` is executed
|
||||
- **THEN** a static server previews the built documentation site
|
||||
|
||||
### Requirement: oxlint linting
|
||||
|
|
@ -79,7 +79,7 @@ The codebase SHALL maintain 0 oxlint warnings and 0 tsgo type errors. `bun run c
|
|||
- **THEN** both lint and typecheck pass with zero issues
|
||||
|
||||
### Requirement: Bun workspace monorepo
|
||||
The project SHALL use Bun workspaces with packages: root (app), packages/core (@open-pencil/core), packages/cli (@open-pencil/cli). The workspace is configured in the root package.json. CLI is runnable via `bun open-pencil` in the workspace.
|
||||
The project SHALL use Bun workspaces with packages: root (app), packages/core (@open-pencil/core), packages/cli (@open-pencil/cli), packages/docs (@open-pencil/docs). The workspace is configured in the root package.json. CLI is runnable via `bun open-pencil` in the workspace.
|
||||
|
||||
#### Scenario: Workspace packages resolve
|
||||
- **WHEN** the app imports from @open-pencil/core
|
||||
|
|
@ -198,3 +198,24 @@ Each tool in schema.ts SHALL have clear description and parameter documentation
|
|||
- **WHEN** AI reads parameter schema
|
||||
- **THEN** each parameter has type and description (e.g., `width: { type: 'number', description: 'Rectangle width in pixels' }`)
|
||||
|
||||
|
||||
### Requirement: CHANGELOG
|
||||
The project SHALL maintain a CHANGELOG.md in the root following Keep a Changelog conventions.
|
||||
|
||||
#### Scenario: Changelog exists
|
||||
- **WHEN** user reads CHANGELOG.md
|
||||
- **THEN** version history with categorized changes (Editor, CLI, File Format, etc.) is listed
|
||||
|
||||
### Requirement: npm trusted publishing
|
||||
Package configs SHALL include `repository` field and `provenance: true` for npm trusted publishing via GitHub Actions.
|
||||
|
||||
#### Scenario: Publish with provenance
|
||||
- **WHEN** GitHub Actions release workflow runs
|
||||
- **THEN** packages are published to npm with provenance attestation
|
||||
|
||||
### Requirement: CI app deployment
|
||||
The project SHALL have a GitHub Actions workflow (`app.yml`) for deploying the web app to app.openpencil.dev.
|
||||
|
||||
#### Scenario: App deploys on push
|
||||
- **WHEN** code is pushed to main branch
|
||||
- **THEN** GitHub Actions builds and deploys the app
|
||||
|
|
|
|||
|
|
@ -4,14 +4,14 @@
|
|||
TBD - created by archiving change vitepress-userdoc. Update Purpose after archive.
|
||||
## Requirements
|
||||
### Requirement: User guide landing page
|
||||
The docs site SHALL have an `index.md` at `docs/user-guide/` with the title "User Guide", a brief description positioning OpenPencil as an open-source, Figma-compatible design editor, and links to all user guide articles organized by category.
|
||||
The docs site SHALL have an `index.md` at `packages/docs/user-guide/` with the title "User Guide", a brief description positioning OpenPencil as an open-source, Figma-compatible design editor, and links to all user guide articles organized by category.
|
||||
|
||||
#### Scenario: Landing page renders
|
||||
- **WHEN** user navigates to /user-guide/
|
||||
- **THEN** a page with "User Guide" title, open-source/Figma-compatible positioning, and categorized links to all articles is displayed
|
||||
|
||||
### Requirement: Canvas navigation article
|
||||
The docs site SHALL have a `canvas-navigation.md` article in `docs/user-guide/` documenting panning (space+drag, middle mouse, trackpad, hand tool), zooming (ctrl+scroll, pinch, keyboard shortcuts), and zoom reset.
|
||||
The docs site SHALL have a `canvas-navigation.md` article in `packages/docs/user-guide/` documenting panning (space+drag, middle mouse, trackpad, hand tool), zooming (ctrl+scroll, pinch, keyboard shortcuts), and zoom reset.
|
||||
|
||||
#### Scenario: Canvas navigation article renders
|
||||
- **WHEN** user navigates to /user-guide/canvas-navigation
|
||||
|
|
|
|||
|
|
@ -1,21 +1,21 @@
|
|||
# vitepress-docs Specification
|
||||
|
||||
## Purpose
|
||||
VitePress documentation site at `docs/` with content derived from PLAN.md, README, and openspec specs. Includes guide pages (getting started, architecture, tech stack), reference pages (keyboard shortcuts, node types, MCP tools), and development pages (contributing, testing, openspec workflow, roadmap).
|
||||
VitePress documentation site at `packages/docs/` as `@open-pencil/docs` workspace package. Content derived from PLAN.md, README, and openspec specs. Includes guide pages (getting started, architecture, tech stack), reference pages (keyboard shortcuts, node types, MCP tools), and development pages (contributing, testing, openspec workflow, roadmap).
|
||||
## Requirements
|
||||
### Requirement: VitePress documentation site
|
||||
The project SHALL have a VitePress documentation site in the `docs/` directory with its own `.vitepress/config.ts` configuration, independent from the app's Vite config.
|
||||
The project SHALL have a VitePress documentation site in the `packages/docs/` directory as `@open-pencil/docs` workspace package, with its own `.vitepress/config.ts` configuration and `package.json`.
|
||||
|
||||
#### Scenario: Docs dev server starts
|
||||
- **WHEN** `bun run docs:dev` is executed
|
||||
- **WHEN** `cd packages/docs && bun run dev` is executed
|
||||
- **THEN** VitePress dev server starts and serves the documentation site
|
||||
|
||||
#### Scenario: Docs build succeeds
|
||||
- **WHEN** `bun run docs:build` is executed
|
||||
- **THEN** VitePress produces a static site in `docs/.vitepress/dist/`
|
||||
- **WHEN** `cd packages/docs && bun run build` is executed
|
||||
- **THEN** VitePress produces a static site in `packages/docs/.vitepress/dist/`
|
||||
|
||||
### Requirement: Landing page
|
||||
The docs site SHALL have an index.md landing page with project name, tagline "Open-source Figma alternative. Fully local, AI-native, programmable.", and quick navigation to guide and reference sections. The feature cards SHALL reflect the five pillars: open source, Figma-compatible, AI-native, fully local, programmable.
|
||||
The docs site SHALL have an index.md landing page with project name, tagline "Open-source Figma alternative. Fully local, AI-native, programmable.", quick navigation to guide and reference sections, download links (GitHub Releases, app.openpencil.dev), and "no subscription, bring your own API key" messaging. The feature cards SHALL reflect the five pillars: open source, Figma-compatible, AI-native, fully local, programmable.
|
||||
|
||||
#### Scenario: Landing page loads
|
||||
- **WHEN** user opens the docs site root URL
|
||||
|
|
@ -81,10 +81,10 @@ The docs site SHALL use dark appearance to match the editor's dark aesthetic.
|
|||
- **THEN** the site renders with VitePress dark theme
|
||||
|
||||
### Requirement: Build artifacts excluded from git
|
||||
`docs/.vitepress/dist` and `docs/.vitepress/cache` SHALL be listed in `.gitignore`.
|
||||
`packages/docs/.vitepress/dist` and `packages/docs/.vitepress/cache` SHALL be listed in `.gitignore`.
|
||||
|
||||
#### Scenario: Gitignore entries
|
||||
- **WHEN** `bun run docs:build` creates output in `docs/.vitepress/dist/`
|
||||
- **WHEN** `bun run docs:build` creates output in `packages/docs/.vitepress/dist/`
|
||||
- **THEN** the output directory is not tracked by git
|
||||
|
||||
### Requirement: Docs reflect sections feature
|
||||
|
|
@ -153,7 +153,7 @@ The VitePress sidebar SHALL include a "Comparison" link in the Guide section aft
|
|||
|
||||
### Requirement: Eval command documentation
|
||||
|
||||
The docs site SHALL include comprehensive documentation for the eval command in `docs/eval-command.md`.
|
||||
The docs site SHALL include comprehensive documentation for the eval command in `packages/docs/eval-command.md`.
|
||||
|
||||
#### Scenario: Eval command page exists
|
||||
- **WHEN** user navigates to `/eval-command`
|
||||
|
|
@ -196,7 +196,7 @@ The docs site SHALL include comprehensive documentation for the eval command in
|
|||
The docs site SHALL update comparison matrices to reflect new features (app menu, eval command, Figma Plugin API).
|
||||
|
||||
#### Scenario: Figma comparison update
|
||||
- **WHEN** user reads `docs/guide/figma-comparison.md`
|
||||
- **WHEN** user reads `packages/docs/guide/figma-comparison.md`
|
||||
- **THEN** Interface & Navigation section includes app menu status
|
||||
|
||||
#### Scenario: Plugin API row
|
||||
|
|
@ -208,7 +208,7 @@ The docs site SHALL update comparison matrices to reflect new features (app menu
|
|||
- **THEN** AI tools row is updated to reflect unified tool definitions and eval integration
|
||||
|
||||
#### Scenario: Penpot comparison update
|
||||
- **WHEN** user reads `docs/guide/comparison.md`
|
||||
- **WHEN** user reads `packages/docs/guide/comparison.md`
|
||||
- **THEN** Architecture section highlights headless scripting advantage (eval command with Plugin API that Penpot lacks)
|
||||
|
||||
### Requirement: App menu documentation
|
||||
|
|
|
|||
437
packages/docs/eval-command.md
Normal file
437
packages/docs/eval-command.md
Normal file
|
|
@ -0,0 +1,437 @@
|
|||
# `open-pencil eval` — Figma-like Plugin API for Headless Scripting
|
||||
|
||||
## Overview
|
||||
|
||||
`bun open-pencil eval <file> --code '<js>'` executes JavaScript against a `.fig` file with a Figma-compatible `figma` global object. This enables headless scripting, batch operations, AI tool execution, and testing — all without the GUI.
|
||||
|
||||
The `figma` object mirrors Figma's Plugin API surface as closely as possible, so existing Figma plugin knowledge and code snippets transfer directly.
|
||||
|
||||
```bash
|
||||
# Create a frame, set auto-layout, add children
|
||||
bun open-pencil eval design.fig --code '
|
||||
const frame = figma.createFrame()
|
||||
frame.name = "Card"
|
||||
frame.resize(300, 200)
|
||||
frame.layoutMode = "VERTICAL"
|
||||
frame.itemSpacing = 12
|
||||
frame.paddingTop = frame.paddingBottom = 16
|
||||
frame.paddingLeft = frame.paddingRight = 16
|
||||
frame.fills = [{ type: "SOLID", color: { r: 1, g: 1, b: 1 } }]
|
||||
|
||||
const title = figma.createText()
|
||||
title.characters = "Hello World"
|
||||
title.fontSize = 24
|
||||
frame.appendChild(title)
|
||||
|
||||
return { id: frame.id, name: frame.name }
|
||||
'
|
||||
|
||||
# Query nodes
|
||||
bun open-pencil eval design.fig --code '
|
||||
const buttons = figma.currentPage.findAll(n => n.type === "FRAME" && n.name.includes("Button"))
|
||||
return buttons.map(b => ({ id: b.id, name: b.name, w: b.width, h: b.height }))
|
||||
'
|
||||
|
||||
# Read from stdin (for multiline scripts / piping)
|
||||
cat transform.js | bun open-pencil eval design.fig --stdin
|
||||
|
||||
# Write changes back
|
||||
bun open-pencil eval design.fig --code '...' --write
|
||||
bun open-pencil eval design.fig --code '...' -o modified.fig
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────┐
|
||||
│ CLI: `open-pencil eval <file> --code '...'` │
|
||||
│ ↓ │
|
||||
│ loadDocument(file) → SceneGraph │
|
||||
│ ↓ │
|
||||
│ FigmaAPI(sceneGraph) → `figma` proxy object │
|
||||
│ ↓ │
|
||||
│ AsyncFunction('figma', wrappedCode)(figmaProxy) │
|
||||
│ ↓ │
|
||||
│ print result as JSON / agentfmt │
|
||||
│ optionally: saveDocument(file) if --write │
|
||||
└──────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Key classes
|
||||
|
||||
| Class | Location | Role |
|
||||
|-------|----------|------|
|
||||
| `FigmaAPI` | `packages/core/src/figma-api.ts` | Proxy object implementing `figma.*` methods against `SceneGraph` |
|
||||
| `FigmaNode` | `packages/core/src/figma-api.ts` | Proxy wrapping `SceneNode` with Figma-style property access (`.fills`, `.resize()`, `.appendChild()`, etc.) |
|
||||
| `eval` command | `packages/cli/src/commands/eval.ts` | CLI command that loads doc, creates API, executes code |
|
||||
|
||||
### Why in `@open-pencil/core`?
|
||||
|
||||
The `FigmaAPI` class lives in core (not CLI) because:
|
||||
- **AI tools reuse it** — the chat panel's `render` tool can execute JSX through the same API
|
||||
- **Test scripts** — unit tests can use the API to set up fixtures
|
||||
- **No DOM deps** — runs headless in Bun, no browser APIs needed
|
||||
|
||||
## `FigmaAPI` — Phased Implementation
|
||||
|
||||
### Phase 1: Core (MVP for eval command)
|
||||
|
||||
These cover ~80% of real plugin scripts:
|
||||
|
||||
#### Document & Page
|
||||
|
||||
| Figma API | Our implementation | Notes |
|
||||
|-----------|--------------------|-------|
|
||||
| `figma.root` | Getter → proxy for root node | `.children` returns page proxies |
|
||||
| `figma.currentPage` | Getter/setter → first page by default | Settable to any page proxy |
|
||||
| `figma.currentPage.selection` | Get/set → tracked selection array | |
|
||||
| `figma.getNodeById(id)` | `graph.getNode(id)` wrapped in proxy | Sync, like Figma's deprecated version |
|
||||
|
||||
#### Node Creation
|
||||
|
||||
| Figma API | Maps to |
|
||||
|-----------|---------|
|
||||
| `figma.createFrame()` | `graph.createNode('FRAME', currentPageId)` |
|
||||
| `figma.createRectangle()` | `graph.createNode('RECTANGLE', ...)` |
|
||||
| `figma.createEllipse()` | `graph.createNode('ELLIPSE', ...)` |
|
||||
| `figma.createText()` | `graph.createNode('TEXT', ...)` |
|
||||
| `figma.createLine()` | `graph.createNode('LINE', ...)` |
|
||||
| `figma.createPolygon()` | `graph.createNode('POLYGON', ...)` |
|
||||
| `figma.createStar()` | `graph.createNode('STAR', ...)` |
|
||||
| `figma.createComponent()` | `graph.createNode('COMPONENT', ...)` |
|
||||
| `figma.createPage()` | `graph.addPage(name)` |
|
||||
| `figma.createSection()` | `graph.createNode('SECTION', ...)` |
|
||||
|
||||
#### Node Properties (via `FigmaNode` proxy)
|
||||
|
||||
Read/write on any node proxy. Property access maps to `SceneNode` fields:
|
||||
|
||||
```ts
|
||||
// Geometry
|
||||
node.x, node.y // direct
|
||||
node.width, node.height // read-only, use node.resize(w, h)
|
||||
node.rotation // direct
|
||||
node.resize(w, h) // updates width + height
|
||||
node.resizeWithoutConstraints(w, h) // same (no constraint engine yet)
|
||||
|
||||
// Visual
|
||||
node.fills // get/set Fill[]
|
||||
node.strokes // get/set Stroke[]
|
||||
node.effects // get/set Effect[]
|
||||
node.opacity // get/set number
|
||||
node.visible // get/set boolean
|
||||
node.locked // get/set boolean
|
||||
node.blendMode // get/set BlendMode
|
||||
node.clipsContent // get/set boolean
|
||||
|
||||
// Corner radius
|
||||
node.cornerRadius // get/set (number or figma.mixed)
|
||||
node.topLeftRadius // get/set
|
||||
node.topRightRadius // get/set
|
||||
node.bottomLeftRadius // get/set
|
||||
node.bottomRightRadius // get/set
|
||||
node.cornerSmoothing // get/set
|
||||
|
||||
// Identity
|
||||
node.id // read-only
|
||||
node.name // get/set
|
||||
node.type // read-only
|
||||
node.parent // read-only → FigmaNode | null
|
||||
node.removed // read-only boolean
|
||||
```
|
||||
|
||||
#### Tree Operations
|
||||
|
||||
```ts
|
||||
node.children // read-only FigmaNode[]
|
||||
node.appendChild(child) // reparent to end
|
||||
node.insertChild(index, child) // reparent at index
|
||||
node.remove() // graph.deleteNode(id)
|
||||
|
||||
// Traversal
|
||||
node.findAll(callback?) // recursive find
|
||||
node.findOne(callback) // first match
|
||||
node.findChild(callback) // direct children only
|
||||
node.findChildren(callback?) // direct children only
|
||||
```
|
||||
|
||||
#### Auto-layout
|
||||
|
||||
```ts
|
||||
node.layoutMode // 'NONE' | 'HORIZONTAL' | 'VERTICAL'
|
||||
node.primaryAxisAlignItems // 'MIN' | 'CENTER' | 'MAX' | 'SPACE_BETWEEN'
|
||||
node.counterAxisAlignItems // 'MIN' | 'CENTER' | 'MAX' | 'BASELINE'
|
||||
node.itemSpacing // number
|
||||
node.counterAxisSpacing // number | null
|
||||
node.paddingTop / Right / Bottom / Left // number
|
||||
node.layoutWrap // 'NO_WRAP' | 'WRAP'
|
||||
|
||||
// Child sizing
|
||||
node.layoutPositioning // 'AUTO' | 'ABSOLUTE'
|
||||
node.layoutGrow // 0 | 1
|
||||
node.layoutSizingHorizontal // 'FIXED' | 'HUG' | 'FILL'
|
||||
node.layoutSizingVertical // 'FIXED' | 'HUG' | 'FILL'
|
||||
```
|
||||
|
||||
#### Text
|
||||
|
||||
```ts
|
||||
node.characters // get/set (maps to node.text)
|
||||
node.fontSize // get/set
|
||||
node.fontName // get/set { family, style }
|
||||
node.fontWeight // get/set
|
||||
node.textAlignHorizontal // get/set
|
||||
node.textAlignVertical // get/set
|
||||
node.textAutoResize // get/set
|
||||
node.letterSpacing // get/set
|
||||
node.lineHeight // get/set
|
||||
node.maxLines // get/set
|
||||
node.textCase // get/set
|
||||
node.textDecoration // get/set
|
||||
```
|
||||
|
||||
#### Stroke details
|
||||
|
||||
```ts
|
||||
node.strokeWeight // get/set (maps to strokes[0].weight)
|
||||
node.strokeAlign // get/set (maps to strokes[0].align)
|
||||
node.dashPattern // get/set
|
||||
```
|
||||
|
||||
#### Misc
|
||||
|
||||
```ts
|
||||
figma.mixed // Symbol sentinel for mixed values
|
||||
figma.group(nodes, parent) // creates GROUP with given children
|
||||
figma.ungroup(node) // ungroups, reparents children
|
||||
figma.flatten(nodes) // NOT IMPLEMENTED YET — returns first node
|
||||
```
|
||||
|
||||
#### Export
|
||||
|
||||
```ts
|
||||
node.exportAsync(settings?) // only works if CanvasKit is loaded
|
||||
// settings: { format: 'PNG'|'JPG'|'SVG', constraint? }
|
||||
```
|
||||
|
||||
### Phase 2: Components & Instances
|
||||
|
||||
| API | Maps to |
|
||||
|-----|---------|
|
||||
| `figma.createComponent()` | `graph.createNode('COMPONENT', ...)` |
|
||||
| `figma.createComponentFromNode(node)` | Convert existing frame to component |
|
||||
| `figma.combineAsVariants(components, parent)` | Create COMPONENT_SET |
|
||||
| Node: `node.createInstance()` | `graph.createInstance(componentId, parentId)` |
|
||||
| Node: `node.detachInstance()` | `graph.detachInstance(id)` |
|
||||
| `figma.getNodeById(id).mainComponent` | `graph.getMainComponent(id)` |
|
||||
|
||||
### Phase 3: Variables
|
||||
|
||||
| API | Maps to |
|
||||
|-----|---------|
|
||||
| `figma.variables.getLocalVariables(type?)` | `graph.variables` filtered |
|
||||
| `figma.variables.getLocalVariableCollections()` | `graph.variableCollections` |
|
||||
| `figma.variables.createVariable(name, collection, type)` | `graph.addVariable(...)` |
|
||||
| `figma.variables.createVariableCollection(name)` | `graph.addCollection(...)` |
|
||||
| `figma.variables.getVariableById(id)` | `graph.variables.get(id)` |
|
||||
| `node.setBoundVariable(field, variable)` | `graph.bindVariable(...)` |
|
||||
| `node.boundVariables` | getter from SceneNode |
|
||||
|
||||
### Phase 4: Styles & Advanced
|
||||
|
||||
| API | Notes |
|
||||
|-----|-------|
|
||||
| `figma.createPaintStyle()` | Requires style storage in SceneGraph |
|
||||
| `figma.createTextStyle()` | Requires style storage in SceneGraph |
|
||||
| `figma.createEffectStyle()` | Requires style storage in SceneGraph |
|
||||
| `figma.loadFontAsync(fontName)` | No-op (we don't have font loading constraints) |
|
||||
| `figma.listAvailableFontsAsync()` | Return system fonts if available |
|
||||
| Boolean operations (`union`, `subtract`, `intersect`, `exclude`) | Requires path boolean engine |
|
||||
| `figma.createNodeFromJSXAsync(jsx)` | Port figma-use's JSX renderer |
|
||||
|
||||
## `FigmaNode` Proxy Design
|
||||
|
||||
The proxy wraps a `SceneNode` and translates Figma property names to our internal names. Key mappings:
|
||||
|
||||
```ts
|
||||
const PROPERTY_MAP: Record<string, string> = {
|
||||
// Figma name → SceneNode field name (only where they differ)
|
||||
'characters': 'text',
|
||||
'strokeWeight': → computed from strokes[0].weight,
|
||||
'strokeAlign': → computed from strokes[0].align,
|
||||
'fontName': → computed from { family: fontFamily, style: ... },
|
||||
'primaryAxisAlignItems': 'primaryAxisAlign',
|
||||
'counterAxisAlignItems': 'counterAxisAlign',
|
||||
'primaryAxisSizingMode': 'primaryAxisSizing', // value mapping: 'AUTO' → 'HUG', 'FIXED' → 'FIXED'
|
||||
'counterAxisSizingMode': 'counterAxisSizing',
|
||||
'layoutSizingHorizontal': → computed from primaryAxisSizing / counterAxisSizing depending on layoutMode
|
||||
'layoutSizingVertical': → computed
|
||||
}
|
||||
```
|
||||
|
||||
Methods on the proxy:
|
||||
|
||||
```ts
|
||||
class FigmaNode {
|
||||
// The proxy is created via: new Proxy(target, handler)
|
||||
// where handler.get intercepts property reads and handler.set intercepts writes
|
||||
|
||||
resize(width: number, height: number): void
|
||||
resizeWithoutConstraints(width: number, height: number): void
|
||||
remove(): void
|
||||
appendChild(child: FigmaNode): void
|
||||
insertChild(index: number, child: FigmaNode): void
|
||||
findAll(callback?: (node: FigmaNode) => boolean): FigmaNode[]
|
||||
findOne(callback: (node: FigmaNode) => boolean): FigmaNode | null
|
||||
findChild(callback: (node: FigmaNode) => boolean): FigmaNode | null
|
||||
findChildren(callback?: (node: FigmaNode) => boolean): FigmaNode[]
|
||||
exportAsync(settings?: ExportSettings): Promise<Uint8Array>
|
||||
|
||||
// Components (Phase 2)
|
||||
createInstance(): FigmaNode
|
||||
detachInstance(): void
|
||||
get mainComponent(): FigmaNode | null
|
||||
}
|
||||
```
|
||||
|
||||
## CLI Command
|
||||
|
||||
```
|
||||
bun open-pencil eval <file> [options]
|
||||
|
||||
Arguments:
|
||||
file .fig file to operate on
|
||||
|
||||
Options:
|
||||
--code, -c JavaScript code to execute (has access to `figma` global)
|
||||
--stdin Read code from stdin instead of --code
|
||||
--write, -w Write changes back to the input file
|
||||
-o, --output Write to a different file
|
||||
--json Output result as JSON (default for non-TTY)
|
||||
--quiet, -q Suppress output, only write file
|
||||
```
|
||||
|
||||
### Execution model
|
||||
|
||||
1. Load `.fig` → `SceneGraph`
|
||||
2. Create `FigmaAPI(graph)` → `figma` proxy
|
||||
3. Wrap user code in async function: `return (async () => { <code> })()`
|
||||
4. Execute with `figma` as sole argument
|
||||
5. Print return value (JSON or agentfmt)
|
||||
6. If `--write` or `-o`: serialize `SceneGraph` back to `.fig`
|
||||
|
||||
### Return value formatting
|
||||
|
||||
- `undefined` / `void` → no output
|
||||
- Primitives → printed directly
|
||||
- Objects/arrays → `JSON.stringify(result, null, 2)` or agentfmt tables
|
||||
- `FigmaNode` → serialized as `{ id, type, name, x, y, width, height, fills, ... }`
|
||||
- Arrays of `FigmaNode` → serialized as list
|
||||
|
||||
## Shared with AI Tools
|
||||
|
||||
The `FigmaAPI` class is the **same API surface** that AI tools use. Currently `src/ai/tools.ts` calls `store.createShape()`, `store.updateNodeWithUndo()`, etc. — these should be refactored to go through `FigmaAPI`:
|
||||
|
||||
```ts
|
||||
// Before (current AI tools)
|
||||
execute: async ({ type, x, y, width, height }) => {
|
||||
const id = store.createShape(type, x, y, width, height)
|
||||
return { id }
|
||||
}
|
||||
|
||||
// After (using FigmaAPI)
|
||||
execute: async ({ type, x, y, width, height }) => {
|
||||
const frame = figma.createFrame()
|
||||
frame.resize(width, height)
|
||||
frame.x = x
|
||||
frame.y = y
|
||||
return { id: frame.id }
|
||||
}
|
||||
```
|
||||
|
||||
This ensures CLI scripts and AI tools behave identically.
|
||||
|
||||
## File Layout
|
||||
|
||||
```
|
||||
packages/core/src/
|
||||
figma-api.ts # FigmaAPI class + FigmaNode proxy (Phase 1–4)
|
||||
figma-api.test.ts # Unit tests against headless SceneGraph
|
||||
|
||||
packages/cli/src/commands/
|
||||
eval.ts # CLI command
|
||||
|
||||
packages/cli/src/commands/eval.test.ts # Integration tests
|
||||
```
|
||||
|
||||
## Test Plan
|
||||
|
||||
### Unit tests (`packages/core/src/figma-api.test.ts`)
|
||||
|
||||
1. **Node creation** — each `createX()` creates correct type, added to current page
|
||||
2. **Property access** — `.fills`, `.x`, `.width`, `.name`, `.characters` read/write correctly
|
||||
3. **Resize** — `.resize(w, h)` updates width/height
|
||||
4. **Tree operations** — `.appendChild()`, `.insertChild()`, `.remove()`, `.parent`, `.children`
|
||||
5. **Traversal** — `.findAll()`, `.findOne()`, `.findChild()`, `.findChildren()` with callbacks
|
||||
6. **Auto-layout** — `.layoutMode`, `.itemSpacing`, `.paddingTop`, etc.
|
||||
7. **Text** — `.characters` maps to `.text`, `.fontName` maps to `{ family, style }`
|
||||
8. **Mixed values** — `.cornerRadius` returns `figma.mixed` when corners differ
|
||||
9. **Selection** — `figma.currentPage.selection` get/set
|
||||
10. **Page switching** — `figma.currentPage = page2` works
|
||||
11. **Group/ungroup** — `figma.group()` creates group, `figma.ungroup()` dissolves it
|
||||
12. **Clone** — node creation produces independent copies
|
||||
|
||||
### CLI integration tests (`packages/cli/src/commands/eval.test.ts`)
|
||||
|
||||
1. **Basic eval** — `eval test.fig --code 'return figma.currentPage.name'` → page name
|
||||
2. **Create + read** — create a frame, return its properties
|
||||
3. **Query nodes** — `findAll` returns correct nodes
|
||||
4. **Write back** — `--write` saves changes, reloading shows them
|
||||
5. **Stdin** — `echo 'return 42' | bun open-pencil eval test.fig --stdin` → `42`
|
||||
6. **JSON output** — `--json` returns valid JSON
|
||||
7. **Error handling** — syntax errors, runtime errors reported cleanly
|
||||
|
||||
## Implementation Order
|
||||
|
||||
1. **`FigmaNode` proxy** — property mapping, `.resize()`, `.remove()`, tree methods
|
||||
2. **`FigmaAPI` class** — `createFrame/Rectangle/...`, `.root`, `.currentPage`, `.getNodeById()`, `.mixed`, `.group()`
|
||||
3. **CLI `eval` command** — argument parsing, code wrapping, output formatting
|
||||
4. **Unit tests** — all 12 test groups above
|
||||
5. **CLI integration tests** — all 7 test groups above
|
||||
6. **Wire to AI tools** — refactor `src/ai/tools.ts` to use `FigmaAPI` where possible
|
||||
7. **Phase 2** — components & instances
|
||||
8. **Phase 3** — variables
|
||||
9. **Phase 4** — styles, boolean ops, JSX renderer
|
||||
|
||||
## Property Mapping Reference
|
||||
|
||||
| Figma Property | SceneNode Field | Type | Notes |
|
||||
|---------------|-----------------|------|-------|
|
||||
| `characters` | `text` | `string` | |
|
||||
| `fontName` | `fontFamily` + `fontWeight` + `italic` | `{ family, style }` | Computed: `style` = "Bold Italic" etc. |
|
||||
| `strokeWeight` | `strokes[0].weight` | `number` | Computed |
|
||||
| `strokeAlign` | `strokes[0].align` | `string` | Computed |
|
||||
| `primaryAxisAlignItems` | `primaryAxisAlign` | `string` | |
|
||||
| `counterAxisAlignItems` | `counterAxisAlign` | `string` | |
|
||||
| `layoutSizingHorizontal` | `primaryAxisSizing` or `counterAxisSizing` | `string` | Depends on `layoutMode` |
|
||||
| `layoutSizingVertical` | (opposite of horizontal) | `string` | |
|
||||
| `absoluteTransform` | computed from `x`, `y`, `rotation` | `Transform` | Read-only |
|
||||
| `absoluteBoundingBox` | `getAbsoluteBounds(id)` | `Rect` | Read-only |
|
||||
| All others | Same name | Same type | Direct passthrough |
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. **Font loading**: `figma.loadFontAsync()` — should it be a no-op (we don't have font gating) or should we track loaded fonts?
|
||||
→ **Decision: No-op that returns resolved Promise.** We don't gate text editing on font loading.
|
||||
|
||||
2. **Export in headless mode**: `node.exportAsync()` requires CanvasKit. Should eval load CanvasKit?
|
||||
→ **Decision: Optional.** If CanvasKit is available (via `--with-canvaskit` flag or env), enable export. Otherwise, throw "Export requires CanvasKit" error.
|
||||
|
||||
3. **`figma.mixed` symbol**: Should we use the actual Figma symbol or our own?
|
||||
→ **Decision: Our own `Symbol('mixed')`.** Exposed as `figma.mixed`.
|
||||
|
||||
4. **Undo**: `figma.commitUndo()` / `figma.triggerUndo()` — relevant in headless?
|
||||
→ **Decision: No-op in CLI.** Undo only matters in the live editor. The AI tools can add undo support separately via EditorStore.
|
||||
|
||||
5. **Write format**: Should `--write` produce `.fig` (Kiwi binary) or also support `.json`?
|
||||
→ **Decision: `.fig` only for now.** JSON export is a separate feature.
|
||||
|
|
@ -6,15 +6,15 @@ Why compare? OpenPencil exists because closed design platforms control what's po
|
|||
|
||||
| Metric | Open Pencil | Penpot |
|
||||
|--------|-------------|--------|
|
||||
| Total LOC | **~14,500** | **~292,000** |
|
||||
| Source files | 53 | ~2,900 |
|
||||
| Total LOC | **~26,000** | **~292,000** |
|
||||
| Source files | 125 | ~2,900 |
|
||||
| Languages | TypeScript, Vue | Clojure, ClojureScript, Rust, JS, SQL, SCSS |
|
||||
| Rendering engine | 1,646 LOC (TS) | 22,000 LOC (Rust) |
|
||||
| UI code | ~4,500 LOC | ~175,000 LOC (CLJS + SCSS) |
|
||||
| Backend | None (local-first) | 32,600 LOC + 151 SQL files |
|
||||
| LOC ratio | **1x** | **~20x** |
|
||||
| LOC ratio | **1x** | **~11x** |
|
||||
|
||||
Open Pencil is **20x smaller** — and that's the whole point. It's not a simplification; it's a fundamentally different architecture.
|
||||
Open Pencil is **11x smaller** — and that's the whole point. It's not a simplification; it's a fundamentally different architecture.
|
||||
|
||||
## 2. Architecture
|
||||
|
||||
|
|
@ -280,4 +280,4 @@ OpenPencil ships with an [`eval` command](/eval-command) that provides a Figma-c
|
|||
| **Self-hosting** | Penpot | Docker-ready vs desktop-only |
|
||||
| **Ecosystem maturity** | Penpot | Years of production vs early stage |
|
||||
|
||||
Open Pencil is architecturally superior for a design tool — leaner, faster, more maintainable, and Figma-compatible by design. Penpot carries the weight of a server-first architecture with 20x more code spread across 4 languages, which creates compounding maintenance burden. The tradeoff is that Penpot already has production collaboration and a plugin ecosystem, while Open Pencil is still building toward those.
|
||||
Open Pencil is architecturally superior for a design tool — leaner, faster, more maintainable, and Figma-compatible by design. Penpot carries the weight of a server-first architecture with 11x more code spread across 4 languages, which creates compounding maintenance burden. The tradeoff is that Penpot already has production collaboration and a plugin ecosystem, while Open Pencil is still building toward those.
|
||||
|
|
|
|||
|
|
@ -188,8 +188,8 @@ Feature-by-feature comparison of Figma Design capabilities with Open Pencil's cu
|
|||
| Feature | Status | Notes |
|
||||
|---------|--------|-------|
|
||||
| .fig file import | ✅ | Full Kiwi codec: 194 definitions, ~390 fields per NodeChange |
|
||||
| .fig file export | ✅ | Kiwi encoding + Zstd compression + thumbnail generation |
|
||||
| Save / Save As | ✅ | ⌘S / ⇧⌘S with native OS dialogs (Tauri) |
|
||||
| .fig file export | ✅ | Kiwi encoding + Zstd compression + thumbnail generation; COMPONENT/COMPONENT_SET correctly mapped to SYMBOL for Figma round-trip |
|
||||
| Save / Save As | ✅ | ⌘S / ⇧⌘S; native dialogs (Tauri), File System Access API (Chrome/Edge), download fallback (Safari) |
|
||||
| Figma clipboard (paste) | ✅ | Decode fig-kiwi binary from Figma clipboard |
|
||||
| Figma clipboard (copy) | ✅ | Encode fig-kiwi binary that Figma can read |
|
||||
| Sketch file import | 🔲 | .sketch file parsing |
|
||||
|
|
|
|||
Loading…
Reference in a new issue