mirror of
https://github.com/storytold/figcraft.git
synced 2026-11-07 02:51:41 +00:00
M6: figcraft-cli connect publish publishes code connect mappings to a design file (#1643)
* M6: figcraft-cli connect publish publishes code connect mappings to a design file figcraft-cli connect publish <file.figcraft> [--mappings figcraft.connect.json] [--dry-run] links each entry's component or set by name through codeConnect.import, warns about and skips links that don't fit, saves the file in place through the engine's file.save, and exits 1 when nothing links. Gap 16. * M6: the generated man pages include connect and connect publish
This commit is contained in:
1 parent
10c66678c3
commit
84e5278209
9 files changed
+144
-13
No files matched your search
+2
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
**Stage: alpha** · next: beta, ~26 points and ~162–292 h away (one Opus 5.5 agent; ~33–67 h wall-clock with 5 agents)
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (while a drag only moves a large selection, its outlines, box, slots, spacing and Design panel values are worked out once and shifted, not rebuilt on every move (#1571); Shift+Cmd/Ctrl+Enter on a selected board shape adds the next shape already joined to it, and goes on typing (#1597); an imported or pasted file finds an installed family under the other names its fonts give it, and with other case and spacing, instead of reporting it missing (#1292); a development example generates figcraft-cli's shell completions and man pages from its clap definitions (part of #1396); `.fig` ZIP image reads share a total byte budget and count skipped images in the import warnings (#1076); German terminology corrections (M10): gradient stops, OpenType stylistic sets and the Home-screen preference; every file a command reads whole by path is a regular file within a cap that fits it: documents 16 GiB, placed images 256 MiB, CLI scripts 1 GiB; a component set's property rows fit the 240 px right panel: variant values wrap two to a line there and every + and − stays in view; the server key is checked by comparing SHA-256 hashes of the presented and the kept key, as the deployment guide says (#1130); opening a file contacts no team library server it names until you check it (#1097); a hostile server can no longer grow a client's memory with made-up people, and a session remembers a bounded number of deleted layer ids (#1121); `doc.list` gives AI agents and plugins the open files' names, not their paths (#1117); with a large selection, frames where nothing changed reuse the selection's outlines, box and slots and the Design panel's instance and position values instead of working them out again (#1571); team libraries and plugins larger than the 16 MiB download burst can be downloaded again, more slowly (#1089); a batch keeps one undo snapshot, not one per step (#1099); files nested 256 levels deep import on the browser's 1 MB stack (#1101); SVG and PDF exports count pattern sources and the renders they embed from the layer's depth, so one cap bounds a whole export (#1082); an explicit variables file path (`variable.import`, the `variables` of `file.open` and `file.import`) is refused past 256 MiB or when it isn't a regular file, as the sibling `variables.json` already was (part of #1114); joining and leaving a file nobody is editing reads and writes nothing on the server: it stays loaded a minute after everyone leaves (#1074); opening a `.figcraft` file unpacks at most 6 GiB in all (2 GiB in the browser), every area together (#1079); hovering the canvas hit-tests once per pointer move, not on every frame, and moving an auto layout frame no longer lays out its contents again on every move (#1571); `.fig` instance expansion no longer walks variable nodes for every instance or copies every outer override row into each nested instance (#1077); `comment.people` lists names that were only @mentioned, and `comment.next` and `comment.prompt` carry the mentions too (#1415); grid auto-placement looks for free cells a 64-bit word at a time and skips what blocks it, with the same placements (#1083); with the pen out, Enter or a double-click ends the path and edits the vector with the Move tool instead of drawing on (#1430); a recovery copy made while a version is previewed holds the file, not the version shown; a FigJam `.jam` file opens as a board (#1342); the Actions palette offers only commands that run from it, and no second row reading like a menu item: Place image opens the file dialog (#1574); share links can comment: a third access level beside edit and view, whose holder sees the file live and adds, replies to, reacts to and resolves comments but can't change the design; Preferences ▸ Scroll wheel zoom: a mouse wheel zooms the canvas about the pointer and Cmd/Ctrl+wheel scrolls (#1390); `comment.list {mentions}` lists only the threads that @mention a name (#1415); in the browser an internal error that stops the page keeps its unsaved files, which open again on reload (#1123); boolean operations refuse input past their caps, and a boolean group past them draws without its result (#1104); figcraft-cli parses its arguments with clap, following RFC-0012 (#1396); the selection and hover outlines of rounded rectangles and frames follow their corners (#1506); component properties keep the order they were made in when one is renamed, and Make slot asks for the slot's name first (#1536, #1537); the browser build publishes files to a server, so Share ▸ Online and Publish to server work there (#917); the browser build uses the fonts installed on the computer once asked, each read when text uses it (#1051); SVG import reads `color()` and `color-mix()` colours, with a note and a grey for those it can't (#1550); the portable Windows zip keeps its settings, recovery copies and plugins in `config\` beside the executable, starting with an installed copy's prLine truncated
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (`figcraft-cli connect publish` publishes a code repository's code connect mappings to a design file (gap 16); while a drag only moves a large selection, its outlines, box, slots, spacing and Design panel values are worked out once and shifted, not rebuilt on every move (#1571); Shift+Cmd/Ctrl+Enter on a selected board shape adds the next shape already joined to it, and goes on typing (#1597); an imported or pasted file finds an installed family under the other names its fonts give it, and with other case and spacing, instead of reporting it missing (#1292); a development example generates figcraft-cli's shell completions and man pages from its clap definitions (part of #1396); `.fig` ZIP image reads share a total byte budget and count skipped images in the import warnings (#1076); German terminology corrections (M10): gradient stops, OpenType stylistic sets and the Home-screen preference; every file a command reads whole by path is a regular file within a cap that fits it: documents 16 GiB, placed images 256 MiB, CLI scripts 1 GiB; a component set's property rows fit the 240 px right panel: variant values wrap two to a line there and every + and − stays in view; the server key is checked by comparing SHA-256 hashes of the presented and the kept key, as the deployment guide says (#1130); opening a file contacts no team library server it names until you check it (#1097); a hostile server can no longer grow a client's memory with made-up people, and a session remembers a bounded number of deleted layer ids (#1121); `doc.list` gives AI agents and plugins the open files' names, not their paths (#1117); with a large selection, frames where nothing changed reuse the selection's outlines, box and slots and the Design panel's instance and position values instead of working them out again (#1571); team libraries and plugins larger than the 16 MiB download burst can be downloaded again, more slowly (#1089); a batch keeps one undo snapshot, not one per step (#1099); files nested 256 levels deep import on the browser's 1 MB stack (#1101); SVG and PDF exports count pattern sources and the renders they embed from the layer's depth, so one cap bounds a whole export (#1082); an explicit variables file path (`variable.import`, the `variables` of `file.open` and `file.import`) is refused past 256 MiB or when it isn't a regular file, as the sibling `variables.json` already was (part of #1114); joining and leaving a file nobody is editing reads and writes nothing on the server: it stays loaded a minute after everyone leaves (#1074); opening a `.figcraft` file unpacks at most 6 GiB in all (2 GiB in the browser), every area together (#1079); hovering the canvas hit-tests once per pointer move, not on every frame, and moving an auto layout frame no longer lays out its contents again on every move (#1571); `.fig` instance expansion no longer walks variable nodes for every instance or copies every outer override row into each nested instance (#1077); `comment.people` lists names that were only @mentioned, and `comment.next` and `comment.prompt` carry the mentions too (#1415); grid auto-placement looks for free cells a 64-bit word at a time and skips what blocks it, with the same placements (#1083); with the pen out, Enter or a double-click ends the path and edits the vector with the Move tool instead of drawing on (#1430); a recovery copy made while a version is previewed holds the file, not the version shown; a FigJam `.jam` file opens as a board (#1342); the Actions palette offers only commands that run from it, and no second row reading like a menu item: Place image opens the file dialog (#1574); share links can comment: a third access level beside edit and view, whose holder sees the file live and adds, replies to, reacts to and resolves comments but can't change the design; Preferences ▸ Scroll wheel zoom: a mouse wheel zooms the canvas about the pointer and Cmd/Ctrl+wheel scrolls (#1390); `comment.list {mentions}` lists only the threads that @mention a name (#1415); in the browser an internal error that stops the page keeps its unsaved files, which open again on reload (#1123); boolean operations refuse input past their caps, and a boolean group past them draws without its result (#1104); figcraft-cli parses its arguments with clap, following RFC-0012 (#1396); the selection and hover outlines of rounded rectangles and frames follow their corners (#1506); component properties keep the order they were made in when one is renamed, and Make slot asks for the slot's name first (#1536, #1537); the browser build publishes files to a server, so Share ▸ Online and Publish to server work there (#917); the browser build uses the fonts installed on the computer once asked, each read when text uses it (#1051); SVG import reads `color()` and `color-mix()` colours, with a note and a grey for those it can't (#1550); the portable Windows zip keeps its Line truncated
|
||||
|
||||
FigCraft is a clean-room, open-source, pure-Rust reimplementation of the Figma design workflow,
|
||||
native on macOS, Windows, Linux and FreeBSD and in the browser, fully drivable by agents through
|
||||
@@ -356,6 +356,7 @@ Ranked; detail and milestones in [`docs/roadmap.md`](docs/roadmap.md).
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-30 | minor | M6, gap 16: `figcraft-cli connect publish <file.figcraft> [--mappings FILE] [--dry-run]` publishes a code repository's code connect mappings (`figcraft.connect.json`, as Export code connections writes it) to a design file: each link connects the components or sets of that name to their code for its language; links that don't fit are warnings and are skipped; the file is saved in place through the engine's `file.save` (whole or not at all, version history kept); a dry run saves nothing; nothing linked is exit code 1. Numbers unchanged. Tests: `files::tests::connect_publish_links_components_and_saves_the_file`, `args` tests. |
|
||||
| 2026-10-30 | minor | M8, #1571 (part 3): the first frame after each move of a drag rebuilt the selection's outlines and box, its slots, the selection spacing and the Design panel's values from the new document. Now the engine notes when a drag has only moved the selected layers, each by the same offset from where it started (`Interaction::moved_by`: not for a layer in an auto layout flow, nor an Alt copy), and the canvas and panel work those out once from the document the drag started from and shift what depends on position (the outlines on screen, the box); the Design panel shows that document's values, all but the position fields, which follow the move. The Export section's values are kept too. Nothing looks different. Dragging 648 of about 1,900 synthetic layers (release build, two runs each): the UI pass 2.10 to 1.44 ms a frame, CPU 135% to 117% of a core; the Design panel 0.9 to 0.5 ms, the overlays 0.5 to 0.3 ms. Not yet: the engine's preview of each move (copying and normalising the document) and the canvas texture's whole upload. Tests: `moved_by_tests.rs`, `canvas/selection_cache_tests.rs`. |
|
||||
| 2026-10-30 | minor | M12, #1597 (the third point): Shift+Cmd/Ctrl+Enter (`board.nextSticky`) now works on a selected or edited shape too: it adds the next shape of the same kind, size and colour on its right (or the `side` given), joined to it by a connector and with typing going on in it, as the "+" beside it does (`board.addConnected`, #1631). On a sticky it adds a sticky beside it without a connector, as before. Numbers unchanged. Tests: `board_connected_tests::the_next_sticky_shortcut_joins_the_next_shape_and_leaves_stickies_as_they_were`, `board_ui_tests::the_next_shape_from_the_keyboard_comes_connected_and_goes_on_typing`. |
|
||||
| 2026-10-30 | minor | M12, #1597: boards create the next shape already connected. A selected sticky or shape shows a "+" beside each of its four sides (clear of the contextual bar, the size label and the toolbar); hovering one shows where the next goes and the connector to it; clicking makes a sticky, or a shape of the same kind, size and colour, 80 units away on that side, joined to it by a connector the way the Connector tool makes one, and starts typing in it. One undo step takes both away. `board.addConnected {id?, side?, edit?}` does it from commands, MCP and plugins. Shift+Cmd/Ctrl+Enter still adds a sticky without a connector. Tests: `board_connected_tests.rs` (engine), `board_connect_tests.rs` (the handles, headless). |
|
||||
|
||||
@@ -41,6 +41,11 @@ pub enum Command {
|
||||
#[command(subcommand)]
|
||||
command: PluginCommand,
|
||||
},
|
||||
/// Publish a code repository's component mappings (code connect) to a design file
|
||||
Connect {
|
||||
#[command(subcommand)]
|
||||
command: ConnectCommand,
|
||||
},
|
||||
Perf(PerfArgs),
|
||||
}
|
||||
|
||||
@@ -228,6 +233,31 @@ pub struct PublishArgs {
|
||||
pub server: String,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum ConnectCommand {
|
||||
Publish(ConnectPublishArgs),
|
||||
}
|
||||
|
||||
/// Link a design file's components to their code as a figcraft.connect.json says, and save it
|
||||
///
|
||||
/// The mappings are what Dev Mode's Export code connections writes, kept in the code repository:
|
||||
/// each links a component (or component set) of that name to its code component for a language.
|
||||
/// Entries that don't fit (an unknown component, a bad template) are reported and skipped.
|
||||
#[derive(Args)]
|
||||
pub struct ConnectPublishArgs {
|
||||
/// The design file (.figcraft), saved in place
|
||||
#[arg(value_name = "FILE")]
|
||||
pub file: String,
|
||||
/// The mappings
|
||||
#[arg(long, value_name = "FILE", default_value = "figcraft.connect.json")]
|
||||
pub mappings: String,
|
||||
/// Report what would be linked without saving the file
|
||||
#[arg(long)]
|
||||
pub dry_run: bool,
|
||||
#[command(flatten)]
|
||||
pub roots: RootArgs,
|
||||
}
|
||||
|
||||
/// Measure the hot paths on synthetic documents and print Markdown tables
|
||||
#[derive(Args)]
|
||||
pub struct PerfArgs {
|
||||
@@ -326,7 +356,7 @@ mod tests {
|
||||
use clap::CommandFactory;
|
||||
use clap::error::ErrorKind;
|
||||
|
||||
use super::{Cli, Command, PluginCommand, parse};
|
||||
use super::{Cli, Command, ConnectCommand, PluginCommand, parse};
|
||||
|
||||
fn try_parse(args: &[&str]) -> Result<Cli, clap::Error> {
|
||||
parse(std::iter::once("figcraft-cli").chain(args.iter().copied()).map(Into::into).collect())
|
||||
@@ -434,6 +464,14 @@ mod tests {
|
||||
panic!("plugin")
|
||||
};
|
||||
assert_eq!((a.folder.as_str(), a.server.as_str()), ("dist/counter", "ws://h:47900/"));
|
||||
let Command::Connect { command: ConnectCommand::Publish(a) } = ok(&["connect", "publish", "ui.figcraft"]) else { panic!("connect") };
|
||||
assert_eq!((a.file.as_str(), a.mappings.as_str(), a.dry_run), ("ui.figcraft", "figcraft.connect.json", false));
|
||||
let Command::Connect { command: ConnectCommand::Publish(a) } =
|
||||
ok(&["connect", "publish", "ui.figcraft", "--mappings", "code/map.json", "--dry-run"])
|
||||
else {
|
||||
panic!("connect")
|
||||
};
|
||||
assert_eq!((a.mappings.as_str(), a.dry_run), ("code/map.json", true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -459,6 +459,34 @@ pub fn run_script(script: &str, input: Option<&str>, out: Option<&str>) -> CliRe
|
||||
Ok(ExitCode::SUCCESS)
|
||||
}
|
||||
|
||||
/// Link the components of `file` (a `.figcraft`) to their code as `mappings` (a
|
||||
/// `figcraft.connect.json`) says, and save it in place unless `dry_run`. Prints what was linked;
|
||||
/// entries that were skipped are warnings on stderr, and linking nothing fails (exit code 1).
|
||||
pub fn connect_publish(file: &str, mappings: &str, dry_run: bool) -> CliResult<ExitCode> {
|
||||
if ext(file) != "figcraft" {
|
||||
return Err(CliError::Usage(format!("{file}: connect publish saves code links in a .figcraft file")));
|
||||
}
|
||||
let mut s = Session::empty();
|
||||
s.execute("file.open", &json!({"path": file})).map_err(|e| CliError::Failed(format!("can't open {file}: {e}")))?;
|
||||
let r = s.execute("codeConnect.import", &json!({"path": mappings})).map_err(|e| CliError::Failed(format!("{mappings}: {e}")))?;
|
||||
for w in r["warnings"].as_array().into_iter().flatten().filter_map(Value::as_str) {
|
||||
eprintln!("warning: {w}");
|
||||
}
|
||||
let linked = r["linked"].as_u64().unwrap_or(0);
|
||||
if linked == 0 {
|
||||
eprintln!("error: no component of {file} matches a link in {mappings}: nothing published");
|
||||
return Ok(ExitCode::from(1));
|
||||
}
|
||||
if dry_run {
|
||||
println!("{linked} component link(s) from {mappings} would be published to {file} (dry run: not saved)");
|
||||
return Ok(ExitCode::SUCCESS);
|
||||
}
|
||||
// The engine's save: written whole or not at all, with the file's version history.
|
||||
s.execute("file.save", &json!({})).map_err(|e| CliError::Failed(format!("can't save {file}: {e}")))?;
|
||||
println!("published {linked} component link(s) from {mappings} to {file}");
|
||||
Ok(ExitCode::SUCCESS)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -557,6 +585,42 @@ mod tests {
|
||||
std::fs::remove_dir_all(dir).unwrap();
|
||||
}
|
||||
|
||||
/// `connect publish` links the file's components as the mappings say and saves the file in
|
||||
/// place; a dry run saves nothing, and mappings that link nothing fail.
|
||||
#[test]
|
||||
fn connect_publish_links_components_and_saves_the_file() {
|
||||
let dir = temp_dir("connect");
|
||||
let file = dir.join("ui.figcraft");
|
||||
let file = file.to_str().unwrap();
|
||||
let mut s = Session::new();
|
||||
let frame = s.execute("frame.create", &json!({"x": 0, "y": 0, "width": 100, "height": 40, "name": "Card"})).unwrap();
|
||||
s.execute("component.create", &json!({"ids": [frame["id"]]})).unwrap();
|
||||
s.execute("file.saveAs", &json!({"path": file})).unwrap();
|
||||
let mappings = dir.join("figcraft.connect.json");
|
||||
let link =
|
||||
|component: &str| json!({"component": component, "language": "react", "importPath": "@acme/ui", "name": "Card", "template": "<Card />"});
|
||||
std::fs::write(&mappings, json!({"version": 1, "links": [link("Card"), link("Nope")]}).to_string()).unwrap();
|
||||
let mappings = mappings.to_str().unwrap();
|
||||
let links = || {
|
||||
let mut s = Session::empty();
|
||||
s.execute("file.open", &json!({"path": file})).unwrap();
|
||||
let doc = s.doc().unwrap();
|
||||
doc.nodes().filter(|n| n.name == "Card").map(|n| n.code_links.len()).sum::<usize>()
|
||||
};
|
||||
|
||||
assert_eq!(connect_publish(file, mappings, true).unwrap(), ExitCode::SUCCESS);
|
||||
assert_eq!(links(), 0, "a dry run saves nothing");
|
||||
assert_eq!(connect_publish(file, mappings, false).unwrap(), ExitCode::SUCCESS, "an unknown component is only a warning");
|
||||
assert_eq!(links(), 1, "Card is linked to its code in the saved file");
|
||||
|
||||
let none = dir.join("none.json");
|
||||
std::fs::write(&none, json!({"version": 1, "links": [link("Nope")]}).to_string()).unwrap();
|
||||
assert_eq!(connect_publish(file, none.to_str().unwrap(), false).unwrap(), ExitCode::from(1));
|
||||
assert!(matches!(connect_publish(dir.join("ui.svg").to_str().unwrap(), mappings, false), Err(CliError::Usage(_))));
|
||||
assert!(matches!(connect_publish(file, dir.join("missing.json").to_str().unwrap(), false), Err(CliError::Failed(_))));
|
||||
std::fs::remove_dir_all(dir).unwrap();
|
||||
}
|
||||
|
||||
/// A script resolves `{"$step": n}` like the same commands through `command.batch`. Each step
|
||||
/// used to run on its own, so the reference reached `parent` unresolved, `node.create` read it
|
||||
/// as "no parent" and the layer landed on the page — with every step reporting success.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! `figcraft-cli`: FigCraft without a window. Inspect, export, convert and render documents, run
|
||||
//! command scripts, list commands, serve MCP on stdio, publish plugins to a server's plugin
|
||||
//! directory and measure performance.
|
||||
//! directory, publish code connect mappings to a design file and measure performance.
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
||||
|
||||
mod args;
|
||||
@@ -10,7 +10,7 @@ mod perf;
|
||||
use std::io::Write;
|
||||
use std::process::ExitCode;
|
||||
|
||||
use args::{Cli, Command, PluginCommand, RootArgs};
|
||||
use args::{Cli, Command, ConnectCommand, PluginCommand, RootArgs};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
/// The automation roots a file subcommand was given, if any. Omitting both leaves the run
|
||||
@@ -93,6 +93,7 @@ fn run(cli: Cli) -> CliResult<ExitCode> {
|
||||
// `--attach` conflicts with the roots: the app runs the commands, only its own roots can confine them.
|
||||
Command::Mcp(a) => mcp(a.attach.as_deref(), roots_of(&a.roots)?),
|
||||
Command::Plugin { command: PluginCommand::Publish(a) } => publish_plugin(&a.folder, &a.server),
|
||||
Command::Connect { command: ConnectCommand::Publish(a) } => confined(&a.roots, || files::connect_publish(&a.file, &a.mappings, a.dry_run)),
|
||||
Command::Perf(a) => {
|
||||
let sizes: Vec<usize> = a.layers.into_iter().map(|n| n.clamp(10, 2_000_000)).collect();
|
||||
let instances: Vec<usize> = a.instances.into_iter().filter(|n| *n > 0).map(|n| n.min(100_000)).collect();
|
||||
|
||||
@@ -99,6 +99,8 @@ fn man_pages_cover_every_subcommand_without_synthetic_help_pages() -> std::io::R
|
||||
pages,
|
||||
[
|
||||
"figcraft-cli-commands.1",
|
||||
"figcraft-cli-connect-publish.1",
|
||||
"figcraft-cli-connect.1",
|
||||
"figcraft-cli-convert.1",
|
||||
"figcraft-cli-export.1",
|
||||
"figcraft-cli-info.1",
|
||||
@@ -122,6 +124,7 @@ fn man_pages_cover_every_subcommand_without_synthetic_help_pages() -> std::io::R
|
||||
("figcraft-cli-export.1", "--format"),
|
||||
("figcraft-cli-export.1", "--automation-read-root"),
|
||||
("figcraft-cli-plugin-publish.1", "--server"),
|
||||
("figcraft-cli-connect-publish.1", "--dry-run"),
|
||||
] {
|
||||
let text = fs::read_to_string(dir.join(page))?.replace("\\-", "-");
|
||||
assert!(text.contains(option), "{page} does not contain {option}");
|
||||
@@ -142,7 +145,7 @@ fn generation_creates_the_output_directory_and_can_run_again() -> std::io::Resul
|
||||
fs::write(&page, "old page")?;
|
||||
generator::generate(&output)?;
|
||||
assert_eq!(fs::read(page)?, first);
|
||||
assert_eq!(fs::read_dir(output)?.count(), 16);
|
||||
assert_eq!(fs::read_dir(output)?.count(), 18);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
+19
-1
@@ -1,7 +1,7 @@
|
||||
# figcraft-cli
|
||||
|
||||
FigCraft without a window: inspect, export, convert and render documents, run command scripts, list
|
||||
commands, serve MCP and measure performance. Build it with
|
||||
commands, serve MCP, publish plugins and code connect mappings, and measure performance. Build it with
|
||||
`cargo build --release -p figcraft-cli` (binary `target/release/figcraft-cli`).
|
||||
|
||||
The CLI follows [RFC-0012](https://github.com/storytold/craft-contrib/blob/main/rfcs/0012-cli-conventions.md),
|
||||
@@ -295,6 +295,24 @@ size and SHA-256. The server's key comes from the address or, better, from the
|
||||
lists); it is never printed. The bundle is checked before it is sent and again by the server. See
|
||||
[plugins.md](plugins.md#plugin-directories).
|
||||
|
||||
### `connect publish <file.figcraft> [--mappings FILE] [--dry-run]`
|
||||
|
||||
Publishes a code repository's component mappings to a design file: each link in the mappings file
|
||||
(default `figcraft.connect.json` in the current folder, the file Dev Mode's **Export code
|
||||
connections** and `codeConnect.export` write) connects every component or component set of that
|
||||
name to its code component for a language, replacing its link for that language, and the file is
|
||||
saved in place, written whole or not at all, with its version history. Links that don't fit (no
|
||||
component of that name, a bad template or language) are printed as warnings and skipped; if nothing
|
||||
is linked, nothing is saved and the exit code is 1. `--dry-run` reports what would be linked
|
||||
without saving. Run it from the repository, for example in CI after the mappings change:
|
||||
|
||||
```sh
|
||||
figcraft-cli connect publish ../design/ui.figcraft --mappings figcraft.connect.json
|
||||
```
|
||||
|
||||
It takes the automation roots like the other file subcommands. See
|
||||
[dev-mode-parity.md](dev-mode-parity.md) for code connect itself.
|
||||
|
||||
### `perf [--layers N[,N...]] [--instances N[,N...]]`
|
||||
|
||||
Builds synthetic documents of each size (default `1000,10000` layers: screens with an auto layout
|
||||
|
||||
@@ -18,7 +18,7 @@ repository, third-party integrations. Remaining: 1–2 h.
|
||||
| Ready for dev list, statuses, "Changed" after edits | done | the list filters Ready for dev, Changed, Completed or any, and badges changed ones (`inspect.readyForDev {status: changed}`, `changed` per item); only sections and frames at the top of a page or section take a status (others are refused) |
|
||||
| Focus view (one frame, previous / next) | done | |
|
||||
| Compare changes (before/after side by side or overlaid, property diffs) | done | against the snapshot taken when a frame was first changed after it was marked ready, or any saved version (`dev.compare {version}`; the view's Compare with menu); a frame that isn't ready for dev compares with versions |
|
||||
| Code connect (map components to repository code) | done | per language (React, Leptos, Vue, SwiftUI, Compose, ArkUI, Flutter): import path, name and a template with `{property}` and `{property ? "a" : "b"}` placeholders; instances (also inside generated layer trees) show the snippet with their values; edited in the Inspect panel with a live preview; `figcraft.connect.json` export/import; generated layer trees bring the import lines of the connected components they call (also in definitions, slots and swapped-in instances) once each at the top, per language. No CLI publishing mappings from the code repository |
|
||||
| Code connect (map components to repository code) | done | per language (React, Leptos, Vue, SwiftUI, Compose, ArkUI, Flutter): import path, name and a template with `{property}` and `{property ? "a" : "b"}` placeholders; instances (also inside generated layer trees) show the snippet with their values; edited in the Inspect panel with a live preview; `figcraft.connect.json` export/import; generated layer trees bring the import lines of the connected components they call (also in definitions, slots and swapped-in instances) once each at the top, per language. `figcraft-cli connect publish` publishes a repository's mappings to a design file (`--dry-run` to check them first) |
|
||||
| Plugins in Dev Mode (Jira, GitHub, etc.) | partial | FigCraft's own WASM plugins that only read run from the Inspect tab, with their panels (callbacks can only read); no third-party integrations |
|
||||
| Measurements (redlines on hover, Alt in the design) | done | distances between the selection and the hovered layer, its parent or a guide, auto layout padding and gaps (Shift+Alt); measurements kept on the canvas (`devMode.addMeasurement` between two layers, along a side or to the layer around it; stored on the layer, saved, synced, undoable) drawn in Dev Mode as labelled redlines that follow the layers, listed, placed and removed in the Inspect tab; a measurement whose other layer was deleted isn't drawn until an undo brings it back |
|
||||
| VS Code extension | out of scope | FigCraft's MCP server and control channel already serve design data, code and connected snippets to agents and editors |
|
||||
@@ -28,6 +28,7 @@ repository, third-party integrations. Remaining: 1–2 h.
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-30 | minor | Gap 16: `figcraft-cli connect publish <file.figcraft> [--mappings figcraft.connect.json] [--dry-run]` publishes a code repository's mappings to a design file (as the incumbent's Code Connect CLI publishes a repository's mappings): it links each entry's component or set by name, warns about and skips links that don't fit, saves the file in place through the engine's save (or not, with `--dry-run`) and fails when nothing links. Dev Mode breadth (~98%) and ready (~78%) unchanged at this rounding; hours unchanged |
|
||||
| 2026-10-26 | minor | Class names from layer names transliterate non-ASCII scripts (#1235): `class_name` runs names through `deunicode` (new dependency: pure Rust tables, zero deps, BSD-3-Clause), so Korean, Cyrillic, Latin-diacritic and CJK names become readable ASCII classes in HTML/CSS/React/Leptos output, component roots and `data-*` variant attributes (`rogeuin-hwamyeon`, `knopka`, `uber`, `an-niu`; before, every one of them came out `layer-`); symbols-only names take the node's kind (`.text`, `.frame`) instead of `layer-`, and the 40-character cut lands on a word boundary. ASCII-only names are unchanged. `html_codegen_tests` updated (its diacritics expectation improves from `layer-1-n-code-name` to `layer-1-unicode-name`) and extended. Percentages and hours unchanged |
|
||||
| 2026-10-24 | minor | Dev Mode refuses design edits from files dropped on the canvas (a dropped document still opens as a file of its own, and the drop overlay shows the Dev Mode note for other files), from the Pages list's add, rename, reorder, duplicate and delete, and from the Assets panel's place, drag, Alt-drop swap, style, Hide when publishing, Update, and adding, removing and swapping libraries. Percentages and hours unchanged |
|
||||
| 2026-10-24 | minor | Measurements kept on the canvas (gap 16): `devMode.addMeasurement {from?, to?, side?}` places one between two layers, along one side of a layer or (neither) to the layer around it, stored on the layer it starts from (saved in `.figcraft`, synced, undoable; old files open without them); Dev Mode draws them as labelled redlines that follow the layers, the Inspect tab lists them (those of the selected layer, or every one on the page), places and removes them; `devMode.removeMeasurement` and `devMode.measurements` remove and list them; a copy keeps those to layers copied with it. The layers panel is read-only in Dev Mode: rows select and expand, but don't rename, reorder, hide or lock. Breadth 97% → 98%, ready 76% → 78%, 2–4 h → 1–2 h |
|
||||
|
||||
+9
-5
@@ -29,7 +29,7 @@ No alpha blockers: the alpha gate in [`roadmap.md`](roadmap.md#alpha-gate) passe
|
||||
| 13 | Vector tail: none left (boolean hit testing on the result landed 2026-10-11) | Feature | — | [target-app-parity](target-app-parity.md) |
|
||||
| 14 | Variables: extended collections checked on real libraries (the tail landed 2026-10-12) | Feature | 0–1 | [components-parity](components-parity.md) |
|
||||
| 15 | Prototyping: media triggers and actions; controllers on macOS, Linux and FreeBSD (controller buttons on key/gamepad triggers, widgets in prototypes landed) | Feature | 2–5 | [prototyping-parity](prototyping-parity.md) |
|
||||
| 16 | Dev Mode: publishing mappings from the code repository; the editor extension is left to the MCP server (measurements kept on the canvas and a read-only layers panel landed 2026-10-24) | Feature | 1–2 | [dev-mode-parity](dev-mode-parity.md) |
|
||||
| 16 | Dev Mode: renaming the file in Dev Mode to check; the editor extension is left to the MCP server (publishing mappings from the code repository landed 2026-10-30) | Feature | 1–2 | [dev-mode-parity](dev-mode-parity.md) |
|
||||
| 17 | Branching: text runs merged whole, branches only their author sees in a live session (branches in live sessions, export settings and guides item by item, and history entries for creation and updates landed 2026-10-24) | Feature | 1–2 | [collaboration-parity](collaboration-parity.md) |
|
||||
| 18 | Display P3 on screen: blocked upstream (egui-wgpu picks an 8-bit sRGB surface); gamut warning and soft-proof sRGB landed, as did several windows | Hardware | 4–8 | [hardware-parity](hardware-parity.md) |
|
||||
| 19 | AI features | AI | 30–60 | [target-app-parity](target-app-parity.md) |
|
||||
@@ -447,10 +447,13 @@ No alpha blockers: the alpha gate in [`roadmap.md`](roadmap.md#alpha-gate) passe
|
||||
- **Landed (2026-10-24):** files dropped on the canvas, the Pages list and the Assets panel no
|
||||
longer edit the design in Dev Mode (a dropped document still opens as a file of its own, and
|
||||
the Pages list only opens pages).
|
||||
- **Missing:** publishing mappings from the code repository with a CLI. An editor extension is out
|
||||
of scope: the MCP server already serves design data, code and the connected snippets to agents
|
||||
and editors. The file name field in the left panel still renames the file in Dev Mode
|
||||
(`file.rename`); whether Figma allows that in Dev Mode needs checking before it changes.
|
||||
- **Landed (2026-10-30):** `figcraft-cli connect publish <file.figcraft> [--mappings
|
||||
figcraft.connect.json] [--dry-run]` publishes a code repository's mappings to a design file, so
|
||||
code owners keep them with the code and publish them from CI ([cli.md](cli.md)).
|
||||
- **Missing:** the file name field in the left panel still renames the file in Dev Mode
|
||||
(`file.rename`); whether Figma allows that in Dev Mode needs checking before it changes. An
|
||||
editor extension is out of scope: the MCP server already serves design data, code and the
|
||||
connected snippets to agents and editors.
|
||||
- **Estimate:** 1–2 h.
|
||||
|
||||
### 17. Branching and merging
|
||||
@@ -562,6 +565,7 @@ No alpha blockers: the alpha gate in [`roadmap.md`](roadmap.md#alpha-gate) passe
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-30 | minor | Gap 16: `figcraft-cli connect publish` publishes a repository's `figcraft.connect.json` to a design file (links by component name, warnings for links that don't fit, `--dry-run`, exit 1 when nothing links). The CLI part leaves the missing list; the Dev Mode file-rename check remains; estimate unchanged. Tests: `files::tests::connect_publish_links_components_and_saves_the_file`, the CLI argument tests |
|
||||
| 2026-10-29 | minor | Colour picker review follow-up: opening the first picker preserves a text undo interaction; placement follows the rendered sidebar; opening controls track their widget ids while moving; linked selection-colour names toggle closed; gradient variable-picker presses keep the parent open. Regression tests cover each case; estimates unchanged. |
|
||||
| 2026-10-29 | minor | Colour pickers dismiss on presses outside their card, including the sidebar, and open 8 pt beside the sidebar aligned to their control (lifted to fit the screen); switching targets resets a dragged position and commits pending edits. Eyedropper sampling stays open. UI regression tests cover added fills/strokes and page colours; headline estimates unchanged. |
|
||||
| 2026-10-29 | minor | Gap 7: the browser draws a zoom's full render a band at a time (eight at most), so a deep zoom no longer holds the page for the whole render; pans and edits meanwhile are patched in once it is drawn. Numbers unchanged |
|
||||
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
# FigCraft roadmap: milestones and what's next
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (while a drag only moves a large selection, its outlines, box, slots, spacing and Design panel values are worked out once and shifted, not rebuilt on every move (#1571); Shift+Cmd/Ctrl+Enter on a selected board shape adds the next shape already joined to it, and goes on typing (#1597); an imported or pasted file finds an installed family under the other names its fonts give it, and with other case and spacing, instead of reporting it missing (#1292); a panic in a grouped closure outside any command rolls the group back like a failed group: documents, selection, undo and redo, history limit and journal as before (#1085); a development example generates figcraft-cli's shell completions and man pages from its clap definitions (part of #1396); `.fig` ZIP image reads share a total byte budget and count skipped images in the import warnings (#1076); compact layer trees with fields and outline (M0.9, part of #996); stored variable and collection references keep their identity when another object is named like their id (follow-up to #1542); every file a command reads whole by path is a regular file within a cap that fits it: documents 16 GiB, placed images 256 MiB, CLI scripts 1 GiB; a component set's property rows fit the 240 px right panel: variant values wrap two to a line there and every + and − stays in view; the server key is checked by comparing SHA-256 hashes of the presented and the kept key, as the deployment guide says (#1130); opening a file contacts no team library server it names until you check it (#1097); a hostile server can no longer grow a client's memory with made-up people, and a session remembers a bounded number of deleted layer ids (#1121); `doc.list` gives AI agents and plugins the open files' names, not their paths (#1117); with a large selection, frames where nothing changed reuse the selection's outlines, box and slots and the Design panel's instance and position values instead of working them out again (#1571); team libraries and plugins larger than the 16 MiB download burst can be downloaded again, more slowly (#1089); a batch keeps one undo snapshot, not one per step (#1099); files nested 256 levels deep import on the browser's 1 MB stack (#1101); SVG and PDF exports count pattern sources and the renders they embed from the layer's depth, so one cap bounds a whole export (#1082); an explicit variables file path (`variable.import`, the `variables` of `file.open` and `file.import`) is refused past 256 MiB or when it isn't a regular file, as the sibling `variables.json` already was (part of #1114); joining and leaving a file nobody is editing reads and writes nothing on the server: it stays loaded a minute after everyone leaves (#1074); opening a `.figcraft` file unpacks at most 6 GiB in all (2 GiB in the browser), every area together (#1079); hovering the canvas hit-tests once per pointer move, not on every frame, and moving an auto layout frame no longer lays out its contents again on every move (#1571); `.fig` instance expansion no longer walks variable nodes for every instance or copies every outer override row into each nested instance (#1077); `comment.people` lists names that were only @mentioned, and `comment.next` and `comment.prompt` carry the mentions too (#1415); grid auto-placement looks for free cells a 64-bit word at a time and skips what blocks it, with the same placements (#1083); with the pen out, Enter or a double-click ends the path and edits the vector with the Move tool instead of drawing on (#1430); a recovery copy made while a version is previewed holds the file, not the version shown; a FigJam `.jam` file opens as a board (#1342); the Actions palette offers only commands that run from it, and no second row reading like a menu item: Place image opens the file dialog (#1574); share links can comment: a third access level beside edit and view, whose holder sees the file live and adds, replies to, reacts to and resolves comments but can't change the design; Preferences ▸ Scroll wheel zoom: a mouse wheel zooms the canvas about the pointer and Cmd/Ctrl+wheel scrolls (#1390); `comment.list {mentions}` lists only the threads that @mention a name (#1415); in the browser an internal error that stops the page keeps its unsaved files, which open again on reload (#1123); boolean operations refuse input past their caps, and a boolean group past them draws without its result (#1104); figcraft-cli parses its arguments with clap, following RFC-0012 (#1396); the selection and hover outlines of rounded rectangles and frames follow their corners (#1506); component properties keep the order they were made in when one is renamed, and Make slot asks for the slot's name first (#1536, #1537); the browser build publishes files to a server, so Share ▸ Online and Publish to server work there (#917); the browser build uses the fonts installed on the computer once asked, each read when text uses it (#1Line truncated
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (compact layer trees with fields and outline (M0.9, part of #996); while a drag only moves a large selection, its outlines, box, slots, spacing and Design panel values are worked out once and shifted, not rebuilt on every move (#1571); Shift+Cmd/Ctrl+Enter on a selected board shape adds the next shape already joined to it, and goes on typing (#1597); an imported or pasted file finds an installed family under the other names its fonts give it, and with other case and spacing, instead of reporting it missing (#1292); a panic in a grouped closure outside any command rolls the group back like a failed group: documents, selection, undo and redo, history limit and journal as before (#1085); a development example generates figcraft-cli's shell completions and man pages from its clap definitions (part of #1396); `.fig` ZIP image reads share a total byte budget and count skipped images in the import warnings (#1076); compact layer trees with fields and outline (M0.9, part of #996); stored variable and collection references keep their identity when another object is named like their id (follow-up to #1542); every file a command reads whole by path is a regular file within a cap that fits it: documents 16 GiB, placed images 256 MiB, CLI scripts 1 GiB; a component set's property rows fit the 240 px right panel: variant values wrap two to a line there and every + and − stays in view; the server key is checked by comparing SHA-256 hashes of the presented and the kept key, as the deployment guide says (#1130); opening a file contacts no team library server it names until you check it (#1097); a hostile server can no longer grow a client's memory with made-up people, and a session remembers a bounded number of deleted layer ids (#1121); `doc.list` gives AI agents and plugins the open files' names, not their paths (#1117); with a large selection, frames where nothing changed reuse the selection's outlines, box and slots and the Design panel's instance and position values instead of working them out again (#1571); team libraries and plugins larger than the 16 MiB download burst can be downloaded again, more slowly (#1089); a batch keeps one undo snapshot, not one per step (#1099); files nested 256 levels deep import on the browser's 1 MB stack (#1101); SVG and PDF exports count pattern sources and the renders they embed from the layer's depth, so one cap bounds a whole export (#1082); an explicit variables file path (`variable.import`, the `variables` of `file.open` and `file.import`) is refused past 256 MiB or when it isn't a regular file, as the sibling `variables.json` already was (part of #1114); joining and leaving a file nobody is editing reads and writes nothing on the server: it stays loaded a minute after everyone leaves (#1074); opening a `.figcraft` file unpacks at most 6 GiB in all (2 GiB in the browser), every area together (#1079); hovering the canvas hit-tests once per pointer move, not on every frame, and moving an auto layout frame no longer lays out its contents again on every move (#1571); `.fig` instance expansion no longer walks variable nodes for every instance or copies every outer override row into each nested instance (#1077); `comment.people` lists names that were only @mentioned, and `comment.next` and `comment.prompt` carry the mentions too (#1415); grid auto-placement looks for free cells a 64-bit word at a time and skips what blocks it, with the same placements (#1083); with the pen out, Enter or a double-click ends the path and edits the vector with the Move tool instead of drawing on (#1430); a recovery copy made while a version is previewed holds the file, not the version shown; a FigJam `.jam` file opens as a board (#1342); the Actions palette offers only commands that run from it, and no second row reading like a menu item: Place image opens the file dialog (#1574); share links can comment: a third access level beside edit and view, whose holder sees the file live and adds, replies to, reacts to and resolves comments but can't change the design; Preferences ▸ Scroll wheel zoom: a mouse wheel zooms the canvas about the pointer and Cmd/Ctrl+wheel scrolls (#1390); `comment.list {mentions}` lists only the threads that @mention a name (#1415); in the browser an internal error that stops the page keeps its unsaved files, which open again on reload (#1123); boolean operations refuse input past their caps, and a boolean group past them draws without its result (#1104); figcraft-cli parses its arguments with clap, following RFC-0012 (#1396); the selection and hover outlines of rounded rectangles and frames follow their corners (#1506); component properties keep the order they were made in when one is renamed, and Make slot asks for the slot's name first (#1536, #1537); the browser build publishes files to a server, so Share ▸ Online and Publish to server work there (#917); the browser build uses the fonts insLine truncated
|
||||
|
||||
Forward-looking plan. Where FigCraft stands today: [`ROADMAP.md`](../ROADMAP.md) and
|
||||
[`target-app-parity.md`](target-app-parity.md); the full ranked list: [`gaps.md`](gaps.md). Hours
|
||||
@@ -76,6 +76,7 @@ blockers.
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-30 | minor | M6, gap 16: `figcraft-cli connect publish` publishes a repository's code connect mappings to a `.figcraft` file (`--dry-run`). Tests: `files::tests`, `args` tests |
|
||||
| 2026-10-30 | minor | M8, #1571 (part 3): the first frame after each move of a drag rebuilt the selection's outlines and box, its slots, the selection spacing and the Design panel's values from the new document. Now the engine notes when a drag has only moved the selected layers, each by the same offset from where it started (`Interaction::moved_by`: not for a layer in an auto layout flow, nor an Alt copy), and the canvas and panel work those out once from the document the drag started from and shift what depends on position (the outlines on screen, the box); the Design panel shows that document's values, all but the position fields, which follow the move. The Export section's values are kept too. Nothing looks different. Dragging 648 of about 1,900 synthetic layers (release build, two runs each): the UI pass 2.10 to 1.44 ms a frame, CPU 135% to 117% of a core; the Design panel 0.9 to 0.5 ms, the overlays 0.5 to 0.3 ms. Not yet: the engine's preview of each move (copying and normalising the document) and the canvas texture's whole upload. Tests: `moved_by_tests.rs`, `canvas/selection_cache_tests.rs`. |
|
||||
| 2026-10-30 | minor | M12, #1597: Shift+Cmd/Ctrl+Enter on a board shape adds the next shape already connected (stickies unchanged). Tests: `board_connected_tests`, `board_ui_tests` |
|
||||
| 2026-10-30 | minor | M12, #1597: boards create the next shape already connected. A selected sticky or shape shows a "+" beside each of its four sides (clear of the contextual bar, the size label and the toolbar); hovering one shows where the next goes and the connector to it; clicking makes a sticky, or a shape of the same kind, size and colour, 80 units away on that side, joined to it by a connector the way the Connector tool makes one, and starts typing in it. One undo step takes both away. `board.addConnected {id?, side?, edit?}` does it from commands, MCP and plugins. Shift+Cmd/Ctrl+Enter still adds a sticky without a connector. Tests: `board_connected_tests.rs` (engine), `board_connect_tests.rs` (the handles, headless). |
|
||||
|
||||
Reference in new issue
Block a user