mirror of
https://github.com/storytold/figcraft.git
synced 2026-11-13 13:52:34 +00:00
M7: Text styles: a face name bound after a weight clears the axis; review fixes (#2174)
A style's fontStyle binding now goes through set_font_style, so it drops the stale wght axis a weight binding left behind. Also: a test and doc line for the token_roles weight role, the apply-order comment, the weight chip opening the weight picker, junk weight values pinned, fixture assertions fixed, and the ROADMAP and parity status lines. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Model-Effort: Opus 5.5 drove; Fable 5.1 reviewed; Sonnet 5.5 wrote at medium
This commit is contained in:
1 parent
ccb33e0bbc
commit
281918e2f2
8 files changed
+57
-9
No files matched your search
+1
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
**Stage: alpha** · next: beta, ~26 points and ~162–292 h away (one Opus 5.5 agent; ~33–67 h wall-clock with 5 agents)
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-07 · **Change:** patch (a text style binds a number variable to its weight (#2174); the handoff mode reads Developer Mode in the interface, its ids and saved fields unchanged; the left rail's and tab strip's icons are larger, 20 pt (#553); Suggest auto layout adds nested auto layout frames by rows and columns and keeps the placement of the design, as the help center describes (#1823); the Local variables window selects several variables (click, Shift+click for a range, Cmd/Ctrl+click to toggle, Escape to clear) and delete, move to a group and scope act on the selected local variables in one undo step (#2002); the desktop UI takes a system face for Ethiopic, Devanagari, Gurmukhi, Gujarati, Tamil, Telugu, Kannada and Malayalam, Sinhala, Lao, Myanmar, Khmer, Armenian and Georgian as it did for Hebrew and Arabic, so a String variable's Amharic value draws in the panels instead of boxes (#2014); a layer's effect binding takes only a field its effect has; ; the boolean operation menu leads each entry with an original icon (#2003); glass takes one sample instead of its dispersion window where the colours split under 1/256 px; glass bends towards its core, and rounded rectangles work it out in closed form; glass's light is a line on its bevel and splay turns the bend towards the centre; glass bends with a thick-lens profile and splits its colours over 8 steps; glass effects take variables and keep their frost within 0–100; web host bridge: `?host=parent` or `data-host="page"` hands a page the app's files, documented in the hosting guide; a connector end moved with Cmd/Ctrl held goes on any part of an object, and stays there as the object moves or resizes; a tap on a plugin panel's button keeps what was typed in its fields; a Dash cap setting (none, round or square) on Dash and Custom strokes, drawn on both ends of every dash; library analytics list how many layers and files use each style and variable; the Controller menu names each button as PlayStation and Switch controllers do; widgets draw lines and SVG pictures (plugin API 6); a connector end dropped along a side of a shape attaches to that side (#1708); regression tests pin a rounded shape's one-sided stroke and SVG ids and font families without XML-forbidden characters (#1351, #1124); a damaged image is refused with a reason instead of showing as an empty box (#1474); an image copied in another app pastes again on the desktop, and in the browser for the first time (#492); SwiftUI and Compose code names a bound variable by its iOS or Android code syntax; 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 Frame tool's Phone presets have the iPhone 17 family (#1691); Show in folder works in the Flatpak build and selects the file on Linux (#1701); a dashed line starts and ends with a half-length dash, on the canvas and in exports; the Pages, Layers and Assets search fields keep their text in the middle of the field (#1703); a board connector's path can be reshaped by dragging its path handles (#1710); boards have 16 more shapes (arrows, polygons, star, plus and flowchart symbols), in pickers that lay them out in a grid; library analytics count insertions and show insertions and detachments over the past 30 days, 90 days or year; a stroke's Custom style takes a dash pattern of any length as a comma-separated list (#1681); a control request can set its own timeout, up to 10 minutes (`timeoutMs`; `figcraft-cli mcp --attach PORT --timeout SECONDS`) (#1696); gap 6: a widget opens its plugin's panel (plugin API 5), whose callbacks redraw it; the keyboard's Menu key opens the context menu, as Shift+F10 does; an Open link action can open in place of the prototype, with "Open in new tab" off (gap 15); a collection of this file can be extended (Extend collection on its row, `variable.extendCollection`), its extension following its edits at once, and extended collections extend again in the file; the eyedropper with Cmd/Ctrl+Shift makes a colour style or variable of what it picks; a live session's welcome sends the document compressed to clients that read it, about seven times smaller (#1665); a control request that times out says truthfully whether it was dropped or is still running and will take effect, and only screenshots and input blame the window (#1673); a board keeps earlier votes' results, listed newest first in Results and deleted one at a time; the object a stamp will stick to is outlined while the stamp tool is over it or a stamp is dragged over it, and board objects take no drops (gap 24); a server can list the files shared from it now for everyone who reaches it, Home ▸ Online shows them and their links carry no secret (`team_files`, part of #742); team library analytics count detached instances per component and file (gap 10); skipping the Open link notLine truncated
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-10 · **Change:** minor (a text style binds a number variable to its weight (#2174); the handoff mode reads Developer Mode in the interface, its ids and saved fields unchanged; the left rail's and tab strip's icons are larger, 20 pt (#553); Suggest auto layout adds nested auto layout frames by rows and columns and keeps the placement of the design, as the help center describes (#1823); the Local variables window selects several variables (click, Shift+click for a range, Cmd/Ctrl+click to toggle, Escape to clear) and delete, move to a group and scope act on the selected local variables in one undo step (#2002); the desktop UI takes a system face for Ethiopic, Devanagari, Gurmukhi, Gujarati, Tamil, Telugu, Kannada and Malayalam, Sinhala, Lao, Myanmar, Khmer, Armenian and Georgian as it did for Hebrew and Arabic, so a String variable's Amharic value draws in the panels instead of boxes (#2014); a layer's effect binding takes only a field its effect has; ; the boolean operation menu leads each entry with an original icon (#2003); glass takes one sample instead of its dispersion window where the colours split under 1/256 px; glass bends towards its core, and rounded rectangles work it out in closed form; glass's light is a line on its bevel and splay turns the bend towards the centre; glass bends with a thick-lens profile and splits its colours over 8 steps; glass effects take variables and keep their frost within 0–100; web host bridge: `?host=parent` or `data-host="page"` hands a page the app's files, documented in the hosting guide; a connector end moved with Cmd/Ctrl held goes on any part of an object, and stays there as the object moves or resizes; a tap on a plugin panel's button keeps what was typed in its fields; a Dash cap setting (none, round or square) on Dash and Custom strokes, drawn on both ends of every dash; library analytics list how many layers and files use each style and variable; the Controller menu names each button as PlayStation and Switch controllers do; widgets draw lines and SVG pictures (plugin API 6); a connector end dropped along a side of a shape attaches to that side (#1708); regression tests pin a rounded shape's one-sided stroke and SVG ids and font families without XML-forbidden characters (#1351, #1124); a damaged image is refused with a reason instead of showing as an empty box (#1474); an image copied in another app pastes again on the desktop, and in the browser for the first time (#492); SwiftUI and Compose code names a bound variable by its iOS or Android code syntax; 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 Frame tool's Phone presets have the iPhone 17 family (#1691); Show in folder works in the Flatpak build and selects the file on Linux (#1701); a dashed line starts and ends with a half-length dash, on the canvas and in exports; the Pages, Layers and Assets search fields keep their text in the middle of the field (#1703); a board connector's path can be reshaped by dragging its path handles (#1710); boards have 16 more shapes (arrows, polygons, star, plus and flowchart symbols), in pickers that lay them out in a grid; library analytics count insertions and show insertions and detachments over the past 30 days, 90 days or year; a stroke's Custom style takes a dash pattern of any length as a comma-separated list (#1681); a control request can set its own timeout, up to 10 minutes (`timeoutMs`; `figcraft-cli mcp --attach PORT --timeout SECONDS`) (#1696); gap 6: a widget opens its plugin's panel (plugin API 5), whose callbacks redraw it; the keyboard's Menu key opens the context menu, as Shift+F10 does; an Open link action can open in place of the prototype, with "Open in new tab" off (gap 15); a collection of this file can be extended (Extend collection on its row, `variable.extendCollection`), its extension following its edits at once, and extended collections extend again in the file; the eyedropper with Cmd/Ctrl+Shift makes a colour style or variable of what it picks; a live session's welcome sends the document compressed to clients that read it, about seven times smaller (#1665); a control request that times out says truthfully whether it was dropped or is still running and will take effect, and only screenshots and input blame the window (#1673); a board keeps earlier votes' results, listed newest first in Results and deleted one at a time; the object a stamp will stick to is outlined while the stamp tool is over it or a stamp is dragged over it, and board objects take no drops (gap 24); a server can list the files shared from it now for everyone who reaches it, Home ▸ Online shows them and their links carry no secret (`team_files`, part of #742); team library analytics count detached instances per component and file (gap 10); skipping the Open link notLine truncated
|
||||
|
||||
FigCraft is a clean-room, open-source, pure-Rust reimplementation of the Figma design workflow,
|
||||
native on macOS, Windows, Linux and FreeBSD and in the browser, fully drivable by agents through
|
||||
|
||||
@@ -113,6 +113,8 @@ pub fn from_layer(n: &Node, field: &str, kind: &StyleKind) -> BTreeMap<String, V
|
||||
/// `st`'s value with its bound fields resolved in `modes`.
|
||||
fn resolved(vars: &Variables, st: &Style, modes: &Modes) -> StyleKind {
|
||||
let mut kind = st.kind.clone();
|
||||
// `bound` is a BTreeMap, so `fontStyle` applies before `fontWeight` and a weight wins when a file carries both
|
||||
// (pinned by `font_weight_wins_over_font_style_when_a_file_carries_both`).
|
||||
for (key, var) in &st.bound {
|
||||
let Some(val) = vars.resolve(var, modes) else { continue };
|
||||
match (&mut kind, list_binding(key), &val) {
|
||||
@@ -136,7 +138,7 @@ fn set_text_field(style: &mut TextStyle, key: &str, val: &VariableValue) {
|
||||
("lineHeight", VariableValue::Number(x)) if x.is_finite() => style.line_height = LineHeight::Pixels(x.clamp(0.0, 100_000.0)),
|
||||
("letterSpacing", VariableValue::Number(x)) if x.is_finite() => style.letter_spacing = LetterSpacing::Pixels(x.clamp(-10_000.0, 10_000.0)),
|
||||
("fontFamily", VariableValue::String(f)) if !f.trim().is_empty() => style.font_family = f.clone(),
|
||||
("fontStyle", VariableValue::String(f)) if !f.trim().is_empty() => style.font_style = f.clone(),
|
||||
("fontStyle", VariableValue::String(f)) if !f.trim().is_empty() => style.set_font_style(f.clone()),
|
||||
("fontWeight", VariableValue::Number(x)) if x.is_finite() => {
|
||||
// The face nearest the number (a static family) and the `wght` axis (a variable font).
|
||||
let w = x.clamp(1.0, 1000.0);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! What a file's number variables measure, so exported tokens carry the right unit: a radius,
|
||||
//! font size, spacing or other length is in pixels, opacity and font weight are bare numbers.
|
||||
//! A number's role comes from the layer fields bound to it, else its scopes, and an alias target
|
||||
//! A number's role comes from the layer fields bound to it (and a text style's weight), else its scopes, and an alias target
|
||||
//! with no role of its own takes the role of the variables that read it (the target of an opacity
|
||||
//! alias stays unitless). A number nothing tells about is a length: design tokens mostly are.
|
||||
|
||||
|
||||
@@ -289,6 +289,24 @@ fn a_number_weight_picks_the_face_and_the_wght_axis() {
|
||||
run(&mut s, "style.bind", json!({"style": body, "field": "fontStyle", "variable": "face"}));
|
||||
let bound = s.doc().unwrap().style(&body).unwrap().bound.clone();
|
||||
assert!(bound.contains_key("fontStyle") && !bound.contains_key("fontWeight"), "{bound:?}");
|
||||
// The name replaces the number's weight: no stale `wght` axis keeps drawing 700.
|
||||
assert_eq!(face(&s, t1), ("Light".to_string(), None));
|
||||
assert_eq!(text_of(&s, t1).weight(), 300);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn junk_weight_values_clamp_to_the_lightest_and_heaviest_face() {
|
||||
let mut s = Session::new();
|
||||
let (t1, _, body) = body_style(&mut s, None);
|
||||
run(&mut s, "variable.create", json!({"collection": "Theme", "name": "weight", "type": "number", "values": {"Light": -50, "Dark": 5000}}));
|
||||
run(&mut s, "style.bind", json!({"style": body, "field": "fontWeight", "variable": "weight"}));
|
||||
let face = |s: &Session| {
|
||||
let t = text_of(s, t1);
|
||||
(t.font_style, t.variations.get("wght").copied())
|
||||
};
|
||||
assert_eq!(face(&s), ("Thin".to_string(), Some(1.0)));
|
||||
run(&mut s, "variable.set", json!({"variable": "weight", "mode": "Light", "value": 5000}));
|
||||
assert_eq!(face(&s), ("Black".to_string(), Some(1000.0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -562,6 +562,19 @@ fn css_export_gives_lengths_a_unit_and_keeps_opacity_bare() {
|
||||
assert!(css.contains("--card: 12px;") && css.contains("--fade: 0.5;") && css.contains("--half: 0.5;"), "{css}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn css_export_keeps_a_style_weight_bare() {
|
||||
let mut s = Session::new();
|
||||
ex(&mut s, "variable.createCollection", json!({"name": "Brand"}));
|
||||
ex(&mut s, "variable.create", json!({"collection": "Brand", "name": "weight", "type": "number", "value": 700}));
|
||||
let t = idv(&ex(&mut s, "text.create", json!({"text": "Hi", "x": 0, "y": 0})));
|
||||
let st = ex(&mut s, "style.create", json!({"type": "text", "name": "Body", "from": t}))["id"].as_str().unwrap_or_default().to_string();
|
||||
ex(&mut s, "style.bind", json!({"style": st, "field": "fontWeight", "variable": "weight"}));
|
||||
let css = ex(&mut s, "variable.export", json!({"format": "css"}))["text"].as_str().unwrap_or_default().to_string();
|
||||
// A weight bound only to a text style is still a bare number, not a length.
|
||||
assert!(css.contains("--weight: 700;") && !css.contains("700px"), "{css}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn review_a_long_alias_chain_keeps_its_role_to_the_end() {
|
||||
let mut s = Session::new();
|
||||
|
||||
@@ -726,6 +726,8 @@ struct TextBinding<'a> {
|
||||
fn text_binding(ui: &mut Ui, binding: TextBinding<'_>, out: &mut Frame) {
|
||||
let TextBinding { style, field, kind, rect, name, lead } = binding;
|
||||
match widgets::bound_field(ui, Id::unique(("ts_bound", style, field)), lead, rect, name) {
|
||||
// The weight slot takes a face name or a number, so its chip opens the picker that offers both.
|
||||
Some(widgets::BindEvent::Open(at)) if matches!(field, "fontWeight" | "fontStyle") => crate::variable_ui::open_style_weight(ui, style, at),
|
||||
Some(widgets::BindEvent::Open(at)) => crate::variable_ui::open_style(ui, style, field, kind, at),
|
||||
Some(widgets::BindEvent::Detach) => out.act = Some(Act::Unbind(style.to_string(), field.to_string())),
|
||||
None => {}
|
||||
|
||||
@@ -488,14 +488,27 @@ fn the_weight_menu_binds_a_number_variable_as_font_weight() {
|
||||
def.font_style
|
||||
};
|
||||
assert_eq!(font_style(), "Bold");
|
||||
// The chip shows it in the style slot, and its detach button clears the weight binding.
|
||||
// The chip shows it in the style slot; clicking it lists the string variable too, and picking that
|
||||
// replaces the number: fontStyle is bound and fontWeight is not.
|
||||
for _ in 0..2 {
|
||||
u.frame(vec![]);
|
||||
}
|
||||
let texts = in_editor(&mut u);
|
||||
let chip = text_at(&texts, "Body weight number");
|
||||
click(&mut u, text_at(&texts, "Body weight number"));
|
||||
assert!(crate::variable_ui::picker_open(&u.ctx), "the chip opens the weight picker");
|
||||
let texts = drawn_texts(&mut u);
|
||||
assert!(texts.iter().any(|(t, _)| t == "Body weight"), "the string variable is offered beside the number one");
|
||||
click(&mut u, text_at(&texts, "Body weight"));
|
||||
assert_eq!(bound_to(&u, "fontStyle").as_deref(), Some("Body weight"));
|
||||
assert_eq!(bound_to(&u, "fontWeight"), None, "the name replaced the number");
|
||||
// The chip's detach button clears the binding.
|
||||
for _ in 0..2 {
|
||||
u.frame(vec![]);
|
||||
}
|
||||
let texts = in_editor(&mut u);
|
||||
let chip = text_at(&texts, "Body weight");
|
||||
click(&mut u, Pos2::new(left + (W - 8.0) / 2.0 - 11.0, chip.y));
|
||||
assert_eq!(bound_to(&u, "fontWeight"), None, "the weight is detached");
|
||||
assert_eq!(bound_to(&u, "fontStyle"), None, "the face name is detached");
|
||||
assert!(u.app.ui.last_error.is_none(), "{:?}", u.app.ui.last_error);
|
||||
}
|
||||
|
||||
@@ -522,7 +535,7 @@ fn text_style_editor_binds_variables_from_its_fields() {
|
||||
click(&mut u, Pos2::new(x, y));
|
||||
assert!(crate::variable_ui::picker_open(&u.ctx), "{field}: the variable picker opens");
|
||||
let texts = drawn_texts(&mut u);
|
||||
assert!(!texts.iter().any(|(t, _)| t == "Body font"), "{field} offers number variables only");
|
||||
assert!(!texts.iter().any(|(t, _)| t == "Body weight"), "{field} offers number variables only");
|
||||
click(&mut u, text_at(&texts, name));
|
||||
assert_eq!(bound_to(&u, field).as_deref(), Some(name));
|
||||
}
|
||||
@@ -536,7 +549,7 @@ fn text_style_editor_binds_variables_from_its_fields() {
|
||||
assert!(crate::variable_ui::picker_open(&u.ctx), "{field}: the variable picker opens");
|
||||
let texts = drawn_texts(&mut u);
|
||||
// "Body size" shows once, on the size field's chip: the picker doesn't list it.
|
||||
assert_eq!(texts.iter().filter(|(t, _)| t == "Body size").count(), 1, "{field} offers string variables only");
|
||||
assert_eq!(texts.iter().filter(|(t, _)| t == "Body size").count(), 1, "{field} does not offer the size variable");
|
||||
click(&mut u, text_at(&texts, name));
|
||||
assert_eq!(bound_to(&u, field).as_deref(), Some(name));
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Components, variants, styles and variables parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-06 · **Change:** minor (the Local variables window selects several variables (click, Shift+click for a range, Cmd/Ctrl+click to toggle, Escape to clear) and delete, move to a group and scope act on the selected local variables in one undo step (#2002); text styles carry paragraph spacing and indent, the list style and the OpenType features, as the Supported in text styles table of Create and apply text styles says (#1829); a layer's effect binding takes only a field its effect has; Go to main component on an instance of a library component opens the library file in a tab and selects the main there; glass effects take variables; Delete on a layer inside an instance hides it as an override instead of being refused, as the help center says: "Figma will only toggle the layer's visibility instead of removing it"; library analytics list how many layers and files use each style and variable; SwiftUI and Compose code names a bound variable by its iOS or Android code syntax; library analytics count insertions and show insertions and detachments over the past 30 days, 90 days or year; a collection of this file can be extended (Extend collection on its row, `variable.extendCollection`), its extension following its edits at once, and extended collections extend again in the file; a typed or stored variable, collection or mode id names only the object with that id (#1613); colour variables can be calculated from other colours per mode — `{alpha}` replaces a colour's alpha, `{mix}` blends two — and follow them (#1280); stored variable and collection references keep their identity when another object is named like their id (follow-up to #1542); a component set's property rows fit the 240 px right panel: variant values wrap two to a line there and every + and − stays in view; opening a file contacts no team library server it names until you check it (#1097); an explicit variables file path (`variable.import`, the `variables` of `file.open` and `file.import`) is refused past 256 MiB or when it isn't a regular file, as the sibling `variables.json` already was (part of #1114); component properties keep the order they were made in when one is renamed, and Make slot asks for the slot's name first (#1536, #1537); components can't contain their own instances, directly or through others (#1084); the instance-swap menus have a search field (#1338); an instance-swap property's default menu scrolls (#1338); variables on a text style's fields from the style editor; instance-swap properties in the component playground; duplicating or Alt-dragging a main component makes an instance of it (#892); a bound colour variable's alpha applies once (#802); variables import reads `variables` outside `meta` and warns when it imports none (#790); variant properties reorder by drag; library files saved right after a read are read again; Hide when publishing note corrected; size and resizing overrides of layers inside instances (#731); variables export in the browser as a download; core editor leftovers: component playground, variables inside styles; layers and assets panels behaviour suite and fixes; extended collections tail; styles and variables behaviour suite and fixes; components behaviour suite and fixes; team libraries on the server, library usage and swapping a library; extended collections, variables import/export, gradient stops and variant properties bound to variables) · **Target:** Figma Design (desktop app 126.10.7)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-10 · **Change:** minor (a text style binds a number variable to its weight (#2174); the Local variables window selects several variables (click, Shift+click for a range, Cmd/Ctrl+click to toggle, Escape to clear) and delete, move to a group and scope act on the selected local variables in one undo step (#2002); text styles carry paragraph spacing and indent, the list style and the OpenType features, as the Supported in text styles table of Create and apply text styles says (#1829); a layer's effect binding takes only a field its effect has; Go to main component on an instance of a library component opens the library file in a tab and selects the main there; glass effects take variables; Delete on a layer inside an instance hides it as an override instead of being refused, as the help center says: "Figma will only toggle the layer's visibility instead of removing it"; library analytics list how many layers and files use each style and variable; SwiftUI and Compose code names a bound variable by its iOS or Android code syntax; library analytics count insertions and show insertions and detachments over the past 30 days, 90 days or year; a collection of this file can be extended (Extend collection on its row, `variable.extendCollection`), its extension following its edits at once, and extended collections extend again in the file; a typed or stored variable, collection or mode id names only the object with that id (#1613); colour variables can be calculated from other colours per mode — `{alpha}` replaces a colour's alpha, `{mix}` blends two — and follow them (#1280); stored variable and collection references keep their identity when another object is named like their id (follow-up to #1542); a component set's property rows fit the 240 px right panel: variant values wrap two to a line there and every + and − stays in view; opening a file contacts no team library server it names until you check it (#1097); an explicit variables file path (`variable.import`, the `variables` of `file.open` and `file.import`) is refused past 256 MiB or when it isn't a regular file, as the sibling `variables.json` already was (part of #1114); component properties keep the order they were made in when one is renamed, and Make slot asks for the slot's name first (#1536, #1537); components can't contain their own instances, directly or through others (#1084); the instance-swap menus have a search field (#1338); an instance-swap property's default menu scrolls (#1338); variables on a text style's fields from the style editor; instance-swap properties in the component playground; duplicating or Alt-dragging a main component makes an instance of it (#892); a bound colour variable's alpha applies once (#802); variables import reads `variables` outside `meta` and warns when it imports none (#790); variant properties reorder by drag; library files saved right after a read are read again; Hide when publishing note corrected; size and resizing overrides of layers inside instances (#731); variables export in the browser as a download; core editor leftovers: component playground, variables inside styles; layers and assets panels behaviour suite and fixes; extended collections tail; styles and variables behaviour suite and fixes; components behaviour suite and fixes; team libraries on the server, library usage and swapping a library; extended collections, variables import/export, gradient stops and variant properties bound to variables) · **Target:** Figma Design (desktop app 126.10.7)
|
||||
|
||||
Design systems are why teams adopt Figma. **Components: breadth ~96%, ready ~72%. Styles and
|
||||
variables: breadth ~95%, ready ~76% (estimated).** QA'd with a synthetic design system of ~300
|
||||
|
||||
Reference in new issue
Block a user