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
|
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
|
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
|
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,
|
show on the main's instances); component descriptions (edited on the main, shown on
|
||||||
local libraries (publish/use across files), component descriptions UI. 10–14 h.
|
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,
|
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.
|
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,
|
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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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 };
|
let Some(n) = doc.node(*id) else { return };
|
||||||
match &n.kind {
|
match &n.kind {
|
||||||
NodeKind::Instance { .. } => instance_ui(app, ui, doc, n),
|
NodeKind::Instance { .. } => instance_ui(app, ui, doc, n),
|
||||||
NodeKind::ComponentSet { .. } => properties_ui(app, ui, doc, n.id),
|
NodeKind::ComponentSet { description } => {
|
||||||
NodeKind::Component { .. } => properties_ui(app, ui, doc, variant_set(doc, n.id).unwrap_or(n.id)),
|
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() {
|
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).
|
/// Local components (and component sets, not their variants), for swap menus: (id, label).
|
||||||
fn local_components(doc: &Document) -> Vec<(NodeId, String)> {
|
fn local_components(doc: &Document) -> Vec<(NodeId, String)> {
|
||||||
let mut out: Vec<(NodeId, String)> = Vec::new();
|
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}));
|
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.
|
// A nested instance inside a main component can expose its properties on the main's instances.
|
||||||
if nested_in_main(doc, id) {
|
if nested_in_main(doc, id) {
|
||||||
row(ui, 32.0, |ui| {
|
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 is_section = |i: &NodeId| doc.node(*i).is_some_and(|n| matches!(n.kind, NodeKind::Section { .. }));
|
||||||
let top_frame = |i: &NodeId| {
|
let top_frame = |i: &NodeId| {
|
||||||
doc.node(*i).is_some_and(|n| {
|
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);
|
let sections = !sel.is_empty() && sel.iter().all(is_section);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue