mirror of
https://github.com/storytold/figcraft.git
synced 2026-11-13 05:42:38 +00:00
Merge remote-tracking branch 'origin/main' into landb/2149
This commit is contained in:
commit
b193563f6a
13 files changed
+229
-37
No files matched your search
@@ -37,7 +37,7 @@ use figcraft_ui_egui::{ControlReply, ControlRequest, RequestGate};
|
||||
use serde_json::{Value, json};
|
||||
use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen::prelude::*;
|
||||
use wasm_bindgen_futures::{JsFuture, future_to_promise};
|
||||
use wasm_bindgen_futures::JsFuture;
|
||||
|
||||
use crate::saved_files::SavedFiles;
|
||||
|
||||
@@ -150,15 +150,6 @@ async fn sleep(ms: i32) {
|
||||
let _ = JsFuture::from(promise).await;
|
||||
}
|
||||
|
||||
/// A reply as a promise outcome: the `result`, or an `Error` carrying the `error`.
|
||||
fn outcome(reply: &Value) -> Result<JsValue, JsValue> {
|
||||
if reply.get("ok").and_then(Value::as_bool) == Some(true) {
|
||||
Ok(to_js(reply.get("result").unwrap_or(&Value::Null)))
|
||||
} else {
|
||||
Err(js_sys::Error::new(reply.get("error").and_then(Value::as_str).unwrap_or("error")).into())
|
||||
}
|
||||
}
|
||||
|
||||
fn set_fn(obj: &js_sys::Object, name: &str, f: JsValue) -> Result<(), JsValue> {
|
||||
js_sys::Reflect::set(obj, &name.into(), &f).map(|_| ())
|
||||
}
|
||||
@@ -169,20 +160,20 @@ pub fn install() -> Result<(), JsValue> {
|
||||
Rc::new(PageApi::new(APP, Methods::default(), request)).install()?;
|
||||
let window = web_sys::window().ok_or("no window")?;
|
||||
let obj: js_sys::Object = js_sys::Reflect::get(&window, &APP.into())?.unchecked_into();
|
||||
set_fn(
|
||||
&obj,
|
||||
"inspect",
|
||||
Closure::<dyn Fn() -> js_sys::Promise>::new(|| future_to_promise(async { outcome(&send("document.inspect", json!({})).await) }))
|
||||
.into_js_value(),
|
||||
)?;
|
||||
// `inspect` is `docInspect` under its earlier name: the crate's function, so it times out too.
|
||||
let doc_inspect = js_sys::Reflect::get(&obj, &"docInspect".into())?;
|
||||
js_sys::Reflect::set(&obj, &"inspect".into(), &doc_inspect)?;
|
||||
// `openFile` runs `file.open` through the crate's `execute`, so a call the app does not answer times out and is cancelled.
|
||||
let execute: js_sys::Function = js_sys::Reflect::get(&obj, &"execute".into())?.unchecked_into();
|
||||
let this = obj.clone();
|
||||
set_fn(
|
||||
&obj,
|
||||
"openFile",
|
||||
Closure::<dyn Fn(String, js_sys::Uint8Array) -> js_sys::Promise>::new(|name: String, bytes: js_sys::Uint8Array| {
|
||||
Closure::<dyn Fn(String, js_sys::Uint8Array) -> JsValue>::new(move |name: String, bytes: js_sys::Uint8Array| {
|
||||
let data = base64::engine::general_purpose::STANDARD.encode(bytes.to_vec());
|
||||
future_to_promise(async move {
|
||||
outcome(&send("engine.execute", json!({"command": "file.open", "params": {"name": name, "data": data}})).await)
|
||||
})
|
||||
execute
|
||||
.call2(&this, &"file.open".into(), &to_js(&json!({"name": name, "data": data})))
|
||||
.unwrap_or_else(|e| js_sys::Promise::reject(&e).into())
|
||||
})
|
||||
.into_js_value(),
|
||||
)?;
|
||||
|
||||
@@ -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"]);
|
||||
}
|
||||
@@ -417,12 +417,16 @@ fn assert_slot_text(s: &mut Session, instance: NodeId, expected: &str) {
|
||||
assert!(html.contains(&format!(">{expected}</p>")), "slot Label HTML: {html}");
|
||||
}
|
||||
|
||||
fn assert_local_slot_text(s: &mut Session, instance: NodeId, expected: &str) {
|
||||
fn assert_stored_slot_text(s: &Session, instance: NodeId, expected: &str) {
|
||||
assert!(
|
||||
matches!(&node(s, instance).kind, NodeKind::Instance { props, .. } if props.get("Text") == Some(&json!(expected))),
|
||||
"instance {instance} must store its own Text value {expected:?}: {:?}",
|
||||
node(s, instance).kind
|
||||
);
|
||||
}
|
||||
|
||||
fn assert_local_slot_text(s: &mut Session, instance: NodeId, expected: &str) {
|
||||
assert_stored_slot_text(s, instance, expected);
|
||||
let properties = props_of(s, instance);
|
||||
let text = properties.iter().find(|p| p["name"] == "Text").unwrap_or_else(|| panic!("instance {instance} has no Text property: {properties:?}"));
|
||||
assert_eq!(text["value"], expected, "instance {instance} Text property");
|
||||
@@ -475,14 +479,15 @@ fn slot_text_properties_do_not_read_an_unbound_base_property() {
|
||||
ex(&mut s, "component.addProperty", json!({"id": base, "name": "Text", "type": "text", "default": "Wrong owner"}));
|
||||
let i = idv(&ex(&mut s, "instance.create", json!({"component": composition, "x": 400})));
|
||||
let nested_base = kids(&s, i)[0];
|
||||
assert_local_slot_text(&mut s, nested_base, "Wrong owner");
|
||||
// Bound to no layer, the base's Text isn't listed on its instance (#2269); its value stays stored.
|
||||
assert_stored_slot_text(&s, nested_base, "Wrong owner");
|
||||
let nested = copy_of(&s, i, main_content);
|
||||
assert_local_slot_text(&mut s, nested, "Default");
|
||||
assert_slot_text(&mut s, nested, "Default");
|
||||
ex(&mut s, "instance.setProperties", json!({"ids": [nested], "properties": {"Text": "Custom"}}));
|
||||
assert_local_slot_text(&mut s, nested, "Custom");
|
||||
assert_slot_text(&mut s, nested, "Custom");
|
||||
assert_local_slot_text(&mut s, nested_base, "Wrong owner");
|
||||
assert_stored_slot_text(&s, nested_base, "Wrong owner");
|
||||
assert_slot_text(&mut s, main_content, "Default");
|
||||
healthy(&s);
|
||||
}
|
||||
|
||||
@@ -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<_>>());
|
||||
}
|
||||
}
|
||||
@@ -128,6 +128,7 @@ name = "Svenska"
|
||||
"Another server, or one that needs its key. The usual server is set in Preferences." = "En annan server, eller en som kräver sin nyckel. Standardservern anges i Inställningar."
|
||||
"Any status" = "Valfri status"
|
||||
"Anyone on your network with this code can edit the file, while this app stays open." = "Alla i ditt nätverk som har den här koden kan redigera filen så länge appen är öppen."
|
||||
"Anyone on your network with this code can look at the file, not change it, while this app stays open." = "Alla i ditt nätverk som har den här koden kan titta på filen, men inte ändra den, så länge appen är öppen."
|
||||
"Anyone with the edit link can change the file; the view link only lets people look. The links carry their secrets: share them like passwords. `figcraft-server links` makes more and revokes them." = "Alla med redigeringslänken kan ändra filen; visningslänken låter bara andra titta. Länkarna innehåller sina hemligheter: dela dem som lösenord. `figcraft-server links` skapar fler och återkallar dem."
|
||||
"Anyone with this code can edit the file." = "Alla med den här koden kan redigera filen."
|
||||
"Anyone with this code can look at the file, not change it." = "Alla med den här koden kan titta på filen, men inte ändra den."
|
||||
@@ -388,6 +389,8 @@ name = "Svenska"
|
||||
"Crop marks" = "Skärmärken"
|
||||
"Ctrl+arrows" = "Ctrl+piltangenter"
|
||||
"Ctrl+click" = "Ctrl+klick"
|
||||
"Ctrl+click a padding field" = "Ctrl+klicka på ett padding-fält"
|
||||
"⌘ click a padding field" = "⌘ klicka på ett padding-fält"
|
||||
"Ctrl+scroll" = "Ctrl+rullning"
|
||||
"Current name" = "Aktuellt namn"
|
||||
"Current version" = "Aktuell version"
|
||||
@@ -407,6 +410,40 @@ name = "Svenska"
|
||||
"Decoration style" = "Dekorationsstil"
|
||||
"Decoration" = "Dekoration"
|
||||
"Deep select" = "Djupmarkering"
|
||||
"Scrub a field faster (2x) or slower (1/2, 1/4)" = "Dragjustera ett fält snabbare (2x) eller långsammare (1/2, 1/4)"
|
||||
"Move up or down while dragging its label" = "Flytta upp eller ned medan du drar dess etikett"
|
||||
"Scrubbing at 2x" = "Dragjustering med 2x hastighet"
|
||||
"Scrubbing at 1x" = "Dragjustering med 1x hastighet"
|
||||
"Scrubbing at 1/2" = "Dragjustering med 1/2 hastighet"
|
||||
"Scrubbing at 1/4" = "Dragjustering med 1/4 hastighet"
|
||||
"Delete {n} variables" = { one = "Radera {n} variabel", other = "Radera {n} variabler" }
|
||||
"Move out of any group" = "Flytta ut ur alla grupper"
|
||||
"Move to group {group}" = "Flytta till gruppen {group}"
|
||||
"Scope: all supported properties" = "Omfång: alla egenskaper som stöds"
|
||||
"Scope: hidden from pickers" = "Omfång: dold i väljare"
|
||||
"Scope: only {scope}" = "Omfång: endast {scope}"
|
||||
"Disconnected. Reconnecting…" = "Frånkopplad. Återansluter…"
|
||||
"Disconnected. Your edits stay here and sync when the connection returns. Reconnecting…" = "Frånkopplad. Dina redigeringar finns kvar här och synkas när anslutningen är tillbaka. Återansluter…"
|
||||
"Dismiss" = "Stäng"
|
||||
"Not connected: {reason}. Your work is still in this tab." = "Inte ansluten: {reason}. Ditt arbete finns kvar i den här fliken."
|
||||
"Retry now" = "Försök igen nu"
|
||||
"Save a copy…" = "Spara en kopia…"
|
||||
"Wrap style" = "Radbrytningsstil"
|
||||
"Text direction" = "Textriktning"
|
||||
"Auto: by the first letter with a direction" = "Auto: efter den första bokstaven som har en riktning"
|
||||
"Left to right" = "Vänster till höger"
|
||||
"Right to left" = "Höger till vänster"
|
||||
"Balance" = "Balans"
|
||||
"Pretty" = "Snygg"
|
||||
"Auto: as many words as fit on each line" = "Auto: så många ord som får plats på varje rad"
|
||||
"Balance: every line close to the same length" = "Balans: alla rader ungefär lika långa"
|
||||
"Pretty: full lines, no one word alone on the last" = "Snygg: fulla rader, inget ensamt ord på den sista"
|
||||
"Set hug contents" = "Ställ in Omslut innehåll"
|
||||
"Set fill container" = "Ställ in Fyll behållaren"
|
||||
"Double-click an edge of an auto layout frame" = "Dubbelklicka på en kant av en auto layout-frame"
|
||||
"{name}, disconnected from its live session" = "{name}, frånkopplad från sin livesession"
|
||||
"⌥ double-click an edge of an auto layout child" = "⌥ dubbelklicka på en kant av ett underordnat objekt i auto layout"
|
||||
"Alt+double-click an edge of an auto layout child" = "Alt+dubbelklicka på en kant av ett underordnat objekt i auto layout"
|
||||
"Default" = "Standard"
|
||||
"Default content" = "Standardinnehåll"
|
||||
"Default value (the top-left variant)" = "Standardvärde (varianten längst upp till vänster)"
|
||||
@@ -494,6 +531,7 @@ name = "Svenska"
|
||||
"Edit dev resource" = "Redigera utvecklarresurs"
|
||||
"Edit link" = "Redigeringslänk"
|
||||
"Edit link copied" = "Redigeringslänken har kopierats"
|
||||
"Edit padding on all sides" = "Redigera padding på alla sidor"
|
||||
"Edit style" = "Redigera stil"
|
||||
"Edit variable" = "Redigera variabel"
|
||||
"Edit with the builder" = "Redigera med byggaren"
|
||||
@@ -531,6 +569,7 @@ name = "Svenska"
|
||||
"Exclusion" = "Uteslutning"
|
||||
"Exit focus view (Esc)" = "Lämna fokusvyn (Esc)"
|
||||
"Expand" = "Expandera"
|
||||
"Expand the interface" = "Expandera gränssnittet"
|
||||
"Export" = "Exportera"
|
||||
"Export all" = "Exportera alla"
|
||||
"Export frames and layers as PNG, JPG, SVG or PDF" = "Exportera frames och lager som PNG, JPG, SVG eller PDF"
|
||||
@@ -1073,6 +1112,7 @@ name = "Svenska"
|
||||
"Pages: {pages}" = "Sidor: {pages}"
|
||||
"Paint bucket" = "Färghink"
|
||||
"Pan the canvas" = "Panorera arbetsytan"
|
||||
"Show and move the placement crosshair (its tool chosen, nothing selected)" = "Visa och flytta placeringshårkorset (med dess verktyg valt och inget markerat)"
|
||||
"Pan with nothing selected (Shift: farther)" = "Panorera när inget är markerat (Shift: längre)"
|
||||
"Panels" = "Paneler"
|
||||
"Paper" = "Papper"
|
||||
@@ -1297,6 +1337,7 @@ name = "Svenska"
|
||||
"See all templates" = "Se alla mallar"
|
||||
"Select layers and they space themselves as their content changes" = "Markera lager så fördelar de avståndet själva när innehållet ändras"
|
||||
"SHA-256: {sha}…" = "SHA-256: {sha}…"
|
||||
"Same as file ({profile})" = "Samma som filen ({profile})"
|
||||
"Same as layer" = "Samma som lagret"
|
||||
"Saturation" = "Mättnad"
|
||||
"Save" = "Spara"
|
||||
@@ -1969,6 +2010,17 @@ name = "Svenska"
|
||||
"⌘ click" = "⌘ klicka"
|
||||
"⌘ scroll" = "⌘ rulla"
|
||||
"⌥ drag" = "⌥ dra"
|
||||
"Selection box: switch to pick select" = "Markeringsruta: växla till plockmarkering"
|
||||
"Pick select: next or previous layer" = "Plockmarkering: nästa eller föregående lager"
|
||||
"Pick select: into a layer, out to its parent" = "Plockmarkering: in i ett lager, ut till dess överordnade"
|
||||
"Pick select: select the picked layer" = "Plockmarkering: markera det valda lagret"
|
||||
"⇥ or ⌥ Space again" = "⇥ eller ⌥ Blanksteg igen"
|
||||
"Tab or Ctrl+Space again" = "Tab eller Ctrl+Blanksteg igen"
|
||||
"↵ / ⇧↵" = "↵ / ⇧↵"
|
||||
"Enter / Shift+Enter" = "Enter / Shift+Enter"
|
||||
"Space" = "Blanksteg"
|
||||
"Nothing to pick: the page is empty" = "Inget att välja: sidan är tom"
|
||||
"Pick select: Tab and Shift+Tab move between layers, Enter goes into one, Shift+Enter out, Space selects, Escape cancels" = "Plockmarkering: Tab och Shift+Tab flyttar mellan lager, Enter går in i ett, Shift+Enter ut, Blanksteg markerar, Esc avbryter"
|
||||
|
||||
[commands]
|
||||
|
||||
@@ -2005,6 +2057,7 @@ name = "Svenska"
|
||||
"board.label" = "Kopplingsetikett"
|
||||
"board.nextSticky" = "Nästa lapp"
|
||||
"board.reattach" = "Koppla om koppling"
|
||||
"board.reshape" = "Omforma koppling"
|
||||
"board.setColor" = "Whiteboardfärg"
|
||||
"board.setStyle" = "Whiteboardstil"
|
||||
"board.shape" = "Form med text"
|
||||
@@ -2047,6 +2100,7 @@ name = "Svenska"
|
||||
"collab.openFile" = "Öppna onlinefil"
|
||||
"collab.peers" = "Personer i den här filen"
|
||||
"collab.publish" = "Publicera på server"
|
||||
"collab.reconnect" = "Återanslut livesessionen"
|
||||
"collab.recoverFile" = "Återställ onlinefil"
|
||||
"collab.registerLinks" = "Öppna inbjudningslänkar i FigCraft"
|
||||
"collab.removePerson" = "Ta bort från session"
|
||||
@@ -2195,6 +2249,7 @@ name = "Svenska"
|
||||
"image.deletePreset" = "Radera förinställning"
|
||||
"image.place" = "Placera bild"
|
||||
"image.presets" = "Bildförinställningar"
|
||||
"image.replace" = "Ersätt bild"
|
||||
"image.savePreset" = "Spara förinställning"
|
||||
"inspect.assets" = "Resurser"
|
||||
"inspect.code" = "Kod"
|
||||
@@ -2212,6 +2267,7 @@ name = "Svenska"
|
||||
"layers.collapseAll" = "Fäll ihop alla"
|
||||
"layout.add" = "Lägg till auto layout"
|
||||
"layout.remove" = "Ta bort auto layout"
|
||||
"layout.removeAll" = "Ta bort all auto layout"
|
||||
"layout.setTrack" = "Ange spår"
|
||||
"layout.suggest" = "Föreslå layout"
|
||||
"libraries.open" = "Öppna bibliotek"
|
||||
@@ -2306,6 +2362,9 @@ name = "Svenska"
|
||||
"pref.showDimensions" = "Visa dimensioner på objekt"
|
||||
"pref.startOnHome" = "Börja på Start"
|
||||
"pref.uiScale" = "Gränssnittsstorlek"
|
||||
"pref.uiScaleDown" = "Mindre gränssnitt"
|
||||
"pref.uiScaleReset" = "Standardstorlek för gränssnittet"
|
||||
"pref.uiScaleUp" = "Större gränssnitt"
|
||||
"pref.userName" = "Ditt namn"
|
||||
"proto.action.add" = "Lägg till prototypåtgärd"
|
||||
"proto.action.move" = "Ändra ordning på prototypåtgärd"
|
||||
@@ -2353,6 +2412,7 @@ name = "Svenska"
|
||||
"selection.applyColorStyle" = "Använd färgformatmall på markeringen"
|
||||
"selection.bindColorVariable" = "Använd färgvariabel på markeringen"
|
||||
"selection.colors" = "Markeringsfärger"
|
||||
"selection.createColorStyle" = "Skapa färgstil från markeringen"
|
||||
"selection.detachColor" = "Koppla bort markeringsfärg"
|
||||
"selection.selectColor" = "Markera lager med den här färgen"
|
||||
"selection.setPaint" = "Ändra markeringens färg"
|
||||
@@ -2399,6 +2459,10 @@ name = "Svenska"
|
||||
"text.caseUpper" = "Versaler"
|
||||
"text.create" = "Text"
|
||||
"text.decoration" = "Dekoration"
|
||||
"text.direction" = "Textriktning"
|
||||
"text.directionAuto" = "Textriktning: auto"
|
||||
"text.directionLtr" = "Textriktning: vänster till höger"
|
||||
"text.directionRtl" = "Textriktning: höger till vänster"
|
||||
"text.edit.begin" = "Redigera text"
|
||||
"text.edit.caret" = "Placera markören"
|
||||
"text.edit.end" = "Sluta redigera text"
|
||||
@@ -2423,6 +2487,7 @@ name = "Svenska"
|
||||
"text.missingFonts" = "Saknade typsnitt"
|
||||
"text.numberList" = "Numrerad lista"
|
||||
"text.onPath" = "Text på bana"
|
||||
"text.openLink" = "Öppna länk"
|
||||
"text.paragraphIndent" = "Styckeindrag"
|
||||
"text.paragraphSpacing" = "Styckeavstånd"
|
||||
"text.pathOptions" = "Alternativ för textbana"
|
||||
@@ -2442,9 +2507,11 @@ name = "Svenska"
|
||||
"text.underline" = "Understrykning"
|
||||
"text.weightDown" = "Minska teckenvikten"
|
||||
"text.weightUp" = "Öka teckenvikten"
|
||||
"text.wrapStyle" = "Radbrytningsstil"
|
||||
"tool.arrow" = "Pil"
|
||||
"tool.comment" = "Kommentar"
|
||||
"tool.connector" = "Koppling"
|
||||
"tool.crosshair" = "Flytta placeringshårkorset"
|
||||
"tool.ellipse" = "Ellips"
|
||||
"tool.eraser" = "Suddgummi"
|
||||
"tool.frame" = "Frame"
|
||||
@@ -2488,6 +2555,7 @@ name = "Svenska"
|
||||
"variable.deleteCollection" = "Radera samling"
|
||||
"variable.duplicate" = "Duplicera variabel"
|
||||
"variable.export" = "Exportera variabler"
|
||||
"variable.extendCollection" = "Utöka samlingen"
|
||||
"variable.import" = "Importera variabler"
|
||||
"variable.list" = "Lista variabler"
|
||||
"variable.moveMode" = "Flytta läge"
|
||||
@@ -2561,6 +2629,7 @@ name = "Svenska"
|
||||
"view.layoutGrids" = "Layoutrutnät"
|
||||
"view.leftSidebar" = "Visa/dölj vänster sidofält"
|
||||
"view.maskOutlines" = "Konturlinjer för masker"
|
||||
"view.minimize" = "Minimera gränssnittet"
|
||||
"view.multiplayerCursors" = "Samarbetsmarkörer"
|
||||
"view.nextFrame" = "Zooma till nästa frame"
|
||||
"view.nextPage" = "Nästa sida"
|
||||
|
||||
@@ -1575,7 +1575,6 @@ status = "draft — needs native review"
|
||||
"Texts" = "உரைகள்"
|
||||
"Texture" = "அமைப்பு"
|
||||
"The bars are hidden. {shortcut} shows them again" = "பட்டைகள் மறைக்கப்பட்டுள்ளன. {shortcut} அவற்றை மீண்டும் காட்டும்"
|
||||
"The canvas previews colors in sRGB; exported PNG, JPG and WebP files are tagged Display P3." = "கேன்வாஸ் நிறங்களை sRGB இல் காட்டுகிறது; ஏற்றுமதி செய்த PNG, JPG, WebP கோப்புகள் Display P3 எனக் குறிக்கப்படும்."
|
||||
"The destination document is unavailable. Nothing was imported." = "இலக்கு ஆவணம் கிடைக்கவில்லை. எதுவும் இறக்குமதி செய்யப்படவில்லை."
|
||||
"The destination document was closed. Nothing was imported." = "இலக்கு ஆவணம் மூடப்பட்டது. எதுவும் இறக்குமதி செய்யப்படவில்லை."
|
||||
"The destination page was removed. Nothing was imported." = "இலக்குப் பக்கம் அகற்றப்பட்டது. எதுவும் இறக்குமதி செய்யப்படவில்லை."
|
||||
|
||||
@@ -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