openpencil/packages/docs/overview/features.md

144 lines
6.7 KiB
Markdown
Raw Normal View History

# Features
## Figma .fig Files
Open and save native Figma files directly. The import/export pipeline uses the same Kiwi binary codec as Figma — 194 schema definitions, ~390 fields per node. Save with <kbd>⌘</kbd><kbd>S</kbd>, Save As with <kbd>⇧</kbd><kbd>⌘</kbd><kbd>S</kbd>.
**Copy & paste with Figma** — select nodes in Figma, <kbd>⌘</kbd><kbd>C</kbd>, switch to OpenPencil, <kbd>⌘</kbd><kbd>V</kbd>. Fills, strokes, auto-layout, text, effects, corner radii, and vector networks are preserved. Works both ways.
## Drawing & Editing
- **Shapes** — Rectangle (<kbd>R</kbd>), Ellipse (<kbd>O</kbd>), Line (<kbd>L</kbd>), Polygon, Star
- **Pen tool** — vector networks (not simple paths), bezier curves with tangent handles
- **Text** — canvas-native editing with IME support, double-click to enter edit mode
- **Rich text** — per-character bold (<kbd>⌘</kbd><kbd>B</kbd>), italic (<kbd>⌘</kbd><kbd>I</kbd>), underline (<kbd>⌘</kbd><kbd>U</kbd>), strikethrough
- **Auto-layout** — flexbox and CSS Grid via Yoga WASM: direction, gap, padding, justify, align, child sizing, grid tracks. <kbd>⇧</kbd><kbd>A</kbd> to toggle
feat: behaviours and preview mode (#893) * feat: author behaviours on main components A main component or component set can behave as a Switch, Checkbox, Slider, or Tabs, after Reka UI's primitives. The behaviour lives in OpenPencil plugin data: boolean values bind to variant or boolean properties with the values meaning on and off, a number keeps its own range since Figma has no number property, and the control's subcomponents bind to the component's slots. A Behaviour section in the properties panel adds, binds, and removes it, each as one undo step, and flags required bindings that are missing. The canvas-only layout's pill becomes a component that preview will reuse. * feat: preview instances with behaviours on the canvas View > Preview (Cmd+Alt+Enter) puts the canvas in preview: a lone canvas switches to the canvas-only layout with a Previewing pill, and a split canvas previews on its own side. Clicking a Switch or Checkbox flips it, dragging a Slider moves its thumb and range, and clicking a Tabs trigger shows its panel. Preview keeps its state on copies of the instances it touched, in a private graph with the document's ids, and the canvas draws those copies in place of the originals, so the document, undo, autosave, and collaborators never see it. Escape or the pill leaves preview, Reset restores every control, and editing shortcuts, labels, and outlines stay off while previewing. * feat: translate behaviour and preview strings; cover preview with an e2e flow * refactor(vue): reuse VariantDefinitionControl for behaviour property options * refactor: split variant actions and preview interactions by domain Variant authoring was one 706-line closure; it is now graph queries (model), undo snapshots (history), property definition edits (definitions), and the editor facade (index). Preview interactions move into play/kinds, one module per control, registered by behaviour kind so a new kind cannot ship without its contract and interaction. Behaviour contracts are keyed by kind. In the Vue SDK, slot and variant authoring controls get their own folders beside component-props and behaviour, and the app's variant section joins slot/ and behaviour/. * refactor: keep the behaviour model in scene-graph's plugin-data registry Master now defines every OpenPencil plugin-data key in one typed registry in scene-graph. The behaviour schema registers there as a field, and the model and contracts move beside slots, exported from the package root; the @open-pencil/core/behaviours subpath is gone. * feat: interaction states and keyboard focus in preview A behaviour can bind a variant property to the default, hover, pressed, focus, and disabled states; binding it maps values named like those states. Preview switches the instance's copy to the matching variant as the pointer hovers, presses, and releases, keeps other values when the set draws the combination and falls back to rest otherwise, and skips disabled instances. Tab moves visible keyboard focus between controls, Space, Enter, arrows, Home, and End use the focused one, and Escape takes visible focus off before leaving preview. A Button kind covers controls that only have states. * feat: toggle, radio, group, progress, collapsible, and accordion behaviours Radio group, toggle group, and accordion hold their items in a slot; each item is an instance with its own behaviour, so a press inside the slot goes to the group, which turns the pressed item on and the others off through the item's own interaction. Progress shares the slider's number handling through rangeControl, and a collapsible shows and hides its content slot from its trigger, remembering its open state even when no property draws it. Tabs and groups share arrow-key navigation. * feat: text field, textarea, and number field behaviours A behaviour value can now be text, bound to a text property, so preview types into a copy of the field through the same property path the editor uses. A bound Filled value switches to the placeholder variant when the field empties. A number field keeps its own range, shows its value through a text property, and steps from its increment and decrement slots and the arrow keys. Text fields show focus from a click, and the focused control receives every key; Option still types, and only Cmd or Ctrl combinations stay shortcuts. * fix: keep behaviour bindings when saving as .fig Saving as .fig gives component properties new GUIDs, but behaviours kept the old ids in their plugin data, so every binding read as missing after reopening. The export now renames the ids behaviours bind with the same GUIDs, on its own copy of the document. * fix: let previewed controls resize layout imported from .fig Layers from a .fig keep the sizes Figma computed, and auto layout prefers them, so an opened collapsible or accordion item kept its closed height in preview. When preview shows, hides, or retypes a layer in a copy, it drops those sizes from the layer's copied ancestors so auto layout sizes them again; untouched layers keep Figma's sizes. * fix: publish behaviours and other plugin content with library assets Every OpenPencil plugin-data field now declares its role: content that exists only as plugin data (behaviours, OkHCL picks), format copies of node fields written for files, or bookkeeping about where a document or node came from. Library snapshots keep a node's content plugin data, including other plugins' entries, and drop the rest; the asset hash counts the same entries, so a behaviour-only change is offered as an update while a .fig round trip still changes nothing. * feat: name behaviour rows by meaning and create what they need The Behaviour section named every main value "Value" under a "Values" heading, and a component without matching properties left an empty picker with no way forward. Rows are now named for the control (On, Checked, Pressed, Text), rows the control needs or already uses come first, and the optional rest folds under More options; a button keeps its states in view. An empty row creates what it needs in one undo step: a text layer and text property, Off and On variants on a set, or a slot frame for a part. The missing chip names the row it means and takes you there. * fix(dom-css): position free layers, hug content, and round ellipses HTML and Tailwind export stacked the layers of frames without auto layout in block flow, wrote fixed pixel sizes for auto layout frames set to Hug and for auto-sizing text, and drew ellipses as boxes. Layers a parent does not lay out are now absolutely positioned at their coordinates inside a relative frame, hugging axes are left to the content, and ellipses get a 50% radius. * feat: run preview as live Reka UI islands over the canvas Preview simulated controls on the canvas: copies of instances, a handler per kind, its own key routing, and append-only text. It now runs them as real components. Each top-level layer that holds an instance with a behaviour becomes an island: its layers are projected to DOM through dom-css into a shadow root laid over the pane at its pan and zoom, and each behaviour mounts its Reka UI primitives on its layers, so text fields are real inputs and focus, keys, and layout are the browser's. Core's resolvePlayState shows instances in a state on a private graph, so the component's variants draw it, and controls are keyed by layer path so a variant switch keeps their DOM. The canvas leaves island layers to the islands, and the canvas play runtime and its key routing are gone. * fix: derive variant properties from Property=Value component names figma.combineAsVariants and Combine as variants only derived variant properties from slash-separated names, so components named as Figma names variants, such as State=On, Size=Large, became a set with no properties. Both now derive each named property and its values, after the slash form. * feat: script and tool access to behaviours by name Behaviour contracts follow Reka UI's anatomy: tabs keep their triggers in the list slot and their content panels in a panels slot, and a slot of repeated parts names the Reka part of its children. A behaviour spec names component properties and slots instead of ids and resolves to the stored behaviour and back, with errors that list what the component has. Scripts get an `openpencil` global next to `figma`, in the Figma API's style: setBehaviour, getBehaviour with bindValue, bindPart, states, and missing, behaviourKinds, and createSlot. The eval tool, the CLI, and app automation compile scripts through one compileScript, so the CLI now returns the last expression as the others do. MCP and AI chat get set_behaviour, get_behaviour, and create_slot. * feat: write controls in design JSX with Reka UI's element names `<Switch.Root modelValue="State">` renders a main component, or a set when its children are variants, that behaves as a switch, and `<Switch.Thumb>` the slot that draws its thumb, one slot across the set's variants. Inputs become the text property of a field, tab triggers and panels go in their List and Panels slots, and a group's items are `<RadioGroup.Item of={…} />` instances in its Items slot. JSX export writes components with behaviours the same way, so they render back unchanged. The authoring reference documents controls, and the codegen and chat prompts now include it verbatim instead of dedenting its code examples. * chore: format the CLI export test * docs: document slots, behaviours, preview, and the openpencil API The components guide covers slots, behaviours, and preview with its shortcut; scripting covers the openpencil global and eval's last- expression result; the MCP and AI chat pages list the new tools; the features overview, README, and roadmap mention working controls. The chat prompt says how to build a control, and the codegen prompt builds components with behaviours on their Reka UI primitives. * chore: format the eval CLI test * docs: explain behaviours and preview islands, and guide the openpencil API A development page explains the behaviour model, the four authoring surfaces, how preview islands turn a control's state into live Reka UI components, and how to add a kind; the architecture page links it. The Core guide sets the rules for OpenPencilAPI: Figma-only `figma`, OpenPencil features on `openpencil` in the same style, one compileScript, names over ids, and docs with every member. Package READMEs mention the openpencil global, PlayIslands, Reka-named JSX, and the behaviour model. Design JSX's behaviour modules move into a behaviours folder instead of a suffixed sibling. * refactor: center pasted layers through translate centerNodesAt repeated translate's loop, which test:dupes reports on master too. * fix: validate behaviour ranges and guess on and off by name A number value now needs max above min and a positive step: the schema, specs, and the panel reject a range a slider cannot step through. Binding a variant property guesses on and off by value name, as specs do, and a boolean property gets no on/off pair. Part bindings are read through partBinding, a replaced document restarts preview from its designed state, and the e2e preview shortcut uses ControlOrMeta. * feat: make the Behaviour section say what to do next A slider's range fields now carry inline Min, Max, Step, and Start labels. States offers Add state variants, which adds a Default, Hover, Pressed, Focus, and Disabled variant and binds them; Add Off and On variants and Add state variants turn a lone main component into a component set first, and a part's slot can be added to a set, in every variant under one slot id. Rows that could do nothing are gone: no empty pickers and no hints to combine variants by hand, and an unbound Disabled is left to the states. A warning line names what is still needed and replaces the missing chip, and the Switch's main value is called Checked. * fix: keep each slot to one part and keep creating slots at hand A slot draws one part, so the Behaviour section no longer offers a slot another part uses, and specs (the openpencil API, tools, and JSX) reject binding one slot to two parts. A part's picker keeps an action to add a new slot in its footer, so adding the first slot no longer hides it for the other parts.
2026-10-06 13:23:05 +00:00
- **Components** — create (<kbd>⌥</kbd><kbd>⌘</kbd><kbd>K</kbd>), component sets (<kbd>⇧</kbd><kbd>⌘</kbd><kbd>K</kbd>), instances with override support, slots, live sync
- **Behaviours and preview** — make components work as Reka UI controls (switch, slider, tabs, text field, accordion, …) and try them live over the canvas with <kbd>⌥</kbd><kbd>⌘</kbd><kbd>↩</kbd> ([Components](/user-guide/components#behaviours-and-preview))
- **Variables** — design tokens with collections, modes (Light/Dark), color/float/string/boolean types, variable binding
- **Sections** — organizational containers with auto-adopting children and title pills
## Properties Panel
feat: check designs live with a Lint panel, canvas markers, and fixes (#804) * feat: check designs live with a Check panel and canvas issue markers Design lint only ran from the CLI and AI tools, and its rules were too noisy to show continuously: on a real imported page 786 of 888 layers had a warning. The rules now report where a finding is actionable (a hardcoded color only when a variable matches it, nesting only where the limit is crossed, instance sublayers through their main component) and carry structured data, and Recommended keeps warnings for likely problems. The app checks the current page after edits settle. The Check tab groups issues by rule with hover highlighting, reveal on click, and one-step variable binding. Errors and warnings are marked on the canvas with clustered markers that roll up to visible ancestors when zoomed out; markers explain themselves on hover, open Check on click, and toggle with View > Design issues. * fix: keep the right panel and markers stable The Check tab made the right-panel tab row overflow at common window widths, so focusing the zoom menu scrolled the row and shifted the panel. Code and AI tabs now drop their labels to screen readers when the row is narrow. Touch target names are matched as whole words: "Rectangle" contained "cta" and marked every rectangle. Markers also stay drawn during interactive edits instead of blinking while a value is scrubbed. * fix(ui): show right panel tab labels whenever they fit * fix(ui): name the design check tab Lint and keep panel tabs consistent The tab was an unlabelled icon between labelled Code and AI tabs. It is now Lint, with the same icon and label anatomy as its neighbours, and its icon takes the severity color instead of a count badge. All labelled tabs show their labels when the row fits and drop them together when it does not. * refactor(ui): build the Lint panel from shared components Issue groups use AppCollapsible, actions use AppButton, and the severity filters are a Reka toggle group with keyboard navigation. Issue rows no longer nest a button inside a button. Panel state, visibility and the focused-issue scroll live in useDesignCheckPanel, the rules menu is its own component, and rule preferences change through preference actions. Severity ordering reuses Core's ranking, detail numbers follow the app language, and the check debounce uses useTimeoutFn. * fix(lint): check the WCAG AA touch target size in the Recommended preset Recommended flagged a 394 × 39 input because it required the 44 × 44 AAA size. It now checks the 24 × 24 AA minimum through a minSize option; Strict and Accessibility keep 44 × 44. * feat(lint): fix design issues from rules, the Lint panel, the CLI, and agents Rules attach fixes as data: a safe fix keeps the design as it looks (bind a color to the variable it matches, round subpixel geometry that layout does not own), a suggestion changes values (snap radius and spacing to the scale, raise small text to the minimum). One Core applier re-validates each fix against the current graph and merges changes per layer. The Lint panel offers a fix per row and Fix all for safe fixes as one undo step; openpencil lint --fix writes the fixed document; the lint and lint_fix tools expose the same to MCP and AI chat. The design-check spec's Close button is now 24 x 20: at 24 x 24 it passes the WCAG AA touch target size that Recommended checks. * feat(lint): pin issues outside the view to the canvas edge Errors and warnings on layers outside the viewport had no marker, so a check could report issues nobody could see. They are now pinned to the canvas edge where a ray from the viewport center toward them leaves it, with a chevron pointing their way; pins in one direction merge like markers. Hovering lists them under the direction they lie in, and clicking reveals and opens the most severe, nearest one. Pins keep clear of UI floating over the canvas: the toolbar marks itself with data-canvas-obstacle, and canvases report such rectangles to the renderer through getOverlayObstacles each frame. * feat(lint): mark layers with design issues in the Layers panel Like an IDE marks files with problems and the folders holding them, a layer with errors or warnings shows the most severe as an icon, and a collapsed layer with issues inside it shows a dot in that color. Suggestions stay in the Lint panel, as on the canvas, and the marks follow the View → Design issues toggle. * feat(lint): show issues per page and across the document Loaded pages beyond the current one are now checked in the background, one page at a time while the editor is idle, and checked again only when an edit touches them; pages a large .fig file has not loaded are left alone until opened rather than forced in. The page list shows each page's errors and warnings like an IDE's problem count, and the Lint panel gains a Document scope that lists every page's issues, tags the ones on other pages, and switches to a row's page when it is opened. * test(lint): use the core-tests alias and no comma operator in lint tests Master now rejects ../../ imports and the comma operator in tests. * refactor(app): create the Lint session with the editor store modules The composition root passed its line budget once master added recent pages; the Lint session belongs with the other per-editor services that the modules factory creates and disposes. * docs(changelog): keep master's latest Unreleased entries
2026-10-04 10:08:39 +00:00
Context-sensitive Design | Code | AI | Lint tabs:
- **Appearance** — opacity, corner radius (uniform or per-corner), visibility
- **Fill** — solid, gradient (linear/radial/angular/diamond), image
- **Stroke** — color, weight, align (inside/center/outside), per-side weights, cap, join, dash
- **Effects** — drop shadow, inner shadow, layer blur, background blur, foreground blur
- **Typography** — font picker with virtual scroll and search, weight, size, alignment, style buttons
- **Layout** — auto-layout controls when enabled
- **Export** — scale, format (PNG/JPG/WEBP/SVG), live preview
feat(code): link the Code tab to canvas layers and sync both ways (#805) * feat(code): link code to canvas layers and underline design issues Code in the Code tab and layers on the canvas were unrelated: finding the element behind a layer, or the layer behind an element, meant reading names. Generated Design JSX and Tailwind JSX report the layer behind each element in the order elements open, and edited Design JSX keeps the source line of every element through the sandbox and renderer, so hovering an element highlights its layer, Cmd/Ctrl-click brings it into view without changing the selection the code shows, and errors and warnings from the design check are underlined on the property that causes them. * feat(code): explain the Code tab when nothing is selected With no selection the editor showed a starter frame that read like a real layer. The tab now says it shows the selected layers' code and offers Write JSX, which opens the editor focused on the starter template. * refactor(code): group code-to-layer linking into its own domain Layer link types, issue mapping, and the hover and reveal behavior move from the Code panel and a component file into src/app/code/layers, with useCodeLayers as the panel's entry point, so app code no longer imports types from components. * fix(code): underline off-scale gaps after the spacing rule renamed its property * feat(code): mark the layer of the element around the cursor Hover highlighting and ⌘-click reveal replaced by one model: the element around the cursor marks its opening and closing tag names and outlines its layer on the canvas while the editor has focus. ⌘-click also collided with CodeMirror's add-a-cursor gesture. Read-only Tailwind JSX now takes a cursor so it links the same way. Leaving the editor now ends a live Design JSX edit as one undo step. Before, canvas edits made after typing never reached the code until the tab was reopened, and their undo entries landed before the edit's. * feat(code): sync the Code tab and the canvas both ways by patching Canvas edits now patch the Design JSX a person wrote instead of waiting for them to leave the editor: each linked element remembers the layer as Design JSX last wrote it, and a canvas change rewrites only the attributes, text and child elements that differ from that base, as CodeMirror changes that keep the cursor, comments, formatting and history. Attributes written as expressions are never overwritten; the code marks them when the canvas now differs. Untouched code is regenerated with a minimal text change. Code edits update layers in place: the new render is reconciled into the existing layers (reconcileRenderedLayers), which keep their ids, so links, selection and canvas edits survive typing. Each edit is one coalesced undo step, replacing the restore-and-rerender preview and the commit on blur. * feat(code): patch reordered layers and aliased properties in edited code Reordering layers on the canvas now moves their elements in code a person wrote: each child element and the blank lines and comments above it form a block kept as written, and the children are written again in the new order, staying linked. Children that cannot move safely, such as a loop between them, keep their order and are marked. Properties accepted under several names now come from one alias table in the Design JSX schema, which the renderer resolves through and the patcher and issue underlines use, so a canvas change to `w` patches `width` where the person wrote that, instead of adding a second attribute. * feat(code): keep the cursor in moved code and patch values written in style A reorder rewrites the children span in one change, which collapsed a cursor or out-of-sync marker inside a moved element to the span's edge. The patch now carries where each block moved and places selections and markers inside it at their new position. Properties the renderer also reads from style={{ … }} come from a table in the Design JSX schema instead of a hand-written list, keeping the rule that an attribute under any of its names wins. The patcher uses it to update a value written in style where it is, as a number or a px string as written; values the renderer cannot read, such as '50%', are marked. The layer patcher is split by concern: syntax helpers, attribute and style patches, child patches, out-of-sync state and transaction assembly. * feat(code): show the code's layer on the canvas as a tinted box The layer of the element around the cursor used the canvas hover slot, so moving the pointer over the canvas replaced it and the two read the same. It now has its own shared editor state, codeFocusNodeId, drawn as the hover outline over a light tint in every pane: hover stays an outline and the selection keeps its handles, without borrowing the dashed outlines that already mean component sets, drag parents and ghosts. * fix(code): write added and removed layers when a reorder cannot move the code When children could not be moved, such as two written on one line, the patch marked the order and returned before adding or removing elements, so a layer created in the same change never reached the code. It now marks the order and still writes additions and removals. * refactor(design-jsx): format the rebased layer description and stroke aliases * feat(code): mount the layer-linked code editor through useCodeMirror Master moved the code editor onto the shared useCodeMirror composable. Its layer links, issue underlines, canvas patches, minimal text updates, autofocus and read-only cursor now sit on that composable instead of a hand-mounted view.
2026-10-04 10:08:40 +00:00
- **Code** — Design JSX and Tailwind JSX for the selection, with live two-way Design JSX editing that patches your code as layers change; the element around the cursor outlines its layer on the canvas, and design issues are underlined on the property that causes them
feat: check designs live with a Lint panel, canvas markers, and fixes (#804) * feat: check designs live with a Check panel and canvas issue markers Design lint only ran from the CLI and AI tools, and its rules were too noisy to show continuously: on a real imported page 786 of 888 layers had a warning. The rules now report where a finding is actionable (a hardcoded color only when a variable matches it, nesting only where the limit is crossed, instance sublayers through their main component) and carry structured data, and Recommended keeps warnings for likely problems. The app checks the current page after edits settle. The Check tab groups issues by rule with hover highlighting, reveal on click, and one-step variable binding. Errors and warnings are marked on the canvas with clustered markers that roll up to visible ancestors when zoomed out; markers explain themselves on hover, open Check on click, and toggle with View > Design issues. * fix: keep the right panel and markers stable The Check tab made the right-panel tab row overflow at common window widths, so focusing the zoom menu scrolled the row and shifted the panel. Code and AI tabs now drop their labels to screen readers when the row is narrow. Touch target names are matched as whole words: "Rectangle" contained "cta" and marked every rectangle. Markers also stay drawn during interactive edits instead of blinking while a value is scrubbed. * fix(ui): show right panel tab labels whenever they fit * fix(ui): name the design check tab Lint and keep panel tabs consistent The tab was an unlabelled icon between labelled Code and AI tabs. It is now Lint, with the same icon and label anatomy as its neighbours, and its icon takes the severity color instead of a count badge. All labelled tabs show their labels when the row fits and drop them together when it does not. * refactor(ui): build the Lint panel from shared components Issue groups use AppCollapsible, actions use AppButton, and the severity filters are a Reka toggle group with keyboard navigation. Issue rows no longer nest a button inside a button. Panel state, visibility and the focused-issue scroll live in useDesignCheckPanel, the rules menu is its own component, and rule preferences change through preference actions. Severity ordering reuses Core's ranking, detail numbers follow the app language, and the check debounce uses useTimeoutFn. * fix(lint): check the WCAG AA touch target size in the Recommended preset Recommended flagged a 394 × 39 input because it required the 44 × 44 AAA size. It now checks the 24 × 24 AA minimum through a minSize option; Strict and Accessibility keep 44 × 44. * feat(lint): fix design issues from rules, the Lint panel, the CLI, and agents Rules attach fixes as data: a safe fix keeps the design as it looks (bind a color to the variable it matches, round subpixel geometry that layout does not own), a suggestion changes values (snap radius and spacing to the scale, raise small text to the minimum). One Core applier re-validates each fix against the current graph and merges changes per layer. The Lint panel offers a fix per row and Fix all for safe fixes as one undo step; openpencil lint --fix writes the fixed document; the lint and lint_fix tools expose the same to MCP and AI chat. The design-check spec's Close button is now 24 x 20: at 24 x 24 it passes the WCAG AA touch target size that Recommended checks. * feat(lint): pin issues outside the view to the canvas edge Errors and warnings on layers outside the viewport had no marker, so a check could report issues nobody could see. They are now pinned to the canvas edge where a ray from the viewport center toward them leaves it, with a chevron pointing their way; pins in one direction merge like markers. Hovering lists them under the direction they lie in, and clicking reveals and opens the most severe, nearest one. Pins keep clear of UI floating over the canvas: the toolbar marks itself with data-canvas-obstacle, and canvases report such rectangles to the renderer through getOverlayObstacles each frame. * feat(lint): mark layers with design issues in the Layers panel Like an IDE marks files with problems and the folders holding them, a layer with errors or warnings shows the most severe as an icon, and a collapsed layer with issues inside it shows a dot in that color. Suggestions stay in the Lint panel, as on the canvas, and the marks follow the View → Design issues toggle. * feat(lint): show issues per page and across the document Loaded pages beyond the current one are now checked in the background, one page at a time while the editor is idle, and checked again only when an edit touches them; pages a large .fig file has not loaded are left alone until opened rather than forced in. The page list shows each page's errors and warnings like an IDE's problem count, and the Lint panel gains a Document scope that lists every page's issues, tags the ones on other pages, and switches to a row's page when it is opened. * test(lint): use the core-tests alias and no comma operator in lint tests Master now rejects ../../ imports and the comma operator in tests. * refactor(app): create the Lint session with the editor store modules The composition root passed its line budget once master added recent pages; the Lint session belongs with the other per-editor services that the modules factory creates and disposes. * docs(changelog): keep master's latest Unreleased entries
2026-10-04 10:08:39 +00:00
- **Lint** — live design lint for the page or selection: low contrast, small touch targets, unbound colors, off-scale spacing, with issue markers on the canvas and one-click variable binding ([Checking Designs](/user-guide/checking-designs))
## Rendering
Skia (CanvasKit WASM) — the same rendering engine as Figma:
- Gradient fills (linear, radial, angular, diamond)
- Image fills with scale modes
- Effects with per-node caching
- Arc data (partial ellipses, donuts)
- Viewport culling and paint reuse
- Snap guides with rotation-aware alignment
- Canvas rulers with selection badges
- Hover highlight that follows actual geometry
## Undo/Redo
Every operation is undoable — creation, deletion, moves, resizes, property changes, reparenting, layout changes, variable operations. Uses an inverse-command pattern. <kbd>⌘</kbd><kbd>Z</kbd> / <kbd>⇧</kbd><kbd>⌘</kbd><kbd>Z</kbd>.
## Multi-Page Documents
Add, delete, rename pages. Each page has independent viewport state. Double-click to rename inline.
## Multi-File Tabs
Open multiple documents in tabs. <kbd>⌘</kbd><kbd>T</kbd> new tab, <kbd>⌘</kbd><kbd>W</kbd> close, <kbd>⌘</kbd><kbd>O</kbd> open file.
## Export
- **Image** — PNG, JPG, WEBP at configurable scale (0.5×–4×). Via panel, context menu, or <kbd>⇧</kbd><kbd>⌘</kbd><kbd>E</kbd>
- **SVG** — shapes, text with style runs, gradients, effects, blend modes
- **Tailwind JSX** — HTML with Tailwind v4 utility classes, ready for React or Vue
- **Copy as** — text, SVG, PNG (<kbd>⇧</kbd><kbd>⌘</kbd><kbd>C</kbd>), or JSX via context menu
CLI: `openpencil export design.fig -f jsx --style tailwind`
## AI Chat
2026-03-09 14:43:11 +00:00
Press <kbd>⌘</kbd><kbd>J</kbd> to open the AI assistant. 90+ tools that can create shapes, set styles, manage layout, work with components and variables, run boolean operations, analyze design tokens, and export assets. Connect Anthropic, OpenAI, Google AI, OpenRouter, or any compatible endpoint.
2026-03-09 14:43:11 +00:00
Tool calls display as collapsible timeline entries. Visual verification — the assistant renders its work and checks it against your request. Full undo support for all AI mutations.
See [AI Chat](/programmable/ai-chat) for setup and provider details.
## MCP Server
2026-03-09 14:43:11 +00:00
Connect Claude Code, Cursor, Windsurf, or any MCP client to read and write `.fig` files headlessly. 90+ tools. Two transports: stdio and HTTP.
```sh
npm install -g @open-pencil/mcp
```
```json
{
"mcpServers": {
"open-pencil": {
"command": "openpencil-mcp"
}
}
}
```
See [MCP Tools reference](/programmable/mcp-server) for the full tool list.
## CLI
Inspect, export, and analyze `.fig` files from the terminal:
```sh
openpencil tree design.fig # Node tree
openpencil find design.fig --type TEXT # Search
openpencil export design.fig -f png # Render
openpencil analyze colors design.fig # Color audit
openpencil analyze clusters design.fig # Repeated patterns
openpencil eval design.fig -c "..." # Figma Plugin API
```
When the desktop app is running, omit the file to control the live editor via RPC:
```sh
openpencil tree # Live document
openpencil export -f png # Screenshot canvas
```
All commands support `--json`. Install: `npm install -g @open-pencil/cli` (or `bun add -g @open-pencil/cli`).
## Real-Time Collaboration
P2P via WebRTC — no server required. Share a link and edit together.
- Live cursors with colored arrows and name pills
- Presence avatars
- Follow mode — click a peer to follow their viewport
- Local persistence via IndexedDB
- Secure room IDs via `crypto.getRandomValues()`
## Desktop & Web
**Desktop** — Tauri v2, ~7 MB. macOS (signed & notarized), Windows, Linux. Native menus, offline, autosave.
**Web** — runs at [app.openpencil.dev](https://app.openpencil.dev), installable as a PWA on mobile with touch-optimized UI.
**Homebrew:**
```sh
brew install --cask openpencil
```
## Google Fonts Fallback
When a font isn't available locally, OpenPencil fetches it from Google Fonts automatically. No manual installation needed when opening .fig files with unfamiliar fonts.