feat: 新增 ArkUI 代码生成和代码连接支持

添加 ArkUI 作为新的代码语言支持,包括:
- 代码生成:支持生成 ArkTS 组件代码,包括 `@Component` 结构体、`@Prop` 参数、`@BuilderParam` 构建器等
- 代码连接:支持将组件连接到 ArkTS 代码,包括导入路径和模板配置
- 测试:添加 ArkUI 相关测试用例,验证代码生成和连接功能的正确性
- 更新文档:更新开发模式 parity 文档、文件格式一致性文档等,添加 ArkUI 相关说明
- 更新国际化:在所有支持的语言中添加 ArkUI 选项提示
This commit is contained in:
Nicek
2026-10-22 20:36:45 +08:00
parent 37921f48ce
commit 7e28d58b3f
30 changed files with 464 additions and 65 deletions
+9 -6
View File
@@ -37,7 +37,7 @@ const MAX_WARNINGS: usize = 50;
/// The languages a component can be connected in.
pub fn language(s: &str) -> Option<CodeLang> {
CodeLang::from_id(s).filter(|l| matches!(l, CodeLang::React | CodeLang::Leptos | CodeLang::SwiftUi | CodeLang::Compose))
CodeLang::from_id(s).filter(|l| matches!(l, CodeLang::React | CodeLang::Leptos | CodeLang::SwiftUi | CodeLang::Compose | CodeLang::ArkUi))
}
/// The layer `id`'s links are stored on: its component set, else its component (an instance's
@@ -64,7 +64,7 @@ pub fn import_line(link: &CodeLink, lang: CodeLang) -> String {
let path = &link.import_path;
match lang {
_ if path.is_empty() => String::new(),
CodeLang::React => format!("import {{ {} }} from {};", link.name.split('.').next().unwrap_or_default(), js_string(path)),
CodeLang::React | CodeLang::ArkUi => format!("import {{ {} }} from {};", link.name.split('.').next().unwrap_or_default(), js_string(path)),
CodeLang::Leptos => format!("use {path}::{};", link.name.split('.').next().unwrap_or_default()),
_ => format!("import {path}"),
}
@@ -175,6 +175,7 @@ fn expr(doc: &Document, c: &Component, p: &Param, v: &Value, lang: CodeLang) ->
(Some(l), CodeLang::React) => format!("<{} />", l.name),
(Some(l), CodeLang::Leptos) => format!("|| view! {{ <{} /> }}", l.name),
(Some(l), CodeLang::SwiftUi) => format!("{}()", l.name),
(Some(l), CodeLang::ArkUi) => format!("() => {{ {}() }}", l.name),
(Some(l), _) => format!("{{ {}() }}", l.name),
(None, _) => value_expr(doc, c, p, v, lang),
}
@@ -222,6 +223,7 @@ fn default_template(c: &Component, lang: CodeLang, name: &str) -> String {
format!("<{name}{} />", params.iter().map(|p| format!(" {}={{{p}}}", crate::component_codegen::rust_ident(p))).collect::<String>())
}
CodeLang::SwiftUi => format!("{name}({})", params.iter().map(|p| format!("{p}: {{{p}}}")).collect::<Vec<_>>().join(", ")),
CodeLang::ArkUi => format!("{name}({{ {} }})", params.iter().map(|p| format!("{p}: {{{p}}}")).collect::<Vec<_>>().join(", ")),
_ => format!("{name}({})", params.iter().map(|p| format!("{p} = {{{p}}}")).collect::<Vec<_>>().join(", ")),
}
}
@@ -239,13 +241,14 @@ pub fn check(c: &Component, link: &CodeLink, lang: CodeLang) -> std::result::Res
}
let path = &link.import_path;
let path_ok = match lang {
CodeLang::React => !path.chars().any(|ch| ch.is_control() || ch == '"' || ch == '\\'),
CodeLang::React | CodeLang::ArkUi => !path.chars().any(|ch| ch.is_control() || ch == '"' || ch == '\\'),
CodeLang::Leptos => path.chars().all(|ch| ch.is_ascii_alphanumeric() || ch == '_' || ch == ':'),
_ => path.chars().all(|ch| ch.is_ascii_alphanumeric() || ch == '_' || ch == '.'),
};
if path.chars().count() > MAX_IMPORT || !path_ok {
return Err(match lang {
CodeLang::React => format!("importPath: a module path without quotes, up to {MAX_IMPORT} characters"),
CodeLang::ArkUi => format!("importPath: a module path without quotes, up to {MAX_IMPORT} characters"),
CodeLang::Leptos => format!("importPath: a Rust module path (letters, digits, _ and ::), up to {MAX_IMPORT} characters"),
_ => format!("importPath: a module or package name (letters, digits, _ and .), up to {MAX_IMPORT} characters"),
});
@@ -282,7 +285,7 @@ fn text(cmd: &str, p: &Value, key: &str) -> Result<Option<String>> {
/// The `language` parameter (default React).
fn lang_param(cmd: &str, p: &Value) -> Result<CodeLang> {
match text(cmd, p, "language")? {
Some(l) => language(&l).ok_or_else(|| bad(cmd, "language: react|leptos|swiftui|compose")),
Some(l) => language(&l).ok_or_else(|| bad(cmd, "language: react|leptos|swiftui|compose|arkui")),
None => Ok(CodeLang::React),
}
}
@@ -312,7 +315,7 @@ pub fn specs() -> Vec<CommandSpec> {
spec!(
"codeConnect.set",
"Connect to code",
"{id? (component, set or instance; default the selection), language?: react|leptos|swiftui|compose (default react), importPath?, name? (default the generated name), template? (default a call passing every property)} links the component (its set, for a variant) to its code component; fields left out keep their current value. Placeholders: {property} (its name or parameter name) and {property ? \"a\" : \"b\"}; other {…} stay as written → {component, language, code (the snippet for the layer), unknown (placeholder names that aren't properties)}",
"{id? (component, set or instance; default the selection), language?: react|leptos|swiftui|compose|arkui (default react), importPath?, name? (default the generated name), template? (default a call passing every property)} links the component (its set, for a variant) to its code component; fields left out keep their current value. Placeholders: {property} (its name or parameter name) and {property ? \"a\" : \"b\"}; other {…} stay as written → {component, language, code (the snippet for the layer), unknown (placeholder names that aren't properties)}",
true,
|s, p| {
let cmd = "codeConnect.set";
@@ -427,7 +430,7 @@ fn import(s: &mut Session, p: &Value) -> Result<Value> {
let (Some(component), Some(lang), Some(name), Some(template)) =
(field("component"), field("language").and_then(language), field("name"), e.get("template").and_then(Value::as_str))
else {
warnings.push(format!("link {}: needs component, language (react|leptos|swiftui|compose), name and template strings", i + 1));
warnings.push(format!("link {}: needs component, language (react|leptos|swiftui|compose|arkui), name and template strings", i + 1));
continue;
};
let link = CodeLink {
+11 -1
View File
@@ -72,6 +72,16 @@ fn connected_instances_show_the_real_snippet() {
json!({"id": c, "language": "leptos", "importPath": "crate::ui", "template": "<Card title={title} dotted={Show dot} />"}),
);
assert_eq!(code(&mut s, i, "leptos"), "use crate::ui::Card;\n\n<Card title={\"Say \\\"hi\\\"\"} dotted={false} />\n");
// ArkUI: an ArkTS import and an object-literal call.
ex(
&mut s,
"codeConnect.set",
json!({"id": c, "language": "arkui", "importPath": "@acme/arkui", "template": "Card({ title: {title}, dotted: {Show dot} })"}),
);
assert_eq!(code(&mut s, i, "arkui"), "import { Card } from \"@acme/arkui\";\n\nCard({ title: \"Say \\\"hi\\\"\", dotted: false })\n");
let v = ex(&mut s, "inspect.componentCode", json!({"id": i, "language": "arkui"}));
assert_eq!(v["usage"], "Card({ title: \"Say \\\"hi\\\"\", dotted: false })\n");
assert_eq!(v["connected"]["import"], "import { Card } from \"@acme/arkui\";");
assert!(s.execute("codeConnect.set", &json!({"id": c, "language": "leptos", "importPath": "crate/ui"})).is_err(), "a Rust path");
ex(&mut s, "codeConnect.remove", json!({"id": c, "language": "leptos"}));
// inspect.componentCode: the snippet as the usage, and the link.
@@ -105,7 +115,7 @@ fn connected_instances_show_the_real_snippet() {
assert!(code(&mut s, i, "swiftui").starts_with("import AcmeUI"));
ex(&mut s, "edit.undo", json!({}));
assert!(code(&mut s, i, "react").starts_with("import { Card }"));
assert_eq!(ex(&mut s, "codeConnect.remove", json!({"id": c}))["removed"], 2);
assert_eq!(ex(&mut s, "codeConnect.remove", json!({"id": c}))["removed"], 3);
assert!(s.execute("codeConnect.remove", &json!({"id": c})).is_err(), "nothing left");
}
+260 -10
View File
@@ -1,9 +1,9 @@
//! Code for a layer, as shown by the Inspect panel and returned by `inspect.code`: CSS for the
//! layer itself, HTML or React for its subtree, and SwiftUI or Jetpack Compose (stacks for auto
//! layout, absolute offsets otherwise). Components become components whose properties are
//! parameters and instances become calls passing their values (see [`crate::component_codegen`]);
//! variables bound to a layer become CSS custom properties. The output is a starting point, not a
//! pixel-exact translation.
//! layer itself, HTML or React for its subtree, and SwiftUI, Jetpack Compose or ArkUI (HarmonyOS;
//! stacks for auto layout, absolute offsets otherwise). Components become components whose
//! properties are parameters and instances become calls passing their values (see
//! [`crate::component_codegen`]); variables bound to a layer become CSS custom properties. The
//! output is a starting point, not a pixel-exact translation.
use figcraft_color::{Color, PaintKind};
use figcraft_doc::*;
@@ -18,13 +18,13 @@ pub fn specs() -> Vec<CommandSpec> {
vec![spec!(
"inspect.code",
"Code for layer",
"{id?, language?: css|html|react|leptos|swiftui|compose (default css; html, react and leptos are the whole layer tree; components take their properties as parameters, instances pass their values; a component connected to code (codeConnect.set) gives its import and snippet instead), unit?: px|rem (CSS, HTML, React and Leptos; default px), rootFontSize? (rem: pixels per rem, default 16)} → {code, language, connected}",
"{id?, language?: css|html|react|leptos|swiftui|compose|arkui (default css; html, react and leptos are the whole layer tree; components take their properties as parameters, instances pass their values; a component connected to code (codeConnect.set) gives its import and snippet instead), unit?: px|rem (CSS, HTML, React and Leptos; default px), rootFontSize? (rem: pixels per rem, default 16)} → {code, language, connected}",
false,
|s, p| {
let cmd = "inspect.code";
let id = targets(s, p).first().copied().ok_or_else(|| bad(cmd, "select a layer"))?;
let lang = match string(p, "language") {
Some(l) => CodeLang::from_id(&l).ok_or_else(|| bad(cmd, "language: css|html|react|leptos|swiftui|compose"))?,
Some(l) => CodeLang::from_id(&l).ok_or_else(|| bad(cmd, "language: css|html|react|leptos|swiftui|compose|arkui"))?,
None => CodeLang::Css,
};
let rem = match string(p, "unit").map(|u| u.to_ascii_lowercase()).as_deref() {
@@ -63,10 +63,13 @@ pub enum CodeLang {
Leptos,
SwiftUi,
Compose,
/// HarmonyOS ArkUI: an ArkTS struct (`@Component`, `build`).
ArkUi,
}
impl CodeLang {
pub const ALL: [CodeLang; 6] = [CodeLang::Css, CodeLang::Html, CodeLang::React, CodeLang::Leptos, CodeLang::SwiftUi, CodeLang::Compose];
pub const ALL: [CodeLang; 7] =
[CodeLang::Css, CodeLang::Html, CodeLang::React, CodeLang::Leptos, CodeLang::SwiftUi, CodeLang::Compose, CodeLang::ArkUi];
pub fn id(self) -> &'static str {
match self {
CodeLang::Css => "css",
@@ -75,6 +78,7 @@ impl CodeLang {
CodeLang::Leptos => "leptos",
CodeLang::SwiftUi => "swiftui",
CodeLang::Compose => "compose",
CodeLang::ArkUi => "arkui",
}
}
pub fn label(self) -> &'static str {
@@ -85,6 +89,7 @@ impl CodeLang {
CodeLang::Leptos => "Leptos",
CodeLang::SwiftUi => "SwiftUI",
CodeLang::Compose => "Compose",
CodeLang::ArkUi => "ArkUI",
}
}
/// Languages whose lengths are CSS pixels (they can be shown in rem).
@@ -95,6 +100,9 @@ impl CodeLang {
if s.eq_ignore_ascii_case("tsx") || s.eq_ignore_ascii_case("jsx") {
return Some(CodeLang::React);
}
if s.eq_ignore_ascii_case("arkts") {
return Some(CodeLang::ArkUi);
}
CodeLang::ALL.into_iter().find(|l| l.id().eq_ignore_ascii_case(s) || l.label().eq_ignore_ascii_case(s))
}
}
@@ -194,7 +202,7 @@ pub fn code_for(doc: &Document, id: NodeId, lang: CodeLang) -> String {
match lang {
CodeLang::Css => css(doc, id),
CodeLang::Html => html(doc, id),
CodeLang::React | CodeLang::Leptos | CodeLang::SwiftUi | CodeLang::Compose => crate::component_codegen::code(doc, id, lang),
CodeLang::React | CodeLang::Leptos | CodeLang::SwiftUi | CodeLang::Compose | CodeLang::ArkUi => crate::component_codegen::code(doc, id, lang),
}
}
@@ -1135,6 +1143,218 @@ fn compose_layer(ctx: &Ctx, n: &Node, depth: usize, out: &mut String) {
}
}
// ------------------------------------------------------------------ ArkUI (ArkTS)
/// A colour as ArkUI writes it: `'#RRGGBB'`, or `'#AARRGGBB'` with transparency.
fn ark_color(c: Color) -> String {
let [r, g, b, a] = c.to_rgba8();
if a == 255 { format!("'#{r:02X}{g:02X}{b:02X}'") } else { format!("'#{a:02X}{r:02X}{g:02X}{b:02X}'") }
}
/// A layer's corner radii as `.borderRadius(...)` (uniform or per corner).
fn ark_radius(n: &Node, mods: &mut Vec<String>) {
if let Some(r) = n.corners.uniform() {
if r > 0.0 {
mods.push(format!(".borderRadius({})", num(r)));
}
} else if n.corners.radii.iter().any(|r| *r > 0.0) {
let [tl, tr, br, bl] = n.corners.radii;
mods.push(format!(".borderRadius({{ topLeft: {}, topRight: {}, bottomRight: {}, bottomLeft: {} }})", num(tl), num(tr), num(br), num(bl)));
}
}
/// ArkUI for a layer and its subtree at `depth`; `extra` attributes go last (a free child's
/// position). A layer a boolean parameter shows sits in an `if`.
pub(crate) fn arkui(ctx: &Ctx, id: NodeId, depth: usize, out: &mut String, extra: &[String]) {
let Some(n) = ctx.doc.node(id) else { return };
if id != ctx.root
&& let Some(p) = ctx.param(n, "visible")
{
let pad = indent(depth);
out.push_str(&format!("{pad}if (this.{}) {{\n", p.ident));
arkui_layer(ctx, n, depth + 1, out, extra);
out.push_str(&format!("{pad}}}\n"));
return;
}
arkui_layer(ctx, n, depth, out, extra);
}
fn arkui_layer(ctx: &Ctx, n: &Node, depth: usize, out: &mut String, extra: &[String]) {
let doc = ctx.doc;
let pad = indent(depth);
if ctx.too_deep(depth) {
out.push_str(&format!("{pad}// {} …\n", one_line(&n.name)));
return;
}
// Nested instances are calls; an instance swap parameter is a builder.
let call = match ctx.param(n, "mainComponent").filter(|_| n.id != ctx.root) {
Some(p) => Some(format!("{pad}this.{}()\n", p.ident)),
None => ctx.call(n, CodeLang::ArkUi, depth),
};
if let Some(call) = call {
out.push_str(&call);
for m in extra {
out.push_str(&format!("{pad} {m}\n"));
}
return;
}
let mut mods: Vec<String> = Vec::new();
match &n.kind {
NodeKind::Text(t) => {
let text = ctx.param(n, "characters").map_or_else(|| literal(&t.characters, false), |p| format!("this.{}", p.ident));
out.push_str(&format!("{pad}Text({text})\n"));
let s = &t.style;
mods.push(format!(".fontSize({})", num(s.font_size)));
mods.push(format!(".fontWeight({})", s.weight().clamp(100, 900)));
mods.push(format!(".fontFamily({})", literal(&s.font_family, false)));
if s.italic() {
mods.push(".fontStyle(FontStyle.Italic)".into());
}
if let Some(c) = first_fill(n) {
mods.push(format!(".fontColor({})", ark_color(c)));
}
match s.line_height {
LineHeight::Pixels(v) => mods.push(format!(".lineHeight({})", num(v))),
LineHeight::Percent(p) => mods.push(format!(".lineHeight('{}%')", num(p))),
LineHeight::Auto => {}
}
match s.letter_spacing {
LetterSpacing::Pixels(v) if v != 0.0 => mods.push(format!(".letterSpacing({})", num(v))),
LetterSpacing::Percent(p) if p != 0.0 => mods.push(format!(".letterSpacing('{}%')", num(p))),
_ => {}
}
match t.align_h {
TextAlignH::Left => {}
TextAlignH::Center => mods.push(".textAlign(TextAlign.Center)".into()),
TextAlignH::Right => mods.push(".textAlign(TextAlign.End)".into()),
TextAlignH::Justified => mods.push(".textAlign(TextAlign.JUSTIFY)".into()),
}
match s.case {
TextCase::Upper => mods.push(".textCase(TextCase.UpperCase)".into()),
TextCase::Lower => mods.push(".textCase(TextCase.LowerCase)".into()),
_ => {}
}
match s.decoration {
TextDecoration::Underline => mods.push(".decoration({ type: TextDecorationType.Underline })".into()),
TextDecoration::Strikethrough => mods.push(".decoration({ type: TextDecorationType.LineThrough })".into()),
TextDecoration::None => {}
}
}
NodeKind::Ellipse { .. } => {
out.push_str(&format!("{pad}Ellipse({{ width: {}, height: {} }})\n", num(n.width), num(n.height)));
if let Some(c) = first_fill(n) {
mods.push(format!(".fill({})", ark_color(c)));
}
if let Some(c) = stroke_color(n) {
mods.push(format!(".stroke({})", ark_color(c)));
mods.push(format!(".strokeWidth({})", num(n.stroke.weight)));
}
}
NodeKind::Rectangle => {
out.push_str(&format!("{pad}Column()\n"));
if let Some(c) = first_fill(n) {
mods.push(format!(".backgroundColor({})", ark_color(c)));
}
ark_radius(n, &mut mods);
mods.push(format!(".width({})", num(n.width)));
mods.push(format!(".height({})", num(n.height)));
}
k if k.is_frame_like() || matches!(k, NodeKind::Group) => {
let open = match n.layout.as_ref().map(|l| (l.mode, l)) {
Some((LayoutMode::Vertical, l)) => format!("Column({{ space: {} }})", num(l.gap)),
Some((_, l)) => format!("Row({{ space: {} }})", num(l.gap)),
None => "Stack({ alignContent: Alignment.TopStart })".to_string(),
};
out.push_str(&format!("{pad}{open} {{\n"));
match ctx.param(n, "slot") {
// A slot shows the instance's content, or else the main's (its default builder).
Some(p) => out.push_str(&format!("{pad} this.{}()\n", p.ident)),
None => {
for c in ctx.kids(n) {
// Free children sit at their position inside a Stack.
let pos: Vec<String> = match doc.node(c).filter(|_| n.layout.is_none()) {
Some(cn) => {
let [_, _, _, _, x, y] = cn.transform.as_coeffs();
vec![format!(".position({{ x: {}, y: {} }})", num(x), num(y))]
}
None => Vec::new(),
};
arkui(ctx, c, depth + 1, out, &pos);
}
}
}
out.push_str(&format!("{pad}}}\n"));
if let Some(l) = &n.layout {
let [t, r, b, lft] = l.padding;
if [t, r, b, lft].iter().any(|v| *v > 0.0) {
mods.push(format!(".padding({{ top: {}, right: {}, bottom: {}, left: {} }})", num(t), num(r), num(b), num(lft)));
}
let justify = match l.main_align {
MainAlign::Min => None,
MainAlign::Center => Some("FlexAlign.Center"),
MainAlign::Max => Some("FlexAlign.End"),
MainAlign::SpaceBetween => Some("FlexAlign.SpaceBetween"),
MainAlign::SpaceAround => Some("FlexAlign.SpaceAround"),
MainAlign::SpaceEvenly => Some("FlexAlign.SpaceEvenly"),
};
if let Some(j) = justify {
mods.push(format!(".justifyContent({j})"));
}
let cross = match l.cross_align {
CrossAlign::Min => None,
CrossAlign::Center => Some("Center"),
CrossAlign::Max => Some(if l.mode == LayoutMode::Vertical { "End" } else { "Bottom" }),
CrossAlign::Baseline => None,
};
if let Some(c) = cross {
let ty = if l.mode == LayoutMode::Vertical { "HorizontalAlign" } else { "VerticalAlign" };
mods.push(format!(".alignItems({ty}.{c})"));
}
}
mods.push(format!(".width({})", num(n.width)));
mods.push(format!(".height({})", num(n.height)));
if let Some(c) = first_fill(n) {
mods.push(format!(".backgroundColor({})", ark_color(c)));
}
if radius(n) > 0.0 {
mods.push(".clip(true)".into());
}
ark_radius(n, &mut mods);
}
_ => {
out.push_str(&format!("{pad}// {}: export as an image asset\n{pad}Image($r('app.media.asset'))\n", n.kind.label()));
mods.push(format!(".width({})", num(n.width)));
mods.push(format!(".height({})", num(n.height)));
}
}
// Ellipses take their stroke as a shape attribute (above).
if !matches!(n.kind, NodeKind::Ellipse { .. })
&& let Some(c) = stroke_color(n)
{
mods.push(format!(".border({{ width: {}, color: {} }})", num(n.stroke.weight), ark_color(c)));
}
if let Some(s) = drop_shadow(n) {
mods.push(format!(
".shadow({{ radius: {}, color: {}, offsetX: {}, offsetY: {} }})",
num(s.radius / 2.0),
ark_color(s.color),
num(s.offset.0),
num(s.offset.1)
));
}
if n.opacity < 1.0 {
mods.push(format!(".opacity({})", num(f64::from(n.opacity))));
}
let rot = figcraft_geom::rotation_degrees(n.transform);
if rot.abs() > 0.01 && rot.is_finite() {
mods.push(format!(".rotate({{ angle: {} }})", num(-rot)));
}
mods.extend(extra.iter().cloned());
for m in mods {
out.push_str(&format!("{pad} {m}\n"));
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -1174,6 +1394,33 @@ mod tests {
assert!(s.execute("inspect.code", &json!({"id": f, "language": "cobol"})).is_err());
}
#[test]
fn arkui_for_an_auto_layout_card() {
let mut s = Session::new();
let f = card(&mut s);
let doc = s.doc().unwrap();
let ark = code_for(doc, f, CodeLang::ArkUi);
for want in [
"Row({ space: ",
"Text(\"Say \\\"hi\\\" $5\\nnow\")",
".fontSize(",
".fontWeight(",
"Ellipse({ width: 24, height: 24 })",
".backgroundColor('#FFFFFF')",
".borderRadius(12)",
".width(",
".height(100)",
".clip(true)",
] {
assert!(ark.contains(want), "ArkUI has {want:?}:\n{ark}");
}
// Through the command, and "arkts" reads as ArkUI too.
let v = s.execute("inspect.code", &json!({"id": f, "language": "arkui"})).unwrap();
assert_eq!(v["code"].as_str().unwrap(), ark);
assert_eq!(v["language"], "arkui");
assert_eq!(s.execute("inspect.code", &json!({"id": f, "language": "arkts"})).unwrap()["language"], "arkui");
}
#[test]
fn free_children_get_offsets_and_deep_trees_are_capped() {
let mut s = Session::new();
@@ -1189,6 +1436,9 @@ mod tests {
assert!(sw.contains("…"), "depth capped");
let kt = code_for(doc, root, CodeLang::Compose);
assert!(kt.contains("Modifier.offset(7.dp, 9.dp)"));
let ark = code_for(doc, root, CodeLang::ArkUi);
assert!(ark.contains(".position({ x: 7, y: 9 })"), "{ark}");
assert!(ark.contains("…"), "depth capped");
assert!(code_for(doc, NodeId(999_999), CodeLang::Css).is_empty());
}
@@ -1204,7 +1454,7 @@ mod tests {
.unwrap();
}
let doc = s.doc().unwrap();
for lang in [CodeLang::SwiftUi, CodeLang::Compose] {
for lang in [CodeLang::SwiftUi, CodeLang::Compose, CodeLang::ArkUi] {
let code = code_for(doc, root, lang);
assert!(code.contains("// deep evil() more() …\n"), "{code}");
for line in code.lines() {
+111 -15
View File
@@ -1,16 +1,17 @@
//! Component code: a component (or component set) becomes a React, Leptos, SwiftUI or Jetpack
//! Compose component whose properties are its parameters (a variant property → a union or enum, boolean →
//! a flag showing the layers bound to it, text → a string, instance swap → a view parameter, slot →
//! optional content), and an instance becomes a call passing the values it sets. Variants of a set
//! share one definition (the set's default variant); in React and Leptos their differences become CSS
//! rules on `data-*` attributes. Returned by `inspect.componentCode` and used by `inspect.code`. A component
//! connected to code (see [`crate::code_connect`]) shows its real snippet instead.
//! Component code: a component (or component set) becomes a React, Leptos, SwiftUI, Jetpack
//! Compose or ArkUI component whose properties are its parameters (a variant property → a union or
//! enum, boolean → a flag showing the layers bound to it, text → a string, instance swap → a view
//! parameter, slot → optional content), and an instance becomes a call passing the values it sets.
//! Variants of a set share one definition (the set's default variant); in React and Leptos their
//! differences become CSS rules on `data-*` attributes. Returned by `inspect.componentCode` and
//! used by `inspect.code`. A component connected to code (see [`crate::code_connect`]) shows its
//! real snippet instead.
use figcraft_doc::*;
use serde_json::{Value, json};
use crate::cmd::CommandSpec;
use crate::codegen::{CodeLang, Ctx, Markup, class_name, compose, indent, literal, swift};
use crate::codegen::{CodeLang, Ctx, Markup, arkui, class_name, compose, indent, literal, swift};
use crate::components::{prop_default_value, slot_edited, slots_of, variant_options, variant_props, variant_set};
use crate::params::*;
use crate::{bad, spec};
@@ -67,14 +68,14 @@ pub fn specs() -> Vec<CommandSpec> {
vec![spec!(
"inspect.componentCode",
"Component code",
"{id? (component, set or instance; default the selection), language?: react|leptos|swiftui|compose (default react)} → {component, name, set, properties: [{name, param, type, default, value? (instances)}], definition, usage? (instances; the connected snippet when the component is connected to code), connected: {importPath, name, template, import, code} | null}",
"{id? (component, set or instance; default the selection), language?: react|leptos|swiftui|compose|arkui (default react)} → {component, name, set, properties: [{name, param, type, default, value? (instances)}], definition, usage? (instances; the connected snippet when the component is connected to code), connected: {importPath, name, template, import, code} | null}",
false,
|s, p| {
let cmd = "inspect.componentCode";
let id = targets(s, p).first().copied().ok_or_else(|| bad(cmd, "select a component or instance"))?;
let lang = match string(p, "language").map(|l| CodeLang::from_id(&l)) {
Some(Some(l @ (CodeLang::React | CodeLang::Leptos | CodeLang::SwiftUi | CodeLang::Compose))) => l,
Some(_) => return Err(bad(cmd, "language: react|leptos|swiftui|compose")),
Some(Some(l @ (CodeLang::React | CodeLang::Leptos | CodeLang::SwiftUi | CodeLang::Compose | CodeLang::ArkUi))) => l,
Some(_) => return Err(bad(cmd, "language: react|leptos|swiftui|compose|arkui")),
None => CodeLang::React,
};
let doc = s.doc()?;
@@ -107,9 +108,9 @@ pub fn specs() -> Vec<CommandSpec> {
)]
}
/// Code for any layer in React, Leptos, SwiftUI or Compose: an instance's call and its component's
/// definition, a component's (or set's) definition, or the layer tree (React and Leptos wrap it
/// in a function component).
/// Code for any layer in React, Leptos, SwiftUI, Compose or ArkUI: an instance's call and its
/// component's definition, a component's (or set's) definition, or the layer tree (React and
/// Leptos wrap it in a function component).
pub fn code(doc: &Document, id: NodeId, lang: CodeLang) -> String {
let Some(n) = doc.node(id) else { return String::new() };
if let Some(code) = crate::code_connect::link(doc, id, lang).and_then(|l| crate::code_connect::connected_code(doc, id, lang, l)) {
@@ -129,6 +130,11 @@ pub fn code(doc: &Document, id: NodeId, lang: CodeLang) -> String {
swift(&Ctx::new(doc, id), id, 0, &mut out, &[]);
out
}
CodeLang::ArkUi => {
let mut out = String::new();
arkui(&Ctx::new(doc, id), id, 0, &mut out, &[]);
out
}
_ => {
let mut out = String::new();
compose(&Ctx::new(doc, id), id, 0, &mut out);
@@ -289,7 +295,7 @@ const RESERVED: &[&str] = &[
"yield",
];
/// Types the generated SwiftUI and Compose code names: a variant enum may not shadow them.
/// Types the generated SwiftUI, Compose and ArkUI code names: a variant enum may not shadow them.
const TYPES: &[&str] = &[
"Alignment",
"AnyView",
@@ -298,6 +304,7 @@ const TYPES: &[&str] = &[
"Boolean",
"Box",
"CaseIterable",
"Circle",
"CircleShape",
"Color",
"Column",
@@ -306,21 +313,30 @@ const TYPES: &[&str] = &[
"EdgeInsets",
"Ellipse",
"EmptyView",
"FlexAlign",
"Font",
"FontStyle",
"FontWeight",
"Group",
"HStack",
"HorizontalAlign",
"Image",
"Modifier",
"Rect",
"RectangleShape",
"RoundedCornerShape",
"RoundedRectangle",
"Row",
"Shape",
"Stack",
"String",
"Text",
"TextAlign",
"TextCase",
"TextDecorationType",
"Unit",
"VStack",
"VerticalAlign",
"View",
"ZStack",
];
@@ -482,6 +498,9 @@ pub(crate) fn value_expr(doc: &Document, c: &Component, p: &Param, value: &Value
(Some(t), CodeLang::Leptos) => format!("|| view! {{ <{} /> }}", rust_type(&t)),
(None, CodeLang::Leptos) => "|| ()".into(),
(Some(t), CodeLang::SwiftUi) => format!("AnyView({t}())"),
// ArkUI takes a builder: `@BuilderParam` is a `() => void`.
(Some(t), CodeLang::ArkUi) => format!("() => {{ {t}() }}"),
(None, CodeLang::ArkUi) => "() => {}".into(),
(Some(t), _) => format!("{{ {t}() }}"),
(None, CodeLang::React) => "null".into(),
(None, CodeLang::SwiftUi) => "AnyView(EmptyView())".into(),
@@ -561,6 +580,11 @@ pub fn usage(doc: &Document, id: NodeId, lang: CodeLang, depth: usize) -> Option
format!("{pad}<{t}{attrs} />\n")
}
CodeLang::SwiftUi => format!("{pad}{t}({})\n", args.iter().map(|(n, e, _)| format!("{n}: {e}")).collect::<Vec<_>>().join(", ")),
CodeLang::ArkUi => {
let list = args.iter().map(|(n, e, _)| format!("{n}: {e}")).collect::<Vec<_>>().join(", ");
// ArkTS calls take an object literal, or nothing at all.
if list.is_empty() { format!("{pad}{t}()\n") } else { format!("{pad}{t}({{ {list} }})\n") }
}
_ => format!("{pad}{t}({})\n", args.iter().map(|(n, e, _)| format!("{n} = {e}")).collect::<Vec<_>>().join(", ")),
})
}
@@ -593,6 +617,13 @@ fn slot_content(doc: &Document, slot: NodeId, lang: CodeLang, depth: usize) -> S
}
format!("AnyView(Group {{\n{out}{}}})", indent(depth + 1))
}
CodeLang::ArkUi => {
let mut out = String::new();
for k in kids {
arkui(&ctx, k, depth + 2, &mut out, &[]);
}
format!("() => {{\n{out}{}}}", indent(depth + 1))
}
_ => {
let mut out = String::new();
for k in kids {
@@ -611,6 +642,7 @@ pub fn definition(doc: &Document, c: &Component, lang: CodeLang) -> String {
CodeLang::SwiftUi => swift_definition(doc, c),
CodeLang::Compose => compose_definition(doc, c),
CodeLang::Leptos => leptos(doc, c),
CodeLang::ArkUi => arkui_definition(doc, c),
_ => react(doc, c),
}
}
@@ -807,3 +839,67 @@ fn compose_definition(doc: &Document, c: &Component) -> String {
out.push_str("}\n");
out
}
/// An ArkUI (`@Component`) struct: variant enums, `@Prop` parameters, a default builder for each
/// instance swap and slot, and the `build` body.
fn arkui_definition(doc: &Document, c: &Component) -> String {
let ctx = Ctx { doc, root: c.comp, comp: Some(c), base: 1 };
let mut out = String::new();
for p in &c.params {
if let ParamKind::Variant(opts) = &p.kind {
out.push_str(&format!("enum {} {{ {} }}\n\n", enum_name(c, p, false), cases(opts, false).join(", ")));
}
}
// A default builder per instance swap or slot, named apart from every parameter.
let mut used: Vec<String> = c.params.iter().map(|p| p.ident.clone()).collect();
let mut builders: Vec<(String, String)> = Vec::new();
for p in &c.params {
if !matches!(p.kind, ParamKind::InstanceSwap | ParamKind::Slot) {
continue;
}
let base = format!("default{}Builder", capitalized(&p.ident));
let name = (1..).map(|k| if k == 1 { base.clone() } else { format!("{base}{k}") }).find(|n| !used.contains(n)).unwrap_or(base);
used.push(name.clone());
builders.push((p.ident.clone(), name));
}
out.push_str("@Component\nexport struct ");
out.push_str(&c.type_name);
out.push_str(" {\n");
for p in &c.params {
match &p.kind {
ParamKind::Variant(_) => {
out.push_str(&format!(" @Prop {}: {} = {}\n", p.ident, enum_name(c, p, false), value_expr(doc, c, p, &p.default, CodeLang::ArkUi)))
}
ParamKind::Boolean => out.push_str(&format!(" @Prop {}: boolean = {}\n", p.ident, p.default.as_bool().unwrap_or(false))),
ParamKind::Text => out.push_str(&format!(" @Prop {}: string = {}\n", p.ident, value_expr(doc, c, p, &p.default, CodeLang::ArkUi))),
ParamKind::InstanceSwap | ParamKind::Slot => {
let name = builders.iter().find(|(ident, _)| *ident == p.ident).map_or("", |(_, name)| name.as_str());
out.push_str(&format!(" @BuilderParam {}: () => void = this.{name}\n", p.ident));
}
}
}
for (ident, name) in &builders {
let Some(p) = c.params.iter().find(|p| p.ident == *ident) else { continue };
out.push_str(&format!("\n @Builder\n {name}() {{\n"));
match &p.kind {
ParamKind::InstanceSwap => {
if let Some(t) = id_of(&p.default).and_then(|i| component_type(doc, i)) {
out.push_str(&format!(" {t}()\n"));
}
}
ParamKind::Slot => {
let layer = slots_of(doc, c.comp).into_iter().find(|(_, slot)| slot == &p.prop).map(|(layer, _)| layer);
let kids = layer.and_then(|l| doc.node(l)).map(|n| ctx.kids(n)).unwrap_or_default();
for k in kids {
arkui(&ctx, k, 2, &mut out, &[]);
}
}
_ => {}
}
out.push_str(" }\n");
}
out.push_str("\n build() {\n");
arkui(&ctx, c.comp, 2, &mut out, &[]);
out.push_str(" }\n}\n");
out
}
@@ -71,6 +71,17 @@ fn components_take_their_properties_as_parameters() {
for want in ["@Composable\nfun Card(", " showDot: Boolean = true,", " title: String = \"Hello\",", "if (showDot) {", "text = title,"] {
assert!(kt.contains(want), "Compose has {want:?}:\n{kt}");
}
let ark = code(&mut s, c, "arkui");
for want in [
"@Component\nexport struct Card {",
" @Prop showDot: boolean = true",
" @Prop title: string = \"Hello\"",
" build() {",
" if (this.showDot) {",
"Text(this.title)",
] {
assert!(ark.contains(want), "ArkUI has {want:?}:\n{ark}");
}
let leptos = code(&mut s, c, "leptos");
for want in [
"use leptos::prelude::*;\n\n#[component]\npub fn Card(\n",
@@ -117,12 +128,14 @@ fn instances_are_calls_passing_the_values_they_set() {
assert!(react.contains("export function Card({"));
assert_eq!(component_code(&mut s, i, "swiftui")["usage"], "Card(showDot: false, title: \"Buy\")\n");
assert_eq!(component_code(&mut s, i, "compose")["usage"], "Card(showDot = false, title = \"Buy\")\n");
assert_eq!(component_code(&mut s, i, "arkui")["usage"], "Card({ showDot: false, title: \"Buy\" })\n");
assert_eq!(component_code(&mut s, i, "leptos")["usage"], "<Card show_dot={false} title=\"Buy\" />\n");
// Text that needs escaping is an expression in JSX and escaped per language.
ex(&mut s, "instance.setProperties", json!({"ids": [i], "properties": {"Title": "Say \"hi\" $5\nnow"}}));
assert_eq!(component_code(&mut s, i, "react")["usage"], "<Card showDot={false} title={\"Say \\\"hi\\\" $5\\nnow\"} />\n");
assert_eq!(component_code(&mut s, i, "swiftui")["usage"], "Card(showDot: false, title: \"Say \\\"hi\\\" $5\\nnow\")\n");
assert_eq!(component_code(&mut s, i, "compose")["usage"], "Card(showDot = false, title = \"Say \\\"hi\\\" \\$5\\nnow\")\n");
assert_eq!(component_code(&mut s, i, "arkui")["usage"], "Card({ showDot: false, title: \"Say \\\"hi\\\" $5\\nnow\" })\n");
assert_eq!(component_code(&mut s, i, "leptos")["usage"], "<Card show_dot={false} title={\"Say \\\"hi\\\" $5\\nnow\"} />\n");
}
@@ -173,6 +186,9 @@ fn variant_sets_share_one_definition() {
let kt = code(&mut s, set, "compose");
assert!(kt.contains("enum class ButtonState { Default, Hover }"), "{kt}");
assert!(kt.contains(" state: ButtonState = ButtonState.Default,"), "{kt}");
let ark = code(&mut s, set, "arkui");
assert!(ark.contains("enum ButtonState { Default, Hover }"), "{ark}");
assert!(ark.contains(" @Prop state: ButtonState = ButtonState.Default"), "{ark}");
let leptos = code(&mut s, set, "leptos");
for want in [
"#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]\npub enum ButtonState {\n #[default]\n Default,\n Hover,\n}",
@@ -189,6 +205,7 @@ fn variant_sets_share_one_definition() {
assert_eq!(component_code(&mut s, i, "react")["usage"], "<Button state=\"Hover\" />\n");
assert_eq!(component_code(&mut s, i, "swiftui")["usage"], "Button(state: .hover)\n");
assert_eq!(component_code(&mut s, i, "compose")["usage"], "Button(state = ButtonState.Hover)\n");
assert_eq!(component_code(&mut s, i, "arkui")["usage"], "Button({ state: ButtonState.Hover })\n");
assert_eq!(component_code(&mut s, i, "leptos")["usage"], "<Button state=ButtonState::Hover />\n");
}
@@ -212,6 +229,14 @@ fn instance_swap_properties_take_views() {
assert!(swift.contains(" var icon: AnyView = AnyView(HeartIcon())"), "{swift}");
let kt = code(&mut s, chip, "compose");
assert!(kt.contains(" icon: @Composable () -> Unit = { HeartIcon() },") && kt.contains("icon()"), "{kt}");
let ark = code(&mut s, chip, "arkui");
for want in [
" @BuilderParam icon: () => void = this.defaultIconBuilder",
" @Builder\n defaultIconBuilder() {\n HeartIcon()\n }",
" this.icon()",
] {
assert!(ark.contains(want), "ArkUI has {want:?}:\n{ark}");
}
let leptos = code(&mut s, chip, "leptos");
assert!(leptos.contains(" #[prop(into, default = (|| view! { <HeartIcon /> }).into())] icon: ViewFn,"), "{leptos}");
assert!(leptos.contains("{icon.run()}"), "{leptos}");
@@ -220,6 +245,7 @@ fn instance_swap_properties_take_views() {
assert_eq!(component_code(&mut s, i, "react")["usage"], "<Chip icon={<StarIcon />} />\n");
assert_eq!(component_code(&mut s, i, "swiftui")["usage"], "Chip(icon: AnyView(StarIcon()))\n");
assert_eq!(component_code(&mut s, i, "compose")["usage"], "Chip(icon = { StarIcon() })\n");
assert_eq!(component_code(&mut s, i, "arkui")["usage"], "Chip({ icon: () => { StarIcon() } })\n");
assert_eq!(component_code(&mut s, i, "leptos")["usage"], "<Chip icon={|| view! { <StarIcon /> }} />\n");
}
@@ -235,6 +261,14 @@ fn slots_take_optional_content() {
assert!(react.contains(" content?: React.ReactNode;") && react.contains("{content ?? ("), "{react}");
assert!(code(&mut s, panel, "swiftui").contains("var content: AnyView? = nil"));
assert!(code(&mut s, panel, "compose").contains("content: (@Composable () -> Unit)? = null,"));
let ark = code(&mut s, panel, "arkui");
for want in [
" @BuilderParam content: () => void = this.defaultContentBuilder",
" @Builder\n defaultContentBuilder() {\n",
" this.content()",
] {
assert!(ark.contains(want), "ArkUI has {want:?}:\n{ark}");
}
let leptos = code(&mut s, panel, "leptos");
assert!(leptos.contains(" #[prop(optional, into)] content: Option<ViewFn>,"), "{leptos}");
assert!(leptos.contains("{match &content { Some(c) => c.run(), None => view! {\n") && leptos.contains("}.into_any() }}\n"), "{leptos}");
@@ -248,6 +282,8 @@ fn slots_take_optional_content() {
assert!(usage.starts_with("<Panel content={(\n") && usage.contains("<Badge />"), "{usage}");
let usage = component_code(&mut s, i, "leptos")["usage"].as_str().unwrap_or_default().to_string();
assert!(usage.starts_with("<Panel content={|| view! {\n") && usage.contains("<Badge />") && usage.ends_with("}} />\n"), "{usage}");
let usage = component_code(&mut s, i, "arkui")["usage"].as_str().unwrap_or_default().to_string();
assert!(usage.starts_with("Panel({ content: () => {\n") && usage.contains("Badge()") && usage.ends_with("} })\n"), "{usage}");
}
#[test]
+2 -2
View File
@@ -52,7 +52,7 @@ pub fn specs() -> Vec<CommandSpec> {
vec![spec!(
"component.playground",
"Component playground",
"{id? (an instance, a component or a component set; default the selection), properties? {name: value} (variant, boolean, text and instance-swap properties, as instance.setProperties takes them), language? css|html|react|swiftui|compose (default react), path? (write the preview there as a PNG), scale? (the PNG's, default 2)} → {component, properties: [{name, type, options?, value}], language, code, width, height, path?} the component with those property values, tried on a copy: its code and its look. Nothing in the document changes",
"{id? (an instance, a component or a component set; default the selection), properties? {name: value} (variant, boolean, text and instance-swap properties, as instance.setProperties takes them), language? css|html|react|swiftui|compose|arkui (default react), path? (write the preview there as a PNG), scale? (the PNG's, default 2)} → {component, properties: [{name, type, options?, value}], language, code, width, height, path?} the component with those property values, tried on a copy: its code and its look. Nothing in the document changes",
false,
|s, p| {
let cmd = "component.playground";
@@ -63,7 +63,7 @@ pub fn specs() -> Vec<CommandSpec> {
Some(_) => return Err(bad(cmd, "properties: {name: value}")),
};
let lang = match string(p, "language") {
Some(l) => CodeLang::from_id(&l).ok_or_else(|| bad(cmd, "language: css|html|react|swiftui|compose"))?,
Some(l) => CodeLang::from_id(&l).ok_or_else(|| bad(cmd, "language: css|html|react|swiftui|compose|arkui"))?,
None => CodeLang::React,
};
let (copy, inst) = build(s.doc()?, target, &values)?;
+1 -1
View File
@@ -228,7 +228,7 @@ status = "draft — needs native review"
"Changes on main" = "প্রধান (main)-এ পরিবর্তন"
"Changes on the branch" = "ব্রাঞ্চে পরিবর্তন"
"Changes unknown" = "পরিবর্তন অজ্ঞাত"
"Choose React, Leptos, SwiftUI or Compose above to connect this component to its code." = "এই কম্পোনেন্টটিকে কোডের সাথে যুক্ত করতে উপরে React, Leptos, SwiftUI বা Compose বেছে নিন।"
"Choose React, Leptos, SwiftUI, Compose or ArkUI above to connect this component to its code." = "এই কম্পোনেন্টটিকে কোডের সাথে যুক্ত করতে উপরে React, Leptos, SwiftUI, Compose বা ArkUI বেছে নিন।"
"Choose another profile to change it." = "পরিবর্তন করতে অন্য একটি প্রোফাইল বেছে নিন।"
"Choose collection" = "সংগ্রহ (collection) বেছে নিন"
"Choose image" = "ছবি বেছে নিন"
+1 -1
View File
@@ -219,7 +219,7 @@ status = "draft — needs native review"
"Changes on main" = "Změny v hlavním souboru"
"Changes on the branch" = "Změny ve větvi"
"Changes unknown" = "Změny nejsou známy"
"Choose React, Leptos, SwiftUI or Compose above to connect this component to its code." = "Pro propojení této komponenty s jejím kódem vyberte nahoře React, Leptos, SwiftUI nebo Compose."
"Choose React, Leptos, SwiftUI, Compose or ArkUI above to connect this component to its code." = "Pro propojení této komponenty s jejím kódem vyberte nahoře React, Leptos, SwiftUI, Compose nebo ArkUI."
"Choose another profile to change it." = "Pro změnu vyberte jiný profil."
"Choose collection" = "Vybrat kolekci"
"Choose image" = "Vybrat obrázek"
+1 -1
View File
@@ -214,7 +214,7 @@ status = "draft — needs native review"
"Changes on main" = "Änderungen in der Hauptdatei"
"Changes on the branch" = "Änderungen im Branch"
"Changes unknown" = "Änderungen unbekannt"
"Choose React, Leptos, SwiftUI or Compose above to connect this component to its code." = "Wählen Sie oben React, Leptos, SwiftUI oder Compose, um diese Komponente mit ihrem Code zu verbinden."
"Choose React, Leptos, SwiftUI, Compose or ArkUI above to connect this component to its code." = "Wählen Sie oben React, Leptos, SwiftUI, Compose oder ArkUI, um diese Komponente mit ihrem Code zu verbinden."
"Choose another profile to change it." = "Wählen Sie ein anderes Profil, um es zu ändern."
"Choose collection" = "Sammlung wählen"
"Choose image" = "Bild wählen"
+1 -1
View File
@@ -228,7 +228,7 @@ name = "English"
"Changes on main" = "Changes on main"
"Changes on the branch" = "Changes on the branch"
"Changes unknown" = "Changes unknown"
"Choose React, Leptos, SwiftUI or Compose above to connect this component to its code." = "Choose React, Leptos, SwiftUI or Compose above to connect this component to its code."
"Choose React, Leptos, SwiftUI, Compose or ArkUI above to connect this component to its code." = "Choose React, Leptos, SwiftUI, Compose or ArkUI above to connect this component to its code."
"Choose another profile to change it." = "Choose another profile to change it."
"Choose collection" = "Choose collection"
"Choose image" = "Choose image"
+1 -1
View File
@@ -229,7 +229,7 @@ status = "draft — needs native review"
"Changes on main" = "Cambios en la principal"
"Changes on the branch" = "Cambios en la rama"
"Changes unknown" = "Cambios desconocidos"
"Choose React, Leptos, SwiftUI or Compose above to connect this component to its code." = "Elige React, Leptos, SwiftUI o Compose arriba para conectar este componente con su código."
"Choose React, Leptos, SwiftUI, Compose or ArkUI above to connect this component to its code." = "Elige React, Leptos, SwiftUI, Compose o ArkUI arriba para conectar este componente con su código."
"Choose another profile to change it." = "Elige otro perfil para cambiarlo."
"Choose collection" = "Elegir colección"
"Choose image" = "Elegir imagen"
+1 -1
View File
@@ -223,7 +223,7 @@ status = "draft — needs native review"
"Changes unknown" = "Muutokset tuntemattomia"
"Checking the fonts installed on this computer…" = "Tarkistetaan tämän tietokoneen asennettuja fontteja…"
"Checking the server…" = "Tarkistetaan palvelinta…"
"Choose React, Leptos, SwiftUI or Compose above to connect this component to its code." = "Valitse yllä React, Leptos, SwiftUI tai Compose yhdistääksesi tämän komponentin sen koodiin."
"Choose React, Leptos, SwiftUI, Compose or ArkUI above to connect this component to its code." = "Valitse yllä React, Leptos, SwiftUI, Compose tai ArkUI yhdistääksesi tämän komponentin sen koodiin."
"Choose another profile to change it." = "Vaihda valitsemalla toinen profiili."
"Choose collection" = "Valitse kokoelma"
"Choose image" = "Valitse kuva"
+1 -1
View File
@@ -223,7 +223,7 @@ status = "draft — needs native review"
"Changes on main" = "Modifications dans le fichier principal"
"Changes on the branch" = "Modifications dans la branche"
"Changes unknown" = "Modifications inconnues"
"Choose React, Leptos, SwiftUI or Compose above to connect this component to its code." = "Choisissez React, Leptos, SwiftUI ou Compose ci-dessus pour relier ce composant à son code."
"Choose React, Leptos, SwiftUI, Compose or ArkUI above to connect this component to its code." = "Choisissez React, Leptos, SwiftUI, Compose ou ArkUI ci-dessus pour relier ce composant à son code."
"Choose another profile to change it." = "Choisissez un autre profil pour le changer."
"Choose collection" = "Choisir une collection"
"Choose image" = "Choisir une image"
+1 -1
View File
@@ -223,7 +223,7 @@ status = "draft — needs native review"
"Changes on main" = "Perubahan di main"
"Changes on the branch" = "Perubahan di branch"
"Changes unknown" = "Perubahan tidak diketahui"
"Choose React, Leptos, SwiftUI or Compose above to connect this component to its code." = "Pilih React, Leptos, SwiftUI, atau Compose di atas untuk menghubungkan component ini ke kodenya."
"Choose React, Leptos, SwiftUI, Compose or ArkUI above to connect this component to its code." = "Pilih React, Leptos, SwiftUI, Compose, atau ArkUI di atas untuk menghubungkan component ini ke kodenya."
"Choose another profile to change it." = "Pilih profil lain untuk mengubahnya."
"Choose collection" = "Pilih koleksi"
"Choose image" = "Pilih gambar"
+1 -1
View File
@@ -224,7 +224,7 @@ status = "draft — needs native review"
"Changes on main" = "メインの変更"
"Changes on the branch" = "ブランチの変更"
"Changes unknown" = "変更内容は不明"
"Choose React, Leptos, SwiftUI or Compose above to connect this component to its code." = "上で React、Leptos、SwiftUI、Compose のいずれかを選び、このコンポーネントをコードに接続。"
"Choose React, Leptos, SwiftUI, Compose or ArkUI above to connect this component to its code." = "上で React、Leptos、SwiftUI、Compose、ArkUI のいずれかを選び、このコンポーネントをコードに接続。"
"Choose another profile to change it." = "変更するには別のプロファイルを選択。"
"Choose collection" = "コレクションを選択"
"Choose image" = "画像を選択"
+1 -1
View File
@@ -230,7 +230,7 @@ status = "draft — reviewed by a native speaker, awaiting a maintainer's approv
"Changes on main" = "ცვლილებები მთავარში"
"Changes on the branch" = "ცვლილებები განშტოებაში"
"Changes unknown" = "ცვლილებები უცნობია"
"Choose React, Leptos, SwiftUI or Compose above to connect this component to its code." = "ამ კომპონენტის კოდთან დასაკავშირებლად ზემოთ აირჩიეთ React, Leptos, SwiftUI ან Compose."
"Choose React, Leptos, SwiftUI, Compose or ArkUI above to connect this component to its code." = "ამ კომპონენტის კოდთან დასაკავშირებლად ზემოთ აირჩიეთ React, Leptos, SwiftUI, Compose ან ArkUI."
"Choose another profile to change it." = "მის შესაცვლელად აირჩიეთ სხვა პროფილი."
"Choose collection" = "კოლექციის არჩევა"
"Choose image" = "გამოსახულების არჩევა"
+1 -1
View File
@@ -214,7 +214,7 @@ status = "draft — needs native review"
"Changes on main" = "메인의 변경 사항"
"Changes on the branch" = "브랜치의 변경 사항"
"Changes unknown" = "변경 내용 알 수 없음"
"Choose React, Leptos, SwiftUI or Compose above to connect this component to its code." = "위에서 React, Leptos, SwiftUI 또는 Compose를 선택해 이 컴포넌트를 코드에 연결하세요."
"Choose React, Leptos, SwiftUI, Compose or ArkUI above to connect this component to its code." = "위에서 React, Leptos, SwiftUI, Compose 또는 ArkUI를 선택해 이 컴포넌트를 코드에 연결하세요."
"Choose another profile to change it." = "변경하려면 다른 프로필을 선택하세요."
"Choose collection" = "컬렉션 선택"
"Choose image" = "이미지 선택"
+1 -1
View File
@@ -223,7 +223,7 @@ status = "draft — needs native review"
"Changes unknown" = "Onbekende wijzigingen"
"Checking the fonts installed on this computer…" = "De lettertypen op deze computer worden gecontroleerd…"
"Checking the server…" = "Server controleren…"
"Choose React, Leptos, SwiftUI or Compose above to connect this component to its code." = "Kies hierboven React, Leptos, SwiftUI of Compose om dit component aan zijn code te koppelen."
"Choose React, Leptos, SwiftUI, Compose or ArkUI above to connect this component to its code." = "Kies hierboven React, Leptos, SwiftUI, Compose of ArkUI om dit component aan zijn code te koppelen."
"Choose another profile to change it." = "Kies een ander profiel om dit te wijzigen."
"Choose collection" = "Kies een verzameling"
"Choose image" = "Afbeelding kiezen"
+1 -1
View File
@@ -226,7 +226,7 @@ status = "draft — needs native review"
"Changes on main" = "Zmiany w gałęzi głównej"
"Changes on the branch" = "Zmiany w gałęzi"
"Changes unknown" = "Nieznane zmiany"
"Choose React, Leptos, SwiftUI or Compose above to connect this component to its code." = "Wybierz React, Leptos, SwiftUI lub Compose powyżej, aby połączyć ten komponent z jego kodem."
"Choose React, Leptos, SwiftUI, Compose or ArkUI above to connect this component to its code." = "Wybierz React, Leptos, SwiftUI, Compose lub ArkUI powyżej, aby połączyć ten komponent z jego kodem."
"Choose another profile to change it." = "Wybierz inny profil, aby go zmienić."
"Choose collection" = "Wybierz kolekcję"
"Choose image" = "Wybierz obraz"
+1 -1
View File
@@ -212,7 +212,7 @@ status = "draft — needs native review"
"Changes on main" = "Alterações no principal"
"Changes on the branch" = "Alterações na branch"
"Changes unknown" = "Alterações desconhecidas"
"Choose React, Leptos, SwiftUI or Compose above to connect this component to its code." = "Escolha React, Leptos, SwiftUI ou Compose acima para conectar este componente ao código dele."
"Choose React, Leptos, SwiftUI, Compose or ArkUI above to connect this component to its code." = "Escolha React, Leptos, SwiftUI, Compose ou ArkUI acima para conectar este componente ao código dele."
"Choose another profile to change it." = "Escolha outro perfil para alterá-lo."
"Choose collection" = "Escolher coleção"
"Choose image" = "Escolher imagem"
+1 -1
View File
@@ -226,7 +226,7 @@ name = "Русский"
"Changes on main" = "Изменения в основной ветке"
"Changes on the branch" = "Изменения в ветке"
"Changes unknown" = "Изменения неизвестны"
"Choose React, Leptos, SwiftUI or Compose above to connect this component to its code." = "Выберите выше React, Leptos, SwiftUI или Compose, чтобы связать компонент с его кодом."
"Choose React, Leptos, SwiftUI, Compose or ArkUI above to connect this component to its code." = "Выберите выше React, Leptos, SwiftUI, Compose или ArkUI, чтобы связать компонент с его кодом."
"Choose another profile to change it." = "Для смены выберите другой профиль."
"Choose collection" = "Выбрать коллекцию"
"Choose image" = "Выбрать изображение"
+1 -1
View File
@@ -223,7 +223,7 @@ name = "Svenska"
"Changes on main" = "Ändringar i main"
"Changes on the branch" = "Ändringar i grenen"
"Changes unknown" = "Okända ändringar"
"Choose React, Leptos, SwiftUI or Compose above to connect this component to its code." = "Välj React, Leptos, SwiftUI eller Compose ovan för att koppla den här komponenten till dess kod."
"Choose React, Leptos, SwiftUI, Compose or ArkUI above to connect this component to its code." = "Välj React, Leptos, SwiftUI, Compose eller ArkUI ovan för att koppla den här komponenten till dess kod."
"Choose another profile to change it." = "Välj en annan profil för att ändra den."
"Choose collection" = "Välj samling"
"Choose image" = "Välj bild"
+1 -1
View File
@@ -223,7 +223,7 @@ name = "Türkçe"
"Changes on main" = "Ana dosyadaki değişiklikler"
"Changes on the branch" = "Daldaki değişiklikler"
"Changes unknown" = "Değişiklikler bilinmiyor"
"Choose React, Leptos, SwiftUI or Compose above to connect this component to its code." = "Bu bileşeni koduna bağlamak için yukarıdan React, Leptos, SwiftUI veya Compose seçin."
"Choose React, Leptos, SwiftUI, Compose or ArkUI above to connect this component to its code." = "Bu bileşeni koduna bağlamak için yukarıdan React, Leptos, SwiftUI, Compose veya ArkUI seçin."
"Choose another profile to change it." = "Değiştirmek için başka bir profil seçin."
"Choose collection" = "Koleksiyon seç"
"Choose image" = "Görüntü seç"
+1 -1
View File
@@ -215,7 +215,7 @@ status = "draft — needs native review"
"Changes on main" = "主文件上的更改"
"Changes on the branch" = "分支上的更改"
"Changes unknown" = "更改未知"
"Choose React, Leptos, SwiftUI or Compose above to connect this component to its code." = "在上方选择 React、Leptos、SwiftUI 或 Compose,将此组件关联到其代码。"
"Choose React, Leptos, SwiftUI, Compose or ArkUI above to connect this component to its code." = "在上方选择 React、Leptos、SwiftUI、Compose 或 ArkUI,将此组件关联到其代码。"
"Choose another profile to change it." = "选择其他配置文件以更改。"
"Choose collection" = "选择集合"
"Choose image" = "选择图片"
+1 -1
View File
@@ -396,7 +396,7 @@ pub fn connect_section(app: &mut FigApp, ui: &mut Ui, doc: &Document, id: NodeId
let text = if connectable {
tr("Link this component to its code component: instances then show your real snippet with their values.")
} else {
tr("Choose React, Leptos, SwiftUI or Compose above to connect this component to its code.")
tr("Choose React, Leptos, SwiftUI, Compose or ArkUI above to connect this component to its code.")
};
widgets::note(ui, text, t.text_secondary);
return;
+4 -3
View File
@@ -1,6 +1,6 @@
# Dev Mode parity
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-22 · **Change:** minor (the assets list shows a preview, the name and an export format per asset) · **Target:** Figma Design Dev Mode (desktop app 126.10.7)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-22 · **Change:** minor (ArkUI code and code connect; the assets list shows a preview, the name and an export format per asset) · **Target:** Figma Design Dev Mode (desktop app 126.10.7)
**Breadth ~96%, ready ~72% (estimated).** The inspect surface is broad, components connect
to the code components of a repository and layers carry dev resources links; left: import lines
@@ -10,13 +10,13 @@ inside generated layer trees. Remaining: 0–1 h.
|---|---|---|
| Dev Mode toggle (Shift+D), Inspect panel | done | |
| 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) | done | plus HTML, React and Leptos (ours; Leptos: `#[component]` functions with `#[prop]` defaults, variant enums and `view!` markup); 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 |
| Code: CSS, iOS (SwiftUI), Android (Compose), HarmonyOS (ArkUI) | done | plus HTML, React and Leptos (ours; Leptos: `#[component]` functions with `#[prop]` defaults, variant enums and `view!` markup; ArkUI: `@Component` structs with `@Prop` parameters, enums, `@BuilderParam` for instance swaps and slots); 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 |
| Assets list with export | done | layers with export settings, icons (small, vector shapes only: SVG) and images (PNG) as a list of a preview, the name and a format to pick (its first export setting's until changed); a click exports the asset in that format, "Export all" uses each layer's own export settings; the icon rule is ours |
| Annotations: pinned properties, categories, coloured callouts, filter | done | |
| Ready for dev list, statuses, "Changed" after edits | done | |
| Focus view (one frame, previous / next) | done | |
| Compare changes (before/after side by side or overlaid, property diffs) | done | only against the snapshot taken when a frame was marked ready; not against any named version |
| Code connect (map components to repository code) | done | per language (React, Leptos, SwiftUI, Compose): import path, name and a template with `{property}` and `{property ? "a" : "b"}` placeholders; instances (also inside generated layer trees) show the snippet with their values; edited in the Inspect panel with a live preview; `figcraft.connect.json` export/import. Inside a generated layer tree, connected instances' imports aren't collected |
| Code connect (map components to repository code) | done | per language (React, Leptos, SwiftUI, Compose, ArkUI): import path, name and a template with `{property}` and `{property ? "a" : "b"}` placeholders; instances (also inside generated layer trees) show the snippet with their values; edited in the Inspect panel with a live preview; `figcraft.connect.json` export/import. Inside a generated layer tree, connected instances' imports aren't collected |
| Plugins in Dev Mode (Jira, GitHub, etc.) | partial | FigCraft's own WASM plugins that only read run from the Inspect tab; no plugin UI at run time, no third-party integrations |
| VS Code extension | out of scope | FigCraft's MCP server and control channel already serve design data, code and connected snippets to agents and editors |
| Dev resources links on layers | done | a URL and a name per link on any layer (`devResource.*`, saved, synced, undoable); instances list their main component's (and its set's) read-only; URLs checked as http/https and never fetched (no page titles: a link without a name is named after its URL); the Inspect tab lists, adds, edits and removes them; `get_node` returns them |
@@ -25,6 +25,7 @@ inside generated layer trees. Remaining: 0–1 h.
| Date | Change | Summary |
|---|---|---|
| 2026-10-22 | minor | ArkUI (HarmonyOS) as a code language: `inspect.code {language: arkui}` and `inspect.componentCode` generate ArkTS — `Column`/`Row`/`Stack` for auto layout and free children (`.position`), `Text` with its font, `Ellipse`, boxes for rectangles, colours as `'#RRGGBB'`/`'#AARRGGBB'`, radii, borders, shadows, rotation; a component becomes `@Component export struct` with `@Prop` parameters, variant enums, and `@BuilderParam` builders for instance swaps and slots, instances become object-literal calls (`Card({ title: "Buy" })`); code connect works per language with an ArkTS import and template. Tests in `codegen::tests::arkui_for_an_auto_layout_card`, `component_codegen_tests`, `code_connect_tests` |
| 2026-10-22 | minor | The Inspect tab's assets are a list of a thumbnail, the name and a format dropdown, instead of a name with a text summary: a slice shows the page content under it (as its export does), the format starts from the asset's first export setting and picking another exports that asset in it; "Export all" is unchanged (`dev_mode_tests::the_assets_list_shows_a_slice_with_a_preview_name_and_export_format`) |
| 2026-10-12 | minor | Dev resources (gap 16): links for developers (a URL and a name) on any layer: `devResource.add` / `edit` / `remove` / `list`, undoable, saved and synced; an instance also lists its main component's (and that component's set's) links read-only with where they come from, and layers inside an instance show their main layer's; URLs are checked as web links (http or https, a host, no spaces, at most 2,048 characters) and never fetched (a link added without a name is named after its URL), opened only when clicked; the Inspect tab lists, adds, edits and removes them; `get_node` and `inspect_document` return them as `devResources`. The editor extension stays out of scope (the MCP server serves agents and editors). Dev Mode breadth 93% → 96%, ready 70% → 72%, 1–3 h → 0–1 h; feature breadth (~82%) and ready for real work (~51%) unchanged at this rounding; full parity ~304–593 h → ~303–591 h |
| 2026-10-11 | minor | Export and inspect behaviour suite written from the public documentation (139 cases in 19 tests; 47 failed before the fixes) and its fixes: an export covers what the layer paints, so a drop shadow adds room only where it falls, strokes on boxes and ellipses reach their real width (they added up to four times as much), hidden layers take no room and a mask group exports the mask's area; a slice exports everything under it; slashes in layer names export into folders; Copy as PNG copies the whole selection. Added: the export settings' contents only and include bounding box, and for SVG outline text (off writes `<text>`), unique id attributes and simplified strokes (stroke attributes where exact); Inspect colour formats (hex, RGB, CSS, HSL, HSB), CSS in rem, a Blend row, positions measured from the frame around a layer (groups aren't frames), CSS for rotation, blend modes, gradient directions and kinds, text alignment, case and decoration; icons and images in the asset list (`file.exportLayers {assets}`). Dev Mode breadth 90% → 93%, ready 64% → 70%, hours unchanged (1–3, dev resources); feature breadth and ready for real work unchanged at this rounding |
+1 -1
View File
@@ -55,7 +55,7 @@ export limit, not a native document nesting limit.
| PDF | yes | yes | one page per frame, real selectable text in subset fonts, links, vectors and gradients as vectors, effects as images; bleed, crop/bleed marks, page boxes, naive CMYK; checked by rendering the PDF back against PNG exports; explicit depth-limit refusal | CMYK output intent 2–4 |
| WebP | — | yes (ours) | | — |
| Copy as CSS / SVG / PNG / text / link | yes | yes | Copy as PNG: the whole selection at 2x | — |
| Dev Mode code (CSS, iOS SwiftUI, Android Compose) | yes | yes, plus HTML and React | see [dev-mode-parity](dev-mode-parity.md) | — |
| Dev Mode code (CSS, iOS SwiftUI, Android Compose, HarmonyOS ArkUI) | yes | yes, plus HTML and React | see [dev-mode-parity](dev-mode-parity.md) | — |
| `.fig` ("Save local copy") | yes | **experimental** | the whole file, also as a copy from File ▸ Save as… (the document keeps its `.figcraft` path and unsaved state); see [Export to `.fig`](#export-to-fig-experimental). Round trips through our own importer; written files checked by opening them in other software: layers, fills, gradients, strokes, shadows, vectors, components and text draw as in FigCraft; resized and scaled instances keep their size and lay their layers out for it, variables with their modes and colour bindings resolve there (a frame set to another mode shows that mode's colours, also inside instances), colour, text, effect and grid styles are listed, and variable-width strokes that are the same on both sides of the path draw with their width | 5–15 (non-colour bindings as bindings, checks of effects, images and prototypes, and against real files) |
### Export to `.fig` (experimental)
+1 -1
View File
@@ -356,7 +356,7 @@ No alpha blockers: the alpha gate in [`roadmap.md`](roadmap.md#alpha-gate) passe
### 16. Dev Mode: the tail after dev resources
- **Landed (2026-10-11):** code connect: a component or set links to its code component per
language (React, SwiftUI, Compose): an import path, the name and a template whose `{property}`
language (React, SwiftUI, Compose, ArkUI): an import path, the name and a template whose `{property}`
and `{property ? "a" : "b"}` placeholders take the instance's values, escaped per language and
never executed. `inspect.code`, `inspect.componentCode` and the Inspect panel show that snippet
instead of generated code (inside generated layer trees too); the panel's Connect to code edits
+9 -6
View File
@@ -294,7 +294,7 @@ Or in a project's `.mcp.json`:
| Tool | What it does |
|---|---|
| `list_commands {filter?}` | Every command: id, label, menu, shortcut, undoable, params doc. |
| `run_command {command, params}` | Run any command (e.g. `align.left`, `layout.add`, `component.create`, `instance.place` (drop an instance at a page point into the frame or auto layout slot under it), `component.exposeInstance` / `instance.exposed` (nested instance properties on instances), `component.makeSlot` / `instance.slotInsert` / `instance.resetSlot` (slots, below), `inspect.code` (CSS, HTML + CSS for a whole frame, React, Leptos, SwiftUI or Jetpack Compose for a layer; components take their properties as parameters and instances are calls, see Dev Mode below), `inspect.properties` (layout, auto layout, typography, fills, strokes and effects with their style and variable names; `colorFormat`: hex, rgb, css, hsl or hsb), `inspect.assets` (layers under the selection with export settings, icons and images, each with its `kind`), `inspect.code` takes `unit: rem` and `rootFontSize` for CSS, HTML, React and Leptos, `style.create`, `variable.bind`, `variable.update` (rename, describe, scope), `variable.set` with `{alias}`, `variable.renameMode` / `variable.removeMode`, `variable.deleteCollection`, `proto.interaction.add`, `codeConnect.set` (components linked to real code, see Dev Mode below)). |
| `run_command {command, params}` | Run any command (e.g. `align.left`, `layout.add`, `component.create`, `instance.place` (drop an instance at a page point into the frame or auto layout slot under it), `component.exposeInstance` / `instance.exposed` (nested instance properties on instances), `component.makeSlot` / `instance.slotInsert` / `instance.resetSlot` (slots, below), `inspect.code` (CSS, HTML + CSS for a whole frame, React, Leptos, SwiftUI, Jetpack Compose or ArkUI for a layer; components take their properties as parameters and instances are calls, see Dev Mode below), `inspect.properties` (layout, auto layout, typography, fills, strokes and effects with their style and variable names; `colorFormat`: hex, rgb, css, hsl or hsb), `inspect.assets` (layers under the selection with export settings, icons and images, each with its `kind`), `inspect.code` takes `unit: rem` and `rootFontSize` for CSS, HTML, React and Leptos, `style.create`, `variable.bind`, `variable.update` (rename, describe, scope), `variable.set` with `{alias}`, `variable.renameMode` / `variable.removeMode`, `variable.deleteCollection`, `proto.interaction.add`, `codeConnect.set` (components linked to real code, see Dev Mode below)). |
| `batch {commands: [{command, params}], label?}` | Several commands as one undo step (`command.batch`); all-or-nothing for open documents and their document state. Failed inner commands are removed from the journal; preferences, clipboard, tool state and UI requests are not rolled back. A step can use what an earlier step returned: `{"$step": n}` anywhere in its params becomes the `id` in step n's result (zero-based), and `{"$step": n, "field": "…"}` another field of it (see Step references below). |
| `create_node {type, x, y, width, height, parent, index, name, text, path, props}` | Create a frame, rectangle, ellipse, polygon, star, line, arrow, text, vector, section, component or slice; returns `{id, node}`. |
| `set_properties {ids, props}` | `node.set` on layers (geometry, fills, strokes, effects, corner radius, auto layout, sizing, constraints, masks (`isMask`, `maskType: alpha\|vector\|luminance`), handoff status (`devStatus: none\|readyForDev\|completed`; marking again clears "Changed"), a section's `sectionContentsHidden`, text…); returns the updated layers. |
@@ -1275,24 +1275,27 @@ permissions: without it nothing is downloaded); `plugin.directory.publish {path
### Dev Mode
- **Component code:** `inspect.code {id, language: react|leptos|swiftui|compose}` on a component,
- **Component code:** `inspect.code {id, language: react|leptos|swiftui|compose|arkui}` on a component,
a component set or a variant writes the component as a reusable unit: React (TSX) with a props
interface, Leptos with a `#[component]` function (`#[prop]` defaults, `view!` markup), SwiftUI
with a `View` struct, Compose with a `@Composable` function. Its component properties are the
with a `View` struct, Compose with a `@Composable` function, ArkUI with a `@Component` ArkTS
struct (`@Prop` parameters, `@BuilderParam` for swaps and slots). Its component properties are the
parameters, with their defaults: variant properties become string unions (React) or enums,
booleans become flags around the layers they show, text properties strings, instance swaps views
(`React.ReactNode`, Leptos `ViewFn`, `AnyView`, a composable lambda) and slots optional content.
(`React.ReactNode`, Leptos `ViewFn`, `AnyView`, a composable lambda, an ArkUI builder) and slots
optional content.
Every variant of a set shares one definition; in React and Leptos the other variants'
differences become CSS rules on `data-*` attributes. On an instance (or a layer tree containing
instances) each instance is a call passing only the values it changes, e.g. `<Button
state="Hover" label="Buy" />`, `<Button state=ButtonState::Hover label="Buy" />`, `Button(state:
.hover, label: "Buy")`, `Button(state = ButtonState.Hover, label = "Buy")`. Names are escaped and
.hover, label: "Buy")`, `Button(state = ButtonState.Hover, label = "Buy")`, `Button({ state:
ButtonState.Hover, label: "Buy" })`. Names are escaped and
unique (keywords get a suffix, `default` → `defaultValue`, Rust's `type` → `type_`; Leptos names
are snake_case; names that would shadow framework types get one too, `Text` → `TextComponent`). `inspect.componentCode
{id?, language?}` returns the parts: `{component, name, set, properties: [{name, param, type,
default, value? (instances)}], definition, usage? (instances)}`.
- **Code connect:** link a component (a variant links its set) to the code component it is built
as, per language (`react`, `leptos`, `swiftui`, `compose`): `codeConnect.set {id?, language?, importPath?,
as, per language (`react`, `leptos`, `swiftui`, `compose`, `arkui`): `codeConnect.set {id?, language?, importPath?,
name?, template?}` (undoable; fields left out keep their value; the defaults are the generated
name and a call passing every property) → `{component, language, code, unknown}`. Templates are
text, never executed: `{property}` (its name or parameter name) is the instance's value escaped
+1 -1
View File
@@ -616,7 +616,7 @@ Carried over from the ROADMAP's "Shipped so far" (2026-10-10). The test count th
toolbar with flyouts and mode switcher, Design / Prototype / Inspect panels (Dev Mode: properties with their styles and variables, annotations
with pinned properties and categories drawn as coloured callouts on the canvas (filtered by category),
a ready-for-dev list, a focus view on one frame with previous / next, Compare changes since a frame
was marked ready (before / after side by side or overlaid, property-level diffs), CSS/HTML/React/SwiftUI/Compose
was marked ready (before / after side by side or overlaid, property-level diffs), CSS/HTML/React/SwiftUI/Compose/ArkUI
code where components take their properties as parameters and instances are calls, components
connected to their code components showing the real snippet, read-only plugins), colour picker
(HSB square, hue/alpha, hex, gradient stops, image fills), main menu generated from the catalog,