Commit graph

27 commits

Author SHA1 Message Date
Danila Poyarkov b93ffea073
feat: add Figma-style variants and component libraries (#512)
Closes #239
2026-08-14 19:31:41 +03:00
Danila Poyarkov 80750e65fc fix(ui): align layout sections with Figma 2026-07-26 10:52:48 +03:00
Luca Candela 18899ff13c
feat(editor): add Figma-style frame presets (#418)
Add frame preset creation and resizing with constraint-aware layout, undo/redo, toolbar flyouts, localization, documentation, and regression coverage.
2026-07-26 00:05:57 +03:00
Danila Poyarkov d8952eb515 refactor(ui): replace sizing design tokens with plain utilities
Drop the spacing/radius sizing tokens (--spacing-control, --spacing-panel-*,
--radius-panel) and their generated classes (h-control, px-panel-x, gap-panel,
rounded-panel, etc.) in favor of direct Tailwind utilities (h-6, px-3, gap-1.5,
rounded). Color tokens are kept since they drive dark/light theming.

Behavior-preserving: 24px controls, 5px radius, identical computed layout.
2026-07-18 08:35:32 +03:00
Danila Poyarkov 23645d5a2d feat(editor): add component property controls
- Model typed component definitions, references, assignments, and preferred swap values

- Add SDK-owned variant, text, boolean, and nested instance-swap controls with mixed-selection undo

- Preserve assignments across variant changes and component synchronization

- Round-trip component property metadata through .fig import and export
2026-07-17 21:59:49 +03:00
Danila Poyarkov d19c05f48e feat(editor): add resize constraints
- Add SDK-owned mixed constraint state, pin controls, and undo-aware updates
- Apply Figma constraint modes recursively during frame resize previews and commits
- Document and test the control, multi-selection behavior, and resize geometry
2026-07-17 18:32:58 +03:00
Danila Poyarkov 27fdef2ea4 feat(app): finish Design panel migration
- Rebuild Typography, Variant, Mask, and Page sections with shared labeled fields
- Expose semantic empty states across list sections
- Replace section test hooks with accessible roles and add focused visual coverage
2026-07-14 17:40:56 +03:00
Danila Poyarkov 12031b76c9 feat(app): migrate position and appearance panels
- Move independent-corner presentation and batched mutations into AppearanceControls

- Rebuild Position and Appearance with aligned panel grids and semantic locators

- Replace type text headers with compact node icons and add visual coverage
2026-07-13 14:47:48 +03:00
Danila Poyarkov 0aae91f4f2 feat(editor): add blend mode and mask controls
- Add selection mask commands, shortcuts, context-menu access, and mask type controls
- Add mixed-selection blend mode editing with single-step undo
- Document the headless mask composable and localize the new controls
2026-07-10 17:01:28 +03:00
Danila Poyarkov c1207f1be3 refactor(ui): organize app component domains
- Move root-level picker, input, and menu components into lowercase domain folders
- Update component namespace linting for kebab-case app domains
- Document FSD-lite component structure in AGENTS.md
2026-07-01 09:58:10 +03:00
rcoenen 33f53324f7
feat(export): persist per-node export settings
* fix(ui): anchor AppSelect dropdown to its trigger

AppSelect wraps its SelectTrigger inside <Tip> (a TooltipTrigger
as-child). reka-ui's Select popper captures its trigger element via
useForwardExpose on mount, but that capture resolves to null when the
trigger sits inside another primitive's as-child slot. With no anchor,
floating-ui positioned the menu at the viewport origin and flipped it
off-screen (~y:-412), so clicking the dropdown appeared to do nothing.

Wrap the trigger in a layout-neutral span, keep the styled <Tip>
tooltip, and pass that span to SelectContent's `reference` prop so the
popper anchors explicitly on every open (PopperContent prefers
props.reference over the broken auto-capture, and re-reads it on each
open/reopen).

Fixes every dropdown built on AppSelect across the inspector (export
scale/format, typography, effects, stroke, flex/grid layout, variants,
fills, gradients, color format).

Verified live: menu opens directly below the trigger, fully on-screen,
across repeated open/select/reopen cycles; tooltip still shows.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(export): editable custom scale with 1024x cap

The export scale was a preset-only dropdown (0.5x–4x). Make it Figma-like:
an editable field where you can type any multiplier (e.g. 9x, 1.5x) in
addition to picking a preset from the chevron menu. Custom values are used
as-is and are never added to the preset list.

Typed input is clamped to [0.01x, 1024x] — an unbounded multiplier would
allocate an enormous canvas and crash the renderer. The clamp lives in the
export data model (clampExportScale, applied in updateScale) so it defends
every caller, and is reused by the input for immediate display feedback
(9999999 -> 1024x, 0.0000001 -> 0.01x). Invalid/zero input reverts.

New ExportScaleInput.vue pairs a text input with a reka DropdownMenu for
presets (mirrors ZoomDropdown; not wrapped in <Tip>, so it positions
correctly). The active preset shows a checkmark.

Verified live across custom entry, clamping (both bounds), decimals,
invalid input, and preset selection.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(export): store export settings per node

Export settings were global and transient: every selection showed a
default 1x PNG row, and nothing was remembered per layer or persisted
with the document.

Store export settings on each SceneNode (exportSettings: ExportSetting[]),
defaulting to empty so the panel shows only its header and add button
until the user adds a row. Settings apply across multi-select and target
the current page when nothing is selected; add/edit/remove are undoable.

Persist settings with the document via open-pencil pluginData in .fig
(lossless, including webp). On import, prefer app pluginData; otherwise
map native Figma exportSettings (PNG/JPEG/SVG/PDF + content scale) without
overwriting raw native fields on re-export.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(export): working format dropdown, JPG/WEBP export, and zip bundling

- Anchor AppSelect dropdown via native title so the format selector is
  clickable in multi-row export panels (was rendering off-screen)
- Add a browser-canvas encode fallback on the renderer for JPG/WEBP, which
  CanvasKit's encodeToBytes returns null for in this build (fixes the
  "Nothing to export" error)
- Bundle multi-format exports into a single zip; a single export still
  downloads the file directly
- Default each added export row to 2x the previous scale (1x -> 2x -> 4x)
- Add e2e coverage for the multi-row dropdown, zip bundling, and direct
  single-file download

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* revert(export): drop AppSelect native-title workaround, superseded by #325

The shared <Tip> path is now handled generally by #325; #321 should not carry
the AppSelect change. Reverts src/components/ui/AppSelect.vue to master.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(export): address review feedback (#321)

- Clear raw native exportSettings when the user edits/clears export rows so they
  don't resurrect from the import fallback on reopen (scene-graph/source-metadata).
- Clamp export scale at the .fig import boundary (plugin + native CONTENT_SCALE),
  not just in the UI; centralize the bounds in core/scene-graph/export-scale and
  reuse them from the vue helpers (single source of truth).
- Export the rows the panel shows (activeSettings) for every target so a
  multi-selection is WYSIWYG — no hidden rows export, and the "mixed" notice shows
  whenever targets diverge.
- Sanitize zip entry names (strip separators, parent refs, control chars) so layer
  names can't escape or corrupt the archive.
- Verify toDataURL() honored the requested MIME in the JPEG/WEBP fallback; reject a
  silent PNG so we never write PNG bytes under a .jpg/.webp extension.

Add regression tests for the native-settings clear and import-scale clamp.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 20:05:34 +03:00
Danila Poyarkov 104114cb3f feat(ui): add boolean operations panel control 2026-05-17 17:44:48 +03:00
Danila Poyarkov c79c9106fb feat(components): add variant inspector UI
Show variant property dropdowns in the design panel when an instance
of a component set is selected. Switching a dropdown value finds the
matching variant component and updates the instance's componentId.

- VariantSection.vue with property dropdowns using AppSelect
- Bridge variant actions to the editor return object
- i18n for en, ru, zh-CN

Ref #239
2026-05-07 19:48:59 +03:00
Danila Poyarkov 6545f20c53
Refactor architecture boundaries across core, app, and packages (#234)
* refactor(core): decompose editor factory and action modules

Split the monolithic editor factory and large action modules into focused
domain helpers:

- create.ts assembles context through bridge modules (clipboard,
  components, structure, undo) and delegates to graph-reads, graph-events,
  layout-runner, component-sync, and state factory
- structure.ts delegates to group, container-wrap, auto-layout-wrap,
  reorder, and state-toggle helpers
- selection.ts delegates to hit-test, overlays, container navigation,
  and read helpers
- clipboard.ts delegates to subtree-history, images, export, copy,
  fonts, and placement helpers
- shapes.ts delegates to pen actions and section-adopt
- components.ts delegates to focus and instances helpers
- alignment.ts delegates to flip-rotate helper
- text.ts uses explicit TextEditSession for snapshot comparison

New focused modules: nudge, variable-bindings, layout-mode,
page-viewports, tool-registry, color-space

Undo: history/position and history/snapshot helpers, hardened
batch/rollback with nested batch support and configurable limit

* refactor(core): split tool definitions by domain

Split the monolithic tool registry into domain-specific modules:

- read/ — selection, find, pages, fonts, components, nodes, query, jsx
- create/ — basic shapes, components, vector, JSX render
- modify/ — paint, effects, geometry, layout, state, text, update
- structure/ — basic, arrange, batch, hierarchy, replace, tree
- variables/ — bindings, collections, read, values
- vector/ — boolean, path, export, viewport
- analyze/ — colors, typography, spacing, clusters, diff, eval
- describe/ — summaries, tree, roles, layout-issues
- stock-photo/ — providers, requests, apply
- codegen/ — component-map, tokens

Split registry into core/extended tiers; refine schema and AI adapter

* refactor(core): restructure kiwi codec and instance overrides

Reorganize the Kiwi .fig codec into domain subdirectories:

- binary/ — codec, schema, protocol
- fig/ — file, import, parse (core, worker, transfer)
- node-change/ — convert, export-node, serialize, plugin-data
- instance-overrides/ — constraints, dsd, populate, props, resolve,
  symbol-overrides, symbol-props, sync, types

Vendored kiwi-schema/ left isolated

* refactor(core): split profiler, icons, IO, and add subpath exports

Profiler: speedscope-export, capture-session, hud-controller
Icons: api, svg, types, render, create-icons tool
IO: format registry and subpath exports
Canvas/color/text/vector: targeted cleanup

Add deliberate subpath exports: random, xpath, vector, color, canvas,
scene-graph, kiwi, design-jsx, io, tools, editor, layout, canvaskit,
profiler, text, lint, rpc, figma-api, constants

* refactor(vue): decompose canvas input, surface lifecycle, and controls

Canvas surface: gl-surface, kit-loader, render-loop, resize-observer

Canvas input handlers:
- move: drop-target, move-snap, duplicate-drag
- select: select-move, select-hover, select-hit
- resize: resize-rect, resize-vector, resize-start
- transform: rotation, marquee, pan, text-selection
- text-edit: navigation, clipboard, textarea lifecycle
- Shared: click-count, space-key, pan, pan-zoom, draw, raf-scheduler

Editor composition:
- commands split: actions, context, metadata, edit, selection, view
- menu-model split: command-groups, builders, types
- Gradient stop composable reuse in primitive root

Controls: fill, layout, typography, appearance, effects, stroke,
okhcl, prop-scrub, node-props, undo-batch, color-variable-binding
Variables/i18n/document/export helpers

Organize canvas, primitives, controls, editor, and variables into
cohesive module directories with package-local import aliases

Expose MenuActionNode/MenuSeparatorNode from public API

* refactor(app): split document IO, editor session, and automation bridge

Document IO: source-state, naming, writer, reload-source, reload-state,
imported-document, watch-targets, save-targets

Editor session: create, modules, types, accessors, computed, refs
Editor canvas: loader-overlay, collaboration-awareness,
context-selection, menu-actions, menu-model

Automation bridge: eval, tools, exports, files, selection, RPC fallback

AI/ACP: transport, map-update, permission, debug, chat effects/storage
Collab: awareness, graph-bindings, yjs-sync, follow, session, types
Shell keyboard: actions, bindings, clipboard, focus, nudging,
raw-events, registry, reserved, shortcuts, space-tool
Shell menu: app-menu, document-name, entry, files

Demo: colors, effects, helpers, section builders (components,
app-preview, effects, standalone, variables) — document.ts reduced
from 981 to 32 lines as pure orchestrator

Move app modules under src/app/ with organized domain structure:
editor, document, ai, collab, shell, automation, demo, tabs

* refactor(app): decompose UI components with provide/inject context

Split monolithic components using Reka UI-inspired namespace folders
with scoped provide/inject context — no prop drilling:

- CollabPanel/ — context, avatars, share, connected, join
- ColorPickerPanel/ — context, area, format, field groups, sliders
- MobileHud/ — context, action toast, tool badge, file menu, presence
- ProviderSettings/ — context, API key/type, endpoint, tokens, photos
- Toolbar/ — actions, types, desktop, mobile, tool button, flyout
- LayoutSection/ — types, auto-layout, flex, grid, padding, size, clip

Properties helpers: fill-okhcl adapter, fill-label, color-style-row
Menu: entry helpers, document-name rename, stale type removal

* refactor(mcp): split server into focused modules

- browser-rpc — WebSocket client management
- mcp-sessions — session lifecycle
- tool-output — response formatting
- tool-schema — Zod schema generation from ToolDefs
- jsx-preprocess — JSX source transformation
- result — result helpers
- tool-registration — MCP tool wiring
- auth — API key validation
- http-options — CORS/request handling
- stdio-bridge — stdio transport adapter

* refactor(cli): split analyze subcommands and shared helpers

- Analyze subcommands: clusters, colors, spacing, typography
- RPC data loading helper
- Migrate imports to targeted core subpath exports

* refactor(docs): split VitePress config and shared table component

Config helpers: sdk-sidebar, seo, labels, sidebars, locale-theme,
root-theme, locales

Shared SdkDataTable component replaces duplicated table markup in
SdkPropsTable, SdkEventsTable, and SdkSlotsTable

Update contributing and testing docs

* refactor(tauri): decompose desktop entrypoint

Split lib.rs into focused service modules:

- fig_container.rs — .fig archive/compression commands
- fonts.rs — font cache and system font enumeration
- menu.rs — native menu construction
- menu_events.rs — menu event dispatch and devtools toggle
- window.rs — main window show/focus lifecycle

* test: share domain test factories and migrate fixtures

New shared helpers:
- tests/helpers/scene.ts — makeSceneGraph factory
- tests/helpers/vector-network.ts — vertex/segment/network builders
- tests/helpers/fig-traversal.ts — all-node collection, type counts
- tests/helpers/undo.ts — undo test utilities
- tests/helpers/editor-history.ts — editor history test helpers

Migrate render, vector, fig-roundtrip, and undo tests to use shared
factories instead of inline fixture construction

* build: add structural lint rules, split vite config, update docs

Structural lint (oxlint.structure.json + lint/plugin.js):
- 20+ custom rules enforcing package boundaries, lifecycle patterns,
  naming conventions, and import discipline

Vite config split: raw-markdown, canvaskit-assets, pwa, server,
aliases, automation plugins

Remove legacy shims and utils superseded by SDK/core modules
Update AGENTS.md, CONTRIBUTING.md, eval-command docs, tsconfig

* fix(vue): normalize canvas directory casing and remove duplicate export

- Rename Canvas/ to canvas/ in git index to match #vue/canvas/* imports
  (PascalCase was correct for component primitives but canvas/ is a
  non-component domain directory)
- Remove duplicate ./random subpath export in core package.json

* fix: add #vue and #core Vite resolve aliases for dev server

* refactor(core): reduce remaining large modules

Split the remaining large core hotspots into cohesive domain modules while preserving public facades and behavior.

- Extract scene graph types, variables, node defaults, and vector-network helpers
- Decompose canvas renderer orchestration, state, paints, colors, lifecycle, labels, and delegated domain methods into renderer/ and labels/ subfolders
- Split Kiwi node-change, binary variable binding, layout, RPC, vector, JSX export, clipboard, design JSX, and Figma proxy helpers
- Replace collision-driven *Fn import aliases with namespace imports and enforce the pattern in lint

Validation:
- bun run check
- bun --filter @open-pencil/vue build
- bun run test:dupes

* fix(app): forward color input attrs

* fix(app): cover section drawing errors

* fix(editor): undo option-drag duplicates

* docs: document domain subfolder convention

* fix(app): handle undo redo on keydown

* refactor(app): dispatch shortcuts from keydown

* refactor: group prefixed domain modules

* refactor(app): use tinykeys for shortcuts

* refactor(core): group symbol override modules

* refactor(core): group fig kiwi container helper

* refactor(canvas): split overlay rendering modules

* refactor(vue): remove unused internal barrels

* fix(app): lay out demo components before instancing

* fix(app): restore demo badge spacing

* perf(canvas): split scene and overlay rendering

* refactor(vue): wrap wheel gesture lifecycle

* fix(canvas): wait for fonts before hiding loader

* docs: update unreleased changelog
2026-04-30 15:14:19 +03:00
Danila Poyarkov 581abf3b93 Fix export panel target handling 2026-03-29 19:16:45 +03:00
Ilya Nikitin 0ff5a87ccb update pen tool space drag spec
Adds a new scenario to the pen tool specifications defining the behavior for relocating the active vertex with spacebar during a curve drag, including asserting that the cursor remains a crosshair instead of switching to the hand tool.

Made-with: Cursor
2026-03-27 21:27:45 +05:00
Danila Poyarkov 77032ab092 Translate visible panel chrome for Russian locale 2026-03-25 18:12:09 +03:00
Danila Poyarkov 0c30d044b2 Extract Vue SDK commands and menu model 2026-03-24 09:50:17 +03:00
Danila Poyarkov 267f251f8a Phase 7: Migrate 10 components from useEditorStore to useEditor
Components now using useEditor() from SDK instead of app's useEditorStore():
- CodePanel, DesignPanel, ImageFillPicker, LayerTree, PagesPanel,
  PropertiesPanel, VariablesDialog, FillSection, PageSection,
  VariablesSection

7 components remain on useEditorStore for app-specific methods:
- AppMenu, Toolbar, MobileHud (saveFigFile, exportSelection, mobile*)
- CanvasMenu (renderExportImage)
- EditorCanvas (cursorCanvasX/Y app state)
- MobileDrawer (mobileDrawerSnap app state)
- ExportSection (exportSelection, renderExportImage)
2026-03-17 10:38:40 +03:00
Danila Poyarkov 8d7dd0a36b Add random.ts helpers, replace all raw crypto.getRandomValues calls
New module packages/core/src/random.ts with randomHex, randomInt,
randomIndex — centralizes all crypto-based randomness.

Replaces:
- Date.now() IDs in VariablesDialog (not unique, not random)
- Hand-rolled hex token in automation/server.ts
- Inline crypto.getRandomValues in clipboard.ts, use-collab.ts

Also adds --color-component Tailwind theme color (#9747ff) and
replaces all 6 hardcoded occurrences across DesignPanel, LayerTree,
NodeContextMenuContent, and menu.ts.
2026-03-16 14:13:36 +03:00
Danila Poyarkov 9890b7c1a1 Enable Tailwind CSS class sorting in oxfmt
Sort classes using the same algorithm as prettier-plugin-tailwindcss,
configured for Tailwind v4 (reads src/app.css for theme).

Also sorts class strings inside twMerge() and tv() calls.

Eliminates 2 nested ternaries that conflicted with the formatter
(oxfmt strips parentheses that the lint rule requires).
2026-03-09 15:46:20 +03:00
Danila Poyarkov d060e32784 Merge origin/master (PR #41 data-test-ids) 2026-03-05 09:03:26 +03:00
Anton Soldatov 3ac016b806
Add data-test-id attributes to all Vue components (#41) 2026-03-04 17:18:26 +03:00
Danila Poyarkov c3e0810620 Fix scrollbar: use utility class instead of global selector
Global * scrollbar styles caused scrollbars to appear on the
menu bar and other elements. Replace with scrollbar-thin utility
applied only to panel scroll containers.
2026-03-04 09:35:30 +03:00
Danila Poyarkov c40c6c78a8 Fix horizontal scrollbar on right panel, style scrollbars for Tauri 2026-03-04 02:21:06 +03:00
Danila Poyarkov 7e06969ab5 Multi-selection properties panel with tests
Show position, appearance, fill, stroke, and effects sections
when multiple nodes are selected. Shared values display normally,
differing values show 'Mixed'. Editing applies to all selected.

- Shared composable (use-multi-props) for isMulti, active, activeNode
- ScrubInput supports MIXED symbol as model-value
- Per-node previous values for correct undo on multi-edit
- Fix flip transform encoding (scale first column only)
- Fix npm publish auth for MCP package
- 22 new tests: flip, alignment, multi-node merging, kiwi roundtrip
2026-03-03 20:35:51 +03:00
Danila Poyarkov b10785fdee Extract DesignPanel from PropertiesPanel 2026-03-01 10:55:52 +03:00