Commit graph

730 commits

Author SHA1 Message Date
Danila Poyarkov 7ac3ee035d fix(dom-css): compile standalone HTML exports 2026-07-04 19:37:58 +03:00
Danila Poyarkov a4272a17bf fix(mcp): target live automation by document and page 2026-07-03 17:05:04 +03:00
Danila Poyarkov 2c69e0949b fix(text): preserve Figma auto-size text imports
- Normalize auto-layout text imported from Figma when fixed bounds match derived text layout
- Recompute auto-size text bounds on typography metric changes
- Clear stale Figma derived text layout caches when text sizing changes
- Cover FIG import text sizing and line-height resize regressions
2026-07-02 17:30:15 +03:00
Danila Poyarkov 13314d6b6d fix(editor): relayout after component sync 2026-07-02 10:35:10 +03:00
Danila Poyarkov d522dc0529 fix(layout): match Figma auto-layout reflow 2026-07-02 08:53:48 +03:00
Danila Poyarkov 3efc7de149 fix(canvas): render CJK fallback text as outlines 2026-07-01 23:22:14 +03:00
Danila Poyarkov 2c171139c1 fix(text): prefer script-specific CJK fallbacks 2026-07-01 22:19:14 +03:00
Danila Poyarkov 5a89b520b2 fix(text): load CJK fallback only when needed 2026-07-01 22:06:19 +03:00
Danila Poyarkov 4880652b6e feat(editor): add Figma-style page management 2026-07-01 20:29:10 +03:00
Danila Poyarkov 9498dc203d chore(tools): harden package and dependency checks 2026-07-01 12:56:45 +03:00
Danila Poyarkov 8cefbedd50 feat(app): add desktop HTTP bridge for online services 2026-06-30 16:52:26 +03:00
rcoenen d2d816b688
fix(collab): replicate document content to joiners
Squashed PR #340 with maintainer follow-up fixes and local validation.
2026-06-30 11:56:58 +03:00
Danila Poyarkov f787486432 chore(core): format overlap helper imports 2026-06-30 11:05:19 +03:00
Danila Poyarkov 832ea3e914 fix(core): import overlap types from scene graph package 2026-06-30 10:58:35 +03:00
Danila Poyarkov c8f7f9ba49 fix(core): update overlap helper package imports 2026-06-30 10:57:36 +03:00
Danila Poyarkov 898431e646 feat: split SceneGraph and Pen packages 2026-06-30 10:54:32 +03:00
Danila Poyarkov 1eb227ed02 feat(dom-css): harden package boundaries 2026-06-30 10:51:37 +03:00
Danila Poyarkov 1ea6631ffd fix(kiwi): export EXCLUDE booleans as XOR 2026-06-30 10:51:05 +03:00
Danila Poyarkov f6a8bd8580 refactor(kiwi): move FIG GUID helpers 2026-06-30 10:50:39 +03:00
Danila Poyarkov 6bf930e9e9 refactor(kiwi): tidy direct Kiwi imports 2026-06-30 10:50:34 +03:00
Danila Poyarkov 7d27cac5ab refactor(kiwi): remove redundant core shims 2026-06-30 10:50:30 +03:00
Danila Poyarkov 8365112c51 refactor(kiwi): move FIG container helpers 2026-06-30 10:49:44 +03:00
Danila Poyarkov d068237ee4 refactor(kiwi): move FIG parse helpers 2026-06-30 10:49:29 +03:00
Danila Poyarkov eda61d815d refactor(kiwi): move FIG codec package 2026-06-30 10:49:14 +03:00
Danila Poyarkov 0c4429d3ff refactor(kiwi): isolate FIG codec types 2026-06-30 10:49:14 +03:00
Danila Poyarkov 086ab2d2ae refactor(kiwi): move variable binding codec 2026-06-30 10:49:14 +03:00
Danila Poyarkov 6877516f06 refactor(kiwi): consume standalone Kiwi package 2026-06-30 10:49:14 +03:00
Danila Poyarkov 8983c11f45 feat(design-jsx): support structured effects 2026-06-30 10:45:15 +03:00
Danila Poyarkov cc1167eed5 feat(design-jsx): support structured paints 2026-06-30 10:44:51 +03:00
Danila Poyarkov 4b65e64c18 feat(design-jsx): support variable refs 2026-06-30 10:44:28 +03:00
Danila Poyarkov 7303a40964
fix(editor): separate preview updates from graph mutations
Separate preview-only scene graph updates from committed mutation events and fix vector resize undo.
2026-06-30 10:28:02 +03:00
Danila Poyarkov 568cdf91ef Merge remote-tracking branch 'joey/feature-overlapping-element-analysis-tool' into pr352-local-merge
# Conflicts:
#	tests/engine/clipboard/fixtures/gold-preview-roundtrip.test.ts
#	tests/engine/io/fig/import/group-reclassify.test.ts
#	tests/engine/io/fig/roundtrip/glyph-blob.test.ts
#	tests/engine/layout/auto-layout/text/measurement.test.ts
2026-06-30 09:47:07 +03:00
Joseph Cumines dc9638ac3e
fix(export): prevent GUID collisions and file corruption on .fig round-trip (#333)
* fix(export): prevent GUID collisions and file corruption on .fig round-trip

Nodes sharing the same imported source.id (component instance children,
cloned subtrees) silently overwrote each other on export because the
GUID assignment reused imported GUID values without checking for
duplicates.  This caused data loss on reimport — only the last node
with a given GUID survived.

- Track all assigned GUID values in a Set for O(1) collision detection.
- Scan imported source.ids for both sessionID 0 and 1 before assigning
  any new GUIDs, so the counter starts past every imported value.
- Fall back to counter-based GUIDs when source.id collides with an
  already-assigned value.

Additional fixes in the same change set:

- cloneTree now deep-copies source.fig via structuredClone, preventing
  mutations on a clone from corrupting the original node's kiwi payload.
- Removed decompressFigKiwiData sync wrapper (zero callers) and the
  silent try/catch fallback in parseFigKiwiContainer that masked
  corrupt data as raw bytes.
- buildFigKiwi uses Bun.zstdCompressSync when available, matching the
  zstd decompression path already used on import.
- Fixed setSavedVersion ordering in read.ts — must run after
  requestRender to capture the post-bump version, preventing spurious
  dirty-state immediately after file reload.

Tests: GUID collision (2 and 3 node), clone isolation, parse failure,
text export zstd compatibility, gold-preview round-trip.

* fix(export): handle EXCLUDE boolean operation and BOOLEAN_OPERATION node type

The internal representation uses EXCLUDE for exclude boolean operations,
but Figma's kiwi schema uses XOR. Map EXCLUDE back to XOR on export so
round-trips through .fig files don't fail. Also add BOOLEAN_OPERATION to
VALID_NODE_TYPES and increase timeout for heavy material3 fixture test.

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

* style: format export-node.ts

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

* test: add type guard after null assertion in guid-collision test

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

* fix(clipboard): detect zstd-compressed data before zlib inflate

fflate's inflateSync silently accepts zstd-compressed data and returns
garbage instead of throwing. Check for the zstd magic bytes (28 b5 2f
fd) before attempting zlib decompression. Also revert the EXCLUDE enum
addition to the kiwi schema since the export-node.ts mapping is
sufficient.

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

* fix(clipboard): add length guard before zstd magic byte check

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

* test(scene-graph): group clone regression coverage

* ci: retrigger CI checks

* test: increase timeouts for heavy .fig fixture tests on slow CI runners

Gold-preview.fig and material3.fig parsing/export tests consistently
exceed the 5s bun:test default timeout on GitHub Actions runners.
Increase to 30s for: beforeAll codec init, clipboard roundtrip,
glyph blob roundtrip, group reclassification, and text measurement.

* test: add individual test timeouts for heavy .fig fixture tests

The beforeAll timeout helped but individual test() calls also need
explicit 30s timeouts since bun:test applies the 5s default per-test.
Fixes remaining CI flakes in glyph-blob roundtrip and clipboard
roundtrip tests.

* test: increase beforeAll timeout for render cache test

The canvas/render cache test loads gold-preview.fig AND initializes
CanvasKit (Skia WASM), which is much slower than the other fixture
tests. Use 60s timeout to account for slow CI runners.

* fix(export): reserve document GUID to prevent 0:0 namespace collision

- Add docGuid (0:0) to assignedGuidValues before processing imported
  node source.ids, preventing an imported node with source.id "0:0"
  from reusing the document's GUID slot
- Add regression test using session-0 source.ids to verify nodes
  survive roundtrip without document GUID collision
- Remove unnecessary async keyword from synchronous component
  metadata test

* fix(export): guard canvas GUID reuse with assignedGuidValues check

- Mirror getOrCreateNodeGuid() collision logic in buildCanvasEntries():
  if an imported page's source.id maps to a GUID already in
  assignedGuidValues, generate a fresh counter-based GUID instead
- Prevents canvas-level last-write-wins when multiple pages share
  the same source.id or a page uses 0:0

* fix(test): use explicit little-endian writes and fix misleading test title

- Replace host-endian Uint32Array writes with DataView.setUint32(offset, value, true) in parse-failures.test.ts to ensure platform-independent fig-kiwi container assembly
- Rename test title from "clone clears source.id from the original" to "clone clears source.id from the clone" to accurately reflect what the assertions verify

* test(io): use file-level timeout for heavy fixture

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Danila Poyarkov <dev@dannote.net>
2026-06-23 18:05:23 +03:00
Joseph Cumines ad22513bc7 fix(tools): address CodeRabbit review on analyze_overlaps
- Export OverlapScope, AnalyzeOverlapsSummary, and OverlapIntersection
  from the analyze barrel so consumers don't deep-import.
- Expand the local rectangle by stroke overflow before transforming
  through the world matrix, so stroked rotated nodes get their true
  rotated footprint instead of an underestimated canvas-space expansion.
- Add clipPolygon to geometry and preserve the clipped polygon through
  the full clip chain in computeNodeBounds, so a later outer clip can't
  reintroduce corners an inner clip already removed.
- Cap returned overlaps to an empty list when limit is 0 or negative;
  summary totals still reflect the complete result set.
- Trim whitespace from scope and severity inputs so " major " resolves.
- Add geometry and overlap tests covering each fix.
2026-06-19 11:38:38 +10:00
Joseph Cumines f862beb08d Add heuristic overlap detection tool and CLI
Implement the analyze overlaps CLI command, RPC endpoint, and ToolDef
to enable heuristic detection of visual overlaps, parent overflows, and
overlay patterns. Geometry computation now uses world-matrix visual
bounds to handle nested ancestor rotations and clipping frames correctly.

Update file-backed CLI commands to use Node fs/promises instead of Bun
runtime file APIs. This is required for the published CLI to function
when installed and executed in a standard Node environment. Add the
package.json subpath export to the core package for metadata consumers.
2026-06-19 09:54:27 +10:00
Joseph Cumines 4bc1698e93
fix(scene-graph): deep-copy cloned nodes and validate bindings 2026-06-10 17:10:25 +03:00
Danila Poyarkov 72e3bc4fb5 chore: organize repo tooling under tools 2026-06-07 09:40:54 +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
rcoenen 27cfcbb932
fix(resize): scale group and boolean children
* fix(resize): scale children when resizing GROUP or BOOLEAN_OPERATION

Resizing a group (or boolean operation) only moved the bounding box and
rescaled the selection outline — the rendered fill stayed at its original
size, so the blue outline and the filled content visibly diverged.

Two root causes, both in the resize preview path:

1. During drag, children were scaled in the preview graph via
   updateNodePreview(), which mutates nodes without emitting a node:updated
   event. The renderer's vector/geometry caches (vectorPathCache,
   fillGeometryCache, strokeGeometryCache) were therefore never invalidated,
   so fills kept drawing from stale, unscaled cached paths while outlines
   drew live from the new dimensions. Fixed by calling
   renderer.invalidateVectorPath(childId) after each child preview update.

