M3: component playground: variant, boolean and text properties

This commit is contained in:
bflatastic authored and BFlat committed 2026-10-13 13:36:54 +07:00
1 parent e02ad435b3
commit e56856f401
13 files changed
+356 -7

No files matched your search

+3 -1
View File
@@ -2,7 +2,7 @@
**Stage: alpha** · next: beta, ~26 points and ~162–292 h away (one Opus 5.5 agent; ~33–67 h wall-clock with 5 agents)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (core editor leftovers: auto gap handles, variables inside styles, inner shadows and background blur on groups, command-made text readable on dark pages, a one-step frame/group convert, delays on mouse triggers and manual overlay offsets, wrapped-line alignment; core editor leftovers: variables inside styles, inner shadows and background blur on groups, command-made text readable on dark pages, a one-step frame/group convert, delays on mouse triggers and manual overlay offsets, wrapped-line alignment; core editor leftovers: inner shadows and background blur on groups, command-made text readable on dark pages, a one-step frame/group convert, delays on mouse triggers and manual overlay offsets, wrapped-line alignment; core editor leftovers: command-made text readable on dark pages, a one-step frame/group convert, delays on mouse triggers and manual overlay offsets, wrapped-line alignment; core editor leftovers: a one-step frame/group convert, delays on mouse triggers and manual overlay offsets, wrapped-line alignment; core editor leftovers: delays on mouse triggers and manual overlay offsets, wrapped-line alignment; core editor leftovers: wrapped-line alignment; `.fig` export: instance sizes, variables with modes, shared styles, variable-width strokes; starter templates, a Templates tab and Getting started tips on the home screen; your online files; everyday editing partials: number fields, outline options, Hide when publishing, cursors, panel widths, dark pages; 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; a public figcraft-server: open hosting, owner tokens, per-address limits, idle expiry, moderation, install.sh; 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; 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-13 · **Change:** minor (core editor leftovers: component playground, auto gap handles, variables inside styles, inner shadows and background blur on groups, command-made text readable on dark pages, a one-step frame/group convert, delays on mouse triggers and manual overlay offsets, wrapped-line alignment; `.fig` export: instance sizes, variables with modes, shared styles, variable-width strokes; starter templates, a Templates tab and Getting started tips on the home screen; your online files; everyday editing partials: number fields, outline options, Hide when publishing, cursors, panel widths, dark pages; 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; a public figcraft-server: open hosting, owner tokens, per-address limits, idle expiry, moderation, install.sh; 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; 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
@@ -131,6 +131,7 @@ Ranked; detail and milestones in [`docs/roadmap.md`](docs/roadmap.md).
## Progress log
- **2026-10-13** · Component playground (M3): with an instance, a component or a component set selected, the Inspect tab shows a Playground: a preview of the component and a control for each variant, boolean and text property. What is tried there changes the preview and the code in the Code section, never the file (no undo step, the canvas instance stays as it is); Reset goes back to the component's or the instance's own values. `component.playground {id?, properties?, language?, path?}` gives the same to scripts: the properties with their values, the code, and a PNG on request (`playground_tests`). Left: instance-swap and slot properties in the playground (the command takes an instance swap; the panel has no control for it), a code connect template's snippet with the tried values. Numbers unchanged.
- **2026-10-13** · Auto layout, on-canvas handles of automatic gaps (M2): the handle between spread rows or columns (and between items with an auto gap) shows the distance the layout gives the gap instead of the stored value, and dragging it makes the gap a fixed one from there, as typing a gap in the panel does; one undo step (`gap_handle_tests`). Before, the drag changed a value that had no effect. The documentation is silent on dragging an automatic gap. Numbers unchanged.
- **2026-10-13** · Variables inside styles (M3): a shared style can bind variables to fields of its value (`style.bind {style, field, variable?}`: a paint style's colours and gradient stops, an effect style's colour, radius, spread and offsets, a grid style's size, count, gutter and offset, a text style's font size, line height, letter spacing, family and style). Every layer using the style shows those fields resolved in its own modes and stays linked when a variable or a mode changes; the style keeps them resolved in the default modes. A style made from a layer keeps that layer's bindings; a new value drops bindings to what is gone. The style editor binds from its property rows (paint colour, shadow colour, blur radius, grid size) and detaches from the variable's chip; the style picker marks styles that use variables. `.fig` export and import carry the bindings on the style, REST layers hand theirs to the style they use. Left: binding text style fields and the other effect and grid fields from the editor (commands only), library variables in the picker, a style's library copy. Numbers unchanged.
- **2026-10-13** · Effects on groups, inner shadows and background blur (M3): a group is no outline of its own, so its inner shadows are cast inwards from around what its layers draw together (their pixels, over them: no shade at a seam between two layers) and its background blur and glass show the backdrop through the outlines of its layers (under them). Canvas and PNG; SVG embeds a group with an inner shadow as a render and its background blur as an image under the layers; PDF places both as images, the blur matching the PNG. Where the group holds one shape the pixels match that shape, or a frame filled like it, carrying the effect (`group_effects_tests` in render, svg and pdf). Offsets and spreads of a group's inner shadow go by whole pixels. Numbers unchanged.
@@ -304,6 +305,7 @@ Ranked; detail and milestones in [`docs/roadmap.md`](docs/roadmap.md).
| Date | Change | Summary |
|---|---|---|
| 2026-10-13 | minor | Component playground (M3): with an instance, a component or a component set selected, the Inspect tab shows a Playground: a preview of the component and a control for each variant, boolean and text property. What is tried there changes the preview and the code in the Code section, never the file (no undo step, the canvas instance stays as it is); Reset goes back to the component's or the instance's own values. `component.playground {id?, properties?, language?, path?}` gives the same to scripts: the properties with their values, the code, and a PNG on request (`playground_tests`). Left: instance-swap and slot properties in the playground (the command takes an instance swap; the panel has no control for it), a code connect template's snippet with the tried values. Numbers unchanged. |
| 2026-10-13 | minor | Auto layout, on-canvas handles of automatic gaps (M2): the handle between spread rows or columns (and between items with an auto gap) shows the distance the layout gives the gap instead of the stored value, and dragging it makes the gap a fixed one from there, as typing a gap in the panel does; one undo step (`gap_handle_tests`). Before, the drag changed a value that had no effect. The documentation is silent on dragging an automatic gap. Numbers unchanged. |
| 2026-10-13 | minor | Variables inside styles (M3): a shared style can bind variables to fields of its value (`style.bind {style, field, variable?}`: a paint style's colours and gradient stops, an effect style's colour, radius, spread and offsets, a grid style's size, count, gutter and offset, a text style's font size, line height, letter spacing, family and style). Every layer using the style shows those fields resolved in its own modes and stays linked when a variable or a mode changes; the style keeps them resolved in the default modes. A style made from a layer keeps that layer's bindings; a new value drops bindings to what is gone. The style editor binds from its property rows (paint colour, shadow colour, blur radius, grid size) and detaches from the variable's chip; the style picker marks styles that use variables. `.fig` export and import carry the bindings on the style, REST layers hand theirs to the style they use. Left: binding text style fields and the other effect and grid fields from the editor (commands only), library variables in the picker, a style's library copy. Numbers unchanged. |
| 2026-10-13 | minor | Effects on groups, inner shadows and background blur (M3): a group is no outline of its own, so its inner shadows are cast inwards from around what its layers draw together (their pixels, over them: no shade at a seam between two layers) and its background blur and glass show the backdrop through the outlines of its layers (under them). Canvas and PNG; SVG embeds a group with an inner shadow as a render and its background blur as an image under the layers; PDF places both as images, the blur matching the PNG. Where the group holds one shape the pixels match that shape, or a frame filled like it, carrying the effect (`group_effects_tests` in render, svg and pdf). Offsets and spreads of a group's inner shadow go by whole pixels. Numbers unchanged. |
+1 -1
View File
@@ -1383,7 +1383,7 @@ fn def_json(name: &str, d: &PropDef) -> Value {
}
/// Properties of a component, set or instance as JSON (`component.properties`).
fn describe_props(doc: &Document, id: NodeId) -> Value {
pub(crate) fn describe_props(doc: &Document, id: NodeId) -> Value {
let Some(n) = doc.node(id) else { return Value::Null };
let (owner_comp, values) = match &n.kind {
NodeKind::Instance { component, props } => (Some(*component), Some(props.clone())),
+1
View File
@@ -93,6 +93,7 @@ fn registry() -> &'static Registry {
list.extend(crate::codegen::specs());
list.extend(crate::devinspect::specs());
list.extend(crate::component_codegen::specs());
list.extend(crate::playground::specs());
list.extend(crate::code_connect::specs());
list.extend(crate::annotations::specs());
list.extend(crate::dev_resources::specs());
+3
View File
@@ -37,6 +37,7 @@ pub mod live;
pub mod measure;
pub mod online;
pub mod params;
pub mod playground;
pub mod plugin_directory;
pub mod plugins;
pub mod proto_run;
@@ -1464,6 +1465,8 @@ mod pdf_print_tests;
#[cfg(test)]
mod pixel_preview_tests;
#[cfg(test)]
mod playground_tests;
#[cfg(test)]
mod plugin_directory_tests;
#[cfg(test)]
mod plugin_panels_tests;
+84
View File
@@ -0,0 +1,84 @@
//! The component playground: a component tried with other property values without placing it or
//! touching the document. [`build`] makes a copy of the document holding one extra instance with
//! the values; what shows it (the Inspect panel, `component.playground`) renders that instance and
//! generates its code.
use figcraft_doc::{Document, ExportFormat, NodeId, NodeKind};
use serde_json::{Map, Value, json};
use crate::cmd::CommandSpec;
use crate::codegen::CodeLang;
use crate::params::*;
use crate::{EngineError, Result, Session, bad, spec};
/// The component a playground for `target` shows: an instance's component, a component or a
/// component set itself. None for other layers.
pub fn component_of(doc: &Document, target: NodeId) -> Option<NodeId> {
match &doc.node(target)?.kind {
NodeKind::Instance { component, .. } => Some(*component),
NodeKind::Component { .. } | NodeKind::ComponentSet { .. } => Some(target),
_ => None,
}
}
/// A copy of `doc` with a new instance of `target`'s component (an instance starts from its own
/// property values) set to `values` (property name → value, as `instance.setProperties` takes
/// them), and that instance's id. `doc` itself is untouched.
pub fn build(doc: &Document, target: NodeId, values: &Map<String, Value>) -> Result<(Document, NodeId)> {
let cmd = "component.playground";
let comp = component_of(doc, target).ok_or_else(|| bad(cmd, "select an instance, a component or a component set"))?;
let mut s = Session::with_document(doc.clone());
// Off to the side of everything (the copy is never shown as a page).
let made = s.execute("instance.create", &json!({"component": comp, "x": 1e6, "y": 1e6}))?;
let inst = id_of(&made["id"]).ok_or_else(|| EngineError::Other("the playground instance wasn't made".into()))?;
let mut all = match &doc.node(target).map(|n| &n.kind) {
Some(NodeKind::Instance { props, .. }) => props.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
_ => Map::new(),
};
all.extend(values.clone());
if !all.is_empty() {
s.execute("instance.setProperties", &json!({"ids": [inst], "properties": all})).map_err(|e| bad(cmd, e.to_string()))?;
}
Ok((s.doc()?.clone(), inst))
}
/// The properties of the playground's instance `inst` in `copy` (as `component.properties` lists
/// an instance's): `{component, set, properties: [{name, type, options?, value}]}`.
pub fn properties(copy: &Document, inst: NodeId) -> Value {
crate::cmd::component::describe_props(copy, inst)
}
pub fn specs() -> Vec<CommandSpec> {
vec![spec!(
"component.playground",
"Component playground",
"{id? (an instance, a component or a component set; default the selection), properties? {name: value} (variant, boolean, text and instance-swap properties, as instance.setProperties takes them), language? css|html|react|swiftui|compose (default react), path? (write the preview there as a PNG), scale? (the PNG's, default 2)} → {component, properties: [{name, type, options?, value}], language, code, width, height, path?} the component with those property values, tried on a copy: its code and its look. Nothing in the document changes",
false,
|s, p| {
let cmd = "component.playground";
let target = targets(s, p).first().copied().ok_or_else(|| bad(cmd, "select an instance, a component or a component set"))?;
let values = match get(p, "properties") {
None | Some(Value::Null) => Map::new(),
Some(Value::Object(m)) => m.clone(),
Some(_) => return Err(bad(cmd, "properties: {name: value}")),
};
let lang = match string(p, "language") {
Some(l) => CodeLang::from_id(&l).ok_or_else(|| bad(cmd, "language: css|html|react|swiftui|compose"))?,
None => CodeLang::React,
};
let (copy, inst) = build(s.doc()?, target, &values)?;
let n = copy.get(inst)?;
let mut out = properties(&copy, inst);
out["language"] = json!(lang.id());
out["code"] = json!(crate::codegen::code_for(&copy, inst, lang));
(out["width"], out["height"]) = (json!(n.width), json!(n.height));
if let Some(path) = string(p, "path") {
let scale = num(p, "scale").filter(|k| k.is_finite()).map_or(2.0, |k| k.clamp(0.01, 16.0));
let png = crate::cmd::file::export_bytes(&copy, &[inst], ExportFormat::Png, scale)?;
std::fs::write(&path, png).map_err(|e| EngineError::Other(format!("{path}: {e}")))?;
out["path"] = json!(path);
}
Ok(out)
}
)]
}
+135
View File
@@ -0,0 +1,135 @@
//! The component playground: a component tried with other variant, boolean and text property
//! values on a copy. The code and the preview follow the values; the document, the selection and
//! the undo history don't change.
use std::sync::Arc;
use serde_json::{Value, json};
use crate::params::id_of;
use crate::{EngineError, Session};
use figcraft_doc::NodeId;
fn id(v: Value) -> NodeId {
id_of(&v["id"]).unwrap()
}
fn run(s: &mut Session, cmd: &str, p: Value) -> Value {
s.execute(cmd, &p).unwrap_or_else(|e| panic!("{cmd} {p}: {e}"))
}
/// A "Button" set with variants State=Rest (blue) and State=Hover (red), each holding a label
/// (text property "Label") and an icon (boolean property "Icon"); an instance of it. Returns
/// (set, instance).
fn button(s: &mut Session) -> (NodeId, NodeId) {
let mut comps = Vec::new();
for (i, (state, fill)) in [("Rest", "#0000FF"), ("Hover", "#FF0000")].into_iter().enumerate() {
let f = id(run(
s,
"frame.create",
json!({"x": 200 * i, "y": 0, "width": 120, "height": 40, "name": format!("State={state}"), "props": {"fill": fill}}),
));
let label = id(run(s, "text.create", json!({"text": "Buy", "x": 40, "y": 10, "parent": f, "name": "Label"})));
let icon = id(run(
s,
"shape.ellipse",
json!({"x": 8, "y": 8, "width": 24, "height": 24, "parent": f, "props": {"fill": "#FFFFFF", "name": "Icon"}}),
));
let c = id(run(s, "component.create", json!({"ids": [f]})));
run(s, "component.addProperty", json!({"id": c, "name": "Label", "type": "text", "default": "Buy"}));
run(s, "component.addProperty", json!({"id": c, "name": "Icon", "type": "boolean", "default": true}));
run(s, "component.bindProperty", json!({"id": label, "property": "Label", "field": "characters"}));
run(s, "component.bindProperty", json!({"id": icon, "property": "Icon", "field": "visible"}));
comps.push(c);
}
let set = id(run(s, "component.combineVariants", json!({"ids": comps})));
let inst = id(run(s, "instance.create", json!({"component": set, "x": 0, "y": 300})));
(set, inst)
}
fn value<'a>(out: &'a Value, name: &str) -> &'a Value {
out["properties"].as_array().unwrap().iter().find(|p| p["name"] == name).map(|p| &p["value"]).unwrap_or(&Value::Null)
}
#[test]
fn the_playground_tries_values_without_touching_the_document() {
let mut s = Session::new();
let (set, inst) = button(&mut s);
let (before, undo, selection) = (s.ds().unwrap().doc.clone(), s.undo_depth(), s.selection().to_vec());
// As the instance is: the default variant, its label, its icon.
let out = run(&mut s, "component.playground", json!({"id": inst}));
assert_eq!((value(&out, "State"), value(&out, "Label"), value(&out, "Icon")), (&json!("Rest"), &json!("Buy"), &json!(true)));
let rest = out["code"].as_str().unwrap().to_string();
// Other values: the reported values and the code follow.
let props = json!({"State": "Hover", "Label": "Pay now", "Icon": false});
let out = run(&mut s, "component.playground", json!({"id": inst, "properties": props}));
assert_eq!((value(&out, "State"), value(&out, "Label"), value(&out, "Icon")), (&json!("Hover"), &json!("Pay now"), &json!(false)));
let code = out["code"].as_str().unwrap();
assert!(code != rest && code.contains("Pay now"), "{code}");
// From the set or one of its variants: the same playground, starting from the defaults.
for target in [set, s.doc().unwrap().node(set).unwrap().children[1]] {
let out = run(&mut s, "component.playground", json!({"id": target, "properties": {"Label": "Hi"}}));
assert_eq!(value(&out, "Label"), &json!("Hi"), "{target}");
}
// An instance with values of its own starts from them.
run(&mut s, "instance.setProperties", json!({"ids": [inst], "properties": {"Label": "Mine"}}));
let (before2, undo2) = (s.ds().unwrap().doc.clone(), s.undo_depth());
assert_eq!(value(&run(&mut s, "component.playground", json!({"id": inst, "properties": {"Icon": false}})), "Label"), &json!("Mine"));
// Nothing changed: the same document, undo history and selection.
assert!(Arc::ptr_eq(&before2, &s.ds().unwrap().doc) && undo2 == s.undo_depth());
assert!(!Arc::ptr_eq(&before, &before2) && undo != undo2, "(only the instance edit above did)");
assert_eq!(s.selection(), selection);
}
#[test]
fn the_preview_shows_the_values() {
let mut s = Session::new();
let (_, inst) = button(&mut s);
let doc = s.doc().unwrap().clone();
let pixel = |values: Value, x: u32, y: u32| {
let (copy, shown) = crate::playground::build(&doc, inst, values.as_object().unwrap()).unwrap();
let pix = figcraft_render::render_node(&copy, shown, 1.0).unwrap();
let i = ((y * pix.width() + x) * 4) as usize;
[pix.data()[i], pix.data()[i + 1], pix.data()[i + 2]]
};
// (values, pixel, colour, why)
for (values, at, want, why) in [
(json!({}), (100, 5), [0, 0, 255], "the rest variant is blue"),
(json!({"State": "Hover"}), (100, 5), [255, 0, 0], "the hover variant is red"),
(json!({}), (20, 20), [255, 255, 255], "the icon shows"),
(json!({"Icon": false}), (20, 20), [0, 0, 255], "the icon is hidden: the fill shows"),
] {
assert_eq!(pixel(values.clone(), at.0, at.1), want, "{values}: {why}");
}
// A PNG on request.
let dir = std::env::temp_dir().join(format!("figcraft-playground-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("p.png");
let out = run(&mut s, "component.playground", json!({"id": inst, "path": path.to_string_lossy(), "scale": 1}));
assert_eq!((out["width"].as_f64(), out["height"].as_f64()), (Some(120.0), Some(40.0)));
assert!(std::fs::read(&path).unwrap().starts_with(b"\x89PNG"));
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn what_is_no_component_or_no_value_is_refused() {
let mut s = Session::new();
let (_, inst) = button(&mut s);
let r = id(run(&mut s, "shape.rectangle", json!({"x": 500, "y": 500, "width": 10, "height": 10})));
assert!(s.execute("component.playground", &json!({"id": r})).unwrap_err().to_string().contains("select an instance"));
assert!(s.execute("component.playground", &json!({"id": inst, "properties": {"State": "Nope"}})).is_err());
assert!(s.execute("component.playground", &json!({"id": inst, "language": "cobol"})).is_err());
let before = s.ds().unwrap().doc.clone();
for j in [json!(null), json!(true), json!(-1), json!("x"), json!([1, [2]]), json!({"a": {"b": 1}}), json!("y".repeat(5000))] {
for p in [
json!({"id": j}),
json!({"id": inst, "properties": j}),
json!({"id": inst, "properties": {"Label": j, "Icon": j, "State": j}}),
json!({"id": inst, "language": j, "scale": j}),
] {
let r = s.execute("component.playground", &p);
assert!(!matches!(r, Err(EngineError::Internal { .. })), "{p}: {r:?}");
}
}
assert!(Arc::ptr_eq(&before, &s.ds().unwrap().doc));
}
+2
View File
@@ -960,6 +960,7 @@ name = "English"
"Preserve scroll position" = "Preserve scroll position"
"Presets" = "Presets"
"Press a key…" = "Press a key…"
"Playground" = "Playground"
"Preview" = "Preview"
"Preview this flow" = "Preview this flow"
"Preview this frame" = "Preview this frame"
@@ -1410,6 +1411,7 @@ name = "English"
"WebSocket URL copied" = "WebSocket URL copied"
"Wedge" = "Wedge"
"Weight" = "Weight"
"What you try here changes this preview and the code below, not the file" = "What you try here changes this preview and the code below, not the file"
"What changed since it was marked ready for dev" = "What changed since it was marked ready for dev"
"What is wrong with this file? The server's operator gets your report." = "What is wrong with this file? The server's operator gets your report."
"What it's for" = "What it's for"
+3
View File
@@ -961,6 +961,7 @@ status = "draft — needs native review"
"Preserve scroll position" = "Conservar posición de desplazamiento"
"Presets" = "Ajustes predefinidos"
"Press a key…" = "Pulsa una tecla…"
"Playground" = "Zona de pruebas"
"Preview" = "Vista previa"
"Preview this flow" = "Vista previa de este flujo"
"Preview this frame" = "Vista previa de este marco"
@@ -1411,6 +1412,7 @@ status = "draft — needs native review"
"WebSocket URL copied" = "URL de WebSocket copiada"
"Wedge" = "Cuña"
"Weight" = "Grosor"
"What you try here changes this preview and the code below, not the file" = "Lo que pruebes aquí cambia esta vista previa y el código de abajo, no el archivo"
"What changed since it was marked ready for dev" = "Qué cambió desde que se marcó como listo para desarrollo"
"What is wrong with this file? The server's operator gets your report." = "¿Qué tiene de malo este archivo? El operador del servidor recibirá tu denuncia."
"What it's for" = "Para qué sirve"
@@ -1643,6 +1645,7 @@ status = "draft — needs native review"
"component.hideWhenPublishing" = "Ocultar al publicar"
"component.list" = "Listar componentes"
"component.makeSlot" = "Convertir en ranura"
"component.playground" = "Probar componente"
"component.properties" = "Propiedades del componente"
"component.pushChanges" = "Enviar cambios al componente principal"
"component.removeSlot" = "Quitar ranura"
+111
View File
@@ -96,6 +96,24 @@ pub struct DevUi {
focus_list: Vec<NodeId>,
focus_key: Option<(u64, u64)>,
code: Option<((u64, u64, NodeId, CodeLang), String)>,
playground: Playground,
}
/// The component playground of the Inspect tab: the layer it is for, the property values tried
/// there, and what they gave (for the document, layer, values and language it was made from).
#[derive(Default)]
struct Playground {
target: Option<NodeId>,
values: serde_json::Map<String, serde_json::Value>,
shown: Option<(PlayKey, Played)>,
}
type PlayKey = (u64, u64, NodeId, String, CodeLang);
struct Played {
preview: Option<egui::TextureHandle>,
properties: Vec<serde_json::Value>,
code: String,
}
/// Whether Dev Mode extras show: the Inspect tab is open and nothing is presented.
@@ -262,6 +280,99 @@ pub fn code(app: &mut FigApp, ui: &Ui, id: NodeId, lang: CodeLang) -> String {
}
}
/// The component playground, for a selected instance, component or component set: a preview of the
/// component and a control for each variant, boolean and text property. The values tried here
/// change the preview and the code only (returned when any is set: the Code section shows it),
/// never the document.
pub fn playground_section(app: &mut FigApp, ui: &mut Ui, doc: &Document, id: NodeId, lang: CodeLang) -> Option<String> {
figcraft_engine::playground::component_of(doc, id)?;
let t = Tokens::get(ui.ctx());
let Ok((uid, revision)) = app.session.ds().map(|d| (d.uid, d.revision)) else { return None };
let play = &mut app.ui.dev.playground;
if play.target != Some(id) {
*play = Playground { target: Some(id), ..Playground::default() };
}
let key: PlayKey = (uid, revision, id, serde_json::Value::Object(play.values.clone()).to_string(), lang);
if play.shown.as_ref().is_none_or(|(k, _)| *k != key) {
// Values the component no longer takes (a property renamed or removed) are dropped.
let built = figcraft_engine::playground::build(doc, id, &play.values).or_else(|_| {
play.values.clear();
figcraft_engine::playground::build(doc, id, &play.values)
});
let played = built.ok().map(|(copy, inst)| Played {
preview: crate::assets_panel::thumbnail(ui.ctx(), &copy, inst, 416.0),
properties: figcraft_engine::playground::properties(&copy, inst)["properties"].as_array().cloned().unwrap_or_default(),
code: code_for(&copy, inst, lang),
});
let key = (uid, revision, id, serde_json::Value::Object(play.values.clone()).to_string(), lang);
play.shown = played.map(|p| (key, p));
}
let Some((_, shown)) = &play.shown else { return None };
let tried = !play.values.is_empty();
widgets::separator(ui);
let buttons = [(Icon::Rotate, tr("Reset"))];
let reset = widgets::section_header(ui, tr("Playground"), if tried { &buttons } else { &[] }, false).is_some();
// The preview, fitted into its box with some room around it.
egui::Frame::NONE.fill(t.field).corner_radius(CornerRadius::same(6)).outer_margin(egui::Margin::symmetric(16, 0)).show(ui, |ui| {
let (r, _) = ui.allocate_exact_size(vec2(ui.available_width(), 136.0), Sense::hover());
if let Some(tex) = &shown.preview {
let size = tex.size_vec2();
let k = ((r.width() - 16.0) / size.x).min((r.height() - 16.0) / size.y).clamp(0.0, 1.0);
let image = Rect::from_center_size(r.center(), size * k);
ui.painter().image(tex.id(), image, Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)), Color32::WHITE);
}
});
ui.add_space(4.0);
let mut set: Option<(String, serde_json::Value)> = None;
for p in &shown.properties {
let (Some(name), Some(kind)) = (p["name"].as_str(), p["type"].as_str()) else { continue };
let value = &p["value"];
match kind {
"variant" => row(ui, 32.0, |ui| {
widgets::label(ui, name, t.text_secondary);
let resp = widgets::dropdown(ui, value.as_str().unwrap_or(""), ui.available_width().max(60.0), None);
crate::a11y::combo(&resp, name, value.as_str().unwrap_or(""), crate::a11y::menu_open(&resp));
menu_popup(&resp).show(|ui| {
for o in p["options"].as_array().into_iter().flatten().filter_map(|o| o.as_str()) {
if crate::right::menu_item(ui, o, "") {
set = Some((name.to_string(), json!(o)));
ui.close();
}
}
});
}),
"boolean" => row(ui, 28.0, |ui| {
let on = value.as_bool().unwrap_or(false);
if widgets::checkbox(ui, on, name) {
set = Some((name.to_string(), json!(!on)));
}
}),
"text" => row(ui, 32.0, |ui| {
widgets::label(ui, name, t.text_secondary);
let cur = value.as_str().unwrap_or("");
let field = Id::new(("playground_text", id, name));
if let Some(new) = widgets::text_field_named(ui, field, cur, ui.available_width().max(60.0), name)
&& new != cur
{
set = Some((name.to_string(), json!(new.chars().take(10_000).collect::<String>())));
}
}),
// Instance swaps and slots show as the component has them.
_ => {}
}
}
widgets::note(ui, tr("What you try here changes this preview and the code below, not the file"), t.text_secondary);
let code = tried.then(|| shown.code.clone());
let play = &mut app.ui.dev.playground;
if reset {
play.values.clear();
}
if let Some((k, v)) = set {
play.values.insert(k, v);
}
code
}
/// Connect to code for the selected component, set or instance (its main), in the Code section's
/// language: a button to connect, then a Connected badge, the import path, name and template (the
/// code above shows the snippet, live while the template is edited).
+3 -1
View File
@@ -2232,6 +2232,8 @@ fn inspect(app: &mut FigApp, ui: &mut Ui) {
use figcraft_engine::codegen::CodeLang;
let lang_id = Id::new("inspect_lang");
let lang = ui.data(|d| d.get_temp::<usize>(lang_id)).and_then(|i| CodeLang::ALL.get(i).copied()).unwrap_or_default();
// A component tried with other property values: its code replaces the layer's below.
let played = crate::dev_mode::playground_section(app, ui, &doc, first, lang);
widgets::separator(ui);
let copy = widgets::section_header(ui, tr("Code"), &[(Icon::Export, tr("Copy"))], false).is_some();
// Pixels or rem (of a 16 px root) for the web languages.
@@ -2260,7 +2262,7 @@ fn inspect(app: &mut FigApp, ui: &mut Ui) {
});
}
});
let code = crate::dev_mode::code(app, ui, first, lang);
let code = played.unwrap_or_else(|| crate::dev_mode::code(app, ui, first, lang));
let code = if rem { figcraft_engine::codegen::px_to_rem(&code, 16.0) } else { code };
egui::Frame::NONE
.fill(t.field)
+3 -2
View File
@@ -1,6 +1,6 @@
# Components, variants, styles and variables parity
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (core editor leftovers: variables inside styles; layers and assets panels behaviour suite and fixes; extended collections tail; styles and variables behaviour suite and fixes; components behaviour suite and fixes; team libraries on the server, library usage and swapping a library; extended collections, variables import/export, gradient stops and variant properties bound to variables) · **Target:** Figma Design (desktop app 126.10.7)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (core editor leftovers: component playground, variables inside styles; layers and assets panels behaviour suite and fixes; extended collections tail; styles and variables behaviour suite and fixes; components behaviour suite and fixes; team libraries on the server, library usage and swapping a library; extended collections, variables import/export, gradient stops and variant properties bound to variables) · **Target:** Figma Design (desktop app 126.10.7)
Design systems are why teams adopt Figma. **Components: breadth ~96%, ready ~72%. Styles and
variables: breadth ~95%, ready ~76% (estimated).** QA'd with a synthetic design system of ~300
@@ -28,7 +28,7 @@ accounts).
| Assets panel: folders by page, frame and slash names (fold away), thumbnails, search, drag into frames and auto layout slots | done | virtualized; `component.list` gives each one's `folder` |
| Hidden from publishing: a leading `.` or `_` in the name | done | the name decides both ways (adding the prefix takes a published component out, removing it brings it back); no Hide when publishing menu item yet |
| Instances pasted into other files follow their mains | done | missing main restorable |
| Component playground (try properties without placing) | missing | 1–2 h |
| Component playground (try properties without placing) | partial | variant, boolean and text properties in the Inspect tab and `component.playground`; instance swaps and slots have no control there yet (0.5–1 h) |
| Real-world libraries QA | **not checked** | [gaps](gaps.md) #3 |
The behaviour of instances, overrides, variants and swaps is checked against the public
@@ -76,6 +76,7 @@ updates with overrides kept: `library_tests.rs`, `components_scale_tests.rs`.
| Date | Change | Summary |
|---|---|---|
| 2026-10-13 | minor | Component playground (M3): with an instance, a component or a component set selected, the Inspect tab shows a Playground: a preview of the component and a control for each variant, boolean and text property. What is tried there changes the preview and the code in the Code section, never the file (no undo step, the canvas instance stays as it is); Reset goes back to the component's or the instance's own values. `component.playground {id?, properties?, language?, path?}` gives the same to scripts: the properties with their values, the code, and a PNG on request (`playground_tests`). Left: instance-swap and slot properties in the playground (the command takes an instance swap; the panel has no control for it), a code connect template's snippet with the tried values. Numbers unchanged. |
| 2026-10-13 | minor | Variables inside styles (M3): a shared style can bind variables to fields of its value (`style.bind {style, field, variable?}`: a paint style's colours and gradient stops, an effect style's colour, radius, spread and offsets, a grid style's size, count, gutter and offset, a text style's font size, line height, letter spacing, family and style). Every layer using the style shows those fields resolved in its own modes and stays linked when a variable or a mode changes; the style keeps them resolved in the default modes. A style made from a layer keeps that layer's bindings; a new value drops bindings to what is gone. The style editor binds from its property rows (paint colour, shadow colour, blur radius, grid size) and detaches from the variable's chip; the style picker marks styles that use variables. `.fig` export and import carry the bindings on the style, REST layers hand theirs to the style they use. Left: binding text style fields and the other effect and grid fields from the editor (commands only), library variables in the picker, a style's library copy. Numbers unchanged. |
| 2026-10-12 | minor | Layers and Assets panels behaviour suite: Assets folders by page, frame and slash names; a leading `.` or `_` keeps a component out of the library even after it was published, and removing it brings the component back |
| 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 |
+4
View File
@@ -1260,6 +1260,10 @@ permissions: without it nothing is downloaded); `plugin.directory.publish {path
### Effects
`component.playground {id?, properties? {name: value}, language?, path?, scale?}` tries a component
with other variant, boolean, text (and instance-swap) property values on a copy and returns its
properties with their values, its code and, with `path`, a PNG of it. The document doesn't change.
`style.bind {style, field, variable?}` binds a variable inside a shared style (no `variable`:
unbinds): `fill` or `fills/<i>/color` and `fills/<i>/stops/<j>/color` on a paint style,
`effects/<i>/<color|radius|spread|offsetX|offsetY|startRadius>` on an effect style,
+3 -2
View File
@@ -1,6 +1,6 @@
# FigCraft parity with Figma
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (core editor leftovers: auto gap handles, variables inside styles, inner shadows and background blur on groups, command-made text readable on dark pages, a one-step frame/group convert, delays on mouse triggers and manual overlay offsets, wrapped-line alignment; core editor leftovers: variables inside styles, inner shadows and background blur on groups, command-made text readable on dark pages, a one-step frame/group convert, delays on mouse triggers and manual overlay offsets, wrapped-line alignment; core editor leftovers: inner shadows and background blur on groups, command-made text readable on dark pages, a one-step frame/group convert, delays on mouse triggers and manual overlay offsets, wrapped-line alignment; core editor leftovers: command-made text readable on dark pages, a one-step frame/group convert, delays on mouse triggers and manual overlay offsets, wrapped-line alignment; core editor leftovers: a one-step frame/group convert, delays on mouse triggers and manual overlay offsets, wrapped-line alignment; core editor leftovers: delays on mouse triggers and manual overlay offsets, wrapped-line alignment; core editor leftovers: wrapped-line alignment; starter templates, a Templates tab and Getting started tips on the home screen: essentials ~57% → ~59%; everyday editing partials: number fields, outline options, Hide when publishing, cursors, panel widths, dark pages, numbers unchanged but menu breadth 211/211; text box sizes measured against documents re-saved by other software: whole-pixel line heights, advances on a 1024-per-em grid, letter spacing between glyphs only, numbers unchanged; .fig export checked by opening written files in other software, numbers unchanged; a public figcraft-server: open hosting, owner tokens, per-address limits, idle expiry, moderation, install.sh; 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; 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-13 · **Change:** minor (core editor leftovers: component playground, auto gap handles, variables inside styles, inner shadows and background blur on groups, command-made text readable on dark pages, a one-step frame/group convert, delays on mouse triggers and manual overlay offsets, wrapped-line alignment; starter templates, a Templates tab and Getting started tips on the home screen: essentials ~57% → ~59%; everyday editing partials: number fields, outline options, Hide when publishing, cursors, panel widths, dark pages, numbers unchanged but menu breadth 211/211; text box sizes measured against documents re-saved by other software: whole-pixel line heights, advances on a 1024-per-em grid, letter spacing between glyphs only, numbers unchanged; .fig export checked by opening written files in other software, numbers unchanged; a public figcraft-server: open hosting, owner tokens, per-address limits, idle expiry, moderation, install.sh; 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; 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
@@ -104,7 +104,7 @@ Breadth: does it exist. Ready: depth, correctness and fidelity for real projects
| 8 | Frames, groups, sections, masks | 5 | 97% | 80% | Visual QA with real files (a one-step frame/group convert, and a behaviour suite from the documentation landed: masked-out areas don't take clicks, mask group bounds and exports follow the mask, sections kept out of frames and groups, frames and sections drawn around layers take them, section resizing leaves contents in place, X/Y measured in the frame) | 1–3 |
| 9 | Auto layout & constraints | 9 | 98% | 74% | Behaviour against real Figma files (behaviour suite from the documentation, `fr` tracks like CSS, vertical wrap, auto gaps evenly/around, cell alignment and wrapped-line alignment landed) ([auto-layout-parity](auto-layout-parity.md)) | 3–6 |
| 10 | Text | 8 | 90% | 70% | Spell check, text metrics on real files and in other fonts (text box sizes measured against documents re-saved by other software: all 230 self-sizing boxes in 7 documents come out to the pixel, one font family; behaviour suite with CJK line breaking, vertical trim and carets by grapheme cluster landed; lists per paragraph with nesting and list spacing, hanging punctuation and lists, decoration style, thickness, offset, skip ink and colour landed) | 3–7 |
| 11 | Components & variants | 9 | 96% | 72% | Real (not synthetic) libraries, `.fig` libraries, component playground (behaviour suite from the documentation, overrides of padding, gap, grids, exports and interactions, refused geometry overrides, variant-switch and Assets-swap override rules, top-left default variant landed) ([components-parity](components-parity.md)) | 4–9 |
| 11 | Components & variants | 9 | 96% | 72% | Real (not synthetic) libraries, `.fig` libraries (a component playground for variant, boolean and text properties, a behaviour suite from the documentation, overrides of padding, gap, grids, exports and interactions, refused geometry overrides, variant-switch and Assets-swap override rules, top-left default variant landed) ([components-parity](components-parity.md)) | 4–9 |
| 12 | Styles & variables | 5 | 95% | 76% | Real-library QA (variables inside styles, 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 | 92% | 74% | Video triggers and media actions, controllers on macOS, Linux and FreeBSD, real prototype QA (behaviour suite from the documentation, delays on mouse triggers, manual overlay offsets, key combinations, controller buttons on key/gamepad triggers (Windows and the browser), widgets reacting in prototypes, interactive components playing their variants, drag-driven animations, physical springs landed) ([prototyping-parity](prototyping-parity.md)) | 2–5 |
| 14 | Import/export | 6 | 86% | 58% | `.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), checking the rest of the experimental `.fig` export (landed; written files open and draw in other software) on effects, images, prototypes and real files, HEIC/video import, CMYK output intent ([file-format-parity](file-format-parity.md)) | 5–10 |
@@ -625,6 +625,7 @@ Carried over from the ROADMAP's "Shipped so far" (2026-10-10). The test count th
| Date | Change | Summary |
|---|---|---|
| 2026-10-13 | minor | Component playground (M3): with an instance, a component or a component set selected, the Inspect tab shows a Playground: a preview of the component and a control for each variant, boolean and text property. What is tried there changes the preview and the code in the Code section, never the file (no undo step, the canvas instance stays as it is); Reset goes back to the component's or the instance's own values. `component.playground {id?, properties?, language?, path?}` gives the same to scripts: the properties with their values, the code, and a PNG on request (`playground_tests`). Left: instance-swap and slot properties in the playground (the command takes an instance swap; the panel has no control for it), a code connect template's snippet with the tried values. Numbers unchanged. |
| 2026-10-13 | minor | Auto layout, on-canvas handles of automatic gaps (M2): the handle between spread rows or columns (and between items with an auto gap) shows the distance the layout gives the gap instead of the stored value, and dragging it makes the gap a fixed one from there, as typing a gap in the panel does; one undo step (`gap_handle_tests`). Before, the drag changed a value that had no effect. The documentation is silent on dragging an automatic gap. Numbers unchanged. |
| 2026-10-13 | minor | Variables inside styles (M3): a shared style can bind variables to fields of its value (`style.bind {style, field, variable?}`: a paint style's colours and gradient stops, an effect style's colour, radius, spread and offsets, a grid style's size, count, gutter and offset, a text style's font size, line height, letter spacing, family and style). Every layer using the style shows those fields resolved in its own modes and stays linked when a variable or a mode changes; the style keeps them resolved in the default modes. A style made from a layer keeps that layer's bindings; a new value drops bindings to what is gone. The style editor binds from its property rows (paint colour, shadow colour, blur radius, grid size) and detaches from the variable's chip; the style picker marks styles that use variables. `.fig` export and import carry the bindings on the style, REST layers hand theirs to the style they use. Left: binding text style fields and the other effect and grid fields from the editor (commands only), library variables in the picker, a style's library copy. Numbers unchanged. |
| 2026-10-13 | minor | Effects on groups, inner shadows and background blur (M3): a group is no outline of its own, so its inner shadows are cast inwards from around what its layers draw together (their pixels, over them: no shade at a seam between two layers) and its background blur and glass show the backdrop through the outlines of its layers (under them). Canvas and PNG; SVG embeds a group with an inner shadow as a render and its background blur as an image under the layers; PDF places both as images, the blur matching the PNG. Where the group holds one shape the pixels match that shape, or a frame filled like it, carrying the effect (`group_effects_tests` in render, svg and pdf). Offsets and spreads of a group's inner shadow go by whole pixels. Numbers unchanged. |