Cause: in the browser, exporting several layers started one download
per file. Chrome holds back every download after the first until the
site is allowed automatic downloads, so the other files didn't arrive.
Fix: file.exportLayers takes `zip: true` (without `dir`) and returns the
files together as one base64 zip `data`, written by a new
figcraft_format::zip_files (the workspace's existing zip dependency).
The UI asks for it when the platform saves by downloads and hands over
one `<document name>.zip`.
Test: ui-egui several_layers_download_as_one_zip (one download, a zip
holding both layers). It fails on the previous commit (two downloads).
Cause: in the web build no file has a path, so Save goes to Save as, and
Save as and every Export go through Services::pick_save / pick_folder.
WebServices keeps the trait's defaults there, which return no path, so
the callback never ran: no download, no message. Only Variables ▸ Export
used the platform's downloads (#368).
Fix: where services.downloads() is true, those routes run their command
without a path and hand the bytes to services.download():
- engine: file.saveAs takes `data: true` and returns the .figcraft file
as base64 `data` with its `name`, marking it saved (no path, nothing
remembered). Programmatic Save as without either still asks for a path.
- ui: FigApp::download decodes a result's `data` and says when nothing
arrived; Save as, one-off export (PNG/JPG/WebP/SVG/PDF), .fig export,
frames to PDF and multi-layer export (one download per file) use it.
Tests: engine save_as_data_hands_back_the_file_and_marks_it_saved;
ui-egui web_save_tests (save, export, refused download) with the fake
browser Services from variables_export_ui_tests. The UI tests fail
without the fix.
A component set took its name from the first variant's name up to the first '/' or '=',
so variants named "State=Default, Size=Small" made a set called "State". The set now
takes the path before the last '/' ("Button/State=Hover" -> "Button",
"Icons/Arrow/Dir=Up" -> "Icons/Arrow"), keeps a plain name as is ("Button"), and
otherwise gets the next numbered default ("Component 1"), as a new component does.
Duplicate and Alt-drag copied a main component as a second main. Within
its file the copy is now an instance of it (components::copy_in_file,
shared by edit.duplicate and the Alt-drag copy), as Copy then Paste
already was. A variant duplicated in its set stays a new variant.
Layer data written with node.set {props: {pluginData}} was saved but no
command read it back. node.get (the command, the control method and MCP
get_node) and document.inspect now report pluginData as {key: value}
when a layer or page has any, so a tool keeping state in a file can
read it on its next run. Widget keys are included: the plugin rules
only stop plugins writing them, and widget.list already reports that
state.