2. On commit, the final scaled values were written with updateNode() but no
   repaint was requested, leaving the canvas frozen on the last mismatched
   drag frame until the next interaction. Fixed by calling requestRepaint()
   after commitGroupResize().

collectDescendants() now also collects children for BOOLEAN_OPERATION nodes,
which render purely from child geometry (no node-level scale), so scaling the
children is the only way to scale the result — same mechanism as GROUP, no
double-transform. Group resize verified visually; boolean resize verified in
-browser via the editor automation API.

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

* fix(editor): preserve vector undo during group resize

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Danila Poyarkov <dev@dannote.net>
2026-06-06 17:05:09 +03:00
rcoenen 922235d2c8
fix(kiwi): import Figma groups as groups
* fix(kiwi): import Figma groups as GROUP nodes instead of FRAME

- Map FRAME nodes with `resizeToFit === true` to `GROUP` type on import
- Write `resizeToFit = true` when exporting `GROUP` nodes back to Kiwi format
- Document findings and diagnosis in packages/docs/development/group-to-frame-import-issue.md
- Extract resolveNodeType helper to satisfy cyclomatic complexity limits

* test(kiwi): harden Figma group import + add coverage

Follow-up hardening on the group import/export fix:

- convert.ts: reorder so the COMPONENT_SET check runs before the FRAME→GROUP
  reclassification, and guard the group check on the absence of auto-layout
  (`stackMode` unset/NONE). Figma auto-layout "hug" frames use
  stackPrimarySizing/stackCounterSizing, not `resizeToFit`, so this prevents a
  component-set or auto-layout frame that happens to carry `resizeToFit` from being
  misclassified as a group.

