- Replace duplicated width and height binding branches with shared semantic fields
- Switch edited Hug and Fill dimensions to Fixed in one reversible transaction
- Add typed LayoutControls actions, documentation, and visual coverage
- Move independent-corner presentation and batched mutations into AppearanceControls
- Rebuild Position and Appearance with aligned panel grids and semantic locators
- Replace type text headers with compact node icons and add visual coverage
- Show one variable picker affordance with a quiet identity pill and accessible combobox
- Keep bound NumberField focus non-destructive until the first value mutation
- Prefer accessible and semantic test selectors over compound test IDs
- Add headless PropertySection and accessible SegmentedControl anatomy
- Make PropertyList controlled and exactly typed with an undo-aware editor adapter
- Consolidate SDK metadata loaders and link untranslated docs to canonical pages
- Add provider-driven binding state, picker composition, and edit policies
- Integrate NumberField interactions with binding transactions and cancellation
- Migrate numeric variable fields and document the public API
- Remove ScrubInput aliases, app wrappers, test hooks, and translated legacy references
- Share a Tailwind NumberField demo between Storybook and VitePress
- Generate component API tables from Vue metadata and type-check examples with Twoslash
- Upgrade the docs stack to VitePress 2
- Add pointer scrubbing, keyboard stepping, mixed and bound state contracts
- Parse safe numeric expressions and preserve deprecated ScrubInput aliases
- Cover the primitive with Storybook, SDK docs, unit tests, and E2E tests
- Introduce Nuxt UI-style Tailwind Variants themes and 26px panel primitives\n- Align section and field action rails across labeled property rows\n- Add Storybook with dark/light states, accessibility tooling, and CI build coverage\n- Standardize UI acronym casing across app and Vue SDK types
- Add selection mask commands, shortcuts, context-menu access, and mask type controls
- Add mixed-selection blend mode editing with single-step undo
- Document the headless mask composable and localize the new controls
- Forward data-test-id attrs through rendered controls instead of exposing test-id props
- Move shell chrome components into the Shell namespace
- Remove internal planning docs from published VitePress docs
- Add guardrails and contributor guidance for test hooks and component placement
* 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>
The `commands.duplicate` action was previously translated as "复制" (Copy) in `zh-CN.json`, identically to the native `menu.copy` action. This caused the context menu to display two identical "复制" options, creating confusion about which action copies to the clipboard and which one clones the object directly on the canvas.
This commit updates the translation of `commands.duplicate` to "克隆" (Clone) to accurately reflect its behavior and clearly distinguish it from the native clipboard copy command.
* 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>
* 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>
- 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
- Start selection moves only after a 4px screen-space threshold
- Keep click selection from previewing or committing 1px geometry changes
- Cover below-threshold and intentional move behavior