Commit graph

718 commits

Author SHA1 Message Date
Danila Poyarkov 7f99431981 feat(app): import DOM/CSS from code panel 2026-06-30 10:48:14 +03:00
Danila Poyarkov 05980797e3 feat(app): open DOM/CSS documents 2026-06-30 10:48:14 +03:00
Danila Poyarkov ec5ef4474e
i18n: add Japanese and complete menu localization (#367)
* i18n: more menu translations

* chore: code formatting

* chore: code formatting

* chore: code formatting

* i18n: more menu translations

* chore: code formatting

* i18n: added new menu translations and japanese language

* i18n: added theme-auto string

* i18n: translated rest of the menu text

* fix(i18n): polish PR 273 locale updates

- Fix obvious translation regressions in existing locales
- Preserve update size interpolation across localized update messages
- Use standard UI terms for theme and boolean operation labels

* fix(i18n): tune localized toggle labels

- Use paired action wording for toggle commands
- Clarify Japanese frame-selection command label

---------

Co-authored-by: Santos Alarcón <santosalarcon86@gmail.com>
2026-06-30 10:25:52 +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
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 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
Danila Poyarkov c4e21af7ee
fix(ui): keep tooltips on floating triggers 2026-06-06 11:31:24 +03:00
Danila Poyarkov 842cfe74f4 feat(demo): showcase text decoration fidelity 2026-05-26 14:24:13 +03:00
Danila Poyarkov b5eacf7ab6 feat(demo): showcase OpenType feature toggles 2026-05-26 12:32:05 +03:00
Danila Poyarkov f4b3453fbd chore(lint): check duplicate type shapes across files
- Add a repo-wide TypeScript AST duplicate shape check to the quality gate
- Extract shared named types for existing cross-file duplicates
- Keep same-file oxlint coverage for editor feedback
2026-05-25 00:55:29 +03:00
Danila Poyarkov 1ed7de6056 chore(lint): reject broad unknown type assertions
- Add AST-based lint checks for broad unknown object assertions and local JsonObject aliases
- Centralize JsonObject in core and package-local MCP RPC JSON typing
- Replace baseline Record<string, unknown> assertions with named shared/domain types
2026-05-25 00:26:02 +03:00
Danila Poyarkov c4f33036b1 Merge remote-tracking branch 'origin/master' into SantosAlarcon/master
# Conflicts:
#	src/components/ui/AppSelect.vue
2026-05-24 15:52:52 +03:00
Danila Poyarkov 655a752662 fix: preserve tooltip and select composition 2026-05-24 15:48:08 +03:00
Danila Poyarkov abd795bd9a chore: format codebase 2026-05-24 12:15:29 +03:00
Danila Poyarkov 98616c8610 fix(canvas): render Figma-style corner smoothing 2026-05-22 23:05:24 +03:00
Danila Poyarkov ed8c62cefc feat(demo): showcase blend modes and masks 2026-05-22 22:40:00 +03:00
Santos Alarcón 091271b05f chore: code formatting 2026-05-22 20:24:02 +02:00
Santos Alarcón 5461b38ce5 More translations 2026-05-22 13:08:55 +02:00
Santos Alarcón Asensio 6b2de4ad7a
Merge branch 'open-pencil:master' into master 2026-05-22 11:27:48 +02:00
Santos Alarcón 74709ffc6c Fix in the tooltip and more i18n strings 2026-05-21 20:19:15 +02:00
Santos Alarcón f155aeb98e fix: AppSelect dropdown doesn't show 2026-05-21 15:19:16 +02:00
Danila Poyarkov a48b23129f fix(fig): stabilize engine test suite 2026-05-21 14:54:17 +03:00
Santos Alarcón 33e89b30d1 refactor: added tooltips and i18n new keys 2026-05-21 13:42:42 +02:00
Danila Poyarkov 1d15469c27 perf(fig): populate imported pages lazily 2026-05-19 20:55:43 +03:00
Danila Poyarkov 05c9a7a576 perf(io): defer imported page layout work 2026-05-19 20:50:17 +03:00
Danila Poyarkov 779e0e493f fix(mcp): improve agent-rendered JSX 2026-05-19 17:57:43 +03:00
Danila Poyarkov 3a85632f38 refactor(ui): localize tooltip labels 2026-05-19 15:41:38 +03:00
Danila Poyarkov 9293145cd6 fix(ui): keep provider settings tooltip working in WebKit 2026-05-19 15:17:48 +03:00
Danila Poyarkov 7d41e4ccf4 refactor(ui): use app tooltips consistently 2026-05-19 15:14:00 +03:00
Danila Poyarkov 8cb6d008fc fix(ai): allow custom OpenRouter models 2026-05-19 15:10:32 +03:00
Danila Poyarkov 8fbc2939e3 fix(properties): keep effect controls visible 2026-05-19 13:20:47 +03:00
Danila Poyarkov 83af1453d8 fix(fonts): load Google font previews 2026-05-19 13:02:15 +03:00
Danila Poyarkov 59550c1c07 fix(fonts): restore font picker selection 2026-05-19 12:52:11 +03:00
Danila Poyarkov e67c15ce42 fix(tauri): use native dirname for save paths 2026-05-19 12:03:21 +03:00
Danila Poyarkov e0d76ca72a fix(mcp): support scoped save paths
Co-authored-by: LCbeijing <LCbeijing@users.noreply.github.com>
2026-05-19 11:29:54 +03:00
Danila Poyarkov f4454aeb44 fix(mcp): report version mismatches 2026-05-19 11:12:01 +03:00
Danila Poyarkov c11d65fe55 Revert "Revert "fix(fig): preserve OpenPencil round trips""
This reverts commit 9ce6096fc1.
2026-05-19 00:23:23 +03:00
Danila Poyarkov 9ce6096fc1 Revert "fix(fig): preserve OpenPencil round trips"
This reverts commit 5ef8bbd93d.
2026-05-19 00:20:47 +03:00
Danila Poyarkov 5ef8bbd93d fix(fig): preserve OpenPencil round trips 2026-05-19 00:16:41 +03:00
Danila Poyarkov 79583056a5 fix(tauri): refresh updater signing key 2026-05-18 23:23:20 +03:00
Danila Poyarkov 7f7b000ff0 fix(app): populate all fig pages on open 2026-05-18 20:24:04 +03:00
Danila Poyarkov f3649550f2 chore: enable stricter oxlint rules 2026-05-18 18:58:30 +03:00
Danila Poyarkov 1c8fe1ee8b feat(editor): add outline stroke command 2026-05-17 20:18:14 +03:00
Danila Poyarkov 655f4fcb89 test(ui): cover boolean outline menu placement 2026-05-17 20:09:53 +03:00
Danila Poyarkov d927a2b443 feat(text): share outline conversion for paths 2026-05-17 19:45:39 +03:00
Danila Poyarkov 5ddd0b966f fix(app): wire renderer into figma api 2026-05-17 18:49:35 +03:00
Danila Poyarkov dc93ec54c5 feat(editor): add flatten selection command 2026-05-17 18:27:54 +03:00
Danila Poyarkov 365c6f70d0 refactor(app): centralize demo color palette 2026-05-17 18:08:48 +03:00
Danila Poyarkov 92328a21d8 build(lint): enforce shared black color constants 2026-05-17 18:05:07 +03:00
Danila Poyarkov 104114cb3f feat(ui): add boolean operations panel control 2026-05-17 17:44:48 +03:00