- Add tests/engine/io/fig/import/group-reclassify.test.ts: unit coverage for the
  reclassification (FRAME+resizeToFit → GROUP, plain frame stays FRAME, auto-layout
  hug frame stays FRAME), plus a real-file assertion that gold-preview.fig imports
  its groups as GROUP.

- Add tests/engine/io/fig/roundtrip/group.test.ts: a created GROUP survives
  export → re-import as a GROUP (validates the export-side resizeToFit write).

- Update exhaustive.test.ts gold-preview golden: 519 nodes that were misimported as
  FRAME are now correctly GROUP (FRAME 4525→4006, GROUP 519), and export size shifts
  594758→594770 because exported groups now carry resizeToFit.

Note: the pre-existing material3 variables roundtrip failure
("Invalid value EXCLUDE for enum BooleanOperation") is unrelated to groups and
also fails on master — out of scope here.

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

* chore(kiwi): remove temporary group-to-frame analysis doc

It was a scratch diagnostic artifact (and contained a hardcoded local path);
the rationale now lives in the fix's commit messages, code comments, and tests.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Danila Poyarkov <dev@dannote.net>
2026-06-06 13:49:19 +03:00
rcoenen a9ee01fc13
fix: make fill hex editable and quiet local font probing
* fix: make fill hex editable and suppress premature local font warnings

