Commit graph

168 commits

Author SHA1 Message Date
Danila Poyarkov fdb3fec236 Add AGENTS.md 2026-02-28 23:18:55 +03:00
Danila Poyarkov d33efa1228 Update PLAN.md with Yoga CSS Grid PR tracking details 2026-02-28 23:17:39 +03:00
Danila Poyarkov 70c88dd72b Replace relative imports with @/ alias
Add @/ → src/ alias in vite.config.ts and tsconfig.json.
Rewrite all ../ imports across 32 files to use @/ paths.
Same-directory ./ imports kept as-is.
2026-02-28 23:08:45 +03:00
Danila Poyarkov 6eca2f8432 Consolidate shared types and remove inline duplicates
- Move Vector, Matrix, Rect to src/types.ts as shared primitives
- GradientTransform is now a type alias for Matrix
- Consolidate Window API declarations (File System Access,
  queryLocalFonts) in src/global.d.ts
- Remove duplicate KiwiNodeChange from fig-export.ts and
  clipboard.ts, reuse NodeChange from kiwi/codec.ts
- Add missing fields to codec NodeChange: frameMaskDisabled,
  stackWrap, stackPositioning, stackChildPrimaryGrow, vectorData
- Widen codec Paint.type and Effect.type unions to match scene-graph
- Replace inline Rect definitions across 5 files with shared type
- fig-export.ts fillPaints now constructs typed Paint objects
2026-02-28 23:06:47 +03:00
Danila Poyarkov 18609e00c3 Fix all lint, typecheck, and formatting issues
- 0 lint warnings (was 66), 0 type errors (was 30+)
- Add typed interfaces for Paint.stops/transform, Effect.blendMode,
  NodeChange.strokeCap/strokeJoin/dashPattern in kiwi codec
- Add CompiledSchema.encodePaint/encodeNodeChange methods
- Remove all no-explicit-any: use Window declaration merging for
  queryLocalFonts, showOpenFilePicker, showSaveFilePicker
- Remove all non-null assertions: add guards, use ?. operator,
  ?? fallbacks throughout
- Add SceneGraph.copyProp typed helper for instance sync
- Suppress kiwi-schema lint warnings via oxlint override
- Remove unused imports, variables, parameters
- Use proper CanvasKit StrokeCap/StrokeJoin enums in renderer
- Run oxfmt with import sorting
2026-02-28 22:57:33 +03:00
Danila Poyarkov 046f23b138 Format codebase with oxfmt 2026-02-28 22:41:34 +03:00
Danila Poyarkov e4eebf2a87 Move number input spinner hiding to global CSS 2026-02-28 22:40:01 +03:00
Danila Poyarkov d43fac94ca Hide number input spinners in Firefox
Add [appearance:textfield] alongside the WebKit spinner hide rule.
Firefox ignores ::-webkit-inner-spin-button but respects
appearance: textfield to remove native number spinners.
2026-02-28 22:38:36 +03:00
Danila Poyarkov 97715ba662 ScrubInput: move cursor and pointerdown to container
Move ew-resize cursor and pointerdown handler from inner spans
to the outer div for simpler event handling.

Note: ew-resize/col-resize cursors are broken in Safari 26.2
(WebKit bug 303845, fixed in Safari 26.3 Beta).
2026-02-28 22:36:39 +03:00
Danila Poyarkov 872fb4f540 Fix splitter resize handle cursor and hit area
Use a real 8px-wide element with negative margins instead of a
pseudo-element on a zero-width handle. The visible line is 1px
centered inside, highlighted on hover and drag. Works in Safari.
2026-02-28 22:28:22 +03:00
Danila Poyarkov 04479460d0 Throttle WebGL surface recreation during panel resize
ResizeObserver was recreating the surface on every tick. Now
coalesced via rAF so it fires at most once per frame — surface
always matches the canvas size so no skew.
2026-02-28 22:26:05 +03:00
Danila Poyarkov a89e886981 Make left and right panels resizable with reka-ui Splitter
Uses SplitterGroup/SplitterPanel/SplitterResizeHandle. Panels
default to 15% width, min 10%, max 30%. Layout persists across
reloads via auto-save-id. Resize handle highlights blue on hover.
2026-02-28 22:22:39 +03:00
Danila Poyarkov ffc1b7f56b Fix Polygon/Star tools not drawing on canvas
Missing from TOOL_TO_NODE map in use-canvas-input.
2026-02-28 22:19:01 +03:00
Danila Poyarkov 2ab75ad25a Add Polygon and Star tools with rendering and flyout shortcuts
- POLYGON (triangle icon) and STAR (star icon) in the shapes flyout
- Renderer draws regular polygons and stars using pointCount and
  starInnerRadius properties on SceneNode
- Flyout dropdown now shows keyboard shortcuts (R, L, O) aligned
  right like Figma's context menu
- Polygon defaults to 3 sides, Star to 5 points with 0.38 inner ratio
- Fill, stroke, hover highlight, and selection outline all work
2026-02-28 21:04:02 +03:00
Danila Poyarkov af3db9f0f1 Add GitHub Actions workflow for Windows + macOS builds
Triggers on version tags (v*) or manual dispatch.
Builds .msi/.exe for Windows x64/arm64 and .dmg for macOS
x64/arm64. Creates a draft GitHub release with all artifacts.
2026-02-28 20:55:26 +03:00
Danila Poyarkov cd9b096a98 Align + and − buttons across Fill, Stroke, Effects sections
Both are now size-5 flex-centered boxes with the same hover style,
so they sit at the same right edge and have equal hit targets.
2026-02-28 20:53:39 +03:00
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