- Let nested actions claim tooltip ownership from parent controls
- Keep only one tooltip visible across pointer and keyboard focus
- Cover composite property fields and preserve WebKit behavior
Enable CanvasKit half-leading for solid and gradient paragraph rendering while preserving the existing font fallback system.\n\nCo-authored-by: jongwong <jongwong@aliyun.com>
- Section titles are no longer collapsible triggers — they render as static
text like Figma, so clicking a section header no longer collapses it
- Drop the 'Font features' group title; ligature/kerning toggles sit
directly in the Typography flow
- Update panel specs and snapshots for always-expanded sections
- Isolate the Copy/Paste submenu scenario from prior context-menu state
- Close shared fixture pages safely when a project-level skip bypasses setup
- Refresh the reviewed typography panel snapshot
- Keep the development MCP bridge alive until Vite shuts down
- Ignore empty host-font responses and cover current browser font policy
- Update interaction selectors, menu labels, resize accounting, and reviewed UI snapshots
- Update design/properties panel snapshots for the 24px control height and
section header bands, and the appearance field test to expect 24px
- Add a light-theme regression test asserting the canvas ruler theme tracks
the active theme tokens instead of staying on the dark fallback
- Let virtualized Layer rows fill the panel width\n- Remove extra root indentation while preserving hierarchy depth\n- Keep icon menu labels beside their icons\n- Refresh reviewed panel snapshots
- Keep Fill and Effects settings inside their shared content rails\n- Use panel spacing tokens instead of ad-hoc offsets\n- Preserve selection when Escape dismisses overlays\n- Label the Font settings trigger for assistive technology
- Restore the empty collection call to action\n- Fill the table width and prevent variable type hint clipping\n- Let dialogs and popup menus handle Escape before editor shortcuts
- Keep embedded number fields from crowding color values\n- Cover paint-row geometry in property-panel E2E tests\n- Refresh the reviewed typography panel snapshot for visible variable actions
- Keep Apply variable actions visible with restrained hover treatment
- Preserve the shared 26px NumberField height in column layouts
- Anchor variable pickers to their trigger instead of the zero-size contents wrapper
- Cover control dimensions, action visibility, and popover placement end to end
- Match numeric keypad input by generated key so NumLock-off navigation remains available
- Coalesce buffered opacity updates into one undo interaction
- Add end-to-end coverage for digit composition, modifiers, and undo
- Theme Variables, collaboration, presence, and provider status states\n- Expose semantic default, resizing, following, connection, and tone attributes\n- Remove the final dynamic-state allowlist and document enforced Vue template rules
- Share typed toolbar slots across desktop, mobile, flyout, and action controls\n- Theme alignment cells, fill tabs, and gradient stop states\n- Expose semantic active and disabled state and retire migrated allowlist entries
- Lock representative property and Layer Tree states with visual snapshots\n- Cover keyboard tab order and keyboard activation for panel controls\n- Reject template-time UI hooks and raw SVG app icons through parsed Vue ASTs
- Add case, justification, vertical alignment, truncation, max-lines, and OpenType controls
- Apply visual text case and measured vertical offsets in CanvasKit rendering
- Preserve nullable preview values, shared text styles, font demand, and .fig metadata
- Add panel, engine, and canvas visual coverage
- Model imported fill, stroke, text, effect, and grid style definitions and references
- Add SDK and app selectors with mixed-selection batching, detach-on-edit, and undo restoration
- Preserve style definitions through .fig export and extracted subgraphs
- Document the shared-style composable and update compatibility coverage
- Add mixed-selection cap, join, and miter-limit controls with one-step undo
- Apply node miter limits across CanvasKit stroke paths and vector outline caches
- Preserve imported and edited stroke geometry through Plugin API and .fig roundtrips
- Add SDK-owned mixed constraint state, pin controls, and undo-aware updates
- Apply Figma constraint modes recursively during frame resize previews and commits
- Document and test the control, multi-selection behavior, and resize geometry
- Register CanvasKit faces under their source family names so CJK and Arabic glyphs paint instead of only shaping metrics
- Add deterministic CJK, Arabic, mixed-script, first-paint, and interactive fallback image oracles
- Cover typed and tool-created text parity with a repository-owned Noto CJK subset
Fixes#324Fixes#291
- Gate ingest rendering until required faces and script fallbacks resolve
- Key CanvasKit artifacts by font registration generation and invalidate affected nodes
- Load character-aware remote subsets with cumulative coverage and exact render aliases
- Use BCP-47 language hints for shaping and CJK fallback order
- Keep baked .fig glyphs as an exhaustion-only fallback
- Remove FillPickerRoot and useFillPicker from source, exports, and localized docs
- Replace the final PanelRow consumer with accessible auto-layout controls
- Document the canonical panel and fill composition contracts
- Rebuild Typography, Variant, Mask, and Page sections with shared labeled fields
- Expose semantic empty states across list sections
- Replace section test hooks with accessible roles and add focused visual coverage
- Share compact item-row and paint-field anatomy across Fill, Stroke, Effects, and Export
- Add COLOR binding transactions with picker rollback and one-step undo
- Replace bespoke panel test hooks with semantic selectors and visual coverage
- Add accessible OkHCL channel sliders and use Reka ColorSlider for standard color channels
- Split fill state and binding-aware swatches from application popover composition
- Add keyboard-operable gradient stops, shared demos, generated docs, and focused coverage
- Add a reactive useColorModel API for format state, channel edits, Reka bridges, and slider presentation
- Move the app color panel and ColorInputRoot onto the shared model and remove superseded picker helpers
- Document the public contract and cover precise RGB plus OkHCL intent round trips
- 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
- 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
* 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>
Figma expects derivedTextData glyph advance values normalized to
font-size units (advance / fontSize), not absolute pixel values.
This caused pasted text to appear horizontally mirrored because
Figma multiplied the already-absolute advances by fontSize again.
- useEditorSetup() / useEditorSetupWithClear() in tests/e2e/fixtures.ts
- Migrated 17 specs to shared fixture, removing ~300 lines of boilerplate
- Moved getSelectedNode/getSelectedNodes to shared store helpers
- Replaced inline getSelectedNode in 4 specs with shared import
- Test duplication: 329 → 308 clones (10.12% → 9.08%)
- Add &no-rulers to all E2E test URLs that use &no-chrome
- Regenerate snapshots without ruler ticks/selection badges
- Remove stale snapshot dirs from old test paths