- FillSection: replace read-only hex span with editable input for solid
  fills (matching existing stroke behavior); gradient/image fills and
  variable-bound fills keep the display span
- FontManager.findLocalFont: guard on localFontAccessState === 'granted'
  before calling queryLocalFonts(), eliminating SecurityError console
  warnings on load when the user hasn't yet granted font access
- .gitignore: add .opencode/

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* style: apply oxfmt formatting to FillSection.vue

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Danila Poyarkov <dev@dannote.net>
2026-06-06 13:28:46 +03:00
박영호 ee2a7472ee
fix(text): handle Hangul IME composition (#319)
Co-authored-by: 05Park <im0505@kakao.com>
2026-06-06 07:21:56 +03:00
박영호 3b36dadfdb
fix(layout): preserve imported instance child bounds 2026-06-03 15:47:17 +03:00
Danila Poyarkov 05e4a29a08 feat(design-jsx): support components and instances 2026-05-31 18:33:56 +03:00
Danila Poyarkov 31a1e17105 fix(mcp): harden tool responses 2026-05-31 18:14:10 +03:00
Danila Poyarkov df9b6f3c3a fix(editor): guard optional renderer measurement 2026-05-31 15:40:07 +03:00
Danila Poyarkov 22ff3acdb5 Merge remote-tracking branch 'origin/master' into figma-compat-render-gaps
# Conflicts:
#	CHANGELOG.md
2026-05-31 13:59:19 +03:00
Danila Poyarkov 6ee0e5f9e8 fix(kiwi): preserve imported boolean geometry 2026-05-31 13:28:41 +03:00
Danila Poyarkov 2dc51375ad Release v0.13.2 2026-05-30 19:14:12 +03:00
Danila Poyarkov 9ede8bf464 Merge remote-tracking branch 'origin/master' into figma-compat-render-gaps 2026-05-30 13:17:29 +03:00