mirror of
https://github.com/storytold/figcraft.git
synced 2026-11-12 05:12:44 +00:00
M8: Shift+T picks the Annotation tool, which opens a layer's note in the Inspect tab (#1840)
Adds ToolId::Annotation and tool.annotation {id?}. With a layer selected (or an id) it asks the UI for that layer's note at once; in the tool a press on a layer selects it and asks, a press on empty canvas does nothing, and the engine never writes the note. annotation.new shows the Inspect tab (not a toggle) and focuses the layer's new-note field once. The tool passes Developer Mode's gates and stays selected until V or Escape (inferred; Figma's placement, cursor and stay-in-tool behaviour are unmeasured). Docs, shortcut counts (137 documented, 136 bound) and tests updated.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove; Fable 5.1 briefed; Sonnet 5.5 wrote at medium
This commit is contained in:
1 parent
3839fe4a0c
commit
2839497d62
17 files changed
+135
-12
No files matched your search
@@ -58,6 +58,7 @@ pub enum ToolId {
|
||||
Text,
|
||||
TextOnPath,
|
||||
Comment,
|
||||
Annotation,
|
||||
Eyedropper,
|
||||
ShapeBuilder,
|
||||
// Board tools.
|
||||
@@ -71,7 +72,7 @@ pub enum ToolId {
|
||||
}
|
||||
|
||||
impl ToolId {
|
||||
pub const ALL: [ToolId; 26] = [
|
||||
pub const ALL: [ToolId; 27] = [
|
||||
ToolId::Move,
|
||||
ToolId::Scale,
|
||||
ToolId::Hand,
|
||||
@@ -89,6 +90,7 @@ impl ToolId {
|
||||
ToolId::Text,
|
||||
ToolId::TextOnPath,
|
||||
ToolId::Comment,
|
||||
ToolId::Annotation,
|
||||
ToolId::Eyedropper,
|
||||
ToolId::ShapeBuilder,
|
||||
ToolId::Sticky,
|
||||
@@ -118,6 +120,7 @@ impl ToolId {
|
||||
ToolId::Text => "text",
|
||||
ToolId::TextOnPath => "textOnPath",
|
||||
ToolId::Comment => "comment",
|
||||
ToolId::Annotation => "annotation",
|
||||
ToolId::Eyedropper => "eyedropper",
|
||||
ToolId::ShapeBuilder => "shapeBuilder",
|
||||
ToolId::Sticky => "sticky",
|
||||
@@ -151,6 +154,7 @@ impl ToolId {
|
||||
ToolId::Text => "Text",
|
||||
ToolId::TextOnPath => "Text on a path",
|
||||
ToolId::Comment => "Comment",
|
||||
ToolId::Annotation => "Annotation",
|
||||
ToolId::Eyedropper => "Pick color",
|
||||
ToolId::ShapeBuilder => "Shape builder",
|
||||
ToolId::Sticky => "Sticky note",
|
||||
|
||||
@@ -103,7 +103,7 @@ impl Session {
|
||||
/// The tool pointer events go to: in Dev Mode every tool but the hand and comments selects.
|
||||
fn acting_tool(&self) -> ToolId {
|
||||
match self.tool.tool {
|
||||
t if self.tool.dev_mode && !matches!(t, ToolId::Hand | ToolId::Comment) => ToolId::Move,
|
||||
t if self.tool.dev_mode && !matches!(t, ToolId::Hand | ToolId::Comment | ToolId::Annotation) => ToolId::Move,
|
||||
t => t,
|
||||
}
|
||||
}
|
||||
@@ -169,6 +169,7 @@ impl Session {
|
||||
self.request_ui("comment.new", json!({"x": ev.x, "y": ev.y}));
|
||||
Ok(())
|
||||
}
|
||||
ToolId::Annotation => self.annotation_down(ev),
|
||||
ToolId::Eyedropper => self.eyedrop(ev),
|
||||
t if t.on_boards() => self.board_down(ev),
|
||||
t if t.draws_shape() => self.draw_down(ev),
|
||||
@@ -199,6 +200,18 @@ impl Session {
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// The Annotation tool's press: the layer under the pointer is selected and the UI is asked
|
||||
/// for its note (the engine writes nothing; the note is typed in the Inspect tab). A press on
|
||||
/// empty canvas does nothing.
|
||||
fn annotation_down(&mut self, ev: &PointerEvent) -> Result<()> {
|
||||
let ds = self.ds()?;
|
||||
let (doc, page) = (ds.doc.clone(), ds.page);
|
||||
let Some(id) = self.hit(&doc, page, ev.p(), ev.tol() * 2.0) else { return Ok(()) };
|
||||
self.set_selection(vec![id]);
|
||||
self.request_ui("annotation.new", json!({"id": id}));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn hover(&mut self, ev: &PointerEvent) -> Result<()> {
|
||||
let tool = self.acting_tool();
|
||||
let ds = self.ds()?;
|
||||
|
||||
@@ -291,6 +291,7 @@ pub static MENU: &[MenuItem] = &[
|
||||
m!("Tools", "Pencil", "tool.pencil"),
|
||||
m!("Tools", "Text", "tool.text"),
|
||||
m!("Tools", "Comment", "tool.comment"),
|
||||
m!("Tools", "Annotation", "tool.annotation"),
|
||||
m!("Tools", "Shape builder", "tool.shapeBuilder"),
|
||||
m!("Tools", "Actions", "app.actions"),
|
||||
];
|
||||
|
||||
@@ -528,7 +528,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
spec!(
|
||||
"tool.set",
|
||||
"Choose tool",
|
||||
"{tool: move|scale|hand|frame|section|slice|rectangle|line|arrow|ellipse|polygon|star|pen|pencil|text|comment|shapeBuilder | on boards: sticky|shape|connector|marker|highlighter|eraser|stamp}",
|
||||
"{tool: move|scale|hand|frame|section|slice|rectangle|line|arrow|ellipse|polygon|star|pen|pencil|text|comment|annotation|shapeBuilder | on boards: sticky|shape|connector|marker|highlighter|eraser|stamp}",
|
||||
false,
|
||||
|s, p| {
|
||||
let t = req_string("tool.set", p, "tool")?;
|
||||
@@ -638,6 +638,19 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
tool(s, ToolId::TextOnPath)
|
||||
}),
|
||||
spec!("tool.comment", "Comment", "{}", false, |s, _| tool(s, ToolId::Comment)),
|
||||
// Shift+T: with a layer selected (or an `id`) the UI is asked for its note at once; in the
|
||||
// tool, a press on a layer selects it and asks (the engine never writes the note).
|
||||
spec!("tool.annotation", "Annotation", "{id?}", false, |s, p| {
|
||||
let r = tool(s, ToolId::Annotation)?;
|
||||
let first = crate::params::targets(s, p).into_iter().find(|i| s.doc().is_ok_and(|d| d.node(*i).is_some_and(|n| !n.is_page())));
|
||||
if let Some(id) = first {
|
||||
if p.get("id").is_some() {
|
||||
crate::cmd::edit::select_and_page(s, vec![id])?;
|
||||
}
|
||||
s.request_ui("annotation.new", json!({"id": id}));
|
||||
}
|
||||
Ok(r)
|
||||
}),
|
||||
spec!("tool.shapeBuilder", "Shape builder", "{}", false, |s, _| tool(s, ToolId::ShapeBuilder)),
|
||||
// `apply: false` is for the UI colour picker: the eyedropper only samples (into
|
||||
// `ToolState::picked`) and the picker writes the colour to the paint it edits.
|
||||
|
||||
@@ -107,6 +107,7 @@ fn each_tool_has_its_cursor_by_modifier_and_what_is_under_the_pointer() {
|
||||
(ToolId::TextOnPath, NONE, ON_SQUARE_EDGE, Text, "the I-beam over a shape's path: a click puts text on it"),
|
||||
(ToolId::Pen, NONE, EMPTY, Pen(PenCursor::New), "the pen: a new point"),
|
||||
(ToolId::Comment, NONE, EMPTY, Comment, "the comment pin"),
|
||||
(ToolId::Annotation, NONE, ON_SQUARE, Crosshair, "the annotation tool marks the layer it is pressed on"),
|
||||
(ToolId::Eyedropper, NONE, ON_SQUARE, Eyedropper, "the eyedropper"),
|
||||
];
|
||||
for (tool, m, at, want, why) in rows {
|
||||
|
||||
@@ -230,7 +230,7 @@ pub fn dev_mode_allows(id: &str) -> bool {
|
||||
// Edits recorded as one grouped undo step, so their spec says no history of its own.
|
||||
const GROUPED: [&str; 1] = ["data.fill"];
|
||||
match id.strip_prefix("tool.") {
|
||||
Some(t) => matches!(t, "move" | "hand" | "comment"),
|
||||
Some(t) => matches!(t, "move" | "hand" | "comment" | "annotation"),
|
||||
None if id == "file.revert" || GROUPED.contains(&id) => false,
|
||||
None => OWN.iter().any(|p| id.starts_with(p)) || crate::cmd::find_command(id).is_none_or(|c| !c.history),
|
||||
}
|
||||
|
||||
@@ -99,6 +99,56 @@ fn the_dev_mode_canvas_selects_and_pans_but_never_edits() {
|
||||
assert!(s.doc().unwrap().node(r).is_some_and(|n| n.transform.as_coeffs()[4] > 100.0), "moved");
|
||||
}
|
||||
|
||||
/// The `annotation.new` requests queued so far, as the layer ids they name.
|
||||
fn note_requests(s: &Session) -> Vec<NodeId> {
|
||||
s.ui_requests.iter().filter(|r| r.kind == "annotation.new").filter_map(|r| crate::params::id_of(&r.data["id"])).collect()
|
||||
}
|
||||
|
||||
fn click(s: &mut Session, x: f64, y: f64) {
|
||||
s.pointer(&ev(PointerKind::Down, x, y)).unwrap();
|
||||
s.pointer(&ev(PointerKind::Up, x, y)).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_annotation_tool_picks_a_layer_and_asks_for_its_note() {
|
||||
for dev_mode in [true, false] {
|
||||
let mut s = Session::new();
|
||||
let f = frame(&mut s, 0.0, "Screen");
|
||||
let r = rect(&mut s, f, 20.0);
|
||||
s.tool.dev_mode = dev_mode;
|
||||
ex(&mut s, "select.set", json!({"ids": []}));
|
||||
ex(&mut s, "tool.annotation", json!({}));
|
||||
assert_eq!(s.tool.tool, ToolId::Annotation);
|
||||
assert!(note_requests(&s).is_empty(), "nothing selected: no note is asked for yet");
|
||||
let (doc_before, undo_before) = (s.doc().unwrap().clone(), s.undo_labels(1000).len());
|
||||
// A press on empty canvas does nothing.
|
||||
click(&mut s, 1000.0, 1000.0);
|
||||
assert!(note_requests(&s).is_empty(), "empty canvas asks for nothing (dev_mode {dev_mode})");
|
||||
// A press on the rectangle selects it and asks for its note.
|
||||
click(&mut s, 40.0, 40.0);
|
||||
assert_eq!(s.selection(), vec![r], "dev_mode {dev_mode}");
|
||||
assert_eq!(note_requests(&s), vec![r], "dev_mode {dev_mode}");
|
||||
assert_eq!(s.tool.tool, ToolId::Annotation, "the tool stays until V or Escape");
|
||||
// The tool writes nothing.
|
||||
assert_eq!(*s.doc().unwrap(), doc_before);
|
||||
assert_eq!(s.undo_labels(1000).len(), undo_before);
|
||||
}
|
||||
// The keyboard flow: with a layer selected, choosing the tool asks at once.
|
||||
let mut s = Session::new();
|
||||
let f = frame(&mut s, 0.0, "Screen");
|
||||
let r = rect(&mut s, f, 20.0);
|
||||
ex(&mut s, "select.set", json!({"ids": [r]}));
|
||||
ex(&mut s, "tool.annotation", json!({}));
|
||||
assert_eq!(note_requests(&s), vec![r]);
|
||||
// And with an `id`.
|
||||
let mut s = Session::new();
|
||||
let f = frame(&mut s, 0.0, "Screen");
|
||||
let r = rect(&mut s, f, 20.0);
|
||||
ex(&mut s, "tool.annotation", json!({"id": r}));
|
||||
assert_eq!(note_requests(&s), vec![r]);
|
||||
assert_eq!(s.selection(), vec![r]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dev_mode_runs_its_own_edits_and_refuses_design_edits_from_menus_and_shortcuts() {
|
||||
for id in [
|
||||
@@ -121,6 +171,7 @@ fn dev_mode_runs_its_own_edits_and_refuses_design_edits_from_menus_and_shortcuts
|
||||
"tool.move",
|
||||
"tool.hand",
|
||||
"tool.comment",
|
||||
"tool.annotation",
|
||||
"inspect.code",
|
||||
] {
|
||||
assert!(dev_mode_allows(id), "{id} runs in Dev Mode");
|
||||
|
||||
@@ -192,6 +192,7 @@ pub static KEYS: &[Binding] = &[
|
||||
k(Tools, "tool.pencil", &["Shift+P"]),
|
||||
k(Tools, "tool.text", &["T"]),
|
||||
k(Tools, "tool.comment", &["C"]),
|
||||
k(Tools, "tool.annotation", &["Shift+T"]),
|
||||
k(Tools, "tool.shapeBuilder", &["Shift+M"]),
|
||||
// Board tools (on boards only), on keys nothing else uses.
|
||||
k(Tools, "tool.sticky", &["Q"]),
|
||||
|
||||
@@ -206,6 +206,8 @@ fn tool_keys_come_from_the_keymap() {
|
||||
assert_eq!(ToolId::Eyedropper.shortcut(), "I");
|
||||
assert_eq!(ToolId::Polygon.shortcut(), "");
|
||||
assert_eq!(crate::catalog::shortcut("tool.text"), Some("T"));
|
||||
assert_eq!(ToolId::Annotation.shortcut(), "Shift+T");
|
||||
assert_eq!(crate::catalog::shortcut("tool.annotation"), Some("Shift+T"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -110,6 +110,7 @@ static ROWS: &[Row] = &[
|
||||
doc(Tools, "Pencil tool", &["Shift+P"], &[], Bound("tool.pencil"), ""),
|
||||
doc(Tools, "Text tool", &["T"], &[], Bound("tool.text"), ""),
|
||||
doc(Tools, "Comment mode", &["C"], &[], Bound("tool.comment"), ""),
|
||||
doc(Tools, "Annotation tool", &["Shift+T"], &[], Bound("tool.annotation"), "Accessibility at Figma, Add an annotation; Developer Mode"),
|
||||
doc(Tools, "Hand tool while held", &["Space"], &[], Gesture("Pan the canvas"), "held, not pressed"),
|
||||
doc(Tools, "Pan with the arrows when nothing is selected (Shift: farther)", &["ArrowRight"], &[], Gesture("Pan with nothing selected (Shift: farther)"),
|
||||
"view.pan; nudges move the selection when there is one"),
|
||||
@@ -416,7 +417,7 @@ fn counts_match_the_docs() {
|
||||
let reserved = documented.iter().filter(|r| !r.reserved.is_empty()).count();
|
||||
let pinned = ROWS.len() - documented.len();
|
||||
// docs/ui-parity.md quotes these.
|
||||
assert_eq!((documented.len(), bound, moved, missing, reserved, pinned), (136, 135, 1, 0, 2, 57));
|
||||
assert_eq!((documented.len(), bound, moved, missing, reserved, pinned), (137, 136, 1, 0, 2, 57));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -122,6 +122,7 @@ name = "English"
|
||||
"Android Vector Drawable" = "Android Vector Drawable"
|
||||
"Angular" = "Angular"
|
||||
"Animation" = "Animation"
|
||||
"Annotation" = "Annotation"
|
||||
"Annotations" = "Annotations"
|
||||
"Annotations shown on the canvas" = "Annotations shown on the canvas"
|
||||
"Another server, or one that needs its key. The usual server is set in Preferences." = "Another server, or one that needs its key. The usual server is set in Preferences."
|
||||
|
||||
@@ -266,6 +266,14 @@ impl FigApp {
|
||||
self.ui.comments.show(id);
|
||||
}
|
||||
}
|
||||
"annotation.new" => {
|
||||
// The Annotation tool: show the Inspect tab (never toggle it away) and put the
|
||||
// caret in the layer's new-note field.
|
||||
if let Some(id) = r.data.get("id").and_then(figcraft_engine::params::id_of) {
|
||||
self.ui.right_tab = RightTab::Inspect;
|
||||
self.ui.dev.focus_new_note = Some(id);
|
||||
}
|
||||
}
|
||||
"comment.new" => {
|
||||
let x = r.data.get("x").and_then(|v| v.as_f64()).unwrap_or(0.0);
|
||||
let y = r.data.get("y").and_then(|v| v.as_f64()).unwrap_or(0.0);
|
||||
|
||||
@@ -196,3 +196,19 @@ fn the_playground_swaps_an_instance_in_the_preview_only() {
|
||||
let props = u.app.session.execute("component.properties", &json!({"id": inst})).unwrap();
|
||||
assert_eq!(props["properties"][0]["value"], json!(heart.0), "{star:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_annotation_tool_opens_the_inspect_tab_with_the_note_field_focused() {
|
||||
let (mut u, _, r) = setup();
|
||||
u.app.ui.right_tab = crate::RightTab::Design;
|
||||
u.frames(2, Vec::new);
|
||||
u.run("tool.annotation", json!({"id": r}));
|
||||
u.frames(2, Vec::new);
|
||||
assert_eq!(u.app.ui.right_tab, crate::RightTab::Inspect, "the request shows the Inspect tab");
|
||||
let field = egui::Id::unique(("annot_new", r)).with("te");
|
||||
assert!(u.ctx.memory(|m| m.has_focus(field)), "the layer's new-note field has the caret");
|
||||
assert_eq!(u.app.ui.dev.focus_new_note, None, "the focus request is spent");
|
||||
// Asking again while already on the Inspect tab does not toggle it away.
|
||||
u.run("tool.annotation", json!({"id": r}));
|
||||
assert_eq!(u.app.ui.right_tab, crate::RightTab::Inspect);
|
||||
}
|
||||
@@ -92,6 +92,9 @@ impl CategoryFilter {
|
||||
/// revision), the page or the choice change.
|
||||
#[derive(Default)]
|
||||
pub struct DevUi {
|
||||
/// The layer whose new-note field takes the keyboard focus once its section draws (the
|
||||
/// Annotation tool asked for that layer's note).
|
||||
pub focus_new_note: Option<NodeId>,
|
||||
pub filter: usize,
|
||||
pub categories: CategoryFilter,
|
||||
pub compare: Option<crate::dev_compare::CompareView>,
|
||||
@@ -618,6 +621,11 @@ pub fn annotations_section(app: &mut FigApp, ui: &mut Ui, doc: &Document, id: No
|
||||
}
|
||||
ui.add_space(6.0);
|
||||
}
|
||||
// The Annotation tool asked for this layer's note: put the caret in the new-note field, once.
|
||||
if app.ui.dev.focus_new_note == Some(id) {
|
||||
app.ui.dev.focus_new_note = None;
|
||||
ui.memory_mut(|m| m.request_focus(Id::unique(("annot_new", id)).with("te")));
|
||||
}
|
||||
// A new annotation starts as a note; properties are pinned to it afterwards.
|
||||
if n.annotations.len() < annotations::MAX_PER_LAYER
|
||||
&& let Some(label) = widgets::multiline_field(ui, Id::unique(("annot_new", id)), "", tr("Add an annotation for developers"), 1)
|
||||
|
||||
@@ -627,7 +627,7 @@ fn flyout(app: &mut FigApp, ui: &mut Ui, g: &Group, bottom_left: egui::Pos2) {
|
||||
/// Choose `tool` from the toolbar. Dev Mode only inspects: a tool that edits leaves it for the
|
||||
/// design.
|
||||
fn choose(app: &mut FigApp, tool: ToolId) {
|
||||
if app.ui.right_tab == crate::RightTab::Inspect && !matches!(tool, ToolId::Move | ToolId::Hand | ToolId::Comment) {
|
||||
if app.ui.right_tab == crate::RightTab::Inspect && !matches!(tool, ToolId::Move | ToolId::Hand | ToolId::Comment | ToolId::Annotation) {
|
||||
app.ui.right_tab = crate::RightTab::Design;
|
||||
}
|
||||
app.run("tool.set", json!({"tool": tool.id()}));
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Dev Mode parity
|
||||
|
||||
> **Last reviewed:** 2026-10-24 · **Last updated:** 2026-11-07 · **Change:** patch (the handoff mode reads Developer Mode in the interface, its ids and saved fields unchanged; class names transliterate non-ASCII layer names (#1235); measurements kept on the canvas; a read-only layers panel in Dev Mode; earlier: import lines in generated layer trees; compare with any saved version; Dev Mode behaviour suite: an inspect-only canvas, Changed in the ready list, statuses only on frames and sections; plugin panels from the Inspect tab) · **Target:** Figma Design Dev Mode (desktop app 126.10.7)
|
||||
> **Last reviewed:** 2026-10-24 · **Last updated:** 2026-11-09 · **Change:** patch (the Annotation tool (Shift+T, `tool.annotation {id?}`): with a layer selected (or an `id`) it asks the UI for that layer's note at once, and in the tool a press on a layer selects it and asks for its note, a press on empty canvas does nothing; the Inspect tab then opens (never toggles away) with the caret in that layer's new-note field, and the engine never writes the note itself (#1840); the handoff mode reads Developer Mode in the interface, its ids and saved fields unchanged; class names transliterate non-ASCII layer names (#1235); measurements kept on the canvas; a read-only layers panel in Dev Mode; earlier: import lines in generated layer trees; compare with any saved version; Dev Mode behaviour suite: an inspect-only canvas, Changed in the ready list, statuses only on frames and sections; plugin panels from the Inspect tab) · **Target:** Figma Design Dev Mode (desktop app 126.10.7)
|
||||
|
||||
**Breadth ~98%, ready ~78% (estimated).** The inspect surface is broad, components connect
|
||||
to the code components of a repository (with their imports, also inside generated layer trees),
|
||||
@@ -15,7 +15,7 @@ repository, third-party integrations. Remaining: 1–2 h.
|
||||
| Properties with their styles and variables; redlines on hover | done | positions from the frame around the layer (groups aren't frames), rotation, radius (CSS corner order), opacity, blend; colours in hex, RGB, CSS, HSL or HSB (`inspect.properties {colorFormat}`, the Inspect tab's Color format) |
|
||||
| Code: CSS, iOS (SwiftUI), Android (Compose), Flutter | done | plus HTML, React, Vue and Leptos (ours; Leptos: `#[component]` functions with `#[prop]` defaults, variant enums and `view!` markup; Flutter is ours too: a `StatelessWidget` for a component, a widget tree for a layer); components take properties as parameters, instances are calls; CSS in px or rem (`inspect.code {unit, rootFontSize}`), with rotation, blend modes, gradient directions and kinds (linear, radial, conic), text alignment, case and decoration; class names from layer names transliterate non-ASCII scripts into readable ASCII (로그인 화면 → `rogeuin-hwamyeon`, кнопка → `knopka`, über → `uber`; symbols-only names take the node's kind, `.text`), and the 40-character cut lands on a word boundary (#1235) |
|
||||
| Assets list with export | done | layers with export settings, icons (small, vector shapes only: SVG) and images (PNG); the icon rule is ours |
|
||||
| Annotations: pinned properties, categories, coloured callouts, filter | done | |
|
||||
| Annotations: pinned properties, categories, coloured callouts, filter | done | the Annotation tool (Shift+T, `tool.annotation`) picks a layer by a click or from the selection and opens its note field in the Inspect tab. Figma's placement, cursor and whether the tool stays selected are unmeasured (Developer Mode is paywalled in the web app); staying selected until V or Escape, like the Comment tool, is inferred, and there is no toolbar button or icon yet |
|
||||
| Ready for dev list, statuses, "Changed" after edits | done | the list filters Ready for dev, Changed, Completed or any, and badges changed ones (`inspect.readyForDev {status: changed}`, `changed` per item); only sections and frames at the top of a page or section take a status (others are refused) |
|
||||
| Focus view (one frame, previous / next) | done | |
|
||||
| Compare changes (before/after side by side or overlaid, property diffs) | done | against the snapshot taken when a frame was first changed after it was marked ready, or any saved version (`dev.compare {version}`; the view's Compare with menu); a frame that isn't ready for dev compares with versions |
|
||||
@@ -29,6 +29,7 @@ repository, third-party integrations. Remaining: 1–2 h.
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-11-09 | patch | The Annotation tool (Shift+T, `tool.annotation {id?}`): selects the layer pressed on (or the selection, or `id`) and asks the UI for its note, which the Inspect tab takes in its new-note field; the engine writes nothing itself, and Developer Mode lets the tool through its gates (#1840). Figma's placement, cursor and whether the tool stays selected are unmeasured (Developer Mode is paywalled in the web app); staying selected until V or Escape, like the Comment tool, is inferred, and there is no toolbar button or icon yet |
|
||||
| 2026-10-30 | minor | Gap 16: `figcraft-cli connect publish <file.figcraft> [--mappings figcraft.connect.json] [--dry-run]` publishes a code repository's mappings to a design file (as the incumbent's Code Connect CLI publishes a repository's mappings): it links each entry's component or set by name, warns about and skips links that don't fit, saves the file in place through the engine's save (or not, with `--dry-run`) and fails when nothing links. Dev Mode breadth (~98%) and ready (~78%) unchanged at this rounding; hours unchanged |
|
||||
| 2026-10-26 | minor | Class names from layer names transliterate non-ASCII scripts (#1235): `class_name` runs names through `deunicode` (new dependency: pure Rust tables, zero deps, BSD-3-Clause), so Korean, Cyrillic, Latin-diacritic and CJK names become readable ASCII classes in HTML/CSS/React/Leptos output, component roots and `data-*` variant attributes (`rogeuin-hwamyeon`, `knopka`, `uber`, `an-niu`; before, every one of them came out `layer-`); symbols-only names take the node's kind (`.text`, `.frame`) instead of `layer-`, and the 40-character cut lands on a word boundary. ASCII-only names are unchanged. `html_codegen_tests` updated (its diacritics expectation improves from `layer-1-n-code-name` to `layer-1-unicode-name`) and extended. Percentages and hours unchanged |
|
||||
| 2026-10-24 | minor | Dev Mode refuses design edits from files dropped on the canvas (a dropped document still opens as a file of its own, and the drop overlay shows the Dev Mode note for other files), from the Pages list's add, rename, reorder, duplicate and delete, and from the Assets panel's place, drag, Alt-drop swap, style, Hide when publishing, Update, and adding, removing and swapping libraries. Percentages and hours unchanged |
|
||||
|
||||
+6
-4
@@ -1,6 +1,6 @@
|
||||
# UI and interaction parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-04 · **Change:** minor (the left rail's and tab strip's icons are larger, 20 pt (#553); pick select: Tab while the keyboard selection box shows (or the chord again) puts a pink outline on a layer and the keys walk the layer tree from it, as Accessibility at Figma says (#1836); scrubbing a field faster or slower by the pointer's height, with a notice of the speed, as the help center's Scrub fields says (#1837); component rows in the Layers panel at AA contrast in the theme's component tint, with the rows inside a selected component in a tone of its row (#1903); Cmd/Ctrl-click on a padding field sets all four sides with the value typed, a documented auto layout shortcut; a fill per region of a vector: with regions selected in edit mode, the Fill section and the fill commands edit those regions' own fills, as Edit vector layers says (#1875); the Fill section of a text whose characters were coloured differently while editing shows mixed content, characters that all share fills the layer doesn't have show those, and a layer-level fill edit reaches every character (#1856); a number field scrubs when dragged with Alt held, as the help center's scrub section says, not only by its label; reuse visible layer selection roots (M8); ; with a shape or frame tool chosen and nothing selected, the arrows show a placement crosshair in the middle of the view and move it, and Enter places there, as Accessibility at Figma says (#1839); text direction per paragraph: Text ▸ Text direction forces a paragraph left to right or right to left, as Add right-to-left text says (#1830), with `text.direction`, `node.set textDirection`, the Type settings control once a right-to-left script is in the text, inspect, the CSS `direction` and the `.fig` lines' `sourceDirectionality`; the boolean operation menu in the Design panel leads each entry with an original icon drawn in code, a filled preview of the result (#2003); a press on the open gradient's handles or stops on the canvas keeps the colour picker open through the drag, as its own control (#1852); a click with the Bend tool on a sharp point curves it, its handles along the line between its neighbours, as Edit vector layers says (#1875); wrap style: Auto, Balance and Pretty on a text layer decide where its lines break, as Explore text properties says (#1827), with the Type settings control, `text.wrapStyle`, `node.set wrapStyle`, inspect and the CSS `text-wrap`; Backspace or Delete at the start of a list item keeps the item's indentation level, as the lists article says; max lines on auto-width text keep it auto width, hugging that many lines, as the text properties article says; superscript and subscript are synthesised where the font has no glyphs for them, from the standard glyphs shrunk and shifted by the font's OS/2 metrics, and a run mixing available and unavailable glyphs is synthesised whole, as Explore text properties says (#1828); Alt+Enter while editing text opens the link at the caret, as the accessibility page documents; Minimize, by its shortcut or the button by the file name, collapses the navigation bar and both sidebars as the help center says, with the right sidebar back while something is selected (#1835); the interface size keys of the help center's accessibility page: Cmd/Ctrl+Shift+Alt with =, - and 0 step the interface through the Preferences sizes and back to the system's size; Cmd/Ctrl+Alt+arrows rotate the selection a degree at a time, 15 with Shift, as the help center's accessibility page says; they resized before; the Layers panel answers Ctrl+1 as well as Alt+1 on Windows and Linux, the two keys the help center gives it; the colour picker's Image tab shows a placeholder and Upload from computer, an asset preview, and a file dropped on it replaces the image, keeping the fill mode, crop and position (#1682); the keyboard's Menu key opens the context menu, as Shift+F10 does; the eyedropper with Cmd/Ctrl+Shift makes a colour style or variable of what it picks; sidebar number and text fields keep their text vertically centered when focused, including typography spacing; with the pen out, Enter or a double-click ends the path and edits the vector with the Move tool instead of drawing on (#1430); the Actions palette offers only commands that run from it, and no second row reading like a menu item: Place image opens the file dialog (#1574); Preferences ▸ Scroll wheel zoom: a mouse wheel zooms the canvas about the pointer and Cmd/Ctrl+wheel scrolls (#1390); boolean operations refuse input past their caps, and a boolean group past them draws without its result (#1104); the selection and hover outlines of rounded rectangles and frames follow their corners (#1506); colour picker review fixes for text undo, fitted sidebars, moving controls and gradient variables; Corner smoothing: an original icon and the shared number-field drag gesture, preserving fractional baselines; guides snap to layers when dragged;Line truncated
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-09 · **Change:** minor (the Annotation tool (Shift+T, `tool.annotation {id?}`): with a layer selected (or an `id`) it asks the UI for that layer's note at once, and in the tool a press on a layer selects it and asks for its note, a press on empty canvas does nothing; the Inspect tab then opens (never toggles away) with the caret in that layer's new-note field, and the engine never writes the note itself (#1840); the left rail's and tab strip's icons are larger, 20 pt (#553); pick select: Tab while the keyboard selection box shows (or the chord again) puts a pink outline on a layer and the keys walk the layer tree from it, as Accessibility at Figma says (#1836); scrubbing a field faster or slower by the pointer's height, with a notice of the speed, as the help center's Scrub fields says (#1837); component rows in the Layers panel at AA contrast in the theme's component tint, with the rows inside a selected component in a tone of its row (#1903); Cmd/Ctrl-click on a padding field sets all four sides with the value typed, a documented auto layout shortcut; a fill per region of a vector: with regions selected in edit mode, the Fill section and the fill commands edit those regions' own fills, as Edit vector layers says (#1875); the Fill section of a text whose characters were coloured differently while editing shows mixed content, characters that all share fills the layer doesn't have show those, and a layer-level fill edit reaches every character (#1856); a number field scrubs when dragged with Alt held, as the help center's scrub section says, not only by its label; reuse visible layer selection roots (M8); ; with a shape or frame tool chosen and nothing selected, the arrows show a placement crosshair in the middle of the view and move it, and Enter places there, as Accessibility at Figma says (#1839); text direction per paragraph: Text ▸ Text direction forces a paragraph left to right or right to left, as Add right-to-left text says (#1830), with `text.direction`, `node.set textDirection`, the Type settings control once a right-to-left script is in the text, inspect, the CSS `direction` and the `.fig` lines' `sourceDirectionality`; the boolean operation menu in the Design panel leads each entry with an original icon drawn in code, a filled preview of the result (#2003); a press on the open gradient's handles or stops on the canvas keeps the colour picker open through the drag, as its own control (#1852); a click with the Bend tool on a sharp point curves it, its handles along the line between its neighbours, as Edit vector layers says (#1875); wrap style: Auto, Balance and Pretty on a text layer decide where its lines break, as Explore text properties says (#1827), with the Type settings control, `text.wrapStyle`, `node.set wrapStyle`, inspect and the CSS `text-wrap`; Backspace or Delete at the start of a list item keeps the item's indentation level, as the lists article says; max lines on auto-width text keep it auto width, hugging that many lines, as the text properties article says; superscript and subscript are synthesised where the font has no glyphs for them, from the standard glyphs shrunk and shifted by the font's OS/2 metrics, and a run mixing available and unavailable glyphs is synthesised whole, as Explore text properties says (#1828); Alt+Enter while editing text opens the link at the caret, as the accessibility page documents; Minimize, by its shortcut or the button by the file name, collapses the navigation bar and both sidebars as the help center says, with the right sidebar back while something is selected (#1835); the interface size keys of the help center's accessibility page: Cmd/Ctrl+Shift+Alt with =, - and 0 step the interface through the Preferences sizes and back to the system's size; Cmd/Ctrl+Alt+arrows rotate the selection a degree at a time, 15 with Shift, as the help center's accessibility page says; they resized before; the Layers panel answers Ctrl+1 as well as Alt+1 on Windows and Linux, the two keys the help center gives it; the colour picker's Image tab shows a placeholder and Upload from computer, an asset preview, and a file dropped on it replaces the image, keeping the fill mode, crop and position (#1682); the keyboard's Menu key opens the context menu, as Shift+F10 does; the eyedropper with Cmd/Ctrl+Shift makes a colour style or variable of what it picks; sidebar number and text fields keep their text vertically centered when focused, including typography spacing; with the pen out, Enter or a double-click ends the path and edits the vector with the Move tool instead of drawing on (#1430); the Actions palette offers only commands that run from it, and no second row reading like a menu item: Place image opens the file dialog (#1574); Preferences ▸ Scroll wheel zoom: a mouse wheel zooms the canvas about the pointer and Cmd/Ctrl+wheel scrolls (#1390); boolean operations refuse input past their caps, and a booleaLine truncated
|
||||
|
||||
Fine canvas control is what Figma users feel first: handles, snapping, nudging, modifier keys,
|
||||
drag behaviour, numeric precision. This checklist covers the tools, gestures and panels. Status:
|
||||
@@ -101,6 +101,7 @@ interaction feel not yet compared.
|
||||
| Text: superscript and subscript | done | `sups` / `subs` use the font's own glyphs where it has the feature and every letter or digit of the run gets one; otherwise the standard glyphs are drawn shrunk and raised or lowered by the font's OS/2 superscript and subscript metrics (0.65 em at 0.35 em up or 0.14 em down without them), and a run mixing available and unavailable glyphs is synthesised whole, as Explore text properties says ("Figma draws a synthesized version by using the font's standard version of the character, shrinking it, and positioning it accordingly"; mixed glyphs "all update to the synthesized form for consistency"), #1828; tests `text::tests::superscript_native_or_synthesised_and_never_mixed`, `text_behaviour_tests::superscript_and_subscript_are_synthesised_without_glyphs_for_them` |
|
||||
| Eyedropper (I) | done | picks the colour seen under the pointer (opaque) into the selection's fill and the recent colours, one undo step (`fx_behaviour_tests`); with Cmd/Ctrl+Shift held it makes a colour style or variable of it instead, as the reference app's eyedropper does: a Create from color window asks which, its name (the next free "Color n") and a variable's collection (the file's first, or a new one made with it in the same undo step), and leaves the selection's fills alone (`eyedropper_create_tests`, `sampled_color_tests`) |
|
||||
| Comment (C), Shift+C to show/hide | done | local only |
|
||||
| Annotation tool (Shift+T) | done | picks the layer under the pointer, or the selection (or an `id`) with `tool.annotation`, and opens its note in the Inspect tab with the new-note field focused; it works in Developer Mode and in Design mode, and stays selected until V or Escape (inferred). Figma's placement, cursor and whether the tool stays selected are unmeasured (Developer Mode is paywalled in the web app); staying selected until V or Escape, like the Comment tool, is inferred, and there is no toolbar button or icon yet |
|
||||
| Text on a path (Figma Draw) | done | the Text on a path tool in the Text flyout (`tool.textOnPath`) shows the I-beam over a shape's or vector's path, and a click there puts new text on the path, starting at the click, with the path's effects and its fill when one shows; the path layer is removed, and the path comes back if nothing is typed. Also from the Design panel or `text.onPath`; the path is edited by resizing only |
|
||||
| Stretch and scatter brushes (Figma Draw) | done | eight presets drawn in code; the Pencil's brush is set by `tool.pencilBrush`, not from the toolbar |
|
||||
| Image crop handles; Crop image | done | Object ▸ Crop image, the right-click menu and the crop button on an image fill row (`image.crop`) switch the image to Crop where it already shows and enter crop mode: the whole image shows, dimmed outside the layer; the layer's box carries L-shaped corner brackets and edge bars in the selection colour (in place of the square handles), and its edges and corners crop it with the picture held still (as Cmd/Ctrl-resizing does anywhere), the image drags to move and its corners scale it (Shift: freely); Enter, Escape or a click outside finishes (`image_crop_tests`, `crop_image_tests`). Not compared side by side |
|
||||
@@ -137,9 +138,9 @@ in the table. `crates/ui-egui/src/shortcut_keys_tests.rs` drives the keys a look
|
||||
|
||||
| | Count | Notes |
|
||||
|---|---|---|
|
||||
| Documented shortcuts (rows) | 136 | each with macOS and Windows/Linux keys |
|
||||
| Bound as documented | 135 (110 before this suite) | 112 keymap bindings, 15 held keys, gestures and presentation keys, 6 vector edit tool keys |
|
||||
| Bound as documented | 135 (110 before this suite) | 112 keymap bindings, 16 held keys, gestures and presentation keys, 6 vector edit tool keys |
|
||||
| Documented shortcuts (rows) | 137 | each with macOS and Windows/Linux keys |
|
||||
| Bound as documented | 136 (110 before this suite) | 113 keymap bindings, 15 held keys, gestures and presentation keys, 6 vector edit tool keys |
|
||||
| Bound as documented | 136 (110 before this suite) | 113 keymap bindings, 16 held keys, gestures and presentation keys, 6 vector edit tool keys |
|
||||
| Bound on other keys (documented conflict) | 1 | Run last plugin: the documentation also gives Ctrl+Alt+P to pixel preview on Windows; the plugin takes Ctrl+Alt+Shift+P there |
|
||||
| Missing | 0 | moving the rotation origin (Alt+R, `transform.rotationOrigin`) and placing an object with Enter after choosing its tool (`tool.place`) were the last two |
|
||||
| Documented keys a platform keeps | 2 | Ctrl+Space (Windows/Linux keyboard box selection) switches some CJK input methods; F6 goes to a browser's address bar in the web build |
|
||||
@@ -187,6 +188,7 @@ in the table. `crates/ui-egui/src/shortcut_keys_tests.rs` drives the keys a look
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-11-09 | minor | Shortcuts: Shift+T picks the new Annotation tool (`tool.annotation {id?}`, Accessibility at Figma, Add an annotation): with a layer selected, or an `id`, it asks the UI for that layer's note at once; in the tool a press on a layer selects it and asks, a press on empty canvas does nothing; the Inspect tab opens with the caret in the new-note field and the engine writes nothing itself; documented shortcuts 137, bound 136 (#1840). Figma's placement, cursor and whether the tool stays selected are unmeasured (Developer Mode is paywalled in the web app); staying selected until V or Escape, like the Comment tool, is inferred, and there is no toolbar button or icon yet |
|
||||
| 2026-11-02 | patch | Shortcuts: a Cmd/Ctrl-click on a padding field (the horizontal or vertical pair, or a side with Individual padding open) makes the value typed go to all four sides, in one undo step (Guide to auto layout, Keyboard shortcut guide); a plain click still edits that axis or side alone. A documented gesture row: 134 documented, 133 bound. Test: `padding_individual_tests` |
|
||||
| 2026-11-01 | patch | Number fields: dragging the field itself with Alt held scrubs it ("You can also hover the input field itself and hold down the modifier key: Mac: ⌥ Option Windows: Alt", Adjust alignment, rotation, position, and dimensions), with the resize cursor and the label's rates (Alt is the enabler there, so it isn't the fine rate); a click with Alt doesn't open the editor. Found by a documentation audit of the ui-parity rows. Test: `field_tests::alt_held_over_a_field_scrubs_it`. Counts unchanged |
|
||||
| 2026-11-02 | patch | M6, #1839: with a shape or frame tool chosen and nothing selected, the arrows show a placement crosshair in the middle of the view and move it by the nudge amounts (`tool.crosshair`), Enter places the object there (`tool.place`), Escape or a tool change takes it away; the canvas draws the crosshair and `ui.inspect` reports it (`tool.crosshair`), the Keyboard shortcuts panel lists the gesture ("Use the arrow keys to activate the crosshair cursor...", Accessibility at Figma). The board Shapes tool places on it too (its 160 px shape centred, as the tool centres a press), and the view pans by the overshoot when the crosshair is nudged past its edge. Tests: `place_crosshair_tests`, `shortcut_keys_tests::arrows_show_and_move_the_placement_crosshair_with_a_shape_tool`. |
|
||||
|
||||
Reference in new issue
Block a user