Commit graph

152 commits

Author SHA1 Message Date
Danila Poyarkov b220fe13e6 Show effect controls inline instead of floating popover
The properties panel has overflow-y: auto which clips absolute
positioned popovers. Expand controls inline below the effect
row instead — simpler and always visible.
2026-02-28 20:49:23 +03:00
Danila Poyarkov c4f6f1caaa Redesign Effects section to match Figma's layout
- Compact row: color swatch + type dropdown + eye toggle + minus
- Expanded controls in a floating popover anchored to the left
- Labeled rows: Position (X/Y), Blur, Spread, Color with hex + opacity
- Type dropdown in popover header with X close button
- Eye/minus icons hidden until row hover (matches fill/stroke rows)
2026-02-28 20:40:03 +03:00
Danila Poyarkov 8b0bbc85b7 Merge branch 'effects-panel' 2026-02-28 20:13:34 +03:00
Danila Poyarkov daa949e9e3 Add Effects section to properties panel
Support adding, removing, and editing drop shadows, inner shadows,
layer blurs, background blurs, and foreground blurs. Each effect row
has visibility toggle, type dropdown, and remove button. Expanded
view shows offset, blur, spread, and color controls for shadows,
or just blur radius for blur effects.
2026-02-28 20:12:37 +03:00
Danila Poyarkov 1a0c6f62df Add independent corner radius controls to Appearance section 2026-02-28 20:12:31 +03:00
Danila Poyarkov a862cc6ca6 Remove 'Create Instance' button from properties panel
Figma doesn't have this — instances are created by dragging from
the Assets panel or duplicating an existing instance.
2026-02-28 20:05:06 +03:00
Danila Poyarkov 4886d465ad Fix ⌥⌘K / ⌥⌘B / ⇧⌘K / ⇧⌘H / ⇧⌘L shortcuts on Mac
Option key on Mac transforms e.key into special characters
(e.g. ⌥K → ˚), so modifier+letter shortcuts must check
e.code (physical key) instead of e.key (character produced).
2026-02-28 20:01:50 +03:00
Danila Poyarkov f77ad708b8 Live component-instance sync with override support
Editing a main component now propagates to all instances:
- Instance children are mapped to component children via componentId
- syncInstances() copies properties from component to instances,
  skipping any property marked in the overrides record
- New children added to a component appear in existing instances
- Store calls syncIfInsideComponent after updateNode, commitMove,
  commitResize — changes propagate automatically
- Demo creates real instances (Button, Badge) in the App Preview

Tests added: instance creation with child mapping, sync propagation,
override preservation, new child addition, detach.
2026-02-28 19:57:52 +03:00
Danila Poyarkov b3954a7d01 Coalesce renders with requestAnimationFrame
Multiple state changes between display refreshes (e.g. rapid
trackpad wheel events during zoom) now produce a single render
per frame instead of one per event. Zoom feels identical but
CPU usage drops since redundant intermediate renders are skipped.
2026-02-28 19:21:14 +03:00
Danila Poyarkov 86a5edfab5 Reuse Paint objects instead of allocating per frame
Pre-allocate auxFill, auxStroke, and opacityPaint on the renderer.
Reconfigure them before each use instead of new/delete cycles.
Reduces per-frame WASM Paint allocations from 29 to 10 (the
remaining 10 are cold-path: pen tool, rulers, one-off overlays).

Hot paths covered: renderNode opacity layers, hover/drop highlights,
selection handles (called 8-16× per frame), section title pills,
component label icons+text, size pill, marquee, layout indicator,
rotation handle, component set dashed border, drop shadows.
2026-02-28 19:19:30 +03:00
Danila Poyarkov bb5b4b57e5 Viewport culling: skip rendering off-screen nodes
Compute the world-space viewport from pan/zoom and skip nodes
whose bounds don't intersect it. Only culls leaf nodes and
clipped containers — unclipped parents are still traversed since
their children may extend beyond the parent bounds.

