From b6d3e002df3fd9f125c44637bee1a32ce04d04c2 Mon Sep 17 00:00:00 2001 From: Muamer Mustafic Date: Mon, 26 Oct 2026 13:14:51 +0100 Subject: [PATCH] M4: truncated text fills its last line, keeps truncating when resized, and auto height takes max lines (#1375, #1376, #1377) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - The last line shown runs on into the word wrapped after it, so the ellipsis fills the line ("Lorem consec…") instead of following the last whole word. - An explicit height (node.set, transform.resize, a resize handle, fixed sizing) on truncating text keeps it truncating (TextData::fix_size). - Max lines on auto-height text hugs up to that many lines and ends in an ellipsis; text.maxLines keeps auto height, Type settings show it as truncating, .fig export writes textTruncation and import keeps auto height. - node.get reports maxLines; text.fit measures every line. Co-authored-by: Cursor --- crates/doc/src/text.rs | 25 ++++++++- crates/engine/src/cmd/arrange.rs | 6 ++- crates/engine/src/cmd/props.rs | 7 +-- crates/engine/src/cmd/text.rs | 24 +++++---- crates/engine/src/inspect.rs | 3 ++ crates/engine/src/text_behaviour_tests.rs | 60 +++++++++++++++++++++ crates/engine/src/textfit.rs | 2 + crates/engine/src/tools.rs | 9 ++-- crates/format/src/fig/export.rs | 2 +- crates/format/src/rest/text.rs | 5 +- crates/text/src/behaviour_tests.rs | 36 +++++++++++++ crates/text/src/layout.rs | 64 ++++++++++++++++------- crates/text/src/tests.rs | 4 +- crates/ui-egui/src/typography.rs | 22 +++++--- docs/roadmap.md | 3 +- docs/target-app-parity.md | 6 ++- 16 files changed, 227 insertions(+), 51 deletions(-) diff --git a/crates/doc/src/text.rs b/crates/doc/src/text.rs index 24d7e088..8c739920 100644 --- a/crates/doc/src/text.rs +++ b/crates/doc/src/text.rs @@ -417,7 +417,8 @@ pub struct TextData { /// line's baseline). #[serde(skip_serializing_if = "crate::node::is_default")] pub leading_trim: LeadingTrim, - /// Max lines when truncating (0 = as many as fit). + /// Max lines when truncating, or before an auto-height text ends in an ellipsis (0 = as many + /// as fit). #[serde(skip_serializing_if = "crate::node::is_default")] pub max_lines: u32, /// OpenType feature toggles (e.g. "liga": false). @@ -510,6 +511,28 @@ impl TextData { self.characters.chars().count() } + /// Whether text that runs out of room ends in an ellipsis: a truncating box, or auto height + /// with a line limit (the box hugs up to `max_lines` lines). + pub fn truncates(&self) -> bool { + match self.auto_resize { + TextAutoResize::Truncate => true, + TextAutoResize::Height => self.max_lines > 0, + TextAutoResize::WidthAndHeight | TextAutoResize::None => false, + } + } + + /// The resize mode once the box is given an explicit `width` and/or `height`: an explicit + /// height fixes the box, an explicit width turns auto width into auto height, and truncating + /// text keeps truncating. + pub fn fix_size(&mut self, width: bool, height: bool) { + self.auto_resize = match self.auto_resize { + _ if height && self.truncates() => TextAutoResize::Truncate, + _ if height => TextAutoResize::None, + TextAutoResize::WidthAndHeight if width => TextAutoResize::Height, + mode => mode, + }; + } + /// The style at char index `i`. pub fn style_at(&self, i: usize) -> &TextStyle { self.runs.iter().find(|r| r.start <= i && i < r.end).map(|r| &r.style).unwrap_or(&self.style) diff --git a/crates/engine/src/cmd/arrange.rs b/crates/engine/src/cmd/arrange.rs index 0180504c..8f8e40d2 100644 --- a/crates/engine/src/cmd/arrange.rs +++ b/crates/engine/src/cmd/arrange.rs @@ -562,7 +562,11 @@ pub(crate) fn resize_cmd(s: &mut Session, p: &Value) -> Result { n.child_layout.sizing_v = Sizing::Fixed; } if let NodeKind::Text(t) = &mut n.kind { - t.auto_resize = if num(p, "height").is_some() { TextAutoResize::None } else { TextAutoResize::Height }; + if num(p, "height").is_some() { + t.fix_size(num(p, "width").is_some(), true); + } else if t.auto_resize != TextAutoResize::Truncate { + t.auto_resize = TextAutoResize::Height; + } } super::props::restore_worlds(doc, &kept)?; } diff --git a/crates/engine/src/cmd/props.rs b/crates/engine/src/cmd/props.rs index 9ee499fc..30a10393 100644 --- a/crates/engine/src/cmd/props.rs +++ b/crates/engine/src/cmd/props.rs @@ -461,11 +461,7 @@ pub fn apply(cmd: &str, doc: &mut Document, id: NodeId, props: &Map TextAutoResize::WidthAndHeight, (_, Sizing::Hug) => TextAutoResize::Height, + _ if t.truncates() => TextAutoResize::Truncate, _ => TextAutoResize::None, }; } diff --git a/crates/engine/src/cmd/text.rs b/crates/engine/src/cmd/text.rs index 813a6036..3822c5c5 100644 --- a/crates/engine/src/cmd/text.rs +++ b/crates/engine/src/cmd/text.rs @@ -291,15 +291,21 @@ pub fn specs() -> Vec { let v = finite(req_num("text.paragraphSpacing", p, "spacing")?).clamp(0.0, 10_000.0); para(s, p, |t| t.paragraph_spacing = v) }), - spec!("text.maxLines", "Max lines", "{lines (0 = no limit; truncates), ids?}", true, |s, p| { - let v = finite(req_num("text.maxLines", p, "lines")?).clamp(0.0, 10_000.0) as u32; - para(s, p, |t| { - t.max_lines = v; - if v > 0 { - t.auto_resize = TextAutoResize::Truncate; - } - }) - }), + spec!( + "text.maxLines", + "Max lines", + "{lines (0 = no limit; truncates: auto-height text hugs up to that many lines, other text becomes a truncating box), ids?}", + true, + |s, p| { + let v = finite(req_num("text.maxLines", p, "lines")?).clamp(0.0, 10_000.0) as u32; + para(s, p, |t| { + t.max_lines = v; + if v > 0 && t.auto_resize != TextAutoResize::Height { + t.auto_resize = TextAutoResize::Truncate; + } + }) + } + ), spec!( "text.setFeatures", "OpenType features", diff --git a/crates/engine/src/inspect.rs b/crates/engine/src/inspect.rs index 33162314..7dccd706 100644 --- a/crates/engine/src/inspect.rs +++ b/crates/engine/src/inspect.rs @@ -188,6 +188,9 @@ pub fn node_json(doc: &Document, id: NodeId, depth: usize) -> Value { o.insert("letterSpacing".into(), json!(t.style.letter_spacing.label())); o.insert("textAlign".into(), json!(t.align_h)); o.insert("textAutoResize".into(), json!(t.auto_resize)); + if t.max_lines > 0 { + o.insert("maxLines".into(), json!(t.max_lines)); + } if let Some(tp) = &t.path { o.insert("textPath".into(), json!(tp)); } diff --git a/crates/engine/src/text_behaviour_tests.rs b/crates/engine/src/text_behaviour_tests.rs index 92a43306..8b33e303 100644 --- a/crates/engine/src/text_behaviour_tests.rs +++ b/crates/engine/src/text_behaviour_tests.rs @@ -310,3 +310,63 @@ fn new_text_starts_in_the_builds_default_font() { assert_eq!(serde_json::to_value(td).unwrap()["style"]["font_family"], figcraft_doc::DEFAULT_FONT); } } + +fn resize_mode(s: &Session, t: NodeId) -> figcraft_doc::TextAutoResize { + s.doc().unwrap().node(t).unwrap().kind.text().unwrap().auto_resize +} + +#[test] +fn a_truncated_text_keeps_truncating_when_its_box_is_resized() { + // #1376: a truncating text is already fixed both ways, so an explicit size changes its box only. + use figcraft_doc::TextAutoResize; + let mut s = Session::new(); + let t = text(&mut s, "Lorem consectetur", json!({"textAutoResize": "truncate", "width": 100, "height": 24})); + assert_eq!(resize_mode(&s, t), TextAutoResize::Truncate); + for (cmd, p, why) in [ + ("node.set", json!({"id": t, "props": {"height": 24}}), "node.set height (the value it has)"), + ("node.set", json!({"id": t, "props": {"width": 120, "height": 30}}), "node.set width and height"), + ("transform.resize", json!({"ids": [t], "height": 40}), "transform.resize height"), + ("transform.resize", json!({"ids": [t], "width": 90}), "transform.resize width"), + ("node.set", json!({"id": t, "props": {"sizingV": "fixed"}}), "fixed vertical sizing"), + ] { + s.execute(cmd, &p).unwrap(); + assert_eq!(resize_mode(&s, t), TextAutoResize::Truncate, "{why}"); + } + // Text that doesn't truncate still turns fixed size. + let plain = text(&mut s, "Lorem consectetur", json!({"textAutoResize": "height", "width": 100})); + s.execute("node.set", &json!({"id": plain, "props": {"height": 24}})).unwrap(); + assert_eq!(resize_mode(&s, plain), TextAutoResize::None); +} + +#[test] +fn max_lines_on_auto_height_text_hugs_that_many_lines() { + // #1377: an auto-height text with max lines grows up to that many lines and ends in an + // ellipsis; the layer reports its max lines. + use figcraft_doc::TextAutoResize; + figcraft_text::scan_system_fonts(); + let mut s = Session::new(); + let t = text(&mut s, "Lorem ipsum dolor sit amet consectetur", json!({"fontSize": 16, "lineHeight": 24})); + s.execute("node.set", &json!({"id": t, "props": {"textAutoResize": "height", "width": 100}})).unwrap(); + assert!(crate::inspect::node_json(s.doc().unwrap(), t, 0).get("maxLines").is_none(), "no limit: not reported"); + // Wrapping needs a font (CI may have none). + if size(&s, t).1 < 48.0 { + return; + } + s.execute("text.maxLines", &json!({"ids": [t], "lines": 1})).unwrap(); + assert_eq!(resize_mode(&s, t), TextAutoResize::Height, "max lines keep auto height"); + assert_eq!(size(&s, t), (100.0, 24.0), "one line tall"); + assert_eq!(crate::inspect::node_json(s.doc().unwrap(), t, 0)["maxLines"], json!(1)); + s.execute("node.set", &json!({"id": t, "props": {"maxLines": 2}})).unwrap(); + assert_eq!(size(&s, t).1, 48.0, "two lines tall"); + // No limit: it grows to all of its lines again. + s.execute("node.set", &json!({"id": t, "props": {"maxLines": 0}})).unwrap(); + assert!(size(&s, t).1 > 48.0); + // An explicit height on a text that truncates makes a truncating box. + s.execute("node.set", &json!({"id": t, "props": {"maxLines": 1}})).unwrap(); + s.execute("node.set", &json!({"id": t, "props": {"height": 30}})).unwrap(); + assert_eq!((resize_mode(&s, t), size(&s, t).1), (TextAutoResize::Truncate, 30.0)); + // Other text still becomes a truncating box with max lines. + let fixed = text(&mut s, "Lorem", json!({"textAutoResize": "none", "width": 100, "height": 100})); + s.execute("text.maxLines", &json!({"ids": [fixed], "lines": 2})).unwrap(); + assert_eq!(resize_mode(&s, fixed), TextAutoResize::Truncate); +} diff --git a/crates/engine/src/textfit.rs b/crates/engine/src/textfit.rs index 4ab6c50e..f9d549e4 100644 --- a/crates/engine/src/textfit.rs +++ b/crates/engine/src/textfit.rs @@ -29,6 +29,8 @@ pub fn specs() -> Vec { /// paragraph of its own after a hard break, which no size can fix). fn measure(t: &TextData, k: f64, width: f64) -> (usize, f64, bool) { let mut t = t.clone(); + // Every line counts, not only those the layer's own line limit shows. + t.max_lines = 0; let n = t.char_count(); t.restyle(0, n, |s| s.font_size = (s.font_size * k).clamp(1.0, 5000.0)); let l = figcraft_text::layout(&t, Some(width), None); diff --git a/crates/engine/src/tools.rs b/crates/engine/src/tools.rs index 629c9a06..165638ff 100644 --- a/crates/engine/src/tools.rs +++ b/crates/engine/src/tools.rs @@ -1821,10 +1821,11 @@ impl Session { n.child_layout.sizing_v = Sizing::Fixed; } if let NodeKind::Text(t) = &mut n.kind { - t.auto_resize = match handle { - Handle::Edge(1) | Handle::Edge(3) => TextAutoResize::Height, - _ => TextAutoResize::None, - }; + match handle { + Handle::Edge(1) | Handle::Edge(3) if t.auto_resize == TextAutoResize::Truncate => {} + Handle::Edge(1) | Handle::Edge(3) => t.auto_resize = TextAutoResize::Height, + _ => t.fix_size(true, true), + } } return Ok(()); } diff --git a/crates/format/src/fig/export.rs b/crates/format/src/fig/export.rs index dad09d95..801aeed9 100644 --- a/crates/format/src/fig/export.rs +++ b/crates/format/src/fig/export.rs @@ -1555,7 +1555,7 @@ impl<'d> Exporter<'d> { ("textTracking", fl(0.0)), ("emojiImageSet", e("APPLE")), ]); - if td.auto_resize == TextAutoResize::Truncate { + if td.truncates() { f.push(("textTruncation", e("ENDING"))); } if td.max_lines > 0 { diff --git a/crates/format/src/rest/text.rs b/crates/format/src/rest/text.rs index 99ceb500..46dc7486 100644 --- a/crates/format/src/rest/text.rs +++ b/crates/format/src/rest/text.rs @@ -69,10 +69,11 @@ fn paragraph(s: &Value, td: &mut TextData) { // Missing means a fixed box. _ => TextAutoResize::None, }; - if text(s, "textTruncation") == Some("ENDING") { + td.max_lines = num(s, "maxLines").filter(|n| *n >= 1.0).map_or(0, |n| n.min(10_000.0) as u32); + // Auto height with a line limit keeps hugging its lines up to the limit. + if text(s, "textTruncation") == Some("ENDING") && !td.truncates() { td.auto_resize = TextAutoResize::Truncate; } - td.max_lines = num(s, "maxLines").filter(|n| *n >= 1.0).map_or(0, |n| n.min(10_000.0) as u32); td.paragraph_spacing = len(s, "paragraphSpacing").unwrap_or(0.0); td.paragraph_indent = len(s, "paragraphIndent").unwrap_or(0.0); td.list_spacing = len(s, "listSpacing").unwrap_or(0.0).min(10_000.0); diff --git a/crates/text/src/behaviour_tests.rs b/crates/text/src/behaviour_tests.rs index eea8e93f..4f5b0774 100644 --- a/crates/text/src/behaviour_tests.rs +++ b/crates/text/src/behaviour_tests.rs @@ -162,6 +162,42 @@ fn resizing_auto_width_auto_height_fixed_and_truncate() { assert!(two.truncated && two.lines.len() == 2); } +#[test] +fn truncation_fills_the_last_line_before_the_ellipsis() { + // #1375: as with CSS `text-overflow: ellipsis`, the last line shown takes as much of the word + // wrapped after it as fits before the ellipsis ("Lorem consec…", not "Lorem…"). + if face().is_none() { + return; + } + let s = "Lorem consectetur"; + let full = layout(&text(s, TextAutoResize::WidthAndHeight), None, None); + let lorem = layout(&text("Lorem", TextAutoResize::WidthAndHeight), None, None).width; + // Room for "Lorem" and most of the next word, not all of it. + let w = lorem + (full.width - lorem) * 0.75; + let line_h = full.lines[0].height; + let mut max_one = text(s, TextAutoResize::Truncate); + max_one.max_lines = 1; + let mut hug_one = text(s, TextAutoResize::Height); + hug_one.max_lines = 1; + for (l, why) in [ + (layout(&text(s, TextAutoResize::Truncate), Some(w), Some(line_h + 1.0)), "cut by the box height"), + (layout(&max_one, Some(w), Some(1000.0)), "cut by max lines"), + (layout(&hug_one, Some(w), None), "auto height with max lines"), + ] { + assert!(l.truncated && l.lines.len() == 1, "{why}"); + let last = l.glyphs.last().unwrap(); + assert_eq!(last.char_start, last.char_end, "{why}: the ellipsis ends the line"); + assert!(l.glyphs.iter().any(|g| g.char_start == 6), "{why}: the wrapped word starts on the line"); + assert!(l.glyphs.iter().all(|g| g.x + g.advance <= w + EPS), "{why}: within the box"); + assert!((l.height - line_h).abs() < EPS, "{why}: one line tall"); + } + // A line that ends its paragraph keeps its words; the ellipsis follows them. + let mut para = text("Lorem\nconsectetur", TextAutoResize::Truncate); + para.max_lines = 1; + let l = layout(¶, Some(w), Some(1000.0)); + assert!(l.truncated && l.glyphs.iter().all(|g| g.char_start < 5 || g.char_start == g.char_end)); +} + #[test] fn vertical_trim_cap_height_to_baseline() { let Some(f) = face() else { return }; diff --git a/crates/text/src/layout.rs b/crates/text/src/layout.rs index 3c64d4ab..56439719 100644 --- a/crates/text/src/layout.rs +++ b/crates/text/src/layout.rs @@ -532,7 +532,7 @@ pub fn layout(text: &TextData, box_width: Option, box_height: Option) let mut out = TextLayout { styles: styles.clone(), ..Default::default() }; let mut y = 0.0; let mut para_start_line = true; - let max_lines = if text.max_lines > 0 && matches!(text.auto_resize, TextAutoResize::Truncate) { text.max_lines as usize } else { usize::MAX }; + let max_lines = if text.max_lines > 0 && text.truncates() { text.max_lines as usize } else { usize::MAX }; // A style's vertical metrics (ascent, descent, line height, cap height) come from its own // font, never a fallback face, so lines mixing scripts keep the spacing of lines that don't. let metrics = |st_idx: usize| -> [f64; 4] { @@ -546,6 +546,25 @@ pub fn layout(text: &TextData, box_width: Option, box_height: Option) None => [size * 0.8, size * 0.2, whole_px(st.line_height.resolve(size, 1.2)), size * 0.7], } }; + // An item's glyph with its pen position at `pen` on the line at `baseline` (none for a space + // the font has no glyph for). + let glyph_at = |it: &Item, pen: f64, baseline: f64| { + let size = styles.get(it.style).map(|st| sanitize_size(st.font_size)).unwrap_or(12.0) * it.scale; + (it.gid != 0 || !it.is_space()).then(|| Glyph { + face: it.face.clone(), + coords: it.coords.clone(), + gid: it.gid, + x: pen + it.dx, + y: baseline - it.dy, + size, + advance: it.adv, + char_start: it.char_start, + char_end: it.char_end, + style: it.style, + rtl: it.level % 2 == 1, + warp: Affine::IDENTITY, + }) + }; let mut first_cap = 0.0; for (li, (s, e, para_end, para)) in lines.iter().enumerate() { if li >= max_lines { @@ -685,23 +704,7 @@ pub fn layout(text: &TextData, box_width: Option, box_height: Option) if is_line_break(it.ch) { continue; } - let size = styles.get(it.style).map(|st| sanitize_size(st.font_size)).unwrap_or(12.0) * it.scale; - if it.gid != 0 || !it.is_space() { - out.glyphs.push(Glyph { - face: it.face.clone(), - coords: it.coords.clone(), - gid: it.gid, - x: pen + it.dx, - y: baseline - it.dy, - size, - advance: it.adv, - char_start: it.char_start, - char_end: it.char_end, - style: it.style, - rtl: it.level % 2 == 1, - warp: Affine::IDENTITY, - }); - } + out.glyphs.extend(glyph_at(it, pen, baseline)); pen += it.adv; if it.stretches() { pen += justify_extra; @@ -757,6 +760,31 @@ pub fn layout(text: &TextData, box_width: Option, box_height: Option) } } if out.truncated { + // The last line shown runs on into the line cut after it, so the ellipsis fills the line + // ("Lorem consec…") instead of following the last whole word ("Lorem…"). + let last = out.lines.len().checked_sub(1); + let wrapped_on = last.and_then(|li| Some((out.lines.get(li)?, lines.get(li)?, lines.get(li + 1)?))); + if let (Some(maxw), Some((line, &(_, e, para_end, _), &(ns, ne, _, _)))) = (wrap, wrapped_on) + && !para_end + && !break_after(e) + && !line.rtl + { + let baseline = line.baseline; + let mut pen = out.glyphs.get(line.glyph_start..line.glyph_end).and_then(<[Glyph]>::last).map_or(line.width, |g| g.x + g.advance); + for it in items.get(ns..ne).unwrap_or(&[]) { + if pen > maxw { + break; + } + if !is_line_break(it.ch) { + out.glyphs.extend(glyph_at(it, pen, baseline)); + pen += it.adv; + } + } + let gend = out.glyphs.len(); + if let Some(l) = out.lines.last_mut() { + l.glyph_end = gend; + } + } add_ellipsis(&mut out, wrap); } diff --git a/crates/text/src/tests.rs b/crates/text/src/tests.rs index d36c1589..09accbd4 100644 --- a/crates/text/src/tests.rs +++ b/crates/text/src/tests.rs @@ -232,7 +232,9 @@ fn truncate_max_lines_with_ellipsis() { assert_eq!(l.lines.len(), 2); let last = l.glyphs.last().unwrap(); assert_eq!(last.gid, face.glyph_for('…')); - // max_lines is ignored unless truncating. + // Auto height truncates at max_lines too; a fixed box shows every line. + t.auto_resize = figcraft_doc::TextAutoResize::Height; + assert_eq!(layout(&t, Some(300.0), None).lines.len(), 2); t.auto_resize = figcraft_doc::TextAutoResize::None; assert_eq!(layout(&t, Some(300.0), Some(1000.0)).lines.len(), 3); } diff --git a/crates/ui-egui/src/typography.rs b/crates/ui-egui/src/typography.rs index f61aa183..13b5785f 100644 --- a/crates/ui-egui/src/typography.rs +++ b/crates/ui-egui/src/typography.rs @@ -527,17 +527,27 @@ fn type_settings( app.set_props(ids, json!({"leadingTrim": if i == 1 { "capHeight" } else { "none" }})); } let resize = common(doc, ids, |n| n.kind.text().map(|t| t.auto_resize)).flatten(); + // Auto-height text with a line limit truncates too. + let truncating = common(doc, ids, |n| n.kind.text().map(TextData::truncates)).flatten(); caption(ui, tr("Truncate text")); - let active = resize.map(|r| usize::from(r == TextAutoResize::Truncate)); - match text_seg(ui, "trunc", &[(tr("Off"), tr_noop("Don't truncate")), ("…", tr_noop("Truncate with an ellipsis"))], active, w) { - Some(1) => app.set_props(ids, json!({"textAutoResize": "truncate"})), - Some(_) if resize != Some(TextAutoResize::Height) && resize != Some(TextAutoResize::WidthAndHeight) => { - app.set_props(ids, json!({"textAutoResize": "none"})) + match text_seg( + ui, + "trunc", + &[(tr("Off"), tr_noop("Don't truncate")), ("…", tr_noop("Truncate with an ellipsis"))], + truncating.map(usize::from), + w, + ) { + Some(1) if truncating != Some(true) => app.set_props(ids, json!({"textAutoResize": "truncate"})), + Some(0) if resize == Some(TextAutoResize::Height) => { + if truncating == Some(true) { + app.set_props(ids, json!({"maxLines": 0})); + } } + Some(0) if resize != Some(TextAutoResize::WidthAndHeight) => app.set_props(ids, json!({"textAutoResize": "none"})), _ => {} } // Max lines only matter while truncating; no limit shows "Auto". - if resize == Some(TextAutoResize::Truncate) { + if truncating == Some(true) { let max = common(doc, ids, |n| n.kind.text().map(|t| t.max_lines)).flatten(); ui.add_space(4.0); let shown = max.map(f64::from).filter(|m| *m > 0.0); diff --git a/docs/roadmap.md b/docs/roadmap.md index c22f88f8..5d6bf925 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,6 +1,6 @@ # FigCraft roadmap: milestones and what's next -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-24 · **Change:** minor (M11 widget fields and property menus; M12 Boards: timer and voting; Installed weights of an embedded font family (#1246); `.sketch` import, best effort; Adobe Fonts found on macOS (#298); M12 Boards; community fixes in #507, #510, #529 and #530: auto-save, a text box by dragging, batch step references, removing someone from a live session, Russian and Turkish catalogs reviewed, Czech and Indonesian drafts, the Japanese draft completed, zoom previews, the web clipboard, Alt-drag snapping and Cmd+D repeat, recovery copies, crash and rollback fixes, colour emoji, `.fig` import fixes; earlier changes are in the revision history below) · **Target:** Figma Design (desktop app 126.10.7) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-26 · **Change:** minor (text truncation fills the last line, survives resizing, and auto height takes Max lines (#1375, #1376, #1377); M11 widget fields and property menus; M12 Boards: timer and voting; Installed weights of an embedded font family (#1246); `.sketch` import, best effort; Adobe Fonts found on macOS (#298); M12 Boards; community fixes in #507, #510, #529 and #530: auto-save, a text box by dragging, batch step references, removing someone from a live session, Russian and Turkish catalogs reviewed, Czech and Indonesian drafts, the Japanese draft completed, zoom previews, the web clipboard, Alt-drag snapping and Cmd+D repeat, recovery copies, crash and rollback fixes, colour emoji, `.fig` import fixes; earlier changes are in the revision history below) · **Target:** Figma Design (desktop app 126.10.7) Forward-looking plan. Where FigCraft stands today: [`ROADMAP.md`](../ROADMAP.md) and [`target-app-parity.md`](target-app-parity.md); the full ranked list: [`gaps.md`](gaps.md). Hours @@ -75,6 +75,7 @@ blockers. | Date | Change | Summary | |---|---|---| +| 2026-10-26 | minor | Text truncation (M4, #1375, #1376, #1377): the last line shown takes as much of the next word as fits before the ellipsis ("Lorem consec…", not "Lorem…"); an explicit height, `transform.resize`, a resize handle or fixed sizing on a truncated text keeps it truncating instead of turning it fixed size; Max lines on auto-height text hugs up to that many lines and ends in an ellipsis (`text.maxLines`, `node.set {maxLines}`, Type settings, `.fig` export and import), and `node.get` / `get_node` report `maxLines`. Regression tests in the text and engine behaviour suites. Numbers unchanged | | 2026-10-24 | minor | Installed weights of an embedded font family (M7, #1246): release builds embed some families with only a few styles (Poppins Regular, Italic and Bold), and the first font scan dropped every installed face of such a family, so an installed Poppins SemiBold or Medium was never offered (a rescan did offer them). The first scan now keeps the embedded faces, which still win for the styles they cover, and offers the installed styles they lack beside them. Regression test in the font database tests. Numbers unchanged | | 2026-10-24 | minor | M12: a shared board timer and voting sessions; 30–60 h → 26–54 h | | 2026-10-24 | minor | M11: plugin widgets take text input (editable fields, `widget.setText`) and a property menu (`widget.menu`), plugin API 4; 8–26 h → 6–22 h | diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 5c8dcc01..8a236656 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1,6 +1,6 @@ # FigCraft parity with Figma -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-24 · **Change:** minor (plugin widgets take text input and a property menu (plugin API 4); boards: templates and tables; boards: a shared timer and voting sessions; boards: the dot grid, connector ends dragged to re-attach, curved connectors; `.sketch` import, best effort: import/export breadth 86% → 87%, file formats 60% → 61%, headline numbers unchanged; Canvas & navigation's remaining work no longer lists outline mode's Include hidden layers and Include object bounds; boards: area 22 (weight 0, outside the weighted sum; CJK line breaking: JIS X 4051's line-start and line-end sets completed with the halfwidth small kana, the halfwidth and spacing (semi-)voiced sound marks, iteration marks 〱–〵 and the halfwidth parentheses; Turkish reviewed, Czech and Indonesian drafts, the Japanese draft completed; reviewed Russian catalog and integer plurals; vector edit mode regions: hover hatch, selection and drag; plugin log byte/count limits (#191); core editor leftovers: component playground, auto gap handles, variables inside styles, inner shadows and background blur on groups, command-made text readable on dark pages, a one-step frame/group convert, delays on mouse triggers and manual overlay offsets, wrapped-line alignment; starter templates, a Templates tab and Getting started tips on the home screen: essentials ~57% → ~59%; everyday editing partials: number fields, outline options, Hide when publishing, cursors, panel widths, dark pages, numbers unchanged but menu breadth 211/211; text box sizes measured against documents re-saved by other software: whole-pixel line heights, advances on a 1024-per-em grid, letter spacing between glyphs only, numbers unchanged; .fig export checked by opening written files in other software, numbers unchanged; a public figcraft-server: open hosting, owner tokens, per-address limits, idle expiry, moderation, install.sh; experimental .fig export; keyboard shortcut parity suite and bindings; Simplified Chinese draft catalog; Latin American Spanish overlay; prototyping tail: gamepad triggers, widgets in prototypes, mode lookups; plugin directory on figcraft-server; Japanese and Korean draft catalogs and CJK UI fonts; plugin widgets and the plugin panel tail; French, German and Brazilian Portuguese draft catalogs; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and edit tail; dev resources links; extended collections tail; branch version history and finer list merges; headline percentages recomputed from the area table: feature breadth ~84%, core ~94%, mainstream ~50%; text lists, hanging punctuation and decoration styles; comments and version history behaviour suite and fixes; canvas navigation, guides and grids behaviour suite and fixes; export and inspect behaviour suite and fixes; .fig import: editable vector networks and more layer kinds; shapes and vector behaviour suite and fixes; paint and effects behaviour suite and fixes; frames, groups, sections and masks behaviour suite and fixes; styles and variables behaviour suite and fixes; prototyping behaviour suite and fixes; components behaviour suite and fixes; embedded image colour profiles; layers panel keyboard: F2 rename, Shift+F10 context menu, keyboard box selection, 10k-layer QA; per-side stroke weight variables; text behaviour suite: CJK line breaking, metrics, vertical trim; selection and transform behaviour suite and fixes; `.fig` import, best effort; auto layout behaviour suite and fixes; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; localization layer with a draft Spanish catalog; hosting figcraft-server: wss in the desktop app, server releases, deployment guide; plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings; feature breadth ~80%; ready ~49%; last major: full re-measure against Figma desktop 126.10.7; rows for the community batches: Selection colors, smart-selection spacing and a Present tab (#753), `.fig` import fidelity, paste from another design app, a `.fig` copy from Save as; control requests while the window is minimised (#720); catalog counts re-measured; automation roots (#715); number variables in the text content picker (#629); frames and sections convert into each other (#719); reviewed Russian catalog and integer plurals; vector edit mode regions: hover hatch, selection and drag; plugin log byte/count limits (#191); core editor leftovers: component playground, auto gap handles, variables inside styles, inner shadows and background blur on groups, command-made text readable on dark pages, a one-step frame/group convert, delays on mouse triggers and manual overlay offsets, wrapped-line alignment; starter templates, a Templates tab and Getting started tips on the home screen: essentials ~57% → ~59%; everyday editing partials: number fields, outline options, Hide when publishing, cursors, panel widths, dark pages, numbers unchanged but menu breadth 211/211; text box sizes measured against documents re-saved by other software: whole-pixel line heights, advances on a 1024-per-em grid, letter spacing between glyphs only, numbers unchanged; .fig export checked by opening written files in other software, numbers unchanged; a public figcraft-server: open hosting, owner tokens, per-address limits, idle expiry, moderation, install.sh; experimental .fig export; keyboard shortcut parity suite and bindings; Simplified Chinese draft catalog; Latin American Spanish overlay; prototyping tail: gamepad triggers, widgets in prototypes, mode lookups; plugin directory on figcraft-server; Japanese and Korean draft catalogs and CJK UI fonts; plugin widgets and the plugin panel tail; French, German and Brazilian Portuguese draft catalogs; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and edit tail; dev resources links; extended collections tail; branch version history and finer list merges; headline percentages recomputed from the area table: feature breadth ~84%, core ~94%, mainstream ~50%; text lists, hanging punctuation and decoration styles; comments and version history behaviour suite and fixes; canvas navigation, guides and grids behaviour suite and fixes; export and inspect behaviour suite and fixes; .fig import: editable vector networks and more layer kinds; shapes and vector behaviour suite and fixes; paint and effects behaviour suite and fixes; frames, groups, sections and masks behaviour suite and fixes; styles and variables behaviour suite and fixes; prototyping behaviour suite and fixes; components behaviour suite and fixes; embedded image colour profiles; layers panel keyboard: F2 rename, Shift+F10 context menu, keyboard box selection, 10k-layer QA; per-side stroke weight variables; text behaviour suite: CJK line breaking, metrics, vertical trim; selection and transform behaviour suite and fixes; `.fig` import, best effort; auto layout behaviour suite and fixes; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; localization layer with a draft Spanish catalog; hosting figcraft-server: wss in the desktop app, server releases, deployment guide; plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings; feature breadth ~80%; ready ~49%; last major: full re-measure against Figma desktop 126.10.7; Canvas & navigation's remaining work no longer lists outline mode's Include hidden layers and Include object bounds; boards: area 22 (weight 0, outside the weighted sum; Turkish reviewed, Czech and Indonesian drafts, the Japanese draft completed; reviewed Russian catalog and integer plurals; vector edit mode regions: hover hatch, selection and drag; plugin log byte/count limits (#191); core editor leftovers: component playground, auto gap handles, variables inside styles, inner shadows and background blur on groups, command-made text readable on dark pages, a one-step frame/group convert, delays on mouse triggers and manual overlay offsets, wrapped-line alignment; starter templates, a Templates tab and Getting started tips on the home screen: essentials ~57% → ~59%; everyday editing partials: number fields, outline options, Hide when publishing, cursors, panel widths, dark pages, numbers unchanged but menu breadth 211/211; text box sizes measured against documents re-saved by other software: whole-pixel line heights, advances on a 1024-per-em grid, letter spacing between glyphs only, numbers unchanged; .fig export checked by opening written files in other software, numbers unchanged; a public figcraft-server: open hosting, owner tokens, per-address limits, idle expiry, moderation, install.sh; experimental .fig export; keyboard shortcut parity suite and bindings; Simplified Chinese draft catalog; Latin American Spanish overlay; prototyping tail: gamepad triggers, widgets in prototypes, mode lookups; plugin directory on figcraft-server; Japanese and Korean draft catalogs and CJK UI fonts; plugin widgets and the plugin panel tail; French, German and Brazilian Portuguese draft catalogs; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and edit tail; dev resources links; extended collections tail; branch version history and finer list merges; headline percentages recomputed from the area table: feature breadth ~84%, core ~94%, mainstream ~50%; text lists, hanging punctuation and decoration styles; comments and version history behaviour suite and fixes; canvas navigation, guides and grids behaviour suite and fixes; export and inspect behaviour suite and fixes; .fig import: editable vector networks and more layer kinds; shapes and vector behaviour suite and fixes; paint and effects behaviour suite and fixes; frames, groups, sections and masks behaviour suite and fixes; styles and variables behaviour suite and fixes; prototyping behaviour suite and fixes; components behaviour suite and fixes; embedded image colour profiles; layers panel keyboard: F2 rename, Shift+F10 context menu, keyboard box selection, 10k-layer QA; per-side stroke weight variables; text behaviour suite: CJK line breaking, metrics, vertical trim; selection and transform behaviour suite and fixes; `.fig` import, best effort; auto layout behaviour suite and fixes; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; localization layer with a draft Spanish catalog; hosting figcraft-server: wss in the desktop app, server releases, deployment guide; plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings; feature breadth ~80%; ready ~49%; last major: full re-measure against Figma desktop 126.10.7; rows for the community batches: Selection colors, smart-selection spacing and a Present tab (#753), `.fig` import fidelity, paste from another design app, a `.fig` copy from Save as; control requests while the window is minimised (#720); catalog counts re-measured; automation roots (#715); number variables in the text content picker (#629); frames and sections convert into each other (#719); reviewed Russian catalog and integer plurals; vector edit mode regions: hover hatch, selection and drag; plugin log byte/count limits (#191); core editor leftovers: component playground, auto gap handles, variables inside styles, inner shadows and background blur on groups, command-made text readable on dark pages, a one-step frame/group convert, delays on mouse triggers and manual overlay offsets, wrapped-line alignment; starter templates, a Templates tab and Getting started tips on the home screen: essentials ~57% → ~59%; everyday editing partials: number fields, outline options, Hide when publishing, cursors, panel widths, dark pages, numbers unchanged but menu breadth 211/211; text box sizes measured against documents re-saved by other software: whole-pixel line heights, advances on a 1024-per-em grid, letter spacing between glyphs only, numbers unchanged; .fig export checked by opening written files in other software, numbers unchanged; a public figcraft-server: open hosting, owner tokens, per-address limits, idle expiry, moderation, install.sh; experimental .fig export; keyboard shortcut parity suite and bindings; Simplified Chinese draft catalog; Latin American Spanish overlay; prototyping tail: gamepad triggers, widgets in prototypes, mode lookups; plugin directory on figcraft-server; Japanese and Korean draft catalogs and CJK UI fonts; plugin widgets and the plugin panel tail; French, German and Brazilian Portuguese draft catalogs; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and edit tail; dev resources links; extended collections tail; branch version history and finer list merges; headline percentages recomputed from the area table: feature breadth ~84%, core ~94%, mainstream ~50%; text lists, hanging punctuation and decoration styles; comments and version history behaviour suite and fixes; canvas navigation, guides and grids behaviour suite and fixes; export and inspect behaviour suite and fixes; .fig import: editable vector networks and more layer kinds; shapes and vector behaviour suite and fixes; paint and effects behaviour suite and fixes; frames, groups, sections and masks behaviour suite and fixes; styles and variables behaviour suite and fixes; prototyping behaviour suite and fixes; components behaviour suite and fixes; embedded image colour profiles; layers panel keyboard: F2 rename, Shift+F10 context menu, keyboard box selection, 10k-layer QA; per-side stroke weight variables; text behaviour suite: CJK line breaking, metrics, vertical trim; selection and transform behaviour suite and fixes; `.fig` import, best effort; auto layout behaviour suite and fixes; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; localization layer with a draft Spanish catalog; hosting figcraft-server: wss in the desktop app, server releases, deployment guide; plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings; feature breadth ~80%; ready ~49%; last major: full re-measure against Figma desktop 126.10.7) · **Target:** Figma Design (desktop app 126.10.7, macOS) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-26 · **Change:** minor (truncated text fills its last line before the ellipsis and keeps truncating when resized; auto-height text with max lines (#1375, #1376, #1377), numbers unchanged; plugin widgets take text input and a property menu (plugin API 4); boards: templates and tables; boards: a shared timer and voting sessions; boards: the dot grid, connector ends dragged to re-attach, curved connectors; `.sketch` import, best effort: import/export breadth 86% → 87%, file formats 60% → 61%, headline numbers unchanged; Canvas & navigation's remaining work no longer lists outline mode's Include hidden layers and Include object bounds; boards: area 22 (weight 0, outside the weighted sum; CJK line breaking: JIS X 4051's line-start and line-end sets completed with the halfwidth small kana, the halfwidth and spacing (semi-)voiced sound marks, iteration marks 〱–〵 and the halfwidth parentheses; Turkish reviewed, Czech and Indonesian drafts, the Japanese draft completed; reviewed Russian catalog and integer plurals; vector edit mode regions: hover hatch, selection and drag; plugin log byte/count limits (#191); core editor leftovers: component playground, auto gap handles, variables inside styles, inner shadows and background blur on groups, command-made text readable on dark pages, a one-step frame/group convert, delays on mouse triggers and manual overlay offsets, wrapped-line alignment; starter templates, a Templates tab and Getting started tips on the home screen: essentials ~57% → ~59%; everyday editing partials: number fields, outline options, Hide when publishing, cursors, panel widths, dark pages, numbers unchanged but menu breadth 211/211; text box sizes measured against documents re-saved by other software: whole-pixel line heights, advances on a 1024-per-em grid, letter spacing between glyphs only, numbers unchanged; .fig export checked by opening written files in other software, numbers unchanged; a public figcraft-server: open hosting, owner tokens, per-address limits, idle expiry, moderation, install.sh; experimental .fig export; keyboard shortcut parity suite and bindings; Simplified Chinese draft catalog; Latin American Spanish overlay; prototyping tail: gamepad triggers, widgets in prototypes, mode lookups; plugin directory on figcraft-server; Japanese and Korean draft catalogs and CJK UI fonts; plugin widgets and the plugin panel tail; French, German and Brazilian Portuguese draft catalogs; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and edit tail; dev resources links; extended collections tail; branch version history and finer list merges; headline percentages recomputed from the area table: feature breadth ~84%, core ~94%, mainstream ~50%; text lists, hanging punctuation and decoration styles; comments and version history behaviour suite and fixes; canvas navigation, guides and grids behaviour suite and fixes; export and inspect behaviour suite and fixes; .fig import: editable vector networks and more layer kinds; shapes and vector behaviour suite and fixes; paint and effects behaviour suite and fixes; frames, groups, sections and masks behaviour suite and fixes; styles and variables behaviour suite and fixes; prototyping behaviour suite and fixes; components behaviour suite and fixes; embedded image colour profiles; layers panel keyboard: F2 rename, Shift+F10 context menu, keyboard box selection, 10k-layer QA; per-side stroke weight variables; text behaviour suite: CJK line breaking, metrics, vertical trim; selection and transform behaviour suite and fixes; `.fig` import, best effort; auto layout behaviour suite and fixes; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; localization layer with a draft Spanish catalog; hosting figcraft-server: wss in the desktop app, server releases, deployment guide; plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings; feature breadth ~80%; ready ~49%; last major: full re-measure against Figma desktop 126.10.7; rows for the community batches: Selection colors, smart-selection spacing and a Present tab (#753), `.fig` import fidelity, paste from another design app, a `.fig` copy from Save as; control requests while the window is minimised (#720); catalog counts re-measured; automation roots (#715); number variables in the text content picker (#629); frames and sections convert into each other (#719); reviewed Russian catalog and integer plurals; vector edit mode regions: hover hatch, selection and drag; plugin log byte/count limits (#191); core editor leftovers: component playground, auto gap handles, variables inside styles, inner shadows and background blur on groups, command-made text readable on dark pages, a one-step frame/group convert, delays on mouse triggers and manual overlay offsets, wrapped-line alignment; starter templates, a Templates tab and Getting started tips on the home screen: essentials ~57% → ~59%; everyday editing partials: number fields, outline options, Hide when publishing, cursors, panel widths, dark pages, numbers unchanged but menu breadth 211/211; text box sizes measured against documents re-saved by other software: whole-pixel line heights, advances on a 1024-per-em grid, letter spacing between glyphs only, numbers unchanged; .fig export checked by opening written files in other software, numbers unchanged; a public figcraft-server: open hosting, owner tokens, per-address limits, idle expiry, moderation, install.sh; experimental .fig export; keyboard shortcut parity suite and bindings; Simplified Chinese draft catalog; Latin American Spanish overlay; prototyping tail: gamepad triggers, widgets in prototypes, mode lookups; plugin directory on figcraft-server; Japanese and Korean draft catalogs and CJK UI fonts; plugin widgets and the plugin panel tail; French, German and Brazilian Portuguese draft catalogs; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and edit tail; dev resources links; extended collections tail; branch version history and finer list merges; headline percentages recomputed from the area table: feature breadth ~84%, core ~94%, mainstream ~50%; text lists, hanging punctuation and decoration styles; comments and version history behaviour suite and fixes; canvas navigation, guides and grids behaviour suite and fixes; export and inspect behaviour suite and fixes; .fig import: editable vector networks and more layer kinds; shapes and vector behaviour suite and fixes; paint and effects behaviour suite and fixes; frames, groups, sections and masks behaviour suite and fixes; styles and variables behaviour suite and fixes; prototyping behaviour suite and fixes; components behaviour suite and fixes; embedded image colour profiles; layers panel keyboard: F2 rename, Shift+F10 context menu, keyboard box selection, 10k-layer QA; per-side stroke weight variables; text behaviour suite: CJK line breaking, metrics, vertical trim; selection and transform behaviour suite and fixes; `.fig` import, best effort; auto layout behaviour suite and fixes; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; localization layer with a draft Spanish catalog; hosting figcraft-server: wss in the desktop app, server releases, deployment guide; plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings; feature breadth ~80%; ready ~49%; last major: full re-measure against Figma desktop 126.10.7; Canvas & navigation's remaining work no longer lists outline mode's Include hidden layers and Include object bounds; boards: area 22 (weight 0, outside the weighted sum; Turkish reviewed, Czech and Indonesian drafts, the Japanese draft completed; reviewed Russian catalog and integer plurals; vector edit mode regions: hover hatch, selection and drag; plugin log byte/count limits (#191); core editor leftovers: component playground, auto gap handles, variables inside styles, inner shadows and background blur on groups, command-made text readable on dark pages, a one-step frame/group convert, delays on mouse triggers and manual overlay offsets, wrapped-line alignment; starter templates, a Templates tab and Getting started tips on the home screen: essentials ~57% → ~59%; everyday editing partials: number fields, outline options, Hide when publishing, cursors, panel widths, dark pages, numbers unchanged but menu breadth 211/211; text box sizes measured against documents re-saved by other software: whole-pixel line heights, advances on a 1024-per-em grid, letter spacing between glyphs only, numbers unchanged; .fig export checked by opening written files in other software, numbers unchanged; a public figcraft-server: open hosting, owner tokens, per-address limits, idle expiry, moderation, install.sh; experimental .fig export; keyboard shortcut parity suite and bindings; Simplified Chinese draft catalog; Latin American Spanish overlay; prototyping tail: gamepad triggers, widgets in prototypes, mode lookups; plugin directory on figcraft-server; Japanese and Korean draft catalogs and CJK UI fonts; plugin widgets and the plugin panel tail; French, German and Brazilian Portuguese draft catalogs; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and edit tail; dev resources links; extended collections tail; branch version history and finer list merges; headline percentages recomputed from the area table: feature breadth ~84%, core ~94%, mainstream ~50%; text lists, hanging punctuation and decoration styles; comments and version history behaviour suite and fixes; canvas navigation, guides and grids behaviour suite and fixes; export and inspect behaviour suite and fixes; .fig import: editable vector networks and more layer kinds; shapes and vector behaviour suite and fixes; paint and effects behaviour suite and fixes; frames, groups, sections and masks behaviour suite and fixes; styles and variables behaviour suite and fixes; prototyping behaviour suite and fixes; components behaviour suite and fixes; embedded image colour profiles; layers panel keyboard: F2 rename, Shift+F10 context menu, keyboard box selection, 10k-layer QA; per-side stroke weight variables; text behaviour suite: CJK line breaking, metrics, vertical trim; selection and transform behaviour suite and fixes; `.fig` import, best effort; auto layout behaviour suite and fixes; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; localization layer with a draft Spanish catalog; hosting figcraft-server: wss in the desktop app, server releases, deployment guide; plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings; feature breadth ~80%; ready ~49%; last major: full re-measure against Figma desktop 126.10.7; rows for the community batches: Selection colors, smart-selection spacing and a Present tab (#753), `.fig` import fidelity, paste from another design app, a `.fig` copy from Save as; control requests while the window is minimised (#720); catalog counts re-measured; automation roots (#715); number variables in the text content picker (#629); frames and sections convert into each other (#719); reviewed Russian catalog and integer plurals; vector edit mode regions: hover hatch, selection and drag; plugin log byte/count limits (#191); core editor leftovers: component playground, auto gap handles, variables inside styles, inner shadows and background blur on groups, command-made text readable on dark pages, a one-step frame/group convert, delays on mouse triggers and manual overlay offsets, wrapped-line alignment; starter templates, a Templates tab and Getting started tips on the home screen: essentials ~57% → ~59%; everyday editing partials: number fields, outline options, Hide when publishing, cursors, panel widths, dark pages, numbers unchanged but menu breadth 211/211; text box sizes measured against documents re-saved by other software: whole-pixel line heights, advances on a 1024-per-em grid, letter spacing between glyphs only, numbers unchanged; .fig export checked by opening written files in other software, numbers unchanged; a public figcraft-server: open hosting, owner tokens, per-address limits, idle expiry, moderation, install.sh; experimental .fig export; keyboard shortcut parity suite and bindings; Simplified Chinese draft catalog; Latin American Spanish overlay; prototyping tail: gamepad triggers, widgets in prototypes, mode lookups; plugin directory on figcraft-server; Japanese and Korean draft catalogs and CJK UI fonts; plugin widgets and the plugin panel tail; French, German and Brazilian Portuguese draft catalogs; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and edit tail; dev resources links; extended collections tail; branch version history and finer list merges; headline percentages recomputed from the area table: feature breadth ~84%, core ~94%, mainstream ~50%; text lists, hanging punctuation and decoration styles; comments and version history behaviour suite and fixes; canvas navigation, guides and grids behaviour suite and fixes; export and inspect behaviour suite and fixes; .fig import: editable vector networks and more layer kinds; shapes and vector behaviour suite and fixes; paint and effects behaviour suite and fixes; frames, groups, sections and masks behaviour suite and fixes; styles and variables behaviour suite and fixes; prototyping behaviour suite and fixes; components behaviour suite and fixes; embedded image colour profiles; layers panel keyboard: F2 rename, Shift+F10 context menu, keyboard box selection, 10k-layer QA; per-side stroke weight variables; text behaviour suite: CJK line breaking, metrics, vertical trim; selection and transform behaviour suite and fixes; `.fig` import, best effort; auto layout behaviour suite and fixes; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; localization layer with a draft Spanish catalog; hosting figcraft-server: wss in the desktop app, server releases, deployment guide; plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings; feature breadth ~80%; ready ~49%; last major: full re-measure against Figma desktop 126.10.7) · **Target:** Figma Design (desktop app 126.10.7, macOS) The authoritative assessment of how close FigCraft is to Figma Design. [`ROADMAP.md`](../ROADMAP.md) summarizes it; [`gaps.md`](gaps.md) is the ranked work list; the measured menu checklist is @@ -285,7 +285,9 @@ Hours in this narrative are the builders' estimates; the tables above supersede unlinks the chars edited). Live previews of a hovered named instance or font style and of a dragged axis slider (one undo step on release, Escape cancels). Visual QA: list markers keep the paragraph's font, renames start with the name selected and centred, Max lines shows while - truncating. Right-to-left paragraphs mirror lists and indents: bullets and numbers (".1") on + truncating. Truncation fills the last line shown before the ellipsis ("Lorem consec…", not + "Lorem…"), a truncated text resized keeps truncating, and auto-height text with Max lines hugs + up to that many lines. Right-to-left paragraphs mirror lists and indents: bullets and numbers (".1") on the right, wrapped lines hanging from the right, the paragraph indent on the right, trailing spaces hanging outside the line (canvas, PNG, PDF and SVG). A behaviour suite from the public documentation (`crates/text/src/behaviour_tests.rs`, `crates/engine/src/text_behaviour_tests.rs`):