M7: dev resources links

- Dev resources: links for developers (a URL and a name) on any layer, stored on the layer (`Node::dev_resources`, a new synced property `devResources` at the end of the property list), saved in the file and undoable.
- Commands: `devResource.add {id?, url, name?}`, `devResource.edit {id?, index, url?, name?}`, `devResource.remove {id?, index?}`, `devResource.list {id?}` (Spanish labels added).
- An instance lists its own links, then its main component's and that component's set's, read-only with `from`; layers inside an instance show their main layer's and take none of their own (refused with a hint). Instances don't copy the main's links (created empty, own links kept across main edits).
- URLs are data: http or https with a host, no whitespace or control characters, at most 2,048 characters; names clipped to 200; at most 32 per layer. Nothing is fetched: a link added without a name is named after its URL (scheme, `www.` and trailing slash dropped). The app opens a link only when it is clicked.
- Inspect tab: a Dev resources section (after Annotations) lists the links with a link icon, name and URL (or "From <main>"), opens on click, edits name and link inline (pencil), removes (minus) and adds from a "Paste a link" field (plus). New strings in the en and es catalogs; links are announced as links to screen readers.
- Agents: `get_node` / `inspect_document` return `devResources` (own and inherited); docs/mcp.md and docs/control-protocol.md describe them.
- The editor extension stays out of scope: the MCP server already serves design data, code and links to agents and editors.
This commit is contained in:
bflatastic 2026-10-12 02:05:16 +07:00 committed by BFlat
parent 8a983f42ce
commit 7e467c2b82
20 changed files with 560 additions and 23 deletions

View File

