mirror of
https://github.com/storytold/figcraft.git
synced 2026-11-03 00:39:44 -06:00
M6: Inspect tab: properties with styles and variables, redlines on hover (#41)
The Inspect tab lists the selected layer's properties for handoff: layout (size, position from the parent, rotation, opacity, radius), auto layout (direction, gap, padding, alignment), typography, fills, strokes and effects, each with the style or variable it comes from. Clicking a value copies it. With the Inspect tab open, hovering measures distances from the selection without holding Alt. New command inspect.properties returns the same groups for agents. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
//! Dev-mode inspection: properties grouped with their styles and variables, positions relative
|
||||
//! to the parent, and redlines on hover without Alt.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use crate::Session;
|
||||
use crate::devinspect::{Section, properties};
|
||||
use crate::params::id_of;
|
||||
use crate::tools::{PointerEvent, PointerKind};
|
||||
|
||||
fn id(v: Value) -> figcraft_doc::NodeId {
|
||||
id_of(&v["id"]).unwrap()
|
||||
}
|
||||
|
||||
fn row<'a>(sections: &'a [Section], title: &str, label: &str) -> &'a crate::devinspect::Prop {
|
||||
sections.iter().find(|s| s.title == title).and_then(|s| s.rows.iter().find(|r| r.label == label)).unwrap_or_else(|| panic!("{title}/{label}"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn properties_group_layout_auto_layout_paints_and_tokens() {
|
||||
let mut s = Session::new();
|
||||
let f = id(s.execute("frame.create", &json!({"x": 100, "y": 50, "width": 300, "height": 200})).unwrap());
|
||||
let r = id(s.execute("shape.rectangle", &json!({"x": 130, "y": 90, "width": 80, "height": 40, "parent": f})).unwrap());
|
||||
s.execute("node.set", &json!({"id": r, "props": {"fill": "#0D99FF80", "cornerRadius": 6, "opacity": 0.5}})).unwrap();
|
||||
s.execute("variable.createCollection", &json!({"name": "T"})).unwrap();
|
||||
s.execute("variable.create", &json!({"collection": "T", "name": "radius/sm", "type": "number", "value": 6})).unwrap();
|
||||
s.execute("variable.bind", &json!({"ids": [r], "field": "cornerRadius", "variable": "radius/sm"})).unwrap();
|
||||
s.execute("select.set", &json!({"ids": [r]})).unwrap();
|
||||
s.execute("style.create", &json!({"type": "paint", "name": "Brand/Blue"})).unwrap();
|
||||
s.execute("style.apply", &json!({"ids": [r], "style": "Brand/Blue"})).unwrap();
|
||||
let doc = s.doc().unwrap();
|
||||
let p = properties(doc, r);
|
||||
assert_eq!(row(&p, "Layout", "Width").value, "80");
|
||||
// Top/left from the parent frame's corner (the rectangle was placed in the frame's coordinates).
|
||||
assert_eq!((row(&p, "Layout", "Top").value.as_str(), row(&p, "Layout", "Left").value.as_str()), ("90", "130"));
|
||||
assert_eq!(row(&p, "Layout", "Opacity").value, "50%");
|
||||
let radius = row(&p, "Layout", "Radius");
|
||||
assert_eq!((radius.value.as_str(), radius.token.as_deref()), ("6", Some("radius/sm")));
|
||||
let fill = &p.iter().find(|s| s.title == "Fills").unwrap().rows[0];
|
||||
assert_eq!(fill.value, "#0D99FF · 50%");
|
||||
assert_eq!(fill.color.as_deref(), Some("#0D99FF80"));
|
||||
assert_eq!(fill.token.as_deref(), Some("Brand/Blue"));
|
||||
assert!(p.iter().all(|s| s.title != "Auto layout" && s.title != "Typography"));
|
||||
|
||||
// Auto layout: direction, gap, padding shorthand, alignment.
|
||||
s.execute("layout.add", &json!({"ids": [f]})).unwrap();
|
||||
s.execute("node.set", &json!({"id": f, "props": {"layoutMode": "vertical", "gap": 12, "padding": [8, 16, 8, 16]}})).unwrap();
|
||||
let p = properties(s.doc().unwrap(), f);
|
||||
assert_eq!(row(&p, "Auto layout", "Direction").value, "Vertical");
|
||||
assert_eq!(row(&p, "Auto layout", "Gap").value, "12");
|
||||
assert_eq!(row(&p, "Auto layout", "Padding").value, "8 16");
|
||||
// At the top level, positions are from the page origin.
|
||||
assert_eq!(row(&p, "Layout", "Left").value, "100");
|
||||
|
||||
// The command returns the same, for MCP.
|
||||
let out = s.execute("inspect.properties", &json!({"id": r})).unwrap();
|
||||
assert_eq!(out["sections"][0]["title"], "Layout");
|
||||
assert!(s.execute("inspect.properties", &json!({"id": 999_999})).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typography_section_for_text() {
|
||||
let mut s = Session::new();
|
||||
let t = id(s.execute("text.create", &json!({"text": "Hello", "x": 0, "y": 0})).unwrap());
|
||||
s.execute("node.set", &json!({"id": t, "props": {"fontSize": 18, "textAlignHorizontal": "center"}})).unwrap();
|
||||
let p = properties(s.doc().unwrap(), t);
|
||||
assert_eq!(row(&p, "Typography", "Size").value, "18");
|
||||
assert!(p.iter().any(|s| s.title == "Typography"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn redlines_measure_on_hover_without_alt() {
|
||||
let mut s = Session::new();
|
||||
let a = id(s.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 50, "height": 50})).unwrap());
|
||||
s.execute("shape.rectangle", &json!({"x": 100, "y": 0, "width": 50, "height": 50})).unwrap();
|
||||
s.execute("select.set", &json!({"ids": [a]})).unwrap();
|
||||
let hover = |s: &mut Session| {
|
||||
s.pointer(&PointerEvent::at(PointerKind::Move, 125.0, 25.0, 1.0)).unwrap();
|
||||
s.tool.overlay.guides.len()
|
||||
};
|
||||
assert_eq!(hover(&mut s), 0, "plain hover doesn't measure");
|
||||
s.tool.redlines = true;
|
||||
assert!(hover(&mut s) > 0, "Inspect measures on hover");
|
||||
}
|
||||
Reference in New Issue
Block a user