Handles rotation by expanding the bounding box to the diagonal.
Section titles and component labels also culled during collection.
Export path (renderSceneToCanvas) uses infinite viewport to render all.
2026-02-28 19:14:00 +03:00
Danila Poyarkov e39f81078f Fix text disappearing on window resize
On resize the renderer is recreated with a fresh TypefaceFontProvider.
loadFont() had an early return for cached fonts that skipped registering
with the new provider, so ParagraphBuilder had no fonts to render with.
2026-02-28 19:08:16 +03:00
Danila Poyarkov c4c4339f77 Run auto layout after demo creation
Children of auto-layout frames were positioned at their createShape
coordinates instead of being laid out. Call computeAllLayouts() after
building the demo tree so everything is properly arranged on load.
2026-02-28 18:59:05 +03:00
Danila Poyarkov 5a2265db3b Richer demo: components, auto layout, gradients, text, dashboard
Showcases the full feature set:
- Components section with Button/Primary, Button/Secondary, Tag,
  Avatar, Card (auto layout + progress bar), Input, Badge
- Color palette swatches
- App Preview section with a dashboard layout: sidebar nav,
  stat cards with auto layout, bar chart with gradient fills,
  table header with space-between layout
- Gradient cards, typography scale, mixed shapes
2026-02-28 18:32:27 +03:00
Danila Poyarkov 33fa850573 Fix component interaction: click selects component, drag by label, no white fill
1. Hit test: components/instances are opaque containers — clicking
   their children selects the component itself. Double-click enters
   and selects the child inside (like Figma).
2. Component label hit testing: clicking the purple name label
   selects the component, enabling drag-by-title (same as sections).
3. Components created from wrapping have no fill (transparent) —
   was incorrectly adding a white background rect.
2026-02-28 18:28:55 +03:00
Danila Poyarkov 058373123f Always-visible purple labels for components, component sets, instances
Like Figma: component/instance names render as permanent purple text
with a diamond icon prefix, at fixed screen-space size (doesn't scale
with zoom). Drawn in the screen-coordinate pass alongside section titles.

- COMPONENT: single diamond ◆ + name, above top-left
- COMPONENT_SET: 4-diamond grid ◆◆◆◆ + name, above top-left
- INSTANCE: single diamond ◆ + name, above top-left
- Variants inside a set: label inside top-left instead of above
- Remove component names from selection labels (no longer needed)
2026-02-28 18:23:46 +03:00
Danila Poyarkov 80faac1cd0 Context menu: pure Tailwind classes, drop custom CSS
Reka-ui portals teleport content to <body>, so scoped styles
can't reach them. Instead of unscoped CSS, use Tailwind utility
classes directly — consistent with every other component.

- data-[highlighted]:bg-hover for keyboard/mouse highlight
- data-[disabled]:text-muted for disabled state
- Component items in #9747ff with /12 hover and /40 disabled
- No <style> block at all
2026-02-28 18:19:56 +03:00
Danila Poyarkov b7242a78c2 Fix context menu not showing: scoped slot pattern + icon fix
- CanvasContextMenu now exposes onContextMenu via scoped slot instead
  of wrapping in a <div> (which broke flex layout — the div had no
  flex-1, so it collapsed and never received contextmenu events)
- EditorCanvas binds the handler directly on its own flex-1 div
- Fix lucide/diamonds icon not found — use lucide/component for
  COMPONENT_SET (the actual 4-diamond component icon)
2026-02-28 18:10:26 +03:00
Danila Poyarkov 0498d661c0 Fix context menu: use @floating-ui/vue instead of reka-ui ContextMenu
reka-ui's ContextMenuTrigger calls event.preventDefault() after
await nextTick(), which is too late to suppress the native context
menu in Tauri's webview. Replace with a custom implementation using
@floating-ui/vue for positioning (already a transitive dependency)
and proper ARIA roles (role=menu/menuitem).