@ -2,7 +2,7 @@
**Stage: alpha** · next: beta, ~26 points and ~170–305 h away (one Opus 5.5 agent; ~35–70 h wall-clock with 5 agents)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (extended collections tail; branch version history and finer list merges; headline percentages recomputed from the area table: feature breadth ~84%, core ~94%, mainstream ~50%; text lists, hanging punctuation and decoration styles; comments and version history behaviour suite and fixes; canvas navigation, guides and grids behaviour suite and fixes; export and inspect behaviour suite and fixes; .fig import: editable vector networks and more layer kinds; shapes and vector behaviour suite and fixes; paint and effects behaviour suite and fixes; frames, groups, sections and masks behaviour suite and fixes; styles and variables behaviour suite and fixes; prototyping behaviour suite and fixes; components behaviour suite and fixes; embedded image colour profiles; layers panel keyboard: F2 rename, Shift+F10 context menu, keyboard box selection, 10k-layer QA; per-side stroke weight variables; text behaviour suite: CJK line breaking, metrics, vertical trim; selection and transform behaviour suite and fixes; `.fig` import, best effort; Windows QA pass with eight fixes; auto layout behaviour suite and fixes; 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; feature breadth ~80%; ready ~49%) · **Target:** Figma Design (desktop app 126.10.7)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (dev resources links; extended collections tail; branch version history and finer list merges; headline percentages recomputed from the area table: feature breadth ~84%, core ~94%, mainstream ~50%; text lists, hanging punctuation and decoration styles; comments and version history behaviour suite and fixes; canvas navigation, guides and grids behaviour suite and fixes; export and inspect behaviour suite and fixes; .fig import: editable vector networks and more layer kinds; shapes and vector behaviour suite and fixes; paint and effects behaviour suite and fixes; frames, groups, sections and masks behaviour suite and fixes; styles and variables behaviour suite and fixes; prototyping behaviour suite and fixes; components behaviour suite and fixes; embedded image colour profiles; layers panel keyboard: F2 rename, Shift+F10 context menu, keyboard box selection, 10k-layer QA; per-side stroke weight variables; text behaviour suite: CJK line breaking, metrics, vertical trim; selection and transform behaviour suite and fixes; `.fig` import, best effort; Windows QA pass with eight fixes; auto layout behaviour suite and fixes; 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; feature breadth ~80%; ready ~49%) · **Target:** Figma Design (desktop app 126.10.7)
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
@ -19,7 +19,7 @@ start with [`docs/gaps.md`](docs/gaps.md).
| Essentials user | **~57%** | estimated: core features 77.8% × 0.92 launch/stability × 0.92 discoverability × 0.88 opening files people send ([method](docs/target-app-parity.md#essentials-user)) |
| Mainstream practitioner | **~50%** | estimated: weekly areas 72.3% × 0.9 interaction × 0.9 stability × 0.85 file exchange with Figma users ([method](docs/target-app-parity.md#mainstream-practitioner)) |
| Remaining to beta | **~170–305 h** one agent | estimated |
| Remaining to full parity | **~302–589 h** one agent (~86–150 h wall-clock with 5 agents) | estimated |
| Remaining to full parity | **~301–587 h** one agent (~86–150 h wall-clock with 5 agents) | estimated |
### Readiness by audience
@ -56,7 +56,7 @@ Figma's own languages on the landed localization layer (15–25 h).
| Dimension | Ready | Hours | Doc |
|---|---|---|---|
| Features (depth) | 61% (core editor 72%) | 42–88 | [target-app-parity](docs/target-app-parity.md) |
| Features (depth) | 61% (core editor 72%) | 41–86 | [target-app-parity](docs/target-app-parity.md) |
| UI/UX fidelity | 58% | 40–60 | [ui-parity](docs/ui-parity.md) |
| File formats | 59% (`.fig` read best effort with editable vectors, no write) | 5–10 | [file-format-parity](docs/file-format-parity.md) |
| Collaboration | 13% (core in; live sessions with document sync, cursors and text carets, the browser joins, invite links open the app; a self-hosted server with edit and view links and team libraries; preview) | 49–98 | [collaboration-parity](docs/collaboration-parity.md) |
@ -86,7 +86,7 @@ Figma's own languages on the landed localization layer (15–25 h).
| Styles & variables | 95% | 76% | 2–4 | [components-parity](docs/components-parity.md) |
| Prototyping | 90% | 74% | 3–6 | [prototyping-parity](docs/prototyping-parity.md) |
| Import/export | 84% | 57% | 5–10 | [file-format-parity](docs/file-format-parity.md) |
| Dev Mode / inspect | 93% | 70% | 1–3 | [dev-mode-parity](docs/dev-mode-parity.md) |
| Dev Mode / inspect | 96% | 72% | 0–1 | [dev-mode-parity](docs/dev-mode-parity.md) |
| Comments, versions, misc | 87% | 63% | 1–3 | [collaboration-parity](docs/collaboration-parity.md) |
| Multiplayer & sharing | 15% | 5% | 45–90 | [collaboration-parity](docs/collaboration-parity.md) |
| Plugins & widgets | 45% | 28% | 15–40 | [plugins](docs/plugins.md) |
@ -213,6 +213,7 @@ Ranked; detail and milestones in [`docs/roadmap.md`](docs/roadmap.md).
| Date | Change | Summary |
|---|---|---|
| 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-12 | minor | Extended collections' tail (gap 14): variables a library adds to a collection this file extends now count as a library update of their own (listed by name without a copy yet) and arrive inheriting when accepted, instead of waiting for the library's next other update; a library collection that is itself an extended collection can be extended again, values flowing down the chain as each file accepts updates with every level's overrides kept (checked end to end). Styles & variables breadth 94% → 95%, ready unchanged (76%), 3–5 h → 2–4 h; gap 14 1–2 h → 0–1 h (real-library checks left); feature breadth (~82%) and ready for real work (~51%) unchanged at this rounding; full parity ~305–594 h → ~304–593 h |
| 2026-10-12 | minor | A branch's own version history and finer list merges (gap 17): versions saved or autosaved on a branch tab go into the branch (viewed, restored, renamed, deleted and duplicated there, saved in the file with it); merging adds a version to the main file's history and the merged or archived branch keeps its own; fills, strokes and effects merge item by item, conflicting only when the same item changed both ways, an item was removed on one side or both sides added different items. Comments, versions, find, misc breadth 85% → 87%, ready 60% → 61%, 2–5 h → 1–4 h; gap 17 1–3 h → 1–2 h; feature breadth (~82%) and ready for real work (~51%) unchanged at this rounding; full parity ~306–595 h → ~305–594 h (the Features table here still showed 80%/58%/4–8 h from before branches landed) |
| 2026-10-12 | minor | Headline percentages recomputed from the area table by its own method (weights × written scores): feature breadth ~82% → ~84% (core editor ~93% → ~94%), weighted ready ~61% → ~63%, core ready ~70% → ~72%, mainstream weekly areas 71.3% → 72.3% and mainstream ~49% → ~50%; several earlier rows had moved the area scores without carrying them into the totals. Hours are unchanged (left for the next full re-measure) |

View File

@ -179,6 +179,7 @@ node_props! {
PluginData => plugin_data,
Annotations => annotations,
CodeLinks => code_links,
DevResources => dev_resources,
}
/// A document-level property. Lists of entries with ids (styles, comments, variable collections,

View File

@ -229,6 +229,14 @@ pub struct CodeLink {
pub template: String,
}
/// A dev resource: a link for developers attached to a layer. The URL is data only: it is shown
/// and opened when someone clicks it, never fetched.
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct DevResource {
pub url: String,
pub name: String,
}
/// A kind of annotation (Development, Accessibility…): its stable id, name and the colour its
/// callouts are drawn in.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
@ -507,6 +515,9 @@ pub struct Node {
/// language.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub code_links: Vec<CodeLink>,
/// Dev resources: links for developers (docs, code, tickets…), in the order they were added.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub dev_resources: Vec<DevResource>,
}
pub(crate) fn yes() -> bool {
@ -616,6 +627,7 @@ impl Node {
plugin_data: BTreeMap::new(),
annotations: Vec::new(),
code_links: Vec::new(),
dev_resources: Vec::new(),
}
}

View File

@ -95,6 +95,7 @@ fn registry() -> &'static Registry {
list.extend(crate::component_codegen::specs());
list.extend(crate::code_connect::specs());
list.extend(crate::annotations::specs());
list.extend(crate::dev_resources::specs());
list.extend(crate::distance::specs());
list.extend(crate::collab::specs());
list.extend(crate::branches::specs());

View File

@ -280,6 +280,8 @@ pub fn create_instance_at(doc: &mut Document, component: NodeId, parent: NodeId,
root.prop_refs.clear();
root.annotations.clear();
root.code_links.clear();
// The main's dev resources show on its instances without being copied.
root.dev_resources.clear();
// The main's interactions play on its instances in prototypes; the instance's own start empty.
root.interactions.clear();
doc.insert(root, parent, index)?;
@ -728,6 +730,8 @@ fn sync_instance_depth(doc: &mut Document, inst: NodeId, depth: usize, spare: BT
fresh.annotations = root.annotations.clone();
// Code links belong to the main component; instances read them from it.
fresh.code_links.clear();
// An instance's own dev resources (its main's are listed with them, not copied).
fresh.dev_resources = root.dev_resources.clone();
fresh.locked = root.locked;
fresh.visible = root.visible;
fresh.var_modes = root.var_modes.clone();

View File

@ -0,0 +1,220 @@
//! Dev resources: links for developers (documentation, code, tickets, design notes…) attached to a
//! layer, each a URL and a name. Dev Mode lists them in the Inspect tab; an instance also shows
//! the links of its main component (and of that component's set), read-only. They are stored on
//! the layer (`Node::dev_resources`: saved, synced, undoable).
//!
//! URLs are data: they are checked (http or https, a host, no spaces or control characters,
//! capped) and never fetched or opened by the engine. A link added without a name is named after
//! its URL (no page title is looked up).
use figcraft_doc::*;
use serde_json::{Value, json};
use crate::cmd::CommandSpec;
use crate::components::{inside_instance, variant_set};
use crate::params::*;
use crate::{Result, Session, bad, spec};
/// Most dev resources on one layer.
pub const MAX_PER_LAYER: usize = 32;
/// Longest URL (bytes).
pub const MAX_URL: usize = 2_048;
/// Longest name (chars).
pub const MAX_NAME: usize = 200;
/// `url` trimmed if it is a web link: http or https, a host, no whitespace or control characters.
pub fn check_url(url: &str) -> std::result::Result<String, String> {
let url = url.trim();
if url.len() > MAX_URL {
return Err(format!("a link is at most {MAX_URL} characters"));
}
if url.chars().any(|c| c.is_whitespace() || c.is_control()) {
return Err("a link has no spaces".into());
}
let lower = url.to_ascii_lowercase();
let rest = ["https://", "http://"].iter().find_map(|p| lower.strip_prefix(p)).ok_or("a link starts with https:// or http://")?;
let host = rest.split(['/', '?', '#']).next().unwrap_or_default();
let host = host.rsplit('@').next().unwrap_or_default();
if host.is_empty() || host.starts_with(':') {
return Err("a link needs a site after https://".into());
}
Ok(url.to_string())
}
/// The name a link gets when none is given: its URL without the scheme, `www.` and a trailing
/// slash.
pub fn default_name(url: &str) -> String {
let rest = url.split_once("://").map_or(url, |(_, r)| r);
let rest = rest.strip_prefix("www.").unwrap_or(rest);
let rest = rest.strip_suffix('/').unwrap_or(rest);
rest.chars().take(MAX_NAME).collect()
}
/// A dev resource as listed: its own (with its index) or inherited `from` a main component, a
/// component set or, inside an instance, the main's layer.
#[derive(Clone, Debug, PartialEq)]
pub struct Listed {
pub resource: DevResource,
pub index: Option<usize>,
pub from: Option<NodeId>,
}
/// Layer `id`'s dev resources: its own first (none for a layer inside an instance, whose links come
/// from the main's layer), then those of its main component and that component's set.
pub fn list(doc: &Document, id: NodeId) -> Vec<Listed> {
let Some(n) = doc.node(id) else { return Vec::new() };
let inherited = |from: NodeId| doc.node(from).into_iter().flat_map(move |m| m.dev_resources.iter().map(move |r| (r, from)));
if inside_instance(doc, id) {
let from = n.source.unwrap_or(id);
return n.dev_resources.iter().map(|r| Listed { resource: r.clone(), index: None, from: Some(from) }).collect();
}
let mut out: Vec<Listed> = n.dev_resources.iter().enumerate().map(|(i, r)| Listed { resource: r.clone(), index: Some(i), from: None }).collect();
if let NodeKind::Instance { component, .. } = n.kind {
let set = variant_set(doc, component);
for (r, from) in inherited(component).chain(set.into_iter().flat_map(inherited)) {
out.push(Listed { resource: r.clone(), index: None, from: Some(from) });
}
}
out
}
/// `list` as JSON: `[{name, url, index? (own), from? (inherited)}]`.
pub fn list_json(doc: &Document, id: NodeId) -> Value {
Value::Array(
list(doc, id)
.into_iter()
.map(|l| {
let mut j = json!({"name": l.resource.name, "url": l.resource.url});
if let Some(i) = l.index {
j["index"] = json!(i);
}
if let Some(f) = l.from {
j["from"] = json!(f);
}
j
})
.collect(),
)
}
/// The layer a command acts on: not a page, not a layer inside an instance.
fn layer(cmd: &str, s: &Session, p: &Value) -> Result<NodeId> {
let id = targets(s, p).first().copied().ok_or_else(|| bad(cmd, "select a layer or pass `id`"))?;
let doc = s.doc()?;
let n = doc.node(id).ok_or_else(|| bad(cmd, "no such layer"))?;
if n.is_page() {
return Err(bad(cmd, "pages can't have dev resources"));
}
if inside_instance(doc, id) {
return Err(bad(cmd, "this layer is inside an instance: add the link to the main component's layer"));
}
Ok(id)
}
/// The `url` parameter, checked.
fn url_param(cmd: &str, p: &Value) -> Result<Option<String>> {
match get(p, "url") {
None | Some(Value::Null) => Ok(None),
Some(Value::String(u)) => check_url(u).map(Some).map_err(|e| bad(cmd, e)),
Some(_) => Err(bad(cmd, "url: a string")),
}
}
/// The `name` parameter, trimmed and clipped (`None` when missing or blank).
fn name_param(cmd: &str, p: &Value) -> Result<Option<String>> {
match get(p, "name") {
None | Some(Value::Null) => Ok(None),
Some(Value::String(n)) => Ok(Some(n.trim().chars().take(MAX_NAME).collect::<String>()).filter(|n| !n.is_empty())),
Some(_) => Err(bad(cmd, "name: a string")),
}
}
/// The own dev resource `index` of `id` must exist.
fn existing(cmd: &str, s: &Session, id: NodeId, p: &Value) -> Result<usize> {
let index = req_index(cmd, p, "index")?;
let count = s.doc()?.node(id).map_or(0, |n| n.dev_resources.len());
if index >= count {
return Err(bad(cmd, format!("no dev resource {index} (the layer has {count} of its own)")));
}
Ok(index)
}
pub fn specs() -> Vec<CommandSpec> {
vec![
spec!(
"devResource.add",
"Add dev resource",
"{id? (default the first selected layer), url (http or https), name? (default the URL without its scheme)} a link for developers on the layer; an instance also shows its main component's → {id, index}",
true,
|s, p| {
let cmd = "devResource.add";
let id = layer(cmd, s, p)?;
let url = url_param(cmd, p)?.ok_or_else(|| bad(cmd, "pass the link's `url`"))?;
let name = name_param(cmd, p)?.unwrap_or_else(|| default_name(&url));
let n = s.doc_mut()?.get_mut(id)?;
if n.dev_resources.len() >= MAX_PER_LAYER {
return Err(bad(cmd, format!("a layer has at most {MAX_PER_LAYER} dev resources")));
}
n.dev_resources.push(DevResource { url, name });
Ok(json!({"id": id, "index": n.dev_resources.len() - 1}))
}
),
spec!("devResource.edit", "Edit dev resource", "{id?, index (of the layer's own), url?, name?} → {id, index}", true, |s, p| {
let cmd = "devResource.edit";
let id = layer(cmd, s, p)?;
let index = existing(cmd, s, id, p)?;
let (url, name) = (url_param(cmd, p)?, name_param(cmd, p)?);
if url.is_none() && name.is_none() {
return Err(bad(cmd, "pass `url` and/or `name`"));
}
let n = s.doc_mut()?.get_mut(id)?;
let r = n.dev_resources.get_mut(index).ok_or_else(|| bad(cmd, "no such dev resource"))?;
if let Some(u) = url {
r.url = u;
}
if let Some(nm) = name {
r.name = nm;
}
Ok(json!({"id": id, "index": index}))
}),
spec!(
"devResource.remove",
"Remove dev resource",
"{id?, index? (omit: every dev resource of the layer's own)} → {removed}",
true,
|s, p| {
let cmd = "devResource.remove";
let id = layer(cmd, s, p)?;
let index = match get(p, "index") {
Some(_) => Some(existing(cmd, s, id, p)?),
None => None,
};
let n = s.doc_mut()?.get_mut(id)?;
if n.dev_resources.is_empty() {
return Err(bad(cmd, "the layer has no dev resources of its own"));
}
let removed = match index {
Some(i) if i < n.dev_resources.len() => {
n.dev_resources.remove(i);
1
}
Some(_) => return Err(bad(cmd, "no such dev resource")),
None => std::mem::take(&mut n.dev_resources).len(),
};
Ok(json!({"removed": removed}))
}
),
spec!(
"devResource.list",
"List dev resources",
"{id? (default the first selected layer)} the layer's links, its own first, then inherited from its main component and that component's set → [{name, url, index? (own), from? (the layer it comes from)}]",
false,
|s, p| {
let id = targets(s, p).first().copied().ok_or_else(|| bad("devResource.list", "select a layer or pass `id`"))?;
let doc = s.doc()?;
doc.node(id).ok_or_else(|| bad("devResource.list", "no such layer"))?;
Ok(list_json(doc, id))
}
),
]
}

View File

@ -0,0 +1,144 @@
//! Dev resources: links for developers on layers, added, edited and removed, shown on a main
//! component's instances, checked as web links (never fetched), saved, synced, undoable and in
//! what agents inspect.
use figcraft_collab::{Change, Prop, Replica};
use figcraft_doc::NodeId;
use serde_json::{Value, json};
use crate::Session;
use crate::params::id_of;
fn ok(s: &mut Session, cmd: &str, p: Value) -> Value {
s.execute(cmd, &p).unwrap_or_else(|e| panic!("{cmd} {p}: {e}"))
}
fn rect(s: &mut Session) -> NodeId {
id_of(&ok(s, "shape.rectangle", json!({"x": 0, "y": 0, "width": 40, "height": 40}))["id"]).unwrap()
}
fn names(s: &mut Session, id: NodeId) -> Vec<String> {
ok(s, "devResource.list", json!({"id": id})).as_array().unwrap().iter().map(|r| r["name"].as_str().unwrap().to_string()).collect()
}
#[test]
fn links_are_added_edited_and_removed() {
let mut s = Session::new();
let r = rect(&mut s);
// A link with a name, and one without: named after its URL (no page title is looked up).
let a = ok(&mut s, "devResource.add", json!({"id": r, "url": "https://storybook.example.com/button", "name": "Button stories"}));
assert_eq!(a["index"], 0);
ok(&mut s, "devResource.add", json!({"id": r, "url": "https://www.example.com/docs/"}));
assert_eq!(names(&mut s, r), ["Button stories", "example.com/docs"]);
// Edit the name or the URL; one undo step each.
ok(&mut s, "devResource.edit", json!({"id": r, "index": 1, "name": "Docs"}));
ok(&mut s, "devResource.edit", json!({"id": r, "index": 1, "url": "https://example.com/v2"}));
let list = ok(&mut s, "devResource.list", json!({"id": r}));
assert_eq!((&list[1]["name"], &list[1]["url"], &list[1]["index"]), (&json!("Docs"), &json!("https://example.com/v2"), &json!(1)));
ok(&mut s, "edit.undo", json!({}));
assert_eq!(ok(&mut s, "devResource.list", json!({"id": r}))[1]["url"], "https://www.example.com/docs/");
// Remove one, then the rest.
ok(&mut s, "devResource.remove", json!({"id": r, "index": 0}));
assert_eq!(names(&mut s, r), ["Docs"]);
assert_eq!(ok(&mut s, "devResource.remove", json!({"id": r}))["removed"], 1);
assert!(names(&mut s, r).is_empty());
assert!(s.execute("devResource.remove", &json!({"id": r})).is_err(), "nothing left to remove");
// The selection is the default target.
ok(&mut s, "select.set", json!({"ids": [r]}));
ok(&mut s, "devResource.add", json!({"url": "http://localhost:6006"}));
assert_eq!(names(&mut s, r), ["localhost:6006"]);
}
#[test]
fn only_web_links_are_kept() {
let mut s = Session::new();
let r = rect(&mut s);
let long = format!("https://example.com/{}", "a".repeat(3000));
for url in ["javascript:alert(1)", "ftp://example.com", "example.com", "https://", "https://exa mple.com", "https://:80/x", "", " ", &long] {
assert!(s.execute("devResource.add", &json!({"id": r, "url": url})).is_err(), "{url}");
}
for p in [json!({"id": r}), json!({"id": r, "url": 5}), json!({"id": r, "url": "https://a.b", "name": ["x"]})] {
assert!(s.execute("devResource.add", &p).is_err(), "{p}");
}
// Spaces around a link are trimmed; HTTPS in capitals is fine; long names are clipped.
let name = "n".repeat(500);
ok(&mut s, "devResource.add", json!({"id": r, "url": " HTTPS://Example.com/A ", "name": name}));
let l = ok(&mut s, "devResource.list", json!({"id": r}));
assert_eq!(l[0]["url"], "HTTPS://Example.com/A");
assert_eq!(l[0]["name"].as_str().unwrap().chars().count(), crate::dev_resources::MAX_NAME);
// Docs silent on a limit: a layer keeps at most 32.
for i in 1..crate::dev_resources::MAX_PER_LAYER {
ok(&mut s, "devResource.add", json!({"id": r, "url": format!("https://example.com/{i}")}));
}
assert!(s.execute("devResource.add", &json!({"id": r, "url": "https://example.com/more"})).is_err());
// Pages have none; edits need a link of the layer's own.
let page = s.page().unwrap();
assert!(s.execute("devResource.add", &json!({"id": page, "url": "https://example.com"})).is_err());
assert!(s.execute("devResource.edit", &json!({"id": r, "index": 99, "name": "x"})).is_err());
assert!(s.execute("devResource.edit", &json!({"id": r, "index": 0})).is_err(), "nothing to change");
}
#[test]
fn a_main_components_links_show_on_its_instances() {
let mut s = Session::new();
let f = id_of(&ok(&mut s, "frame.create", json!({"x": 0, "y": 0, "width": 100, "height": 40}))["id"]).unwrap();
let r = id_of(&ok(&mut s, "shape.rectangle", json!({"x": 5, "y": 5, "width": 10, "height": 10, "parent": f}))["id"]).unwrap();
ok(&mut s, "devResource.add", json!({"id": r, "url": "https://example.com/icon", "name": "Icon"}));
let main = id_of(&ok(&mut s, "component.create", json!({"ids": [f]}))["id"]).unwrap();
ok(&mut s, "devResource.add", json!({"id": main, "url": "https://github.com/acme/ui/blob/main/Button.tsx", "name": "Button.tsx"}));
let inst = id_of(&ok(&mut s, "instance.create", json!({"component": main, "x": 200, "y": 0}))["id"]).unwrap();
// The instance lists the main's link, read-only (no index), and can have links of its own.
let l = ok(&mut s, "devResource.list", json!({"id": inst}));
assert_eq!((&l[0]["name"], &l[0]["from"], l[0].get("index")), (&json!("Button.tsx"), &json!(main), None));
ok(&mut s, "devResource.add", json!({"id": inst, "url": "https://jira.example.com/UI-12", "name": "UI-12"}));
assert_eq!(names(&mut s, inst), ["UI-12", "Button.tsx"]);
// Editing the main reaches the instance; the instance keeps its own across that.
ok(&mut s, "devResource.edit", json!({"id": main, "index": 0, "name": "Button source"}));
ok(&mut s, "node.set", json!({"id": main, "props": {"opacity": 0.5}}));
assert_eq!(names(&mut s, inst), ["UI-12", "Button source"]);
assert_eq!(s.doc().unwrap().node(inst).unwrap().dev_resources.len(), 1, "the main's links aren't copied");
// Layers inside an instance show their main layer's links and take none of their own.
let inner = s.doc().unwrap().node(inst).unwrap().children[0];
let l = ok(&mut s, "devResource.list", json!({"id": inner}));
assert_eq!((&l[0]["name"], l[0].get("index")), (&json!("Icon"), None));
assert!(l[0]["from"].is_u64());
assert!(s.execute("devResource.add", &json!({"id": inner, "url": "https://example.com"})).is_err());
}
#[test]
fn a_component_sets_links_show_on_its_variants_instances() {
// Docs silent: links on a component set show on instances of any of its variants.
let mut s = Session::new();
let a = rect(&mut s);
let b = rect(&mut s);
let ca = id_of(&ok(&mut s, "component.create", json!({"ids": [a]}))["id"]).unwrap();
let cb = id_of(&ok(&mut s, "component.create", json!({"ids": [b]}))["id"]).unwrap();
let set = id_of(&ok(&mut s, "component.combineVariants", json!({"ids": [ca, cb]}))["id"]).unwrap();
ok(&mut s, "devResource.add", json!({"id": set, "url": "https://example.com/button", "name": "Button docs"}));
let inst = id_of(&ok(&mut s, "instance.create", json!({"component": cb, "x": 300, "y": 0}))["id"]).unwrap();
let l = ok(&mut s, "devResource.list", json!({"id": inst}));
assert_eq!((&l[0]["name"], &l[0]["from"]), (&json!("Button docs"), &json!(set)));
}
#[test]
fn links_save_sync_and_show_in_inspect() {
let mut s = Session::new();
let r = rect(&mut s);
let before = s.doc().unwrap().clone();
ok(&mut s, "devResource.add", json!({"id": r, "url": "https://example.com/spec", "name": "Spec"}));
// A change the collaboration layer syncs.
let d = figcraft_collab::diff(&Replica::new(before), s.doc().unwrap()).unwrap();
assert!(d.changes.iter().any(|c| matches!(c, Change::Set { id, prop: Prop::DevResources, .. } if *id == r)), "{:?}", d.changes);
// What agents inspect (MCP get_node / inspect_document).
let j = crate::inspect::node_json(s.doc().unwrap(), r, 0);
assert_eq!(j["devResources"], json!([{"name": "Spec", "url": "https://example.com/spec", "index": 0}]));
// Saved and reopened.
let dir = std::env::temp_dir().join(format!("figcraft-devres-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("f.figcraft").to_string_lossy().into_owned();
ok(&mut s, "file.saveAs", json!({"path": path}));
let mut t = Session::empty();
ok(&mut t, "file.open", json!({"path": path}));
assert_eq!(names(&mut t, r), ["Spec"]);
std::fs::remove_dir_all(&dir).ok();
}

View File

@ -141,6 +141,10 @@ pub fn node_json(doc: &Document, id: NodeId, depth: usize) -> Value {
if !n.code_links.is_empty() {
o.insert("codeLinks".into(), camel(serde_json::to_value(&n.code_links).unwrap_or(Value::Null)));
}
let dev = crate::dev_resources::list_json(doc, n.id);
if dev.as_array().is_some_and(|a| !a.is_empty()) {
o.insert("devResources".into(), dev);
}
if !n.interactions.is_empty() {
o.insert("interactions".into(), serde_json::to_value(&n.interactions).unwrap_or(Value::Null));
}

View File

@ -19,6 +19,7 @@ pub mod codegen;
pub mod collab;
pub mod component_codegen;
pub mod components;
pub mod dev_resources;
pub mod devcompare;
pub mod devinspect;
pub mod distance;
@ -1273,6 +1274,8 @@ mod components_behaviour_tests;
#[cfg(test)]
mod components_scale_tests;
#[cfg(test)]
mod dev_resources_tests;
#[cfg(test)]
mod devcompare_tests;
#[cfg(test)]
mod devfocus_tests;

View File

@ -258,6 +258,9 @@ fn every_command_survives_junk_params() {
json!({"variations": {"wght": 1e308, "": "x", "slnt": null}, "props": {"fontVariations": [1], "variationSettings": {"wdth": "-1e9"}}, "text": 7, "cursor": -3}),
json!({"style": 3, "props": {"listStyle": 5, "indentation": 1e308, "listSpacing": "NaN", "hangingList": "yes", "start": -5, "end": 1e12,
"textDecorationThickness": {"unit": "pixels", "value": "x"}, "textDecorationOffset": -1e308, "textDecorationColor": [1]}}),
// Links (dev resources) and indexes as junk.
json!({"id": 2, "url": "javascript:alert(1)", "index": 1e308, "name": {"x": 1}}),
json!({"id": 2, "url": "https://\u{0}x", "index": -1, "name": ""}),
];
for spec in all_commands() {
if matches!(

View File

@ -36,6 +36,7 @@ name = "English"
"Add auto layout" = "Add auto layout"
"Add background behind overlay" = "Add background behind overlay"
"Add content to this slot" = "Add content to this slot"
"Add dev resource" = "Add dev resource"
"Add effect" = "Add effect"
"Add export setting" = "Add export setting"
"Add fill" = "Add fill"
@ -340,6 +341,7 @@ name = "English"
"Detach variable" = "Detach variable"
"Details" = "Details"
"Dev Mode" = "Dev Mode"
"Dev resources" = "Dev resources"
"Development" = "Development"
"Device" = "Device"
"Device at 100%" = "Device at 100%"
@ -375,6 +377,7 @@ name = "English"
"Edit as an expression" = "Edit as an expression"
"Edit colour" = "Edit colour"
"Edit comment" = "Edit comment"
"Edit dev resource" = "Edit dev resource"
"Edit link" = "Edit link"
"Edit link copied" = "Edit link copied"
"Edit style" = "Edit style"
@ -472,6 +475,7 @@ name = "English"
"Frame tools" = "Frame tools"
"Frames" = "Frames"
"From the library \"{name}\": read-only" = "From the library \"{name}\": read-only"
"From {name}" = "From {name}"
"Frost" = "Frost"
"Gap" = "Gap"
"Gap between columns" = "Gap between columns"
@ -620,6 +624,7 @@ name = "English"
"Lines deleted" = "Lines deleted"
"Link" = "Link"
"Link copied" = "Link copied"
"Link docs, code or tickets for developers." = "Link docs, code or tickets for developers."
"Link this component to its code component: instances then show your real snippet with their values." = "Link this component to its code component: instances then show your real snippet with their values."
"List spacing" = "List spacing"
"List style" = "List style"
@ -816,6 +821,7 @@ name = "English"
"Paragraph indent applies to left-aligned text" = "Paragraph indent applies to left-aligned text"
"Paragraph spacing" = "Paragraph spacing"
"Pass through" = "Pass through"
"Paste a link (https://…)" = "Paste a link (https://…)"
"Paste a team library link (a view link)" = "Paste a team library link (a view link)"
"Paste here" = "Paste here"
"Paste the invite link the host shared with you." = "Paste the invite link the host shared with you."
@ -897,6 +903,7 @@ name = "English"
"Remove all" = "Remove all"
"Remove annotation" = "Remove annotation"
"Remove auto layout" = "Remove auto layout"
"Remove dev resource" = "Remove dev resource"
"Remove export setting" = "Remove export setting"
"Remove guide" = "Remove guide"
"Remove interaction" = "Remove interaction"

View File

@ -37,6 +37,7 @@ status = "draft — needs native review"
"Add auto layout" = "Añadir diseño automático"
"Add background behind overlay" = "Añadir fondo tras la superposición"
"Add content to this slot" = "Añadir contenido a esta ranura"
"Add dev resource" = "Añadir recurso de desarrollo"
"Add effect" = "Añadir efecto"
"Add export setting" = "Añadir ajuste de exportación"
"Add fill" = "Añadir relleno"
@ -341,6 +342,7 @@ status = "draft — needs native review"
"Detach variable" = "Separar variable"
"Details" = "Detalles"
"Dev Mode" = "Modo desarrollo"
"Dev resources" = "Recursos de desarrollo"
"Development" = "Desarrollo"
"Device" = "Dispositivo"
"Device at 100%" = "Dispositivo al 100%"
@ -376,6 +378,7 @@ status = "draft — needs native review"
"Edit as an expression" = "Editar como expresión"
"Edit colour" = "Editar color"
"Edit comment" = "Editar comentario"
"Edit dev resource" = "Editar recurso de desarrollo"
"Edit link" = "Enlace de edición"
"Edit link copied" = "Enlace de edición copiado"
"Edit style" = "Editar estilo"
@ -473,6 +476,7 @@ status = "draft — needs native review"
"Frame tools" = "Herramientas de marco"
"Frames" = "Marcos"
"From the library \"{name}\": read-only" = "De la biblioteca \"{name}\": solo lectura"
"From {name}" = "De {name}"
"Frost" = "Escarcha"
"Gap" = "Espacio"
"Gap between columns" = "Espacio entre columnas"
@ -621,6 +625,7 @@ status = "draft — needs native review"
"Lines deleted" = "Líneas eliminadas"
"Link" = "Enlace"
"Link copied" = "Enlace copiado"
"Link docs, code or tickets for developers." = "Enlaza documentación, código o tickets para desarrolladores."
"Link this component to its code component: instances then show your real snippet with their values." = "Vincula este componente a su componente de código: así las instancias muestran tu fragmento real con sus valores."
"List spacing" = "Espaciado de lista"
"List style" = "Estilo de lista"
@ -817,6 +822,7 @@ status = "draft — needs native review"
"Paragraph indent applies to left-aligned text" = "La sangría de párrafo se aplica al texto alineado a la izquierda"
"Paragraph spacing" = "Espacio entre párrafos"
"Pass through" = "Atravesar"
"Paste a link (https://…)" = "Pega un enlace (https://…)"
"Paste a team library link (a view link)" = "Pega el enlace de una biblioteca de equipo (de lectura)"
"Paste here" = "Pegar aquí"
"Paste the invite link the host shared with you." = "Pega el enlace de invitación que te compartió el anfitrión."
@ -898,6 +904,7 @@ status = "draft — needs native review"
"Remove all" = "Quitar todo"
"Remove annotation" = "Quitar anotación"
"Remove auto layout" = "Quitar diseño automático"
"Remove dev resource" = "Quitar recurso de desarrollo"
"Remove export setting" = "Quitar ajuste de exportación"
"Remove guide" = "Quitar guía"
"Remove interaction" = "Quitar interacción"
@ -1442,6 +1449,10 @@ status = "draft — needs native review"
"dev.focus" = "Vista de enfoque"
"dev.resolveChange" = "Resolver cambio"
"dev.unfocus" = "Salir de la vista de enfoque"
"devResource.add" = "Añadir recurso de desarrollo"
"devResource.edit" = "Editar recurso de desarrollo"
"devResource.list" = "Listar recursos de desarrollo"
"devResource.remove" = "Quitar recurso de desarrollo"
"distribute.horizontal" = "Distribuir espaciado horizontal"
"distribute.vertical" = "Distribuir espaciado vertical"
"doc.home" = "Inicio"

View File

@ -814,6 +814,11 @@ pub fn button(resp: &Response, tip: &str) {
});
}
/// A link, named by its text.
pub fn link(resp: &Response, label: &str) {
node(resp, label, |n| n.set_role(Role::Link));
}
/// A button opening a menu or flyout, `expanded` while it is open.
pub fn menu_button(resp: &Response, label: &str, expanded: bool) {
node(resp, label, |n| {

View File

@ -2,8 +2,8 @@
//! drawn as callouts in their category's colour on the canvas and edited in the panel, the
//! categories and the filter of the callouts, the handoff status of the selected frames (with the
//! focus view and Compare changes), the list of frames and sections that are ready for dev, the
//! focus view's bar, Connect to code (a component's link to its code component) and the plugins
//! that only read the document.
//! focus view's bar, Connect to code (a component's link to its code component), dev resources
//! (links for developers) and the plugins that only read the document.
use egui::{Align2, Color32, CornerRadius, Id, Rect, Sense, Stroke, StrokeKind, Ui, pos2, vec2};
use figcraft_doc::{Annotation, AnnotationCategory, CodeLink, DevStatus, Document, NodeId};
@ -12,6 +12,7 @@ use figcraft_engine::code_connect;
use figcraft_engine::codegen::{CodeLang, code_for};
use figcraft_engine::devinspect::{self, DevItem, Prop};
use figcraft_engine::plugins::Access;
use figcraft_engine::{components, dev_resources};
use serde_json::json;
use crate::a11y::Tip;
@ -467,6 +468,102 @@ pub fn annotations_section(app: &mut FigApp, ui: &mut Ui, doc: &Document, id: No
ui.add_space(4.0);
}
/// The selected layer's dev resources: links for developers, opened in the browser when clicked.
/// Its own can be renamed, changed and removed; a main component's (and its set's) show on its
/// instances, read-only, with where they come from.
pub fn dev_resources_section(app: &mut FigApp, ui: &mut Ui, doc: &Document, id: NodeId) {
let t = Tokens::get(ui.ctx());
let Some(n) = doc.node(id).filter(|n| !n.is_page()) else { return };
let list = dev_resources::list(doc, id);
let can_add = !components::inside_instance(doc, id) && n.dev_resources.len() < dev_resources::MAX_PER_LAYER;
let (adding_id, editing_id) = (Id::new(("devres_adding", id)), Id::new(("devres_editing", id)));
let mut adding = can_add && ui.data(|d| d.get_temp::<bool>(adding_id)).unwrap_or(false);
let mut editing = ui.data(|d| d.get_temp::<usize>(editing_id)).filter(|i| *i < n.dev_resources.len());
widgets::separator(ui);
let buttons: &[(Icon, &str)] = if can_add { &[(Icon::Plus, tr("Add dev resource"))] } else { &[] };
if widgets::section_header(ui, tr("Dev resources"), buttons, list.is_empty() && !adding).is_some() {
adding = !adding;
}
for item in &list {
let (name, url) = (&item.resource.name, &item.resource.url);
if let Some(i) = item.index.filter(|i| editing == Some(*i)) {
widgets::sublabels(ui, tr("Name"), Some(tr("Link")));
let col = widgets::col_width(ui.available_width());
let mut edit = None;
row(ui, 32.0, |ui| {
ui.spacing_mut().item_spacing.x = 8.0;
if let Some(v) = widgets::text_field(ui, Id::new(("devres_name", id, i)), name, col, tr("Name")) {
edit = Some(json!({"id": id, "index": i, "name": v}));
}
if let Some(v) = widgets::text_field(ui, Id::new(("devres_url", id, i)), url, col, "https://…") {
edit = Some(json!({"id": id, "index": i, "url": v}));
}
if widgets::icon_button(ui, Icon::Check, tr("Done")) {
editing = None;
}
});
if let Some(p) = edit {
app.run("devResource.edit", p);
}
continue;
}
let (r, resp) = ui.allocate_exact_size(vec2(ui.available_width(), 36.0), Sense::click());
let buttons_w = if item.index.is_some() { 52.0 } else { 0.0 };
let text_w = r.width() - PAD * 2.0 - 22.0 - buttons_w;
let hovered = resp.hovered();
if hovered {
ui.painter().rect_filled(r.shrink2(vec2(PAD / 2.0, 1.0)), CornerRadius::same(4), t.hover);
ui.ctx().set_cursor_icon(egui::CursorIcon::PointingHand);
}
icons::paint(ui.painter(), Rect::from_center_size(pos2(r.min.x + PAD + 7.0, r.center().y), vec2(14.0, 14.0)), Icon::Link, t.icon_secondary);
let x = r.min.x + PAD + 22.0;
ui.painter().text(pos2(x, r.center().y - 7.0), Align2::LEFT_CENTER, widgets::elide(ui, name, text_w), semibold(11.0), ACCENT);
let below = match item.from.and_then(|f| doc.node(f)) {
Some(m) => trf("From {name}", &[("name", &m.name)]),
None => url.clone(),
};
ui.painter().text(pos2(x, r.center().y + 7.0), Align2::LEFT_CENTER, widgets::elide(ui, &below, text_w), regular(10.0), t.text_secondary);
crate::a11y::link(&resp, name);
// Opened only on a click, never fetched.
if resp.on_hover_text(url.as_str()).clicked() {
ui.ctx().open_url(egui::OpenUrl::new_tab(url));
}
if let Some(i) = item.index {
let del = Rect::from_center_size(pos2(r.max.x - PAD - 4.0, r.center().y), vec2(22.0, 22.0));
let pen = del.translate(vec2(-26.0, 0.0));
if widgets::icon_button_at(ui, Id::new(("devres_edit", id, i)), pen, Icon::Pencil, tr("Edit dev resource")) {
editing = Some(i);
}
if widgets::icon_button_at(ui, Id::new(("devres_del", id, i)), del, Icon::Minus, tr("Remove dev resource")) {
app.run("devResource.remove", json!({"id": id, "index": i}));
editing = None;
}
}
}
if adding {
let w = ui.available_width() - PAD * 2.0;
let mut added = None;
row(ui, 32.0, |ui| added = widgets::text_field(ui, Id::new(("devres_new", id)), "", w, tr("Paste a link (https://…)")));
if let Some(url) = added.filter(|u| !u.trim().is_empty())
&& app.run("devResource.add", json!({"id": id, "url": url})).is_some()
{
adding = false;
}
} else if list.is_empty() && can_add {
widgets::note(ui, tr("Link docs, code or tickets for developers."), t.text_secondary);
}
ui.data_mut(|d| {
d.insert_temp(adding_id, adding);
match editing {
Some(i) => {
d.insert_temp(editing_id, i);
}
None => d.remove::<usize>(editing_id),
}
});
ui.add_space(4.0);
}
/// The category of annotation `a` as a dropdown: the one picked (`Value::Null` for none).
fn category_menu(ui: &mut Ui, doc: &Document, id: Id, a: &Annotation) -> Option<serde_json::Value> {
let current = annotations::category_of(doc, a);

View File

@ -2160,6 +2160,7 @@ fn inspect(app: &mut FigApp, ui: &mut Ui) {
widgets::section_header(ui, &name, &[], false);
crate::dev_mode::status(app, ui, &doc, &sel);
crate::dev_mode::annotations_section(app, ui, &doc, first);
crate::dev_mode::dev_resources_section(app, ui, &doc, first);
use figcraft_engine::devinspect::ColorFormat;
let fmt_id = Id::new("inspect_color_format");
let fmt = ui.data(|d| d.get_temp::<usize>(fmt_id)).and_then(|i| ColorFormat::ALL.get(i).copied()).unwrap_or_default();

View File

@ -67,7 +67,8 @@ children), `gridCell` (a grid auto layout child's column, row and spans), clip,
component/instance links, overrides, `slot` (a slot layer's property name), boundVariables, interactions, exports,
`devStatus` (handoff status), `devChanged` (changed since it was marked ready for dev),
`annotations` (Dev Mode notes: `[{label, properties, category?}]`), `codeLinks` (a component's
links to code: `[{language, importPath, name, template}]`). The auto layout props
links to code: `[{language, importPath, name, template}]`), `devResources` (links for developers,
its own and inherited: `[{name, url, index?, from?}]`). The auto layout props
that `node.set` accepts are listed in [mcp.md](mcp.md#conventions-agents-must-know).
### `ui.inspect {}`

View File

@ -1,9 +1,10 @@
# Dev Mode parity
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (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-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)
**Breadth ~93%, ready ~70% (estimated).** The inspect surface is broad and components connect
to the code components of a repository; left: dev resources links. Remaining: 1–3 h.
**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.
| Feature | Status | Notes |
|---|---|---|
@ -18,12 +19,13 @@ to the code components of a repository; left: dev resources links. Remaining: 1
| 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 |
| 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 | missing | 1 h |
| 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 |
## Revision history
| Date | Change | Summary |
|---|---|---|
| 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 `<text>`), 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 |
| 2026-10-10 | major | Created from the 2026-10-10 re-measure |

View File

@ -1,6 +1,6 @@
# Where FigCraft falls short of Figma
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (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-12 · **Change:** minor (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, gamepad | Feature | 3–6 | [prototyping-parity](prototyping-parity.md) |
| 16 | Dev Mode: dev resources links, editor extension (code connect and Dev Mode plugins landed) | Feature | 1–3 | [dev-mode-parity](dev-mode-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) |
| 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) |
@ -224,7 +224,7 @@ No alpha blockers: the alpha gate in [`roadmap.md`](roadmap.md#alpha-gate) passe
The REST importer warns and skips them (`crates/format/src/rest/proto.rs`).
- **Estimate:** 3–6 h after video fills (gap 12).
### 16. Dev Mode: dev resources links, editor extension
### 16. Dev Mode: the tail after dev resources
- **Landed (2026-10-11):** code connect: a component or set links to its code component per
language (React, SwiftUI, Compose): an import path, the name and a template whose `{property}`
and `{property ? "a" : "b"}` placeholders take the instance's values, escaped per language and
@ -234,11 +234,18 @@ No alpha blockers: the alpha gate in [`roadmap.md`](roadmap.md#alpha-gate) passe
as `figcraft.connect.json` (`codeConnect.export` / `codeConnect.import`, validated and capped)
so code owners can version them. Plugins whose manifest asks only to read run from the Inspect
tab.
- **Missing:** dev resources links on layers; import lines for connected instances inside a
- **Landed (2026-10-12):** dev resources: 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 links (and its set's) read-only with where they come from; layers
inside an instance show their main layer's. URLs are checked as web links (http or https, a
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
serves design data, code and the connected snippets to agents and editors.
- **Estimate:** 1–3 h.
- **Estimate:** 0–1 h.
### 17. Branching and merging
- **Landed (2026-10-11):** branches of a file (`branch.*`): File ▸ Create branch… (or the Branches
@ -307,6 +314,7 @@ No alpha blockers: the alpha gate in [`roadmap.md`](roadmap.md#alpha-gate) passe
| Date | Change | Summary |
|---|---|---|
| 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-12 | minor | Extended collections' tail (gap 14): variables a library adds to a collection this file extends now count as a library update of their own (listed by name without a copy yet) and arrive inheriting when accepted, instead of waiting for the library's next other update; a library collection that is itself an extended collection can be extended again, values flowing down the chain as each file accepts updates with every level's overrides kept (checked end to end). Styles & variables breadth 94% → 95%, ready unchanged (76%), 3–5 h → 2–4 h; gap 14 1–2 h → 0–1 h (real-library checks left); feature breadth (~82%) and ready for real work (~51%) unchanged at this rounding; full parity ~305–594 h → ~304–593 h |
| 2026-10-12 | minor | A branch's own version history and finer list merges (gap 17): versions saved or autosaved on a branch tab go into the branch (viewed, restored, renamed, deleted and duplicated there, saved in the file with it); merging adds a version to the main file's history and the merged or archived branch keeps its own; fills, strokes and effects merge item by item, conflicting only when the same item changed both ways, an item was removed on one side or both sides added different items. Comments, versions, find, misc breadth 85% → 87%, ready 60% → 61%, 2–5 h → 1–4 h; gap 17 1–3 h → 1–2 h; feature breadth (~82%) and ready for real work (~51%) unchanged at this rounding; full parity ~306–595 h → ~305–594 h |
| 2026-10-12 | minor | Comments and version history behaviour suite and fixes added to gap 4's landed list (gap 17 unchanged: a branch's own version history is still left); ranking and hours unchanged |

View File

@ -921,6 +921,17 @@ Plugin settings are kept per plugin with the preferences (in a headless session,
Connect to code section edits them in the Code section's language, with a live preview and a
Connected badge. An editor extension isn't needed: agents and editors read the same snippets
through this server. Plugins whose manifest asks only for `read` also run from the Inspect tab.
- **Dev resources:** links for developers (docs, code, tickets) on any layer, each a URL and a
name. `devResource.add {id?, url, name?}` → `{id, index}` (no name: the URL without its scheme,
`www.` and a trailing slash; no page title is looked up), `devResource.edit {id?, index, url?,
name?}`, `devResource.remove {id?, index?}` (no index: all of the layer's own), all undoable, and
`devResource.list {id?}` → `[{name, url, index? (its own), from? (the layer it comes from)}]`: an
instance lists its own, then its main component's and that component's set's (read-only); a
layer inside an instance shows its main layer's and takes none of its own. URLs must be `http://`
or `https://` with a host, without spaces or control characters, at most 2,048 characters; names
are clipped to 200 characters; a layer holds at most 32 links. Links are data: never fetched or
opened by the engine (the app opens one when it is clicked). `get_node` and `inspect_document`
list them as `devResources`; the Inspect tab lists, adds, edits and removes them.
- **Annotations:** notes for developers on any layer, each with optional pinned properties that
Dev Mode shows with their current values (and the style or variable behind them).
`annotation.add {id?, label?, properties?}` (a note, pinned properties or both; returns `{id,

View File

@ -1,6 +1,6 @@
# FigCraft parity with Figma
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (extended collections tail; branch version history and finer list merges; headline percentages recomputed from the area table: feature breadth ~84%, core ~94%, mainstream ~50%; text lists, hanging punctuation and decoration styles; comments and version history behaviour suite and fixes; canvas navigation, guides and grids behaviour suite and fixes; export and inspect behaviour suite and fixes; .fig import: editable vector networks and more layer kinds; shapes and vector behaviour suite and fixes; paint and effects behaviour suite and fixes; frames, groups, sections and masks behaviour suite and fixes; styles and variables behaviour suite and fixes; prototyping behaviour suite and fixes; components behaviour suite and fixes; embedded image colour profiles; layers panel keyboard: F2 rename, Shift+F10 context menu, keyboard box selection, 10k-layer QA; per-side stroke weight variables; text behaviour suite: CJK line breaking, metrics, vertical trim; selection and transform behaviour suite and fixes; `.fig` import, best effort; auto layout behaviour suite and fixes; 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; feature breadth ~80%; ready ~49%; last major: full re-measure against Figma desktop 126.10.7) · **Target:** Figma Design (desktop app 126.10.7, macOS)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (dev resources links; extended collections tail; branch version history and finer list merges; headline percentages recomputed from the area table: feature breadth ~84%, core ~94%, mainstream ~50%; text lists, hanging punctuation and decoration styles; comments and version history behaviour suite and fixes; canvas navigation, guides and grids behaviour suite and fixes; export and inspect behaviour suite and fixes; .fig import: editable vector networks and more layer kinds; shapes and vector behaviour suite and fixes; paint and effects behaviour suite and fixes; frames, groups, sections and masks behaviour suite and fixes; styles and variables behaviour suite and fixes; prototyping behaviour suite and fixes; components behaviour suite and fixes; embedded image colour profiles; layers panel keyboard: F2 rename, Shift+F10 context menu, keyboard box selection, 10k-layer QA; per-side stroke weight variables; text behaviour suite: CJK line breaking, metrics, vertical trim; selection and transform behaviour suite and fixes; `.fig` import, best effort; auto layout behaviour suite and fixes; 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; feature breadth ~80%; ready ~49%; last major: full re-measure against Figma desktop 126.10.7) · **Target:** Figma Design (desktop app 126.10.7, macOS)
The authoritative assessment of how close FigCraft is to Figma Design. [`ROADMAP.md`](../ROADMAP.md)
summarizes it; [`gaps.md`](gaps.md) is the ranked work list; the measured menu checklist is
@ -21,7 +21,7 @@ summarizes it; [`gaps.md`](gaps.md) is the ranked work list; the measured menu c
| Essentials user | **~57%** | estimated ([below](#essentials-user): core features 77.8% × discounts 0.92 × 0.92 × 0.88) |
| Mainstream practitioner | **~50%** | estimated ([below](#mainstream-practitioner): weekly areas 72.3% × discounts 0.9 × 0.9 × 0.85) |
| Remaining to beta | **~170–305 h** one Opus 5.5 agent (~35–70 h wall-clock with 5 parallel agents) | estimated |
| Remaining to full parity | **~302–589 h** one agent (~86–150 h wall-clock with 5 parallel agents) | estimated |
| Remaining to full parity | **~301–587 h** one agent (~86–150 h wall-clock with 5 parallel agents) | estimated |
| Stage | **alpha**: the alpha gate passes ([roadmap](roadmap.md#alpha-gate)); multiplayer and checking the best-effort `.fig` import on real files are beta gaps (see [ROADMAP](../ROADMAP.md#stage)) | |
The previous figures (ROADMAP of 2026-10-10, "feature depth ~79%", "full parity 115–195 h") were
@ -108,21 +108,21 @@ Breadth: does it exist. Ready: depth, correctness and fidelity for real projects
| 12 | Styles & variables | 5 | 95% | 76% | Variables inside styles, real-library QA (behaviour suite from the documentation, style folders and order, mode order, code syntax, extended collections with their tail (new library variables as updates, chains through libraries) and variables import/export landed) ([components-parity](components-parity.md)) | 2–4 |
| 13 | Prototyping | 6 | 90% | 74% | Video triggers and media actions, gamepad triggers, delays on mouse triggers, manual overlay offsets, real prototype QA (behaviour suite from the documentation, key combinations, interactive components playing their variants, drag-driven animations, physical springs landed) ([prototyping-parity](prototyping-parity.md)) | 3–6 |
| 14 | Import/export | 6 | 84% | 57% | `.fig` import's tail (best-effort import, editable vector networks, stand-ins for non-design layers and variable prototype actions landed; no real-file checks yet), `.fig` export, HEIC/video import, CMYK output intent ([file-format-parity](file-format-parity.md)) | 5–10 |
| 15 | Dev Mode / inspect | 2 | 93% | 70% | Dev resources links, import lines for connected instances inside generated layer trees; an editor extension is left to the MCP server (inspect behaviour suite with colour formats, rem and icon and image assets, code connect and plugins in Dev Mode landed) ([dev-mode-parity](dev-mode-parity.md)) | 1–3 |
| 15 | Dev Mode / inspect | 2 | 96% | 72% | Import lines for connected instances inside generated layer trees; an editor extension is left to the MCP server (inspect behaviour suite with colour formats, rem and icon and image assets, code connect, plugins in Dev Mode and dev resources links landed) ([dev-mode-parity](dev-mode-parity.md)) | 0–1 |
| 16 | Comments, versions, find, misc | 3 | 87% | 63% | Comments are local only; comparing two versions; branches in live sessions; text runs and other lists besides fills, strokes and effects merge whole (branches with review and merge, their own version history and per-item list merges, and a comments and version history behaviour suite from the documentation landed) | 1–3 |
| 17 | Multiplayer & sharing | 8 | 15% | 5% | Core landed (`crates/collab`: ids, change sets, server ordering, convergence tested); live sessions (preview): WebSocket transport, hosting from the desktop app, joining from the app or the browser, invite links that open the app, cursors, selections and follow, per-person undo, offline resume; a self-hosted `figcraft-server` with persistence and share links to edit or view. Left: accounts and teams, cursor chat ([collaboration-parity](collaboration-parity.md)) | 45–90 |
| 18 | Plugins & widgets | 4 | 45% | 28% | Own sandboxed WASM plugins landed (#101: manifest, permissions, menu, Run last, Manage, SDK, 2 examples), then plugin panels called back per button or changed value and per-plugin settings (ABI v2). Left: distribution, widgets; panels one at a time without a colour picker; Figma's plugins are JavaScript and don't run (a JS engine would be an owner decision) ([plugins.md](plugins.md)) | 15–40 |
| 19 | Team libraries & organization | 2 | 70% | 40% | Libraries kept on `figcraft-server` under their file and enabled by a share link (per-asset versions, background update checks, overrides kept), usage counts per component and file, swapping a library by names. Left: team-wide library lists, accounts and permissions, analytics over time, pushed update notices | 4–8 |
| 20 | Figma Draw extras | 2 | 80% | 55% | Have: variable width, lasso, shape builder, noise/texture, text on a path, pattern fills, stretch and scatter brushes, pen pressure (Windows pens, browser styluses). Missing: multi-edit, pressure from macOS/Linux tablets | 2–5 |
| 21 | AI features | 3 | 0% | 0% | First draft, rename layers, image edit/remove background, visual search, Make: needs local models (owner decision) | 30–60 |
| | **Weighted (÷113)** | 113 | **~84%** | **~63%** | | **144–303** |
| | Core editor only (1–16, ÷94) | 94 | ~94% | ~72% | | 42–89 |
| | **Weighted (÷113)** | 113 | **~84%** | **~63%** | | **143–301** |
| | Core editor only (1–16, ÷94) | 94 | ~94% | ~72% | | 41–87 |
## By dimension (ready for real work)
| Dimension | Weight | Ready | Hours | Evidence | Doc |
|---|---|---|---|---|---|
| Features (depth) | 35 | 61% | 42–88 (areas 1–13, 15, 16, 20) | The table above; core editor ~72% | this file |
| Features (depth) | 35 | 61% | 41–86 (areas 1–13, 15, 16, 20) | The table above; core editor ~72% | this file |
| UI/UX fidelity | 15 | 58% | 40–60 | Look & feel compared side by side with UI3 (~65% by the builders); tool-by-tool interaction (cursors, modifiers while dragging, hover states, popovers) not yet compared; accessibility names and keyboard navigation landed | [ui-parity](ui-parity.md) |
| File formats | 12 | 59% | 5–10 | `.fig` read, best effort, with editable vector networks (tested on generated fixtures, not real files), no `.fig` write; REST JSON import, SVG in/out, PNG/JPG/WebP/PDF export solid, with a behaviour suite from the documentation | [file-format-parity](file-format-parity.md) |
| Collaboration | 10 | 13% | 55–110 | Local comments, version history and branches with review and merge; the multiplayer core (sync model, server ordering, client reconciliation, convergence tests) landed in #103, and live sessions (preview) co-edit a file hosted by a desktop app or kept on a self-hosted `figcraft-server` with edit and view links, joined from the app or the browser (`docs/collab-protocol.md`) | [collaboration-parity](collaboration-parity.md) |
@ -133,7 +133,7 @@ Breadth: does it exist. Ready: depth, correctness and fidelity for real projects
| Platforms | 3 | 75% | 10–20 | macOS, Windows (x64/x86/arm64), Linux, FreeBSD, web all packaged (v0.2.0); Figma also has iOS/Android apps; non-macOS QA thin | [hardware-parity](hardware-parity.md) |
| Ecosystem/plugins | 3 | 28% | 15–40 | Own WASM plugins with a Rust SDK and two examples (#101), plugin panels and settings (ABI v2); no Figma plugin compatibility, no widgets, no distribution or community files; the CLI, control channel and MCP server are ahead of Figma for agents | [gaps](gaps.md) |
| AI features | 2 | 0% | 30–60 | None | [gaps](gaps.md) |
| **Ready for real work** | 100 | **~51%** | **~302–589** | | |
| **Ready for real work** | 100 | **~51%** | **~301–587** | | |
Dimension hours add up to the full-parity total without double counting: the feature-area rows
for import/export (14), multiplayer and team libraries (17, 19), plugins (18) and AI (21) are
@ -612,6 +612,7 @@ Carried over from the ROADMAP's "Shipped so far" (2026-10-10). The test count th
| Date | Change | Summary |
|---|---|---|
| 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-12 | minor | Extended collections' tail (gap 14): variables a library adds to a collection this file extends now count as a library update of their own (listed by name without a copy yet) and arrive inheriting when accepted, instead of waiting for the library's next other update; a library collection that is itself an extended collection can be extended again, values flowing down the chain as each file accepts updates with every level's overrides kept (checked end to end). Styles & variables breadth 94% → 95%, ready unchanged (76%), 3–5 h → 2–4 h; gap 14 1–2 h → 0–1 h (real-library checks left); feature breadth (~82%) and ready for real work (~51%) unchanged at this rounding; full parity ~305–594 h → ~304–593 h |
| 2026-10-12 | minor | A branch's own version history and finer list merges (gap 17): versions saved or autosaved on a branch tab go into the branch (viewed, restored, renamed, deleted and duplicated there, saved in the file with it); merging adds a version to the main file's history and the merged or archived branch keeps its own; fills, strokes and effects merge item by item, conflicting only when the same item changed both ways, an item was removed on one side or both sides added different items. Comments, versions, find, misc breadth 85% → 87%, ready 60% → 61%, 2–5 h → 1–4 h; gap 17 1–3 h → 1–2 h; feature breadth (~82%) and ready for real work (~51%) unchanged at this rounding; full parity ~306–595 h → ~305–594 h |
| 2026-10-12 | minor | Headline percentages recomputed from the area table by its own method (weights × written scores): feature breadth ~82% → ~84% (core editor ~93% → ~94%), weighted ready ~61% → ~63%, core ready ~70% → ~72%, mainstream weekly areas 71.3% → 72.3% and mainstream ~49% → ~50%; several earlier rows had moved the area scores without carrying them into the totals. Hours are unchanged (left for the next full re-measure) |