From 47d37167d844aba07cdb7f84d05acbbfbf328da9 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Tue, 13 Oct 2026 09:59:12 +0700 Subject: [PATCH] M7: Dev Mode: import lines for connected instances in generated layer trees The code of a layer tree (a frame, a component, an instance of a component that isn't connected) leads with the import lines of every connected instance written into it: one line per import path, in path order, each name once, per language. Collected as the snippets are written, so only instances the code reaches bring one; a connected layer's own code is its import and snippet as before. --- crates/engine/src/code_connect.rs | 30 +++++++-- crates/engine/src/code_imports_tests.rs | 90 +++++++++++++++++++++++++ crates/engine/src/component_codegen.rs | 42 ++++++++++-- crates/engine/src/lib.rs | 2 + docs/dev-mode-parity.md | 9 +-- docs/gaps.md | 12 ++-- docs/mcp.md | 3 +- 7 files changed, 169 insertions(+), 19 deletions(-) create mode 100644 crates/engine/src/code_imports_tests.rs diff --git a/crates/engine/src/code_connect.rs b/crates/engine/src/code_connect.rs index 1938766..c18e3dc 100644 --- a/crates/engine/src/code_connect.rs +++ b/crates/engine/src/code_connect.rs @@ -59,16 +59,36 @@ pub fn link(doc: &Document, id: NodeId, lang: CodeLang) -> Option<&CodeLink> { doc.node(owner(doc, id)?)?.code_links.iter().find(|l| l.language == lang.id()) } -/// The import line of `link` ("" without an import path). -pub fn import_line(link: &CodeLink, lang: CodeLang) -> String { - let path = &link.import_path; +/// What a link's import brings in: `Card` for the code component `Card.Header`. +fn imported(link: &CodeLink) -> &str { + link.name.split('.').next().unwrap_or_default() +} + +/// The line importing `names` from `path`. +fn import_of(path: &str, names: &[&str], lang: CodeLang) -> String { match lang { - _ if path.is_empty() => String::new(), - CodeLang::React => format!("import {{ {} }} from {};", link.name.split('.').next().unwrap_or_default(), js_string(path)), + CodeLang::React => format!("import {{ {} }} from {};", names.join(", "), js_string(path)), _ => format!("import {path}"), } } +/// The import line of `link` ("" without an import path). +pub fn import_line(link: &CodeLink, lang: CodeLang) -> String { + if link.import_path.is_empty() { String::new() } else { import_of(&link.import_path, &[imported(link)], lang) } +} + +/// The import lines of `links` (the connected instances in a generated layer tree): one line per +/// import path, in path order, each naming what it brings in once, in name order. "" when none +/// has an import path. +pub fn import_lines(links: &[CodeLink], lang: CodeLang) -> String { + let mut by_path: std::collections::BTreeMap<&str, std::collections::BTreeSet<&str>> = std::collections::BTreeMap::new(); + for l in links.iter().filter(|l| !l.import_path.is_empty()) { + by_path.entry(&l.import_path).or_default().insert(imported(l)); + } + let lines: Vec = by_path.iter().map(|(path, names)| import_of(path, &names.iter().copied().collect::>(), lang)).collect(); + lines.join("\n") +} + /// Layer `id`'s code with `link`: the import line and the snippet (an instance's values, else the /// defaults). `None` when `id` isn't a component, set or instance. pub fn connected_code(doc: &Document, id: NodeId, lang: CodeLang, link: &CodeLink) -> Option { diff --git a/crates/engine/src/code_imports_tests.rs b/crates/engine/src/code_imports_tests.rs new file mode 100644 index 0000000..70007cb --- /dev/null +++ b/crates/engine/src/code_imports_tests.rs @@ -0,0 +1,90 @@ +//! Import lines in generated layer trees: the code for a frame (or a component, or an instance +//! of a component that isn't connected) leads with the imports of every connected instance +//! written into it, one line per import path, names in order and once each, per language; +//! instances that aren't connected, and connected ones the code doesn't reach, bring none. + +use crate::Session; +use crate::components::tests::{ex, idv}; +use figcraft_doc::*; +use serde_json::json; + +fn code(s: &mut Session, id: NodeId, lang: &str) -> String { + ex(s, "inspect.code", json!({"id": id, "language": lang}))["code"].as_str().unwrap_or_default().to_string() +} + +/// A component named `name` at `y`, connected in `lang` to `code_name` from `path` ("" for none). +fn component(s: &mut Session, name: &str, y: f64, links: &[(&str, &str, &str)]) -> NodeId { + let f = idv(&ex(s, "frame.create", json!({"x": 600, "y": y, "width": 80, "height": 40, "name": name}))); + let c = idv(&ex(s, "component.create", json!({"ids": [f]}))); + for (lang, path, code_name) in links { + ex(s, "codeConnect.set", json!({"id": c, "language": lang, "importPath": path, "name": code_name, "template": format!("<{code_name} />")})); + } + c +} + +fn instance(s: &mut Session, component: NodeId, parent: NodeId, x: f64) -> NodeId { + idv(&ex(s, "instance.create", json!({"component": component, "x": x, "y": 10, "parent": parent}))) +} + +#[test] +fn a_layer_trees_code_leads_with_the_imports_of_its_connected_instances() { + let mut s = Session::new(); + let tag = component(&mut s, "Tag", 0.0, &[("react", "@acme/ui", "Tag"), ("swiftui", "AcmeUI", "Tag"), ("compose", "com.acme.ui.Tag", "Tag")]); + let avatar = component(&mut s, "Avatar", 60.0, &[("react", "@acme/ui", "Avatar"), ("swiftui", "AcmeUI", "Avatar")]); + let icon = component(&mut s, "Icon", 120.0, &[("react", "@acme/icons", "Icons.Star"), ("compose", "com.acme.icons.Star", "Star")]); + let local = component(&mut s, "Local", 180.0, &[("react", "", "Local")]); + let plain = component(&mut s, "Plain", 240.0, &[]); + let unused = component(&mut s, "Unused", 300.0, &[("react", "@acme/unused", "Unused")]); + let frame = idv(&ex(&mut s, "frame.create", json!({"x": 0, "y": 0, "width": 500, "height": 200, "name": "Header"}))); + let group = idv(&ex(&mut s, "frame.create", json!({"x": 300, "y": 0, "width": 180, "height": 100, "name": "Actions", "parent": frame}))); + // Two tags, an icon, an avatar deeper down, and instances without a link or an import path. + for (c, parent, x) in + [(tag, frame, 0.0), (icon, frame, 90.0), (tag, frame, 180.0), (avatar, group, 0.0), (local, group, 90.0), (plain, frame, 270.0)] + { + instance(&mut s, c, parent, x); + } + let react = code(&mut s, frame, "react"); + let want = "import { Icons } from \"@acme/icons\";\nimport { Avatar, Tag } from \"@acme/ui\";\nimport \"./header.css\";\n\n"; + assert!(react.starts_with(want), "{react}"); + assert_eq!(react.matches("@acme/ui").count(), 1, "once, however many instances"); + assert_eq!((react.matches("").count(), react.matches("").count(), react.matches("").count()), (2, 1, 1)); + assert!(!react.contains("@acme/unused") && !react.contains("import { Local }") && !react.contains("import { Plain }")); + // Per language: a module or a package each, in order, once. + let swift = code(&mut s, frame, "swiftui"); + assert!(swift.starts_with("import AcmeUI\n\n") && swift.matches("import ").count() == 1, "{swift}"); + let compose = code(&mut s, frame, "compose"); + assert!(compose.starts_with("import com.acme.icons.Star\nimport com.acme.ui.Tag\n\n") && compose.matches("import ").count() == 2, "{compose}"); + // The part of the tree asked for: only what is in it. + let part = code(&mut s, group, "react"); + assert!(part.starts_with("import { Avatar } from \"@acme/ui\";\nimport \"./actions.css\";\n\n"), "{part}"); + // CSS and HTML have no imports. + assert!(!code(&mut s, frame, "css").contains("import") && !code(&mut s, frame, "html").contains("@acme")); + // A layer tree without connected instances is as it was. + let bare = idv(&ex(&mut s, "frame.create", json!({"x": 0, "y": 400, "width": 100, "height": 100, "name": "Bare"}))); + instance(&mut s, plain, bare, 0.0); + assert!(code(&mut s, bare, "react").starts_with("import \"./bare.css\";\n\n")); + assert!(!code(&mut s, bare, "swiftui").contains("import ")); + let _ = unused; +} + +#[test] +fn a_components_definition_brings_the_imports_of_what_it_is_made_of() { + let mut s = Session::new(); + let tag = component(&mut s, "Tag", 0.0, &[("react", "@acme/ui", "Tag")]); + let f = idv(&ex(&mut s, "frame.create", json!({"x": 0, "y": 0, "width": 300, "height": 100, "name": "Card"}))); + instance(&mut s, tag, f, 10.0); + let card = idv(&ex(&mut s, "component.create", json!({"ids": [f]}))); + // The component, and an instance of it (its call, then the definition): the import leads both. + let want = "import { Tag } from \"@acme/ui\";\n"; + assert!(code(&mut s, card, "react").starts_with(want)); + let i = idv(&ex(&mut s, "instance.create", json!({"component": card, "x": 0, "y": 300}))); + let of_instance = code(&mut s, i, "react"); + assert!(of_instance.starts_with(&format!("{want}\n")), "{of_instance}"); + assert_eq!(of_instance.matches("@acme/ui").count(), 1); + // A connected layer's own code is its import and snippet, as before. + let t = instance(&mut s, tag, f, 100.0); + assert_eq!(code(&mut s, t, "react"), "import { Tag } from \"@acme/ui\";\n\n\n"); + // Nothing is left collecting: the next layer's code starts clean. + let empty = idv(&ex(&mut s, "frame.create", json!({"x": 0, "y": 600, "width": 50, "height": 50, "name": "Empty"}))); + assert!(code(&mut s, empty, "react").starts_with("import \"./empty.css\";")); +} diff --git a/crates/engine/src/component_codegen.rs b/crates/engine/src/component_codegen.rs index 97f147c..f118872 100644 --- a/crates/engine/src/component_codegen.rs +++ b/crates/engine/src/component_codegen.rs @@ -107,14 +107,39 @@ pub fn specs() -> Vec { )] } +thread_local! { + /// The links of the connected instances written into the code [`code`] is generating + /// (`None`: nothing is collecting them). + static CONNECTED: std::cell::RefCell>> = const { std::cell::RefCell::new(None) }; +} + +/// Start (`Some`) or stop collecting the links of connected instances as their snippets are +/// written; what was collected until now. +fn collect_connected(next: Option>) -> Vec { + CONNECTED.with(|c| c.try_borrow_mut().map(|mut c| std::mem::replace(&mut *c, next)).ok().flatten()).unwrap_or_default() +} + /// Code for any layer in React, SwiftUI or Compose: an instance's call and its component's /// definition, a component's (or set's) definition, or the layer tree (React wraps it in a -/// function component). +/// function component). The import lines of the connected instances in that code lead it. pub fn code(doc: &Document, id: NodeId, lang: CodeLang) -> String { - let Some(n) = doc.node(id) else { return String::new() }; if let Some(code) = crate::code_connect::link(doc, id, lang).and_then(|l| crate::code_connect::connected_code(doc, id, lang, l)) { return code; } + collect_connected(Some(Vec::new())); + let code = generated(doc, id, lang); + let imports = crate::code_connect::import_lines(&collect_connected(None), lang); + match (imports.is_empty(), code.starts_with("import ")) { + (true, _) => code, + // With the imports the code has already. + (false, true) => format!("{imports}\n{code}"), + (false, false) => format!("{imports}\n\n{code}"), + } +} + +/// [`code`] without the import lines of connected instances. +fn generated(doc: &Document, id: NodeId, lang: CodeLang) -> String { + let Some(n) = doc.node(id) else { return String::new() }; let comp = main_of(doc, id).and_then(|m| describe(doc, m)); match (&n.kind, comp) { (NodeKind::Instance { .. }, Some(c)) => format!("{}\n{}", usage(doc, id, lang, 0).unwrap_or_default(), definition(doc, &c, lang)), @@ -460,9 +485,18 @@ pub fn usage(doc: &Document, id: NodeId, lang: CodeLang, depth: usize) -> Option let c = main_of(doc, id).and_then(|m| describe(doc, m))?; let values = instance_values(doc, id, &c)?; let pad = if lang == CodeLang::React { " ".repeat(depth) } else { indent(depth) }; - // A connected component: its real snippet. shortcut: inside a layer tree it brings no import - // line; collect them at the top of the tree's code when people ask for it. + // A connected component: its real snippet; its import goes to the top of the code it is in. if let Some(link) = crate::code_connect::link(doc, id, lang) { + CONNECTED.with(|list| { + if let Ok(mut list) = list.try_borrow_mut() + && let Some(list) = list.as_mut() + { + // Once each: a tree can hold thousands of instances of one component. + if !list.contains(link) { + list.push(link.clone()); + } + } + }); return Some(crate::code_connect::render(doc, &c, &values, link, lang, &pad)); } let slots = slots_of(doc, id); diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index 7d37e31..9264a6c 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -1367,6 +1367,8 @@ mod canvas_fixes_tests; #[cfg(test)] mod code_connect_tests; #[cfg(test)] +mod code_imports_tests; +#[cfg(test)] mod collab_ids_tests; #[cfg(test)] mod collab_live_tests; diff --git a/docs/dev-mode-parity.md b/docs/dev-mode-parity.md index 514bf91..71ddc27 100644 --- a/docs/dev-mode-parity.md +++ b/docs/dev-mode-parity.md @@ -1,10 +1,10 @@ # Dev Mode parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (dev resources links; inspect behaviour suite and fixes: colour formats, rem, blend, positions from the frame, CSS for rotation, gradients and text, icons and images as assets; code connect and Dev Mode plugins) · **Target:** Figma Design Dev Mode (desktop app 126.10.7) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (import lines for connected instances in generated layer trees; dev resources links; inspect behaviour suite and fixes: colour formats, rem, blend, positions from the frame, CSS for rotation, gradients and text, icons and images as assets; code connect and Dev Mode plugins) · **Target:** Figma Design Dev Mode (desktop app 126.10.7) **Breadth ~96%, ready ~72% (estimated).** The inspect surface is broad, components connect -to the code components of a repository and layers carry dev resources links; left: import lines -inside generated layer trees. Remaining: 0–1 h. +to the code components of a repository and layers carry dev resources links; left: publishing +mappings from the code repository with a CLI. Remaining: 0–1 h. | Feature | Status | Notes | |---|---|---| @@ -16,7 +16,7 @@ inside generated layer trees. Remaining: 0–1 h. | Ready for dev list, statuses, "Changed" after edits | done | | | Focus view (one frame, previous / next) | done | | | Compare changes (before/after side by side or overlaid, property diffs) | done | only against the snapshot taken when a frame was marked ready; not against any named version | -| Code connect (map components to repository code) | done | per language (React, SwiftUI, Compose): 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. Inside a generated layer tree, connected instances' imports aren't collected | +| Code connect (map components to repository code) | done | per language (React, SwiftUI, Compose): 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. The code of a layer tree (a frame, a component, an instance of a component that isn't connected) leads with the import lines of every connected instance written into it: one line per import path, in path order, each name once (`import { Avatar, Tag } from "@acme/ui";`; SwiftUI and Compose: one `import` per module or package) | | Plugins in Dev Mode (Jira, GitHub, etc.) | partial | FigCraft's own WASM plugins that only read run from the Inspect tab; no plugin UI at run time, no third-party integrations | | 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 | | Dev resources links on layers | done | a URL and a name per link on any layer (`devResource.*`, saved, synced, undoable); instances list their main component's (and its set's) read-only; URLs checked as http/https and never fetched (no page titles: a link without a name is named after its URL); the Inspect tab lists, adds, edits and removes them; `get_node` returns them | @@ -25,6 +25,7 @@ inside generated layer trees. Remaining: 0–1 h. | Date | Change | Summary | |---|---|---| +| 2026-10-13 | minor | Import lines inside generated layer trees (gap 16): `inspect.code` and the Inspect panel put the imports of every connected instance in a layer tree at the top of its code, deduplicated and ordered, per language (React names from one path share a line); instances without a link or an import path bring none. Breadth and ready unchanged at this rounding | | 2026-10-12 | minor | Dev resources (gap 16): links for developers (a URL and a name) on any layer: `devResource.add` / `edit` / `remove` / `list`, undoable, saved and synced; an instance also lists its main component's (and that component's set's) links read-only with where they come from, and layers inside an instance show their main layer's; URLs are checked as web links (http or https, a host, no spaces, at most 2,048 characters) and never fetched (a link added without a name is named after its URL), opened only when clicked; the Inspect tab lists, adds, edits and removes them; `get_node` and `inspect_document` return them as `devResources`. The editor extension stays out of scope (the MCP server serves agents and editors). Dev Mode breadth 93% → 96%, ready 70% → 72%, 1–3 h → 0–1 h; feature breadth (~82%) and ready for real work (~51%) unchanged at this rounding; full parity ~304–593 h → ~303–591 h | | 2026-10-11 | minor | Export and inspect behaviour suite written from the public documentation (139 cases in 19 tests; 47 failed before the fixes) and its fixes: an export covers what the layer paints, so a drop shadow adds room only where it falls, strokes on boxes and ellipses reach their real width (they added up to four times as much), hidden layers take no room and a mask group exports the mask's area; a slice exports everything under it; slashes in layer names export into folders; Copy as PNG copies the whole selection. Added: the export settings' contents only and include bounding box, and for SVG outline text (off writes ``), unique id attributes and simplified strokes (stroke attributes where exact); Inspect colour formats (hex, RGB, CSS, HSL, HSB), CSS in rem, a Blend row, positions measured from the frame around a layer (groups aren't frames), CSS for rotation, blend modes, gradient directions and kinds, text alignment, case and decoration; icons and images in the asset list (`file.exportLayers {assets}`). Dev Mode breadth 90% → 93%, ready 64% → 70%, hours unchanged (1–3, dev resources); feature breadth and ready for real work unchanged at this rounding | | 2026-10-11 | minor | Code connect and Dev Mode plugins (gap 16): a component or set links to its code component per language (import path, name, a template with `{property}` and `{property ? "a" : "b"}` placeholders); `inspect.code`, `inspect.componentCode` and the Inspect panel show that snippet with the instance's values (also inside generated layer trees), edited in Connect to code with a live preview; links are saved, synced and undoable, and travel as `figcraft.connect.json` (`codeConnect.export` / `codeConnect.import`). Plugins that only read run from the Inspect tab. Dev Mode breadth 80% → 90%, ready 62% → 64%, 4–8 h → 1–3 h; feature breadth (~79%) and ready for real work (~47%) unchanged; full parity ~380–725 h → ~375–720 h | diff --git a/docs/gaps.md b/docs/gaps.md index a7f7fb2..c3f90a9 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -1,6 +1,6 @@ # Where FigCraft falls short of Figma -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (session restore; `.fig` export: instance sizes, variables with modes, shared styles, variable-width strokes; sharing with a join code; text box sizes measured against documents re-saved by other software; live sessions per document; .fig export checked by opening written files in other software; experimental .fig export; keyboard shortcut parity suite and bindings; Simplified Chinese draft catalog; Latin American Spanish overlay; prototyping tail: gamepad triggers, widgets in prototypes, mode lookups; plugin directory on figcraft-server; Japanese and Korean draft catalogs and CJK UI fonts; plugin widgets and the plugin panel tail; French, German and Brazilian Portuguese draft catalogs; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and edit tail; dev resources links; extended collections tail; branch version history and finer list merges; comments and version history behaviour suite and fixes; canvas navigation, guides and grids behaviour suite and fixes; `.fig` import: editable vector networks, more layer kinds, variable prototype actions; `.fig` import, best effort: gap 1 down to its tail; Windows QA pass: everyday workflows on Windows 11, eight fixes incl. unsaved work kept after a crash or a forced quit; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; localization layer with a draft Spanish catalog; hosting figcraft-server: wss in the desktop app, server releases, deployment guide; plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings: gap 14 down to its tail) · **Target:** Figma Design (desktop app 126.10.7) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (import lines inside generated layer trees; session restore; `.fig` export: instance sizes, variables with modes, shared styles, variable-width strokes; sharing with a join code; text box sizes measured against documents re-saved by other software; live sessions per document; .fig export checked by opening written files in other software; experimental .fig export; keyboard shortcut parity suite and bindings; Simplified Chinese draft catalog; Latin American Spanish overlay; prototyping tail: gamepad triggers, widgets in prototypes, mode lookups; plugin directory on figcraft-server; Japanese and Korean draft catalogs and CJK UI fonts; plugin widgets and the plugin panel tail; French, German and Brazilian Portuguese draft catalogs; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and edit tail; dev resources links; extended collections tail; branch version history and finer list merges; comments and version history behaviour suite and fixes; canvas navigation, guides and grids behaviour suite and fixes; `.fig` import: editable vector networks, more layer kinds, variable prototype actions; `.fig` import, best effort: gap 1 down to its tail; Windows QA pass: everyday workflows on Windows 11, eight fixes incl. unsaved work kept after a crash or a forced quit; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; localization layer with a draft Spanish catalog; hosting figcraft-server: wss in the desktop app, server releases, deployment guide; plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings: gap 14 down to its tail) · **Target:** Figma Design (desktop app 126.10.7) Every known shortfall against Figma, one entry each, ranked by how much it stops a Figma user from doing real work in FigCraft. Agents: pick work from the top unless the owner says otherwise. The @@ -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: import lines inside generated layer trees; the editor extension is left to the MCP server (code connect, Dev Mode plugins and dev resources landed) | Feature | 0–1 | [dev-mode-parity](dev-mode-parity.md) | +| 16 | Dev Mode: publishing code mappings from the repository with a CLI; the editor extension is left to the MCP server (code connect, Dev Mode plugins, dev resources and import lines in layer trees landed) | Feature | 0–1 | [dev-mode-parity](dev-mode-parity.md) | | 17 | Branching: branches in live sessions, lists other than fills, strokes and effects merged whole (a branch's own version history and per-item list merges landed 2026-10-12) | 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) | @@ -321,9 +321,10 @@ No alpha blockers: the alpha gate in [`roadmap.md`](roadmap.md#alpha-gate) passe host, no spaces, capped) and never fetched: a link added without a name is named after its URL, and a link opens only when clicked. The Inspect tab lists, adds, edits and removes them; `get_node` / `inspect_document` return them as `devResources`. -- **Missing:** import lines for connected instances inside a - generated layer tree (only the selected layer's own import is shown); publishing mappings from - the code repository with a CLI. An editor extension is out of scope: the MCP server already +- **Landed (2026-10-13):** import lines inside generated layer trees: the code of a frame, a + component or an unconnected instance leads with the imports of every connected instance written + into it, one line per import path, ordered, each name once, per language. +- **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. - **Estimate:** 0–1 h. @@ -399,6 +400,7 @@ No alpha blockers: the alpha gate in [`roadmap.md`](roadmap.md#alpha-gate) passe | Date | Change | Summary | |---|---|---| +| 2026-10-13 | minor | Gap 16: import lines for connected instances inside generated layer trees landed (deduplicated and ordered per language); what is left is publishing mappings from the code repository with a CLI; estimate and ranking unchanged | | 2026-10-13 | minor | Gap 18: window places are restored at the next start (session restore: windows, tabs, pages, views and panels come back as they were left); what is left is wide-gamut colour on screen and the screen-reader tree in other windows, both blocked upstream; estimate and ranking unchanged | | 2026-10-13 | minor | Gap 1: the experimental `.fig` export writes instance sizes (resized, filling an auto layout, scaled), variables with modes and bindings, shared styles and variable-width strokes, and the import reads them back. Checked by opening written files in other software: resized and scaled instances keep their size and lay their layers out for it, variables with their modes and colour bindings resolve there (a frame set to another mode shows that mode's colours, also inside instances), colour, text, effect and grid styles are listed, and variable-width strokes that are the same on both sides of the path draw with their width; number, string and boolean bindings and bindings on effects, layout guides and gradient stops come across as the values they give but did not show as bound there (their entries now also name the layer field they set, not yet checked there), and strokes wider on one side of the path are written as filled outlines, no longer editable strokes there (the stroke itself is kept as plugin data and our own import puts it back; not yet checked there). Hours and the ranking unchanged | | 2026-10-12 | minor | Gap 2: your online files landed (the app records the files it put on a server with their owner tokens, Home ▸ Online lists them and opens them again as their owner, with sharing, rename, removal and forget from the list; one request per server tells which are still there; tabs mark files in a live session). What is left is unchanged: accounts, teams and per-person permissions, shared comments and versions. Hours unchanged | diff --git a/docs/mcp.md b/docs/mcp.md index c7f6b0d..d194784 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -1192,7 +1192,8 @@ permissions: without it nothing is downloaded); `plugin.directory.publish {path strings, any other `{…}` stays as written (`unknown` lists placeholder names that aren't properties). Then `inspect.code` on an instance or the component returns the import line and the snippet instead of generated code (`connected: true`), connected instances inside a generated - layer tree are their snippet, and `inspect.componentCode` returns `connected: {importPath, name, + layer tree are their snippet with their import lines at the top of the tree's code (one per + import path, in order, each name once), and `inspect.componentCode` returns `connected: {importPath, name, template, import, code}` with the snippet as `usage`. Example: template `` with import path `@acme/ui` gives `import { Button } from "@acme/ui";` then `