- e.preventDefault() called synchronously in our handler
- VirtualElement anchor at click coordinates
- flip/shift middleware for viewport-aware positioning
- onClickOutside + Escape to dismiss
- Submenu hover for 'Move to page'
- Remove redundant contextmenu handler from use-canvas-input
2026-02-28 18:05:11 +03:00
Danila Poyarkov ea946b04e6 Add right-click context menu with full Figma-style actions
- CanvasContextMenu: reka-ui ContextMenu with dark theme matching editor
- Clipboard: Copy, Cut, Paste here, Duplicate, Delete
- Z-order: Bring to front (]), Send to back ([)
- Move to page: submenu listing all other pages
- Grouping: Group (⌘G), Ungroup (⇧⌘G), Add auto layout (⇧A)
- Components: Create component (⌥⌘K), Create component set (⇧⌘K),
  Create instance, Go to main component, Detach instance (⌥⌘B)
  — component items styled in purple
- Visibility: Show/Hide (⇧⌘H), Lock/Unlock (⇧⌘L)
- Context-aware: items show/hide based on selection type and count
- Right-click selects node under cursor before showing menu
- Store: bringToFront, sendToBack, toggleVisibility, toggleLock,
  moveToPage, renameNode operations
- Keyboard: ], [, ⇧⌘H, ⇧⌘L shortcuts
2026-02-28 17:58:55 +03:00
Danila Poyarkov 3c5795f90b Add components, component sets, and instances
- SceneNode: componentId and overrides fields for instance tracking
- SceneGraph: cloneTree, createInstance, detachInstance, getMainComponent,
  getInstances methods
- Editor store: createComponentFromSelection (converts frame/group or wraps
  selection), createComponentSetFromComponents (groups multiple components),
  createInstanceFromComponent, detachInstance, goToMainComponent
- Renderer: purple (#9747ff) selection outlines, hover highlights, size pills,
  and name labels for component types; dashed purple border for component sets
- Layers panel: diamond icons for components/instances with purple tint
- Properties panel: component type indicator, Go to Main Component and
  Detach Instance buttons for instances, Create Instance for components
- Keyboard shortcuts: Ctrl+Alt+K (create component), Ctrl+Shift+K
  (create component set), Ctrl+Alt+B (detach instance)
- Clipboard: preserve COMPONENT/COMPONENT_SET/INSTANCE types
- Fig export: emit proper Kiwi node types instead of FRAME
2026-02-28 17:46:28 +03:00
Danila Poyarkov cf454a855e Fill type picker with solid/gradient/image tabs and gradient stop editing
- FillPicker component: 3 category tabs (Solid, Gradient, Image), gradient
  subtype dropdown (Linear/Radial/Angular/Diamond), draggable gradient stop
  bar, per-stop color/position/opacity editing with ScrubInput
- FillSection uses FillPicker instead of ColorPicker for full Fill editing
- Default gradient transforms for each subtype so renderer picks them up
- colorToRgba255/rgba255ToColor helpers in color.ts
- Custom SVG icons for fill type tabs (solid square, gradient fade)
2026-02-28 17:26:50 +03:00
Danila Poyarkov 07f0f559da Add fig-import unit tests for Tier 1 rendering features
23 tests covering: node type mapping (ROUNDED_RECTANGLE, COMPONENT,
INSTANCE, SYMBOL, POLYGON), gradient fills (linear, radial), image
fills with hash, effects (drop shadow, inner shadow), stroke options
(cap, join, dash pattern), text properties (auto-resize, font weight
mapping with space normalization), arc data (partial ellipse, donut),
constraints, blend modes, independent stroke weights, stacked fills.
2026-02-28 16:43:43 +03:00
Danila Poyarkov e2e0e75173 Tier 1 rendering parity: gradients, images, effects, strokes, arcs
Data model:
- Gradient fills (LINEAR, RADIAL, ANGULAR, DIAMOND) with stops + transform
- Image fills with hash reference, scale mode, transform
- Blend modes on fills, nodes, effects
- Stroke cap, join, dash pattern, independent border weights
- Arc data for partial ellipses/donuts
- Text: vertical align, auto-resize, case, decoration, max lines
- Constraints: horizontal/vertical
- New node types: ROUNDED_RECTANGLE, COMPONENT, COMPONENT_SET,
  INSTANCE, CONNECTOR, SHAPE_WITH_TEXT

Import:
- All paint types imported (not just SOLID)
- Image hashes extracted from .fig ZIP images/ directory
- Font weight mapping (Thin→100 through Black→900)
- Arc data, stroke options, constraints, text properties

Renderer:
- Gradient shaders via CanvasKit (linear, radial, sweep)
- Image fills with FILL/FIT scale modes and shader matrix
- Drop shadow and inner shadow effects
- Arc/donut rendering for ellipses with arcData
- Stroke cap/join/dash pattern support
- Image cache with cleanup on destroy

Export:
- Gradient/image fills round-trip through Kiwi
- New node types mapped to Figma equivalents

UI:
- Fill section shows gradient/image type labels for non-SOLID fills
2026-02-28 16:40:34 +03:00
Danila Poyarkov 66043081af Format Rust code with rustfmt 2026-02-28 16:24:31 +03:00
Danila Poyarkov edf6da6738 Build .fig ZIP in Rust instead of fflate
fflate's zipSync produces ZIP files that Figma rejects. Move the entire
fig-kiwi container + Zstd compression + ZIP packaging into a single
Rust command (build_fig_file). Zstd now includes content size via
set_pledged_src_size. Web fallback still uses fflate/deflate.
2026-02-28 16:16:49 +03:00
Danila Poyarkov 5aae8d01fc Include content size in Zstd frame header
Figma requires the Zstd content size to be present in the frame header.
Use zstd::Encoder with include_contentsize(true) instead of encode_all.
2026-02-28 16:03:58 +03:00
Danila Poyarkov 2b91f51de8 Generate thumbnail for .fig export
Render the current page to a 400×225 offscreen CanvasKit surface,
fit content with padding, and include as thumbnail.png in the ZIP.
Falls back to a 1×1 placeholder when renderer is unavailable (tests).
2026-02-28 15:57:15 +03:00
Danila Poyarkov f31ded3c08 Fix .fig export: include thumbnail.png required by Figma
Figma's importer requires thumbnail.png to exist in the ZIP archive.
Without it, import fails with 'Internal error during import' regardless
of how correct the Kiwi data is.

Also: STORE compression for canvas.fig (already internally compressed),
and match Figma's default DOCUMENT/CANVAS fields.
2026-02-28 15:52:35 +03:00
Danila Poyarkov dc3c5a47b1 Fix .fig import/export round-trip
- Parse fig-kiwi container format properly: strip header, extract
  schema and data chunks, decompress (Zstd or deflate)
- Import uses the schema embedded in the fig-kiwi container (supports
  newer Figma files with unknown fields)
- Export uses the codec's schema via getCompiledSchema/getSchemaBytes
- Consolidate schema loading: remove duplicate figma-schema.bin fetch
  from clipboard and export; both now use codec's initCodec()
- Fix IS_TAURI for non-browser environments (SSR/Bun)
2026-02-28 15:12:24 +03:00
Danila Poyarkov 495e419e79 Zstd compression via Tauri Rust command, deflate fallback for browser
- Added zstd crate to Rust deps and zstd_compress Tauri command
- Export uses native Zstd in Tauri, deflate in browser (our reader
  handles both)
- Removed zstd-wasm dependency (decompress-only, didn't work)
2026-02-28 14:56:06 +03:00
Danila Poyarkov 197323162f Fix .fig export: Zstd compression, IS_TAURI constant, fs permissions
- Replace deflate with Zstd compression via zstd-wasm for .fig export
  (Figma expects Zstd, not zlib deflate)
- Extract IS_TAURI constant from repeated window check
- Fix Tauri fs permissions: use allow-write-file / allow-read-file
  with glob scope instead of bare allow-write / allow-read
2026-02-28 14:48:22 +03:00
Danila Poyarkov 150bd7a256 Merge branch 'save-fig' 2026-02-28 14:26:47 +03:00
Danila Poyarkov 0524092f3e Merge branch 'autolayout-tests' 2026-02-28 14:04:30 +03:00
Danila Poyarkov 55c3ea35dc Section title text inverts based on pill background luminance 2026-02-28 14:03:57 +03:00
Danila Poyarkov 27f57da340 Improved demo: section with Desktop/Mobile frames, color swatches
- 'Design System' section containing Desktop and Mobile frames
- Desktop: header, hero card, avatar, two content cards
- Mobile: status bar, banner, list items, FAB button
- Color palette swatches (Primary, Success, Warning, Danger, Purple)
- Updated E2E tests for new demo structure
- Reverted section title pill to always use fill color (not blue)
2026-02-28 14:02:46 +03:00
Danila Poyarkov 27f83c721e Add layout tests, fix cross-axis sizing bugs
- Fix axis mapping for nested frames with different layout directions
  (e.g. vertical child inside horizontal parent had swapped sizing)
- Fix stretch alignment not applying to leaf children when set on parent
- Fix layoutGrow being ignored on children with FIXED primaryAxisSizing
- Add 42 unit tests covering all layout scenarios
2026-02-28 13:59:39 +03:00
Danila Poyarkov 8d3d3444ad Tauri native dialog for Save/Open with fs plugin 2026-02-28 13:54:21 +03:00
Danila Poyarkov 59a98f9186 Show frame name label for direct children of sections 2026-02-28 13:51:18 +03:00
Danila Poyarkov b83dfcdbe4 Sections are top-level only: can't nest inside frames or groups
Per Figma behavior, sections can only live in pages (CANVAS) or
other sections. Reparent guard skips sections dropped into frames/
groups. Drop target highlight suppressed when dragging sections
over non-section containers.
2026-02-28 13:49:58 +03:00
Danila Poyarkov 584661315f Section title hit test uses exact pill width from font metrics
Moved section title hit testing from scene-graph (no font access) to
renderer where exact pill width is computed from glyph widths. Only
the title pill area is clickable, not the full width above the section.
Hit test passed through use-canvas → use-canvas-input via callback.
2026-02-28 13:46:39 +03:00
Danila Poyarkov 60ed0f195d Fix missing import for section title hit testing 2026-02-28 13:43:02 +03:00
Danila Poyarkov 715f6e3281 Nested section titles inside body, auto-adopt siblings on creation
- Top-level section titles render above the body
- Nested section titles render inside at the top-left
- Drawing a section auto-reparents any siblings fully contained
  within its bounds, adjusting coordinates to relative
- Undoable adopt operation with snapshotted positions
2026-02-28 13:39:04 +03:00
Danila Poyarkov adc5595f25 Section titles: fixed screen size, ellipsis truncation
Title pill rendered in screen space (UI overlay layer), not canvas
space. Font size stays constant regardless of zoom. Text truncates
with ellipsis when section is narrower than the full name.
Pill width capped to section's screen width.
2026-02-28 13:35:07 +03:00
Danila Poyarkov cadbd1330e Merge hover-highlight, implement sections
Sections (Figma-like):
- SECTION node type with rounded rect body + title pill above
- S shortcut, Frame flyout includes Section
- Default dark gray fill + subtle stroke, 5px corner radius
- Sections are containers with click-through hit testing
- Drag outside section reparents to parent

Toolbar flyouts with Reka UI DropdownMenu:
- Split button: tool icon activates tool, chevron opens dropdown
- Frame flyout: Frame / Section
- Rectangle flyout: Rectangle / Ellipse / Line
- Active sub-tool icon shown on parent button
2026-02-28 13:31:21 +03:00
Danila Poyarkov 086fb7d652 File → Save / Save As (.fig export) 2026-02-28 13:28:21 +03:00
Danila Poyarkov 3f7f30aaf0 Merge branch 'hover-highlight' 2026-02-28 13:17:48 +03:00
Danila Poyarkov fd2e38e5ee Fix page rename: blur commits, Enter/Escape just blur 2026-02-28 13:15:53 +03:00
Danila Poyarkov 803b69eee6 Split PagesPanel into separate component, inline page rename
- PagesPanel.vue: pages list with click to switch, double-click
  for inline rename (input field, Enter/Escape/blur commits)
- LayersPanel.vue: removed pages logic, composes PagesPanel
- Layer drag-drop uses currentPageId instead of rootId
2026-02-28 13:14:03 +03:00
Danila Poyarkov adc95c7379 Shape-aware hover highlight for ellipses, rounded rects, vectors 2026-02-28 13:12:53 +03:00