From 8408cc72b8db4af3c9408eaa2ced97dc34e7c213 Mon Sep 17 00:00:00 2001 From: Skyfuller1303 Date: Fri, 16 Oct 2026 02:40:50 +0200 Subject: [PATCH] M1: import component slot content from .fig files Slot properties of a component, the frames that reference them and the content that instances assign to them now come across from .fig files. A slot definition becomes a native slot property. The referencing frame of an expanded instance keeps following the frame of its main, and when the instance assigns slot content (including an explicitly empty slot) the frame carries that content as its own, independently owned children. Nested instances inside assigned content keep their own source links. An assignment on an outer instance wins over one made by an instance nested in the main, and a slot without an assignment keeps the default content of its main. Assignments that are malformed, use the empty-GUID sentinel or point at a layer that is not slot content are skipped with an import note. Preferred component keys cannot be mapped to nodes of the file and are dropped with a note, as are slot settings the editor cannot represent. Public REST snapshots, the experimental .fig writer and property resources are not changed. --- crates/format/src/fig/map.rs | 107 +++++++++++- crates/format/src/fig/mod.rs | 2 + crates/format/src/fig/slot_tests.rs | 225 ++++++++++++++++++++++++++ crates/format/src/rest/fig_sources.rs | 11 +- crates/format/src/rest/mod.rs | 8 +- 5 files changed, 346 insertions(+), 7 deletions(-) create mode 100644 crates/format/src/fig/slot_tests.rs diff --git a/crates/format/src/fig/map.rs b/crates/format/src/fig/map.rs index 0fe44662..6860327f 100644 --- a/crates/format/src/fig/map.rs +++ b/crates/format/src/fig/map.rs @@ -295,6 +295,10 @@ struct Inst<'a> { path: Vec, overrides: Rc>, props: Rc>>, + /// Slot property GUID → private slot-content node GUID. + slot_contents: Rc>, + /// Layers imported from a slot's independent content subtree. + slot_content: bool, /// Mains being expanded (an instance inside its own main stops there). symbols: Vec, } @@ -768,7 +772,13 @@ impl<'a> Mapper<'a> { // Private provenance for the .fig importer; ordinary REST import ignores it. o.insert( crate::rest::FIG_SOURCE_FIELD.into(), - json!({"origin": id, "expanded": inst.is_some(), "wireFrame": own == "FRAME", "wireInstance": own == "INSTANCE"}), + json!({ + "origin": id, + "expanded": inst.is_some(), + "wireFrame": own == "FRAME", + "wireInstance": own == "INSTANCE", + "slotContent": inst.is_some_and(|i| i.slot_content), + }), ); o.insert("type".into(), json!(ty)); copy_str(&mut o, &view, "name", "name"); @@ -797,6 +807,16 @@ impl<'a> Mapper<'a> { self.component(&mut o, &view, &rest_id); } self.prop_refs(&mut o, &view, inst); + let slot_content = if own == "FRAME" { + prop_ref(&view, "SLOT_CONTENT_ID").and_then(|def| inst.and_then(|i| i.slot_contents.get(&def).cloned())) + } else { + None + }; + if slot_content.is_some() + && let Some(source) = o.get_mut(crate::rest::FIG_SOURCE_FIELD).and_then(Value::as_object_mut) + { + source.insert("slotOverride".into(), json!(true)); + } let child_space = if matches!(ty, "GROUP" | "BOOLEAN_OPERATION") { space * view.transform().unwrap_or_default() } else { Affine::IDENTITY }; let mut kids = Vec::new(); if ty == "INSTANCE" { @@ -808,6 +828,14 @@ impl<'a> Mapper<'a> { kids.extend(self.layer_filtered(&c, Some(&sub), child_space, Some(&view), fit, depth + 1, None)); } } + } else if let Some(content) = slot_content { + let mut content_inst = inst.cloned(); + if let Some(content_inst) = &mut content_inst { + content_inst.slot_content = true; + } + for c in self.children(&content) { + kids.extend(self.layer_filtered(&c, content_inst.as_ref(), child_space, Some(&view), None, depth + 1, included)); + } } else { // Inside an instance a layer's children are its main's: they follow it from the main's // size to its own. @@ -1282,6 +1310,11 @@ impl<'a> Mapper<'a> { None => continue, }, "VARIANT" => json!({"type": "VARIANT"}), + "SLOT" => { + self.warn_non_default_slot_config(d); + self.warn_dropped_slot_preferred_values(d); + json!({"type": "SLOT"}) + } _ => { self.warn(format!("{} component properties are not supported and were skipped", ty.to_ascii_lowercase())); continue; @@ -1298,6 +1331,34 @@ impl<'a> Mapper<'a> { } } + /// Preferred slot values are source component keys, but this mapper has no proven way to map + /// those keys to file GUIDs. They are dropped instead of guessing native NodeIds. + fn warn_dropped_slot_preferred_values(&mut self, def: &Kv<'a>) { + let entries = def.get("preferredValues").and_then(|v| v.get("instanceSwapValues")).map(Kv::list).unwrap_or_default(); + if !entries.is_empty() { + self.warn("preferred slot component keys cannot be resolved to file nodes and were skipped"); + } + } + + fn warn_non_default_slot_config(&mut self, def: &Kv<'a>) { + let Some(config) = def.get("slotPropConfig") else { return }; + let non_default = [ + ("stretchChildOnInsert", Some(false)), + ("displayByDefault", Some(false)), + ("minChildren", None), + ("maxChildren", None), + ("allowPreferredValuesOnly", Some(false)), + ] + .into_iter() + .any(|(field, bool_default)| { + let Some(value) = config.get(field) else { return false }; + if let Some(default) = bool_default { value.bool() != Some(default) } else { value.f64() != Some(0.0) } + }); + if non_default { + self.warn("non-default slot settings are not represented and were skipped"); + } + } + /// A layer's component property references, and inside an instance the values they take. fn prop_refs(&mut self, o: &mut Obj, v: &View<'a>, inst: Option<&Inst<'a>>) { let mut refs = Obj::new(); @@ -1306,6 +1367,7 @@ impl<'a> Mapper<'a> { Some("VISIBLE") => "visible", Some("TEXT_DATA") => "characters", Some("OVERRIDDEN_SYMBOL_ID") => "mainComponent", + Some("SLOT_CONTENT_ID") => "slot", _ => continue, }; let Some(def) = @@ -1313,7 +1375,11 @@ impl<'a> Mapper<'a> { else { continue; }; - if let Some((name, _)) = self.prop_defs.get(&def) { + // Expanded frames must follow their main frame's source link. A direct slot reference + // makes the engine resolve ownership against the instance, where the property is not defined. + if !(field == "slot" && inst.is_some()) + && let Some((name, _)) = self.prop_defs.get(&def) + { refs.insert(field.into(), json!(name)); } let Some(val) = inst.and_then(|i| i.props.get(&def)) else { continue }; @@ -1445,15 +1511,50 @@ impl<'a> Mapper<'a> { rows.sort_by_key(|row| (std::cmp::Reverse(row.source_depth), row.ordinal)); } let mut all_props: BTreeMap> = outer.map(|i| (*i.props).clone()).unwrap_or_default(); + let mut slot_contents: BTreeMap = outer.map(|i| (*i.slot_contents).clone()).unwrap_or_default(); for a in assignments { if let (Some(d), Some(val)) = (a.get("defID").and_then(guid), a.get("value")) { all_props.entry(d).or_insert(val); } + let Some(def) = a.get("defID").and_then(guid) else { continue }; + if self.prop_defs.get(&def).is_none_or(|(_, ty)| *ty != "SLOT") { + continue; + } + let var_value = a.get("varValue"); + if var_value.and_then(|v| v.get("dataType")).and_then(Kv::str) != Some("SLOT_CONTENT_ID") { + self.warn("malformed slot content assignment was skipped"); + continue; + } + let target = var_value.and_then(|v| v.get("value")).and_then(|v| v.get("slotContentIdValue")).and_then(|v| v.get("guid")).and_then(guid); + let Some(target) = target else { + self.warn("malformed slot content assignment was skipped"); + continue; + }; + if target == "4294967295:4294967295" { + self.warn("slot content assignment uses the empty GUID sentinel and was skipped"); + continue; + } + if !self.nodes.get(&target).is_some_and(|n| n.get("isSlotContent").and_then(Kv::bool) == Some(true)) { + self.warn("slot content assignment target is missing or is not slot content"); + continue; + } + slot_contents.entry(def).or_insert(target); } let mut symbols = outer.map(|i| i.symbols.clone()).unwrap_or_default(); symbols.push(sym.clone()); let root = outer.map_or_else(|| rest_id.to_string(), |i| i.root.clone()); - Some((Inst { root, path: own_path, overrides: Rc::new(overrides), props: Rc::new(all_props), symbols }, sym)) + Some(( + Inst { + root, + path: own_path, + overrides: Rc::new(overrides), + props: Rc::new(all_props), + slot_contents: Rc::new(slot_contents), + slot_content: false, + symbols, + }, + sym, + )) } fn prototype(&mut self, o: &mut Obj, v: &View<'a>) { diff --git a/crates/format/src/fig/mod.rs b/crates/format/src/fig/mod.rs index 81457f8d..ceb13346 100644 --- a/crates/format/src/fig/mod.rs +++ b/crates/format/src/fig/mod.rs @@ -21,6 +21,8 @@ pub mod kiwi; mod map; mod network; #[cfg(test)] +mod slot_tests; +#[cfg(test)] mod source_identity_tests; #[cfg(test)] mod tests; diff --git a/crates/format/src/fig/slot_tests.rs b/crates/format/src/fig/slot_tests.rs new file mode 100644 index 00000000..670e2f6f --- /dev/null +++ b/crates/format/src/fig/slot_tests.rs @@ -0,0 +1,225 @@ +//! Component slot content from `.fig` files: SLOT definitions, the frames that reference them and +//! the content subtrees that instances assign, on synthetic messages. + +use serde_json::Value; + +use super::kiwi::Kv; +use super::map::to_rest; + +fn g(local: u32) -> Kv<'static> { + Kv::Object(vec![("sessionID", Kv::Uint(9)), ("localID", Kv::Uint(u64::from(local)))]) +} + +fn o(fields: Vec<(&'static str, Kv<'static>)>) -> Kv<'static> { + Kv::Object(fields) +} + +fn node(local: u32, kind: &'static str, parent: Option, mut fields: Vec<(&'static str, Kv<'static>)>) -> Kv<'static> { + let mut all = vec![("guid", g(local)), ("type", Kv::Enum(kind)), ("name", Kv::Str("Synthetic".into()))]; + if let Some(parent) = parent { + all.push(("parentIndex", o(vec![("guid", g(parent)), ("position", Kv::Str("!".into()))]))); + } + all.append(&mut fields); + o(all) +} + +fn mapped(msg: &Kv<'_>) -> Value { + to_rest(msg, "Synthetic resources").0 +} + +fn named_id<'b>(v: &'b Value, id: &str) -> Option<&'b Value> { + if v.get("id").and_then(Value::as_str) == Some(id) { + return Some(v); + } + v.get("children").and_then(Value::as_array).into_iter().flatten().find_map(|v| named_id(v, id)) +} + +fn changes<'a>(msg: &'a mut Kv<'static>) -> &'a mut Vec> { + let Kv::Object(fields) = msg else { panic!() }; + let (_, Kv::Array(rows)) = fields.iter_mut().find(|(key, _)| *key == "nodeChanges").unwrap() else { panic!() }; + rows +} + +fn slot_message(content: Option, preferred: bool) -> Kv<'static> { + let slot_ref = o(vec![ + ("variableField", Kv::Enum("SLOT_CONTENT_ID")), + ("variableData", o(vec![("value", o(vec![("propRefValue", o(vec![("defId", g(70))]))]))])), + ]); + let mut instance_fields = vec![("symbolData", o(vec![("symbolID", g(10))]))]; + if let Some(content) = content { + instance_fields.push(( + "componentPropAssignments", + Kv::Array(vec![o(vec![ + ("defID", g(70)), + ("value", o(vec![])), + ( + "varValue", + o(vec![ + ("value", o(vec![("slotContentIdValue", o(vec![("guid", g(content))]))])), + ("dataType", Kv::Enum("SLOT_CONTENT_ID")), + ("resolvedDataType", Kv::Enum("SLOT_CONTENT_ID")), + ]), + ), + ])]), + )); + } + let mut definition = vec![("id", g(70)), ("name", Kv::Str("Content".into())), ("type", Kv::Enum("SLOT"))]; + if preferred { + definition.push(( + "preferredValues", + o(vec![("instanceSwapValues", Kv::Array(vec![o(vec![("type", Kv::Enum("COMPONENT")), ("key", Kv::Str("unresolved-key".into()))])]))]), + )); + } + o(vec![( + "nodeChanges", + Kv::Array(vec![ + node(1, "DOCUMENT", None, vec![]), + node(2, "CANVAS", Some(1), vec![]), + node(3, "CANVAS", Some(1), vec![("internalOnly", Kv::Bool(true))]), + node(10, "SYMBOL", Some(2), vec![("componentPropDefs", Kv::Array(vec![o(definition)]))]), + node(12, "FRAME", Some(10), vec![("parameterConsumptionMap", o(vec![("entries", Kv::Array(vec![slot_ref]))]))]), + node(13, "RECTANGLE", Some(12), vec![]), + node(20, "INSTANCE", Some(2), instance_fields), + node(30, "FRAME", Some(3), vec![("isSlotContent", Kv::Bool(true))]), + node(31, "RECTANGLE", Some(30), vec![]), + node(32, "ELLIPSE", Some(30), vec![]), + node(33, "FRAME", Some(3), vec![("isSlotContent", Kv::Bool(true))]), + ]), + )]) +} +#[test] +fn slot_definition_reference_and_arbitrary_override_children_are_imported() { + let rest = mapped(&slot_message(Some(30), false)); + let main = named_id(&rest["document"], "9:10").unwrap(); + assert_eq!(main["componentPropertyDefinitions"]["Content#9:70"]["type"], "SLOT"); + let main_slot = named_id(&rest["document"], "9:12").unwrap(); + assert_eq!(main_slot["componentPropertyReferences"]["slot"], "Content#9:70"); + let slot = named_id(&rest["document"], "I9:20;9:12").unwrap(); + assert!(slot.get("componentPropertyReferences").is_none()); + assert_eq!(slot[crate::rest::FIG_SOURCE_FIELD]["slotOverride"], true); + let kids = slot["children"].as_array().unwrap(); + assert_eq!(kids.len(), 2); + assert_eq!(kids[0]["id"], "I9:20;9:31"); + assert_eq!(kids[1]["id"], "I9:20;9:32"); + + let (doc, warnings) = crate::rest::import_fig(&rest, &crate::rest::no_images).unwrap(); + assert!(warnings.is_empty(), "{warnings:?}"); + let main = doc + .node(*doc.pages.first().unwrap()) + .unwrap() + .children + .iter() + .find_map(|id| doc.node(*id).filter(|n| n.name == "Synthetic" && matches!(n.kind, figcraft_doc::NodeKind::Component { .. }))) + .unwrap(); + assert!( + matches!(&main.kind, figcraft_doc::NodeKind::Component { props, .. } if matches!(props.get("Content"), Some(figcraft_doc::PropDef::Slot { preferred }) if preferred.is_empty())) + ); + let instance = doc.ids().filter_map(|id| doc.node(id)).find(|n| matches!(n.kind, figcraft_doc::NodeKind::Instance { .. })).unwrap(); + let slot = doc.node(instance.children[0]).unwrap(); + assert!(!slot.prop_refs.contains_key("slot")); + assert_eq!(slot.source, Some(main.children[0])); + assert!(slot.overrides.contains("slot")); + assert_eq!(slot.children.len(), 2); + assert!(slot.children.iter().all(|id| doc.node(*id).is_some_and(|child| child.source.is_none()))); + + let (public, _) = crate::rest::import(&rest, &crate::rest::no_images).unwrap(); + let public_instance = public.ids().filter_map(|id| public.node(id)).find(|n| matches!(n.kind, figcraft_doc::NodeKind::Instance { .. })).unwrap(); + let public_slot = public.node(public_instance.children[0]).unwrap(); + assert!(!public_slot.overrides.contains("slot")); +} +#[test] +fn nested_instance_in_own_slot_content_keeps_its_own_source_tree() { + let mut msg = slot_message(Some(30), false); + let rows = changes(&mut msg); + let slot_frame = rows.iter_mut().find(|row| row.get("guid").and_then(|guid| guid.get("localID")).and_then(Kv::f64) == Some(31.0)).unwrap(); + let Kv::Object(fields) = slot_frame else { panic!() }; + fields.iter_mut().find(|(key, _)| *key == "type").unwrap().1 = Kv::Enum("FRAME"); + rows.extend([ + node(34, "INSTANCE", Some(31), vec![("symbolData", o(vec![("symbolID", g(40))]))]), + node(40, "SYMBOL", Some(2), vec![]), + node(41, "FRAME", Some(40), vec![]), + node(42, "RECTANGLE", Some(41), vec![]), + ]); + + let rest = mapped(&msg); + let (doc, warnings) = crate::rest::import_fig(&rest, &crate::rest::no_images).unwrap(); + assert!(warnings.is_empty(), "{warnings:?}"); + let outer = doc + .ids() + .filter_map(|id| doc.node(id)) + .find(|n| { + matches!(n.kind, figcraft_doc::NodeKind::Instance { .. }) + && !doc.ancestors(n.id).iter().any(|a| doc.node(*a).is_some_and(|p| matches!(p.kind, figcraft_doc::NodeKind::Instance { .. }))) + }) + .unwrap(); + let slot = doc.node(outer.children[0]).unwrap(); + let own_frame = slot.children.iter().filter_map(|id| doc.node(*id)).find(|n| matches!(n.kind, figcraft_doc::NodeKind::Frame)).unwrap(); + assert!(own_frame.source.is_none()); + + let nested = slot + .children + .iter() + .filter_map(|id| doc.node(*id)) + .flat_map(|n| n.children.iter()) + .filter_map(|id| doc.node(*id)) + .find(|n| matches!(n.kind, figcraft_doc::NodeKind::Instance { .. })) + .unwrap(); + let figcraft_doc::NodeKind::Instance { component, .. } = nested.kind else { panic!("not an instance") }; + assert_eq!(nested.source, Some(component)); + let main_frame = doc.node(doc.node(component).unwrap().children[0]).unwrap(); + let expanded_frame = doc.node(nested.children[0]).unwrap(); + assert_eq!(expanded_frame.source, Some(main_frame.id)); + assert_eq!(expanded_frame.children.len(), 1); + assert_eq!(doc.node(expanded_frame.children[0]).unwrap().source, Some(main_frame.children[0])); +} +#[test] +fn outer_slot_assignment_wins_over_a_nested_instances_own() { + let mut msg = slot_message(Some(30), false); + let assignment = |content: u32| { + Kv::Array(vec![o(vec![ + ("defID", g(70)), + ("value", o(vec![])), + ( + "varValue", + o(vec![ + ("value", o(vec![("slotContentIdValue", o(vec![("guid", g(content))]))])), + ("dataType", Kv::Enum("SLOT_CONTENT_ID")), + ("resolvedDataType", Kv::Enum("SLOT_CONTENT_ID")), + ]), + ), + ])]) + }; + // The nested instance inside the main assigns the empty slot content; the outer instance's content wins. + let slot_ref = o(vec![ + ("variableField", Kv::Enum("SLOT_CONTENT_ID")), + ("variableData", o(vec![("value", o(vec![("propRefValue", o(vec![("defId", g(70))]))]))])), + ]); + changes(&mut msg).extend([ + node(14, "INSTANCE", Some(10), vec![("symbolData", o(vec![("symbolID", g(40))])), ("componentPropAssignments", assignment(33))]), + node(40, "SYMBOL", Some(2), vec![]), + node(41, "FRAME", Some(40), vec![("parameterConsumptionMap", o(vec![("entries", Kv::Array(vec![slot_ref]))]))]), + ]); + let rest = mapped(&msg); + let slot = named_id(&rest["document"], "I9:20;9:14;9:41").unwrap(); + assert_eq!(slot["children"].as_array().map_or(0, Vec::len), 2, "{slot}"); +} +#[test] +fn unassigned_slot_keeps_default_and_bad_content_target_warns() { + let rest = mapped(&slot_message(None, false)); + let slot = named_id(&rest["document"], "I9:20;9:12").unwrap(); + assert_eq!(slot["children"].as_array().unwrap().len(), 1); + assert!(slot.get("overrides").is_none()); + + let (rest, warnings) = to_rest(&slot_message(Some(999), false), "Synthetic"); + assert!(warnings.iter().any(|w| w.contains("slot content assignment target is missing or is not slot content")), "{warnings:?}"); + let slot = named_id(&rest["document"], "I9:20;9:12").unwrap(); + assert_eq!(slot["children"].as_array().unwrap().len(), 1); + + let (rest, warnings) = to_rest(&slot_message(Some(33), true), "Synthetic"); + let main = named_id(&rest["document"], "9:10").unwrap(); + assert_eq!(main["componentPropertyDefinitions"]["Content#9:70"]["type"], "SLOT"); + assert!(warnings.iter().any(|w| w.contains("preferred slot component keys cannot be resolved")), "{warnings:?}"); + let empty = named_id(&rest["document"], "I9:20;9:12").unwrap(); + assert_eq!(empty["children"].as_array().map_or(0, Vec::len), 0); + assert_eq!(empty[crate::rest::FIG_SOURCE_FIELD]["slotOverride"], true); +} diff --git a/crates/format/src/rest/fig_sources.rs b/crates/format/src/rest/fig_sources.rs index 61097e52..2793f932 100644 --- a/crates/format/src/rest/fig_sources.rs +++ b/crates/format/src/rest/fig_sources.rs @@ -17,6 +17,7 @@ struct Origin<'a> { rest: &'a str, guid: &'a str, expanded: bool, + slot_content: bool, wire_frame: bool, wire_instance: bool, component: Option<&'a str>, @@ -75,7 +76,8 @@ impl<'a> Sources<'a> { && meta.get("wireInstance").and_then(Value::as_bool) == Some(true) && node.get("type").and_then(Value::as_str) == Some("INSTANCE"); let component = wire_instance.then(|| node.get("componentId").and_then(Value::as_str).filter(|id| guid(id))).flatten(); - self.nodes.insert(id, Origin { rest, guid: origin, expanded, wire_frame, wire_instance, component }); + let slot_content = meta.get("slotContent").and_then(Value::as_bool).unwrap_or(false); + self.nodes.insert(id, Origin { rest, guid: origin, expanded, slot_content, wire_frame, wire_instance, component }); } fn origin(&self, id: NodeId) -> Option> { @@ -211,6 +213,9 @@ impl<'a> Sources<'a> { let found = (|| { let meta = self.origin(id)?; let _ = doc.node(id)?; + if meta.slot_content { + return if matches!(doc.node(id)?.kind, NodeKind::Instance { .. }) { self.root_source(id, doc) } else { None }; + } if !meta.expanded { return self.root_source(id, doc); } @@ -269,8 +274,8 @@ impl<'a> Sources<'a> { } let mut missing = self.rejected; for (&id, meta) in &self.nodes { - let needs_source = - (meta.expanded && !self.owned_layer(id, doc, ids)) || doc.node(id).is_some_and(|n| matches!(n.kind, NodeKind::Instance { .. })); + let needs_source = (!meta.slot_content && meta.expanded && !self.owned_layer(id, doc, ids)) + || doc.node(id).is_some_and(|n| matches!(n.kind, NodeKind::Instance { .. })); if !needs_source { continue; } diff --git a/crates/format/src/rest/mod.rs b/crates/format/src/rest/mod.rs index 282c6a75..6088519b 100644 --- a/crates/format/src/rest/mod.rs +++ b/crates/format/src/rest/mod.rs @@ -451,6 +451,10 @@ impl<'a> Ctx<'a> { let id = self.doc.alloc_id(); let label = kind.label(); let mut n = Node::new(id, kind, text(j, "name").map_or(label, clip_str), w, h); + if self.fig_sources.is_some() && j.get(FIG_SOURCE_FIELD).and_then(|source| source.get("slotOverride")).and_then(Value::as_bool) == Some(true) + { + n.overrides.insert("slot".into()); + } n.transform = local(space.world, world); self.common(&mut n, j, parent_mode); let (container, follows, mode) = (n.kind.is_container(), n.kind.bounds_follow_children(), n.layout.as_ref().map(|l| l.mode)); @@ -605,7 +609,8 @@ impl<'a> Ctx<'a> { n.child_layout = props::child_layout(j, n.layout.as_ref().map(|l| l.mode), parent_mode); n.exports = props::export_settings(j); for (field, prop) in j.get("componentPropertyReferences").and_then(Value::as_object).into_iter().flatten() { - if let (Some(p), true) = (prop.as_str(), matches!(field.as_str(), "visible" | "characters" | "mainComponent")) { + let supported = matches!(field.as_str(), "visible" | "characters" | "mainComponent") || (field == "slot" && self.fig_sources.is_some()); + if let (Some(p), true) = (prop.as_str(), supported) { n.prop_refs.insert(field.clone(), p.to_string()); } } @@ -709,6 +714,7 @@ impl<'a> Ctx<'a> { continue; } Some("VARIANT") => continue, + Some("SLOT") if self.fig_sources.is_some() => PropDef::Slot { preferred: Vec::new() }, _ => { self.warn("unknown component property types were skipped"); continue;