Commit graph

68 commits

Author SHA1 Message Date
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 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 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 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 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 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 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 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 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 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 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 1d79473bc6 Pages support: multi-page documents like Figma
Document structure: Document (root) → Pages (CANVAS) → Nodes.
Each page has its own viewport (pan/zoom) and background color.

- CANVAS node type for pages
- SceneGraph: addPage(), getPages(), getAbsolutePosition stops at CANVAS
- Store: currentPageId, switchPage(), addPage(), deletePage(), renamePage()
- All operations (create, select, paste, hit test, render) scoped to current page
- Per-page viewport state saved/restored on page switch
- Pages list in LayersPanel with add/switch/double-click rename
- .fig import creates proper pages from DOCUMENT→CANVAS hierarchy
- Renderer takes pageId to render correct page's children
- Unit test for pages, updated E2E tests for page-scoped graph
2026-02-28 13:12:27 +03:00
Danila Poyarkov ffd305c125 Page properties panel with canvas background color picker
When nothing is selected, the right panel shows Page section with
a color picker for the canvas background (like Figma). The page
color is stored in reactive state and passed to the renderer.
2026-02-28 11:42:13 +03:00
Danila Poyarkov 66e68f7349 Drag outside frame reparents to parent, clip content toggle
- Dragging a child fully outside its parent frame's bounds
  automatically reparents it to the grandparent (like Figma)
- clipsContent property on SceneNode (default true for frames)
- Renderer respects clipsContent — unclipped frames show
  children that overflow
- Clip content checkbox in Layout panel for frames
2026-02-28 11:21:03 +03:00
Danila Poyarkov fd0b02642f Fix layers panel reactivity and accidental reparenting on click
- Force TreeRoot re-mount via :key when tree structure changes,
  fixing Reka UI not reacting to items prop updates
- Only reparent into frames on mouseup when user actually dragged,
  preventing click-to-select from moving nodes into overlapping frames
- Expose store on window for test introspection
- 6 new E2E tests for layers panel
2026-02-28 11:15:19 +03:00
Danila Poyarkov ce46dea54d Sort nodes by position when wrapping in auto-layout, fix null node crash
Nodes are sorted top-to-bottom (then left-to-right) before
reparenting into auto-layout frame. Positions are snapshotted
as plain IDs before mutation to avoid reactive proxy issues.

Property panel sections guard against null node with v-if,
preventing crashes during selection transitions.
2026-02-28 10:36:43 +03:00
Danila Poyarkov b0a35a4d24 Fix rotation icon to rotate-ccw, add rotate 90° button 2026-02-28 09:57:45 +03:00
Danila Poyarkov c66e466fb2 Position section: alignment buttons, rotation icon, flip H/V
- 6 alignment buttons (left/center/right, top/center/bottom) for
  multi-select alignment
- Rotation input with rotate-cw icon and degree suffix
- Flip horizontal and flip vertical buttons
- Matching Figma's Position panel layout
2026-02-28 09:55:02 +03:00
Danila Poyarkov d0048114a7 Fix layers panel chevron direction 2026-02-28 09:29:57 +03:00
Danila Poyarkov 3f176d2b59 Fix layers panel: include visible in LayerNode interface
LayerNode was missing the visible field, so item.value.visible was
always undefined — eye-off icon showed on every layer.
2026-02-28 09:28:13 +03:00
Danila Poyarkov de83a716cd Remove prop drilling from property sections
All sections now get selected node from useNodeProps() composable
instead of receiving it as a prop from PropertiesPanel.
2026-02-28 09:27:03 +03:00
Danila Poyarkov 2af2a280c6 Appearance section: opacity + corner radius + visibility toggle
- Opacity with blend icon, corner radius with radius icon
- Eye/eye-off toggle in section header
- Layers panel: hidden layers dimmed (opacity-50) with permanent
  eye-off indicator
