openpencil/tests/engine
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
..
acp chore: enable stricter oxlint rules 2026-05-18 18:58:30 +03:00
app feat(demo): showcase text decoration fidelity 2026-05-26 14:24:13 +03:00
cli chore: enable stricter oxlint rules 2026-05-18 18:58:30 +03:00
clipboard chore: format codebase 2026-05-24 12:15:29 +03:00
color test(engine): move root tests into domains 2026-05-16 12:30:27 +03:00
editor fix(resize): scale group and boolean children 2026-06-06 17:05:09 +03:00
figma/api chore(lint): reject broad unknown type assertions 2026-05-25 00:26:02 +03:00
geometry fix(io): include text decoration export bounds 2026-05-27 00:53:47 +03:00
hit-test chore(tests): enforce non-null assertion rule 2026-05-06 11:44:18 +03:00
icons test(engine): move root tests into domains 2026-05-16 12:30:27 +03:00
io feat(export): persist per-node export settings 2026-06-06 20:05:34 +03:00
kiwi chore(lint): reject broad unknown type assertions 2026-05-25 00:26:02 +03:00
layout fix(layout): preserve imported instance child bounds 2026-06-03 15:47:17 +03:00
lint test(engine): move root tests into domains 2026-05-16 12:30:27 +03:00
mcp fix(mcp): harden tool responses 2026-05-31 18:14:10 +03:00
pen chore(tests): narrow no-empty-function exceptions 2026-05-06 12:32:54 +03:00
profiler chore: format codebase 2026-05-24 12:15:29 +03:00
random test(engine): move root tests into domains 2026-05-16 12:30:27 +03:00
render feat(design-jsx): support components and instances 2026-05-31 18:33:56 +03:00
scene-graph chore(lint): reject broad unknown type assertions 2026-05-25 00:26:02 +03:00
snap test(engine): move root tests into domains 2026-05-16 12:30:27 +03:00
tauri fix(fig): stabilize engine test suite 2026-05-21 14:54:17 +03:00
text fix(text): handle Hangul IME composition (#319) 2026-06-06 07:21:56 +03:00
tools chore(lint): reject broad unknown type assertions 2026-05-25 00:26:02 +03:00
vector chore: format codebase 2026-05-24 12:15:29 +03:00
vue chore: format codebase 2026-05-24 12:15:29 +03:00
perf.bench.ts chore: enable stricter oxlint rules 2026-05-18 18:58:30 +03:00