mirror of
https://github.com/storytold/figcraft.git
synced 2026-11-13 02:34:33 +00:00
Merge pull request #2269 from TrevorBrown-dev/fix/instance-lists-its-variant-props-2022
M3: an instance lists the properties its variant uses; long property names stay on one line (#2022)
This commit is contained in:
@@ -298,6 +298,30 @@ pub fn prop_list(doc: &Document, c: NodeId) -> Vec<(String, PropDef)> {
|
||||
}
|
||||
}
|
||||
|
||||
/// The properties an instance of `component` lists: [`prop_list`] without the boolean, text and
|
||||
/// instance swap properties that no layer of the component reads. A set's variants share its
|
||||
/// definitions, so one a variant's layers don't bind (another variant's text, a leftover of a
|
||||
/// deleted variant) stays defined, and shows on the set, but not on this variant's instances
|
||||
/// (#2022). Slots stay. Walks as [`slots_of`] does: a nested instance counts, its own layers and
|
||||
/// the content put into slots are not entered.
|
||||
pub fn instance_prop_list(doc: &Document, component: NodeId) -> Vec<(String, PropDef)> {
|
||||
let mut used: std::collections::HashSet<String> = std::collections::HashSet::new();
|
||||
let mut stack = vec![component];
|
||||
let mut seen = 0usize;
|
||||
while let Some(c) = stack.pop() {
|
||||
seen += 1;
|
||||
if seen > 1_000_000 {
|
||||
break;
|
||||
}
|
||||
let Some(n) = doc.node(c) else { continue };
|
||||
used.extend(n.prop_refs.values().cloned());
|
||||
if c == component || (!is_instance(doc, c) && !holds_content(n)) {
|
||||
stack.extend(n.children.iter().rev().copied());
|
||||
}
|
||||
}
|
||||
prop_list(doc, component).into_iter().filter(|(k, d)| !takes_value(d) || used.contains(k)).collect()
|
||||
}
|
||||
|
||||
/// Whether instances store a value for the property (every kind but slots).
|
||||
pub fn takes_value(d: &PropDef) -> bool {
|
||||
!matches!(d, PropDef::Slot { .. })
|
||||
|
||||
@@ -395,8 +395,11 @@ fn unbind_delete_and_vary(s: &mut Session, c: NodeId, i: NodeId, title: NodeId,
|
||||
ex(s, "component.deleteProperty", json!({"id": c, "name": "Heading"}));
|
||||
assert!(node(s, title).prop_refs.is_empty());
|
||||
assert!(matches!(&node(s, i).kind, NodeKind::Instance { props, .. } if !props.contains_key("Heading")));
|
||||
let desc = ex(s, "component.properties", json!({"id": i}));
|
||||
// "Show dot" is still defined but no layer reads it any more: the main lists it, the instance doesn't (#2022).
|
||||
let desc = ex(s, "component.properties", json!({"id": c}));
|
||||
assert_eq!(desc["properties"].as_array().unwrap().len(), 1);
|
||||
let desc = ex(s, "component.properties", json!({"id": i}));
|
||||
assert!(desc["properties"].as_array().unwrap().is_empty());
|
||||
// A variant property on a lone component makes it a component set.
|
||||
let set = idv(&ex(s, "component.addProperty", json!({"id": c, "name": "Theme", "type": "variant", "default": "Light"})));
|
||||
assert!(matches!(node(s, set).kind, NodeKind::ComponentSet { .. }));
|
||||
|
||||
@@ -8,7 +8,7 @@ use super::{
|
||||
req_string, req_targets, set_boolean_default, slot_edited, slots_of, string, takes_value, targets, variant_conflicts, variant_name,
|
||||
variant_options, variant_props, variant_set,
|
||||
};
|
||||
use crate::components::prop_list;
|
||||
use crate::components::{instance_prop_list, prop_list};
|
||||
|
||||
/// Whether instance `id` has a component property `name` that a `kind` variable can drive (a
|
||||
/// boolean property takes a boolean variable, a text or variant property a string one).
|
||||
@@ -606,7 +606,8 @@ pub(crate) fn describe_props(doc: &Document, id: NodeId) -> Value {
|
||||
}
|
||||
// Slots: the frames (on a main) or this instance's copy, with whether it holds its own content.
|
||||
let slots = slots_of(doc, if values.is_some() { id } else { variant_set(doc, comp).unwrap_or(comp) });
|
||||
for (k, d) in prop_list(doc, comp) {
|
||||
let defs = if values.is_some() { instance_prop_list(doc, comp) } else { prop_list(doc, comp) };
|
||||
for (k, d) in defs {
|
||||
let mut v = def_json(&k, &d);
|
||||
if !takes_value(&d) {
|
||||
let layers: Vec<NodeId> = slots.iter().filter(|(_, n)| *n == k).map(|(l, _)| *l).collect();
|
||||
|
||||
@@ -77,8 +77,9 @@ fn every_kind_of_property_keeps_its_place_on_rename() {
|
||||
let icon = idv(&ex(&mut s, "component.create", json!({"ids": [icon_f]})));
|
||||
let nested = idv(&ex(&mut s, "instance.create", json!({"component": icon, "x": 60, "y": 10, "parent": f})));
|
||||
let small = idv(&ex(&mut s, "component.create", json!({"ids": [f]})));
|
||||
let zoom_layer = idv(&ex(&mut s, "shape.rectangle", json!({"x": 150, "y": 10, "width": 10, "height": 10, "parent": small})));
|
||||
// Made in an order that isn't by name: Zoom (boolean), Label (text), Glyph (instance swap), Body (slot).
|
||||
ex(&mut s, "component.addProperty", json!({"id": small, "name": "Zoom", "type": "boolean"}));
|
||||
ex(&mut s, "component.addProperty", json!({"id": small, "name": "Zoom", "type": "boolean", "layer": zoom_layer}));
|
||||
ex(&mut s, "component.addProperty", json!({"id": small, "name": "Label", "type": "text", "layer": label}));
|
||||
ex(&mut s, "component.addProperty", json!({"id": small, "name": "Glyph", "type": "instanceSwap", "layer": nested}));
|
||||
ex(&mut s, "component.makeSlot", json!({"id": body}));
|
||||
@@ -133,3 +134,27 @@ fn make_slot_takes_the_name_typed() {
|
||||
by_name.sort();
|
||||
assert_eq!(listed, by_name);
|
||||
}
|
||||
|
||||
/// An instance lists the properties its own variant's layers use (#2022): in Figma's data a set
|
||||
/// has one text property per variant (and leftovers of deleted ones), measured: an instance of
|
||||
/// variant A lists `State` and `A label`, never `B label` or a property no layer reads.
|
||||
#[test]
|
||||
fn an_instance_lists_only_the_properties_its_variant_uses() {
|
||||
let mut s = Session::new();
|
||||
let mut variants = Vec::new();
|
||||
let mut texts = Vec::new();
|
||||
for (k, name) in ["Type=Flag, State=Default", "Type=Flag, State=Selected"].iter().enumerate() {
|
||||
let f = idv(&ex(&mut s, "frame.create", json!({"x": 0, "y": k * 60, "width": 120, "height": 40, "name": name})));
|
||||
texts.push(idv(&ex(&mut s, "text.create", json!({"text": "Label", "x": 4, "y": 4, "parent": f, "name": "Label"}))));
|
||||
variants.push(idv(&ex(&mut s, "component.create", json!({"ids": [f]}))));
|
||||
}
|
||||
let set = idv(&ex(&mut s, "component.combineVariants", json!({"ids": variants})));
|
||||
ex(&mut s, "component.addProperty", json!({"id": set, "name": "Default label", "type": "text", "layer": texts[0]}));
|
||||
ex(&mut s, "component.addProperty", json!({"id": set, "name": "Selected label", "type": "text", "layer": texts[1]}));
|
||||
ex(&mut s, "component.addProperty", json!({"id": set, "name": "Gone label", "type": "text"}));
|
||||
assert_eq!(names(&mut s, set), ["Type", "State", "Default label", "Selected label", "Gone label"], "the set keeps every definition");
|
||||
let inst = idv(&ex(&mut s, "instance.create", json!({"component": variants[0], "x": 400, "y": 0})));
|
||||
assert_eq!(names(&mut s, inst), ["Type", "State", "Default label"]);
|
||||
ex(&mut s, "instance.setProperties", json!({"ids": [inst], "properties": {"State": "Selected"}}));
|
||||
assert_eq!(names(&mut s, inst), ["Type", "State", "Selected label"]);
|
||||
}
|
||||
|
||||
@@ -186,3 +186,43 @@ fn property_values_written_only_as_variables_are_applied() {
|
||||
assert_eq!(props.get("Hint"), Some(&Value::Bool(false)));
|
||||
assert_eq!(props.get("Label"), Some(&Value::String("Name on card".into())));
|
||||
}
|
||||
|
||||
/// #2022: a set keeps each named text definition under its own name (Figma writes one per variant,
|
||||
/// plus leftovers of deleted variants), and each variant's text reads its own copy.
|
||||
#[test]
|
||||
fn a_set_keeps_each_named_text_definition_and_layers_read_their_own() {
|
||||
let text = |chars: &str| o(vec![("textValue", o(vec![("characters", s(chars))]))]);
|
||||
let names = ["Type=Flag / State=Default label", "Type=Flag / State=Selected label", "Type=Issue / State=Default label"];
|
||||
let set_defs: Vec<Kv<'static>> =
|
||||
names.iter().zip([70, 71, 72]).zip(["One", "Two", "Gone"]).map(|((n, id), c)| definition(id, n, "TEXT", text(c))).collect();
|
||||
let copies = |first: u32| Kv::Array((0..3).map(|k| o(vec![("id", g(first + k)), ("parentPropDefId", g(70 + k))])).collect());
|
||||
let msg = message(vec![
|
||||
node(9, "FRAME", "Chip", Some(2), vec![("isStateGroup", Kv::Bool(true)), ("componentPropDefs", Kv::Array(set_defs))]),
|
||||
node(10, "SYMBOL", "Type=Flag, State=Default", Some(9), vec![("componentPropDefs", copies(80))]),
|
||||
node(11, "TEXT", "Label", Some(10), vec![bound("TEXT_DATA", 80)]),
|
||||
node(20, "SYMBOL", "Type=Flag, State=Selected", Some(9), vec![("componentPropDefs", copies(90))]),
|
||||
node(21, "TEXT", "Label", Some(20), vec![bound("TEXT_DATA", 91)]),
|
||||
]);
|
||||
let (rest, _, _) = to_rest(&msg, "Synthetic", crate::rest::MAX_NODES);
|
||||
let defs = &by_id(&rest["document"], "9:9")["componentPropertyDefinitions"];
|
||||
for (k, n) in names.iter().enumerate() {
|
||||
assert_eq!(defs[format!("{n}#9:{}", 70 + k)]["type"], "TEXT", "{n}");
|
||||
}
|
||||
assert_eq!(defs.as_object().map(|d| d.len()), Some(3), "{defs}");
|
||||
assert_eq!(by_id(&rest["document"], "9:11")["componentPropertyReferences"]["characters"], format!("{}#9:70", names[0]));
|
||||
assert_eq!(by_id(&rest["document"], "9:21")["componentPropertyReferences"]["characters"], format!("{}#9:71", names[1]));
|
||||
|
||||
let (doc, _) = crate::rest::import_fig(&rest, &crate::rest::no_images).unwrap();
|
||||
let refs: Vec<Option<String>> = doc
|
||||
.ids()
|
||||
.filter_map(|id| doc.node(id))
|
||||
.filter(|n| n.name == "Label" && n.source.is_none())
|
||||
.map(|n| n.prop_refs.get("characters").cloned())
|
||||
.collect();
|
||||
assert_eq!(refs.len(), 2, "{refs:?}");
|
||||
assert!(refs.contains(&Some(names[0].to_string())) && refs.contains(&Some(names[1].to_string())), "{refs:?}");
|
||||
for set in doc.ids().filter_map(|id| doc.node(id)).filter(|n| matches!(n.kind, NodeKind::Component { .. })) {
|
||||
let NodeKind::Component { props, .. } = &set.kind else { continue };
|
||||
assert!(names.iter().all(|n| props.contains_key(*n)), "{} holds {:?}", set.name, props.keys().collect::<Vec<_>>());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,12 +19,19 @@ fn id(v: serde_json::Value) -> NodeId {
|
||||
fn setup() -> (Ui, NodeId, NodeId) {
|
||||
let mut s = Session::new();
|
||||
let mut variants = Vec::new();
|
||||
let mut icons = Vec::new();
|
||||
for (k, name) in ["Size=Small, State=Default", "Size=Large, State=Default", "Size=Small, State=Hover"].iter().enumerate() {
|
||||
let f = id(s.execute("frame.create", &json!({"x": 0, "y": k * 60, "width": 80, "height": 40, "name": name})).unwrap());
|
||||
// A layer in every variant reads "Icon": an instance lists only the properties its variant uses.
|
||||
let icon = id(s.execute("shape.rectangle", &json!({"x": 4, "y": 4, "width": 10, "height": 10, "parent": f})).unwrap());
|
||||
icons.push(icon);
|
||||
variants.push(id(s.execute("component.create", &json!({"ids": [f]})).unwrap()));
|
||||
}
|
||||
let set = id(s.execute("component.combineVariants", &json!({"ids": variants})).unwrap());
|
||||
s.execute("component.addProperty", &json!({"id": set, "name": "Icon", "type": "boolean", "default": true})).unwrap();
|
||||
s.execute("component.addProperty", &json!({"id": set, "name": "Icon", "type": "boolean", "default": true, "layer": icons[0]})).unwrap();
|
||||
for icon in &icons[1..] {
|
||||
s.execute("component.bindProperty", &json!({"layer": icon, "property": "Icon"})).unwrap();
|
||||
}
|
||||
let inst = id(s.execute("instance.create", &json!({"component": set, "x": 400, "y": 0})).unwrap());
|
||||
s.execute("select.set", &json!({"ids": [set]})).unwrap();
|
||||
(Ui::new(s), set, inst)
|
||||
@@ -123,3 +130,25 @@ fn escape_cancels_a_property_drag_and_keeps_the_selection() {
|
||||
assert_eq!(undo_len(&u), undo);
|
||||
assert_eq!(u.app.session.ds().unwrap().selection, [set], "the Escape only cancels the drag");
|
||||
}
|
||||
|
||||
/// A long property name is cut to one line inside its label box instead of wrapping over the rows
|
||||
/// above and below (#2022); the name itself stays whole.
|
||||
#[test]
|
||||
fn a_long_property_name_stays_on_one_line() {
|
||||
let long = "Flag Default label with a long name";
|
||||
let mut s = Session::new();
|
||||
let f = id(s.execute("frame.create", &json!({"x": 0, "y": 0, "width": 120, "height": 40, "name": "Chip"})).unwrap());
|
||||
let t = id(s.execute("text.create", &json!({"text": "Label", "x": 4, "y": 4, "parent": f})).unwrap());
|
||||
let c = id(s.execute("component.create", &json!({"ids": [f]})).unwrap());
|
||||
s.execute("component.addProperty", &json!({"id": c, "name": long, "type": "text", "layer": t})).unwrap();
|
||||
let inst = id(s.execute("instance.create", &json!({"component": c, "x": 400, "y": 0})).unwrap());
|
||||
s.execute("select.set", &json!({"ids": [inst]})).unwrap();
|
||||
let mut u = Ui::new(s);
|
||||
let texts = crate::export_name_tests::painted(&mut u);
|
||||
let panel = u.app.ui.right_panel_rect.expect("the right panel shows");
|
||||
let labels: Vec<(String, egui::Rect)> = texts.into_iter().filter(|(t, r)| t.starts_with("Flag") && r.min.x >= panel.min.x).collect();
|
||||
let [(text, rect)] = labels.as_slice() else { panic!("one property label: {labels:?}") };
|
||||
assert!(text.ends_with('…'), "cut with an ellipsis, not {text:?}");
|
||||
assert!(rect.height() <= crate::widgets::FIELD_H, "one line, not {} tall", rect.height());
|
||||
assert_eq!(names(&mut u, inst), [long], "the name itself stays whole");
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ use crate::layers_drag_tests::Ui;
|
||||
use crate::widgets::FIELD_H;
|
||||
|
||||
/// The text painted this frame, with each piece's box.
|
||||
fn painted(u: &mut Ui) -> Vec<(String, Rect)> {
|
||||
pub(crate) fn painted(u: &mut Ui) -> Vec<(String, Rect)> {
|
||||
fn walk(shape: &Shape, out: &mut Vec<(String, Rect)>) {
|
||||
match shape {
|
||||
Shape::Text(t) => out.push((t.galley.text().to_string(), t.visual_bounding_rect())),
|
||||
|
||||
@@ -14,8 +14,8 @@ use egui::{Id, Sense, Ui, vec2};
|
||||
use figcraft_doc::{Document, Node, NodeId, NodeKind, PropDef, VariableType};
|
||||
use figcraft_engine::cmd::component::{can_add_variant, can_combine_variants};
|
||||
use figcraft_engine::components::{
|
||||
PROP_BINDING, default_variant, inside_instance, owning_component, owning_instance, prop_list, slot_def, slot_edited, slots_of, variant_conflicts,
|
||||
variant_options, variant_props, variant_set,
|
||||
PROP_BINDING, default_variant, inside_instance, instance_prop_list, owning_component, owning_instance, prop_list, slot_def, slot_edited,
|
||||
slots_of, variant_conflicts, variant_options, variant_props, variant_set,
|
||||
};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
@@ -748,7 +748,7 @@ fn instance_props(app: &mut FigApp, ui: &mut Ui, doc: &Document, n: &Node, comps
|
||||
}
|
||||
}
|
||||
// Boolean, text and instance-swap properties.
|
||||
for (k, d) in prop_list(doc, *component) {
|
||||
for (k, d) in instance_prop_list(doc, *component) {
|
||||
let v = props.get(&k).cloned().unwrap_or_else(|| figcraft_engine::components::prop_default_value(&d));
|
||||
row(ui, 32.0, |ui| {
|
||||
prop_label(ui, &k, type_icon(&d));
|
||||
@@ -968,8 +968,12 @@ fn comp_rows(ui: &mut Ui, found: &CompSearch, checked: &impl Fn(NodeId) -> bool)
|
||||
|
||||
fn prop_label(ui: &mut Ui, name: &str, icon: Icon) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let (r, _) = ui.allocate_exact_size(vec2(84.0, 20.0), Sense::hover());
|
||||
let (r, resp) = ui.allocate_exact_size(vec2(84.0, 20.0), Sense::hover());
|
||||
icons::paint(ui.painter(), egui::Rect::from_min_size(egui::pos2(r.min.x, r.center().y - 6.0), vec2(12.0, 12.0)), icon, t.icon_secondary);
|
||||
let g = ui.painter().layout(name.to_string(), crate::theme::regular(11.0), t.text_secondary, r.width() - 18.0);
|
||||
let shown = widgets::elide(ui, name, r.width() - 18.0);
|
||||
let g = ui.painter().layout_no_wrap(shown.clone(), crate::theme::regular(11.0), t.text_secondary);
|
||||
ui.painter().galley(egui::pos2(r.min.x + 18.0, r.center().y - g.size().y / 2.0), g, t.text_secondary);
|
||||
if shown != name {
|
||||
resp.on_hover_text(name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Components, variants, styles and variables parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-07 · **Change:** minor (with several layers selected the Design panel header's component button offers Create multiple components (#2088); the Local variables window selects several variables (click, Shift+click for a range, Cmd/Ctrl+click to toggle, Escape to clear) and delete, move to a group and scope act on the selected local variables in one undo step (#2002); text styles carry paragraph spacing and indent, the list style and the OpenType features, as the Supported in text styles table of Create and apply text styles says (#1829); a layer's effect binding takes only a field its effect has; Go to main component on an instance of a library component opens the library file in a tab and selects the main there; glass effects take variables; Delete on a layer inside an instance hides it as an override instead of being refused, as the help center says: "Figma will only toggle the layer's visibility instead of removing it"; library analytics list how many layers and files use each style and variable; SwiftUI and Compose code names a bound variable by its iOS or Android code syntax; library analytics count insertions and show insertions and detachments over the past 30 days, 90 days or year; a collection of this file can be extended (Extend collection on its row, `variable.extendCollection`), its extension following its edits at once, and extended collections extend again in the file; a typed or stored variable, collection or mode id names only the object with that id (#1613); colour variables can be calculated from other colours per mode — `{alpha}` replaces a colour's alpha, `{mix}` blends two — and follow them (#1280); stored variable and collection references keep their identity when another object is named like their id (follow-up to #1542); a component set's property rows fit the 240 px right panel: variant values wrap two to a line there and every + and − stays in view; opening a file contacts no team library server it names until you check it (#1097); an explicit variables file path (`variable.import`, the `variables` of `file.open` and `file.import`) is refused past 256 MiB or when it isn't a regular file, as the sibling `variables.json` already was (part of #1114); component properties keep the order they were made in when one is renamed, and Make slot asks for the slot's name first (#1536, #1537); components can't contain their own instances, directly or through others (#1084); the instance-swap menus have a search field (#1338); an instance-swap property's default menu scrolls (#1338); variables on a text style's fields from the style editor; instance-swap properties in the component playground; duplicating or Alt-dragging a main component makes an instance of it (#892); a bound colour variable's alpha applies once (#802); variables import reads `variables` outside `meta` and warns when it imports none (#790); variant properties reorder by drag; library files saved right after a read are read again; Hide when publishing note corrected; size and resizing overrides of layers inside instances (#731); variables export in the browser as a download; 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)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-07 · **Change:** minor (an instance lists only the properties its variant's layers use and long property names are cut to one line (#2022); with several layers selected the Design panel header's component button offers Create multiple components (#2088); the Local variables window selects several variables (click, Shift+click for a range, Cmd/Ctrl+click to toggle, Escape to clear) and delete, move to a group and scope act on the selected local variables in one undo step (#2002); text styles carry paragraph spacing and indent, the list style and the OpenType features, as the Supported in text styles table of Create and apply text styles says (#1829); a layer's effect binding takes only a field its effect has; Go to main component on an instance of a library component opens the library file in a tab and selects the main there; glass effects take variables; Delete on a layer inside an instance hides it as an override instead of being refused, as the help center says: "Figma will only toggle the layer's visibility instead of removing it"; library analytics list how many layers and files use each style and variable; SwiftUI and Compose code names a bound variable by its iOS or Android code syntax; library analytics count insertions and show insertions and detachments over the past 30 days, 90 days or year; a collection of this file can be extended (Extend collection on its row, `variable.extendCollection`), its extension following its edits at once, and extended collections extend again in the file; a typed or stored variable, collection or mode id names only the object with that id (#1613); colour variables can be calculated from other colours per mode — `{alpha}` replaces a colour's alpha, `{mix}` blends two — and follow them (#1280); stored variable and collection references keep their identity when another object is named like their id (follow-up to #1542); a component set's property rows fit the 240 px right panel: variant values wrap two to a line there and every + and − stays in view; opening a file contacts no team library server it names until you check it (#1097); an explicit variables file path (`variable.import`, the `variables` of `file.open` and `file.import`) is refused past 256 MiB or when it isn't a regular file, as the sibling `variables.json` already was (part of #1114); component properties keep the order they were made in when one is renamed, and Make slot asks for the slot's name first (#1536, #1537); components can't contain their own instances, directly or through others (#1084); the instance-swap menus have a search field (#1338); an instance-swap property's default menu scrolls (#1338); variables on a text style's fields from the style editor; instance-swap properties in the component playground; duplicating or Alt-dragging a main component makes an instance of it (#892); a bound colour variable's alpha applies once (#802); variables import reads `variables` outside `meta` and warns when it imports none (#790); variant properties reorder by drag; library files saved right after a read are read again; Hide when publishing note corrected; size and resizing overrides of layers inside instances (#731); variables export in the browser as a download; 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
|
||||
@@ -12,6 +12,8 @@ accounts).
|
||||
|
||||
| Feature | Status | Notes |
|
||||
|---|---|---|
|
||||
| 2026-11-11 | minor | M3, #2022: an instance lists only the properties its variant's layers use (its variant properties, slots, and the boolean, text and instance swap properties a layer of its current variant reads), as measured in Figma's web app: an instance of variant A lists `State` and `A label`, never `B label` or a property no layer reads. The set and its variants still list every definition, so unused ones can be deleted there. A long property name in the panel is cut to one line with an ellipsis, the full name on hover, instead of wrapping over the rows around it. Tests: `prop_order_tests::an_instance_lists_only_the_properties_its_variant_uses`, `component_props_reorder_tests::a_long_property_name_stays_on_one_line`, `prop_value_tests::a_set_keeps_each_named_text_definition_and_layers_read_their_own`. Grade unchanged. |
|
||||
| Create component, instances, detach, reset, go to main, restore main, push changes | done | push from a nested layer goes to the outer main |
|
||||
| Create component, instances, detach, reset, go to main, restore main, push changes | done | push from a nested layer goes to the outer main; with two or more layers selected the Design panel header's component button opens a menu with Create component, Create multiple components (each layer its own component in place, one undo step) and Create component set (each layer a component, wrapped in place in a set 20 px around: variants `Property 1=<layer name>` left to right, `prop=value` names kept, layers that all share a name give a set of that name with `Property 1=Default` variants; measured in Figma), one layer keeps the plain click (#2088) |
|
||||
| Per-layer overrides with reset; kept across main edits, nested swaps, copy/paste | done | main edits match a full rebuild exactly at 2,000 instances |
|
||||
| What an instance overrides: fills, strokes, effects, visibility, layer names, text content and every text property (resizing included), auto layout padding, gap and alignment, layout grids, export settings, prototype interactions, nested swaps and property values; the size and resizing (hug, fill, fixed, min and max) of layers inside it | done | each kept when the main changes that property, reset per group; opacity and corner radius too (documentation silent); size and resizing are kept through relayout, undo, save, `.fig` and REST import and detach, and stay with the instance on push |
|
||||
@@ -22,7 +24,7 @@ accounts).
|
||||
| Switching variants keeps overrides where names match and both variants had the same value; text layers keep theirs | done | |
|
||||
| Default variant: the top-left one (new instances, Assets thumbnail, generated code) | done | |
|
||||
| Variant property order (names rewritten), duplicate combinations reported | done | drag a property in the Properties section to reorder it, one undo step (#679), or `component.editProperty {index}` |
|
||||
| Properties: boolean, text, instance swap (preferred values), variant | done | listed in the order they were made, a rename keeping the place (#1536; the shared ones don't reorder by drag yet); every component menu scrolls past 320 pt, the default of an instance-swap property's too (#1338); the instance-swap menus (an instance's property and swap menus, a property's default, the playground) open with a search field that filters by name as you type, with Up, Down and Enter to pick and Escape to clear then close; the slot and preferred components menus have no search yet |
|
||||
| Properties: boolean, text, instance swap (preferred values), variant | done | listed in the order they were made, a rename keeping the place (#1536; the shared ones don't reorder by drag yet); every component menu scrolls past 320 pt, the default of an instance-swap property's too (#1338); the instance-swap menus (an instance's property and swap menus, a property's default, the playground) open with a search field that filters by name as you type, with Up, Down and Enter to pick and Escape to clear then close; the slot and preferred components menus have no search yet; an instance lists the properties its variant's layers use, so a set with one text property per variant shows one text field per instance (#2022; measured in Figma's web app, 2026-11-11); long names are cut to one line, the full name on hover |
|
||||
| Slots (preferred components, fill menu, kept across updates) | done | Figma 2025 feature; Make slot asks for the name first (a field under the frame's Slot row), and slots are renamed in the component's Properties list (#1537); no rename with only the slot's frame selected yet |
|
||||
| Exposed nested instances | done | hidden ones hide their properties |
|
||||
| Descriptions | done | |
|
||||
|
||||
Reference in New Issue
Block a user