- Removed duplicate corner radius from LayoutSection
2026-02-28 09:20:31 +03:00
Danila Poyarkov bfe9726035 ScrubInput: entire field is draggable, click-to-edit, suffix styling
- Value + suffix area is now the drag handle (cursor: ew-resize)
- Click without drag enters edit mode (2px dead zone)
- Suffix rendered as separate muted span (right-aligned)
- No more tiny icon-only drag area
2026-02-28 09:16:02 +03:00
Danila Poyarkov c4a3ee6b4a Replace plain number inputs with ScrubInput for drag-to-adjust
Fill opacity, stroke opacity, and stroke weight now use ScrubInput
for consistent drag-to-adjust behavior matching W/H/position inputs.
2026-02-28 09:11:50 +03:00
Danila Poyarkov 8d35d4dbaf Deduplicate hex formatting: colorToHexRaw() in engine/color.ts
Removes duplicate hexString() wrappers from FillSection, StrokeSection,
and ColorPicker — all now use culori-based colorToHexRaw().
2026-02-28 09:08:40 +03:00
Danila Poyarkov babeecc76e Polish fill/stroke sections: editable hex, opacity %, eye toggle, minus
- Hex input is now editable (was read-only label)
- Per-fill/stroke opacity percentage input
- Eye/eye-off icon for visibility toggle (on hover)
- Minus button for removal (replaces ×)
- Stroke weight shown below stroke list
2026-02-28 09:06:04 +03:00
Danila Poyarkov 7b0f6c10f7 Fix text alignment icons: use direct template tags for auto-import 2026-02-28 09:01:54 +03:00
Danila Poyarkov 7e9cd46e0b Split PropertiesPanel into sections, add Typography panel
- PropertiesPanel.vue: 513 → 67 lines (composition only)
- Extracted: PositionSection, LayoutSection, AppearanceSection,
  FillSection, StrokeSection, TypographySection
- Shared useNodeProps() composable for update/commit logic
- Typography panel: font family picker with search (queryLocalFonts),
  weight select, font size, line height, letter spacing, text alignment
- Font picker shows each family in its own typeface
- Loads selected font into both CanvasKit and browser on pick
2026-02-28 09:00:25 +03:00
Danila Poyarkov 5c0afd4d06 Pass fontWeight to both CanvasKit text style and textarea overlay 2026-02-28 08:49:28 +03:00
Danila Poyarkov 2715f0d8be Text tool: start empty with cursor, blue editing outline
- Create text nodes with empty text instead of 'Text' placeholder
- Immediately enter editing mode on creation (blinking cursor)
- Blue border on textarea during editing (matches Figma)
2026-02-28 08:47:42 +03:00
Danila Poyarkov 98eeb9582c Fix text editing: hide canvas text/selection during edit, load Inter font
- Skip rendering text node on canvas while textarea overlay is active
- Skip selection handles for the node being edited
- Remove border from textarea (was causing double outline)
- Add @font-face for Inter-Regular.ttf so textarea matches CanvasKit
2026-02-28 08:45:37 +03:00
Danila Poyarkov bcd8bf6bca Add IconsResolver for auto-import, explicit imports only for dynamic maps 2026-02-28 08:29:39 +03:00
Danila Poyarkov 3ddc671bc5 Replace emoji icons with Lucide via Iconify (unplugin-icons)
- unplugin-icons + @iconify-json/lucide for compile-time icon bundling
- Toolbar: proper cursor, frame, square, pen, type, hand icons
- Layers panel: per-type node icons + chevron-right for expand/collapse
- Remove icon field from ToolDef (icons mapped in components)
2026-02-28 08:24:42 +03:00
Danila Poyarkov f0d817c747 Refactor: vendor kiwi-schema, extract types/constants, culori colors
- Fork kiwi-schema from source TypeScript (evanw/kiwi), drop npm dep + patch
- Deduplicate GUID/Color types into src/types.ts
- Extract UI colors and default fills into src/constants.ts
- Replace hand-rolled hex parsing with culori library
- Extract demo shapes from App.vue into src/demo.ts
- useUrlSearchParams from VueUse for test mode detection
- Move fig-file/fig-import into kiwi/ (format layer)
- Delete PoC files (poc-yoga.ts, poc-test.ts)
2026-02-28 08:08:04 +03:00