M4: component descriptions in the Design panel (#28)
- Components and component sets get a Description field (multi-line, saved when it loses focus; undo refreshes it). Instances show their main component's description (or the set's) under the swap menu. - The frame status/resize block no longer shows for instances, where it doesn't apply. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
5a66859ec6
commit
b8687d7c0d
|
|
@ -60,8 +60,9 @@ dimension measured side by side.
|
|||
panel, add variant, instance swap UI, per-layer overrides with reset, restore main, instance
|
||||
structure protection; Assets panel with live thumbnails, search and drag-to-canvas (drops into
|
||||
the frame or auto layout slot under the pointer); exposed nested instances (their properties
|
||||
show on the main's instances). *Left:* nested override edge cases, slots,
|
||||
local libraries (publish/use across files), component descriptions UI. 10–14 h.
|
||||
show on the main's instances); component descriptions (edited on the main, shown on
|
||||
instances). *Left:* nested override edge cases, slots, local libraries (publish/use across
|
||||
files). 10–14 h.
|
||||
5. **Prototyping depth**: *landed:* on-canvas connections with drag-to-connect, interaction editor,
|
||||
flows, smart animate, overlays, scroll/fixed/sticky, change-to and set-variable in the presenter.
|
||||
Custom bezier and custom spring easings with editor fields; device frames (phone, tablet,
|
||||
|
|
@ -144,7 +145,7 @@ Scores by behaviour, 0–100%. Hours are what remains (one agent).
|
|||
| Frames, groups, sections, masks | 5 | 75% | hiding section contents UI, section-level prototype/dev affordances | 2–4 |
|
||||
| Auto layout & constraints | 9 | 60% | grid/wrap-aware drag-reorder, drag to grid cell, nested hug/fill edge cases | 6–10 |
|
||||
| Text | 8 | 60% | right-to-left/bidi, text style picker depth, axis hover preview | 8–12 |
|
||||
| Components & variants | 9 | 60% | libraries, slots, descriptions, nested override edge cases | 10–15 |
|
||||
| Components & variants | 9 | 60% | libraries, slots, nested override edge cases | 10–15 |
|
||||
| Styles & variables | 5 | 60% | aliasing UI, scoping, binding padding/font size/opacity/text, string and boolean bindings in the panel | 6–10 |
|
||||
| Prototyping | 6 | 65% | multi-selection editing, scrolling inside device frames | 4–7 |
|
||||
| Import/export | 6 | 50% | PDF (in progress), .fig import, export panel polish, REST import of prototypes/variables | 18–32 |
|
||||
|
|
|
|||
|
|
@ -23,8 +23,14 @@ pub fn instance_section(app: &mut FigApp, ui: &mut Ui, doc: &Document, sel: &[No
|
|||
let Some(n) = doc.node(*id) else { return };
|
||||
match &n.kind {
|
||||
NodeKind::Instance { .. } => instance_ui(app, ui, doc, n),
|
||||
NodeKind::ComponentSet { .. } => properties_ui(app, ui, doc, n.id),
|
||||
NodeKind::Component { .. } => properties_ui(app, ui, doc, variant_set(doc, n.id).unwrap_or(n.id)),
|
||||
NodeKind::ComponentSet { description } => {
|
||||
properties_ui(app, ui, doc, n.id);
|
||||
description_ui(app, ui, n.id, description);
|
||||
}
|
||||
NodeKind::Component { description, .. } => {
|
||||
properties_ui(app, ui, doc, variant_set(doc, n.id).unwrap_or(n.id));
|
||||
description_ui(app, ui, n.id, description);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if !matches!(n.kind, NodeKind::Instance { .. }) && n.source.is_some() && owning_instance(doc, n.id).is_some() {
|
||||
|
|
@ -38,6 +44,50 @@ pub fn instance_section(app: &mut FigApp, ui: &mut Ui, doc: &Document, sel: &[No
|
|||
}
|
||||
}
|
||||
|
||||
/// A component's description: a multi-line field, saved when it loses focus.
|
||||
fn description_ui(app: &mut FigApp, ui: &mut Ui, id: NodeId, description: &str) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
widgets::separator(ui);
|
||||
widgets::sublabels(ui, "Description", None);
|
||||
let buf_id = Id::new(("comp_desc", id));
|
||||
// The draft lives only while the field has focus; otherwise it shows the stored text.
|
||||
let focused = ui.memory(|m| m.has_focus(buf_id.with("te")));
|
||||
let mut text: String = if focused { ui.data(|d| d.get_temp(buf_id)).unwrap_or_else(|| description.to_string()) } else { description.to_string() };
|
||||
let mut done = None;
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_space(crate::widgets::PAD);
|
||||
let w = ui.available_width() - crate::widgets::PAD;
|
||||
let r = egui::Frame::NONE.fill(t.field).corner_radius(egui::CornerRadius::same(5)).inner_margin(egui::Margin::same(6)).show(ui, |ui| {
|
||||
ui.add(
|
||||
egui::TextEdit::multiline(&mut text)
|
||||
.id(buf_id.with("te"))
|
||||
.frame(egui::Frame::NONE)
|
||||
.font(crate::theme::regular(11.0))
|
||||
.hint_text("Describe how and when to use this component")
|
||||
.desired_rows(3)
|
||||
.desired_width(w - 12.0),
|
||||
)
|
||||
});
|
||||
if r.inner.lost_focus() {
|
||||
done = Some(text.clone());
|
||||
}
|
||||
});
|
||||
match done {
|
||||
Some(new) => {
|
||||
ui.data_mut(|d| d.remove::<String>(buf_id));
|
||||
if new != description {
|
||||
app.run("node.set", json!({"id": id, "props": {"description": new.chars().take(10_000).collect::<String>()}}));
|
||||
}
|
||||
}
|
||||
None => {
|
||||
if ui.memory(|m| m.has_focus(buf_id.with("te"))) {
|
||||
ui.data_mut(|d| d.insert_temp(buf_id, text));
|
||||
}
|
||||
}
|
||||
}
|
||||
ui.add_space(8.0);
|
||||
}
|
||||
|
||||
/// Local components (and component sets, not their variants), for swap menus: (id, label).
|
||||
fn local_components(doc: &Document) -> Vec<(NodeId, String)> {
|
||||
let mut out: Vec<(NodeId, String)> = Vec::new();
|
||||
|
|
@ -411,6 +461,22 @@ fn instance_ui(app: &mut FigApp, ui: &mut Ui, doc: &Document, n: &Node) {
|
|||
app.run("component.goToMain", json!({"id": id}));
|
||||
}
|
||||
});
|
||||
// The main component's description (the set's when the variant has none).
|
||||
let desc = [Some(*component), set]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter_map(|c| match doc.node(c).map(|n| &n.kind) {
|
||||
Some(NodeKind::Component { description, .. } | NodeKind::ComponentSet { description }) => Some(description.trim().to_string()),
|
||||
_ => None,
|
||||
})
|
||||
.find(|d| !d.is_empty());
|
||||
if let Some(d) = desc {
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_space(crate::widgets::PAD);
|
||||
ui.add(egui::Label::new(egui::RichText::new(d).font(crate::theme::regular(11.0)).color(t.text_secondary)).wrap());
|
||||
});
|
||||
ui.add_space(4.0);
|
||||
}
|
||||
// A nested instance inside a main component can expose its properties on the main's instances.
|
||||
if nested_in_main(doc, id) {
|
||||
row(ui, 32.0, |ui| {
|
||||
|
|
|
|||
|
|
@ -26,7 +26,9 @@ pub fn section(app: &mut FigApp, ui: &mut Ui, doc: &Document, sel: &[NodeId]) {
|
|||
let is_section = |i: &NodeId| doc.node(*i).is_some_and(|n| matches!(n.kind, NodeKind::Section { .. }));
|
||||
let top_frame = |i: &NodeId| {
|
||||
doc.node(*i).is_some_and(|n| {
|
||||
n.kind.is_frame_like() && n.parent.and_then(|p| doc.node(p)).is_some_and(|p| p.is_page() || matches!(p.kind, NodeKind::Section { .. }))
|
||||
n.kind.is_frame_like()
|
||||
&& !matches!(n.kind, NodeKind::Instance { .. })
|
||||
&& n.parent.and_then(|p| doc.node(p)).is_some_and(|p| p.is_page() || matches!(p.kind, NodeKind::Section { .. }))
|
||||
})
|
||||
};
|
||||
let sections = !sel.is_empty() && sel.iter().all(is_section);
|
||||
|
|
|
|||
Loading…
Reference in New Issue