mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-30 05:16:56 -06:00
Merge pull request #669 from yelintong/feat/flutter-codegen
M7: Flutter code generation
This commit is contained in:
@@ -208,7 +208,7 @@ exposes to agents, and exported at 2x by FigCraft's own renderer. The source fil
|
||||
</td>
|
||||
<td width="50%" valign="top">
|
||||
<h3>Prototype and hand off</h3>
|
||||
Connections, triggers, actions and Smart Animate-style transitions, variables with conditionals (if / else if / else built in the interaction editor, expressions like <code>count + 1</code>), flows, a presentation view with device frames, hotspot hints and an Inspect panel, and a live preview pane. Alt to measure distances and padding on the canvas, Inspect mode with measurements, annotations in colour-coded categories, a ready-for-dev list with a focus view and Compare changes (before / after with property-level diffs since a frame was marked ready), and code (CSS, HTML, React, Vue, Leptos, SwiftUI, Compose) where components take their properties as parameters, comment threads pinned to frames (replies, @mentions, reactions, read state) and named version history. Export any layer at any scale to PNG, JPG, WebP, SVG or PDF.
|
||||
Connections, triggers, actions and Smart Animate-style transitions, variables with conditionals (if / else if / else built in the interaction editor, expressions like <code>count + 1</code>), flows, a presentation view with device frames, hotspot hints and an Inspect panel, and a live preview pane. Alt to measure distances and padding on the canvas, Inspect mode with measurements, annotations in colour-coded categories, a ready-for-dev list with a focus view and Compare changes (before / after with property-level diffs since a frame was marked ready), and code (CSS, HTML, React, Vue, Leptos, SwiftUI, Compose, ArkUI, Flutter) where components take their properties as parameters, comment threads pinned to frames (replies, @mentions, reactions, read state) and named version history. Export any layer at any scale to PNG, JPG, WebP, SVG or PDF.
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
@@ -434,6 +434,7 @@ Ranked; detail and milestones in [`docs/roadmap.md`](docs/roadmap.md).
|
||||
| 2026-10-17 | minor | Community fixes from #524 up (batches #727, #728, #733, #735, #736 and #729; 55 PRs) and a crash guard. `.fig` import: explicit instance identity, overrides by key, text style precedence, grid track sizes, slot content, connector labels, skip ink, paragraph options and hanging lists, list start numbers, vertical trim, per-point stroke joins, text on a path, image adjustments and embedded images; large files open within budgets from the file's own size; an import report after opening; imported bytes stay out of the command journal. Files and paste: save a copy as `.fig`; Cmd/Ctrl+V pastes images copied in other apps and layers copied from another design app (`edit.pasteDesignLayers`); paste over and paste properties; clipboard reads on Linux and the BSDs time out after 2 s. Editor: live font preview on hover; Shift+I and Resources open Assets; Shift-drag moves the selection; thin strokes take a nearby double-click; dropped points merge; Flatten keeps open paths; the Actions palette gets tabs, ranked search, a keyboard highlight and recents; the colour picker opens on a new fill or stroke; text strokes follow their alignment; selection highlights after line breaks; a draggable pages/layers divider; variant properties reorder by drag; submenus keep their hover with a safe triangle; menu actions that need a value ask for it; File ▸ Rename edits the name in place. Engine: bad paint indexes, non-finite geometry and pages in copy/cut are refused; a param naming a layer says so when it isn't one; a CLI script is one undo step and resolves `$step`. Server and collaboration: message limits follow `max_file_mb` (client capped at 1 GiB); a damaged generation field no longer drops flushed edits; the web build joins a session from a `#join=` link. Also Swedish command labels, Nix CI through the shared workflow, paint bounds once per render, and a guard that turns a panic inside the boolean-op library into bad input (the stroke draws unclipped). Numbers unchanged. |
|
||||
| 2026-10-17 | minor | Number variables on text content (M3, #629): the Typography section's variable picker also lists number variables for a text layer's content, as the engine already accepted (the text shows the number, e.g. 42.5, and follows the variable). Other text fields keep their own types. Numbers unchanged. |
|
||||
| 2026-10-17 | minor | Frames and sections convert into each other (M1, #719): `object.convertToSection` turns a frame on the page or in a section into a section in place (same box, name, fill and layers, which stay where they are; what only a frame has goes: auto layout, clipping, layout grids, guides, scrolling), and `object.convertToFrame` now also turns a section into a frame that keeps its fill and doesn't clip, so the look stays. Nested frames, instances, main components and sections holding sections are refused with the reason; one undo step each. Both are in the canvas context menu (Convert to section / Convert to frame) and in Spanish. Numbers unchanged. |
|
||||
| 2026-10-16 | minor | Flutter code (ours; the incumbent generates CSS, iOS and Android only): `inspect.code`, `inspect.componentCode` and `codeConnect.set` take `language: flutter` (`dart` is an alias): a `StatelessWidget` for a component (properties as constructor parameters with their defaults, instances as calls, variants as a Dart enum, swaps a `Widget`, slots a `List<Widget>?`) and a Dart widget tree for a layer (`Container` with a `BoxDecoration`, `Row`/`Column` with `spacing`, `Expanded`, `Stack` + `Positioned`, `Text` with a `TextStyle`, `Image.asset`, `Opacity`), plus Dart `import '…';` lines in Code Connect. The Dev Mode language list includes it. Numbers unchanged |
|
||||
| 2026-10-16 | minor | Adobe Fonts found on macOS (M7, #298): fonts activated through Adobe Fonts rendered in the fallback font without a warning, because Creative Cloud keeps them in its own folder as hidden files named by number, which the font scan never read. The macOS scan now includes `~/Library/Application Support/Adobe/CoreSync/plugins/livetype`, and files without an extension are indexed when they start with a font signature. Fonts are read in place. Regression test in the font database tests; docs/cli.md lists the folder. Numbers unchanged |
|
||||
| 2026-10-16 | minor | Community fixes from #478 up (batches #591, #598, #602; 69 PRs) and a control-channel fix: a line that isn't JSON now ends a `--control` connection, so a web page can't drive the app through a browser request (#589). Highlights: page copies keep navigation, flows, prototype settings and guides; Find and Replace keeps line endings and case-insensitive matches; text hyperlinks survive import and export; recent colours record only what was picked; MCP screenshots respect `maxSize`; Swedish catalog (UI strings), Czech plurals; bounded canvas overscan while panning; Korean fallback at the run's weight; many vector-edit, guide, page and .fig round-trip fixes. Numbers unchanged |
|
||||
| 2026-10-15 | minor | Drop files anywhere to open them (M7): dragging files over the window shows a "Drop to open" (home) or "Drop to import" (canvas) overlay. On the home screen each supported file opens as a document of its own and other files are turned away with a toast; on the canvas the files import at the drop point. Both wait behind a small card that is on screen before a large file blocks the window. A canvas import stays with the document and page that received the drop even if the tab or page changes meanwhile, and is cancelled with a message if that document or page is gone. Numbers unchanged. |
|
||||
|
||||
@@ -222,7 +222,7 @@ pub struct Annotation {
|
||||
/// (`figcraft_engine::code_connect`).
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct CodeLink {
|
||||
/// `react`, `swiftui` or `compose`.
|
||||
/// `react`, `leptos`, `vue`, `swiftui`, `compose`, `arkui` or `flutter`.
|
||||
pub language: String,
|
||||
/// What the snippet imports from (`@acme/ui`, a Swift module, a Kotlin name).
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
|
||||
@@ -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|leptos|vue|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",
|
||||
"{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|leptos|vue|swiftui|compose|arkui|flutter (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|leptos|vue|swiftui|compose|arkui"))?,
|
||||
Some(l) => CodeLang::from_id(&l).ok_or_else(|| bad(cmd, "language: css|html|react|leptos|vue|swiftui|compose|arkui|flutter"))?,
|
||||
None => CodeLang::React,
|
||||
};
|
||||
let (copy, inst) = build(s.doc()?, target, &values)?;
|
||||
|
||||
@@ -106,6 +106,17 @@ fn components_take_their_properties_as_parameters() {
|
||||
] {
|
||||
assert!(leptos.contains(want), "Leptos has {want:?}:\n{leptos}");
|
||||
}
|
||||
let dart = code(&mut s, c, "flutter");
|
||||
for want in [
|
||||
"import 'package:flutter/material.dart';\n\nclass Card extends StatelessWidget {\n",
|
||||
" const Card({\n super.key,\n this.showDot = true,\n this.title = \"Hello\",\n });\n",
|
||||
" final bool showDot;\n final String title;\n",
|
||||
" Widget build(BuildContext context) {\n return Container(",
|
||||
"if (showDot)\n",
|
||||
"Text(\n title,",
|
||||
] {
|
||||
assert!(dart.contains(want), "Flutter has {want:?}:\n{dart}");
|
||||
}
|
||||
// The command describes the properties.
|
||||
let v = component_code(&mut s, c, "react");
|
||||
assert_eq!(v["name"], "Card");
|
||||
@@ -140,6 +151,7 @@ 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, "flutter")["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.
|
||||
@@ -147,6 +159,7 @@ fn instances_are_calls_passing_the_values_they_set() {
|
||||
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, "flutter")["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");
|
||||
// A quoted title becomes a binding whose attribute value is escaped: the expression itself
|
||||
@@ -175,6 +188,8 @@ fn nested_instances_in_a_layer_tree_are_calls() {
|
||||
assert!(!react.contains("Hello"), "the instance's layers aren't spelled out: {react}");
|
||||
assert!(code(&mut s, screen, "swiftui").contains("Card(title: \"Inbox\")"));
|
||||
assert!(code(&mut s, screen, "compose").contains("Card(title = \"Inbox\")"));
|
||||
let dart = code(&mut s, screen, "flutter");
|
||||
assert!(dart.contains("Card(title: \"Inbox\")") && !dart.contains("Hello"), "{dart}");
|
||||
let leptos = code(&mut s, screen, "leptos");
|
||||
assert!(leptos.contains("pub fn HomeScreen() -> impl IntoView {") && leptos.contains("<Card title=\"Inbox\" />"), "{leptos}");
|
||||
assert!(!leptos.contains("Hello"), "{leptos}");
|
||||
@@ -232,6 +247,19 @@ fn variant_sets_share_one_definition() {
|
||||
assert_eq!(component_code(&mut s, i, "leptos")["usage"], "<Button state=ButtonState::Hover />\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flutter_variant_sets_share_one_enum() {
|
||||
let mut s = Session::new();
|
||||
let (set, _, _) = button_set(&mut s);
|
||||
let dart = code(&mut s, set, "flutter");
|
||||
assert!(dart.contains("enum ButtonState { defaultValue, hover }\n"), "{dart}");
|
||||
assert!(dart.contains(" this.state = ButtonState.defaultValue,\n"), "{dart}");
|
||||
assert!(dart.contains(" final ButtonState state;\n"), "{dart}");
|
||||
let i = idv(&ex(&mut s, "instance.create", json!({"component": set, "x": 0, "y": 600})));
|
||||
ex(&mut s, "instance.setProperties", json!({"ids": [i], "properties": {"State": "Hover"}}));
|
||||
assert_eq!(component_code(&mut s, i, "flutter")["usage"], "Button(state: ButtonState.hover)\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn instance_swap_properties_take_views() {
|
||||
let mut s = Session::new();
|
||||
@@ -252,6 +280,10 @@ 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 dart = code(&mut s, chip, "flutter");
|
||||
assert!(dart.contains(" final Widget icon;\n") && dart.contains(" this.icon = const HeartIcon(),\n"), "{dart}");
|
||||
assert!(dart.contains("child: icon\n"), "the nested instance is the parameter: {dart}");
|
||||
assert!(!dart.contains("child: Container("), "the swap's widget tree isn't spelled out: {dart}");
|
||||
let ark = code(&mut s, chip, "arkui");
|
||||
for want in [
|
||||
" @BuilderParam icon: () => void = this.defaultIconBuilder",
|
||||
@@ -272,6 +304,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, "flutter")["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");
|
||||
assert_eq!(component_code(&mut s, i, "vue")["usage"], "<Chip :icon=\"StarIcon\" />\n");
|
||||
@@ -289,6 +322,9 @@ 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 dart = code(&mut s, panel, "flutter");
|
||||
assert!(dart.contains(" final List<Widget>? content;\n") && dart.contains(" this.content,\n"), "{dart}");
|
||||
assert!(dart.contains("...?content,\n") && dart.contains("if (content == null) ...[\n"), "{dart}");
|
||||
let ark = code(&mut s, panel, "arkui");
|
||||
for want in [
|
||||
" @BuilderParam content: () => void = this.defaultContentBuilder",
|
||||
@@ -305,11 +341,14 @@ fn slots_take_optional_content() {
|
||||
let badge = idv(&ex(&mut s, "component.create", json!({"ids": [g]})));
|
||||
let i = idv(&ex(&mut s, "instance.create", json!({"component": panel, "x": 400, "y": 0})));
|
||||
assert_eq!(component_code(&mut s, i, "react")["usage"], "<Panel />\n", "the main's content isn't passed");
|
||||
assert_eq!(component_code(&mut s, i, "flutter")["usage"], "Panel()\n", "the main's content isn't passed");
|
||||
ex(&mut s, "instance.slotInsert", json!({"id": i, "slot": "Content", "component": badge, "replace": true}));
|
||||
let usage = component_code(&mut s, i, "react")["usage"].as_str().unwrap_or_default().to_string();
|
||||
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, "flutter")["usage"].as_str().unwrap_or_default().to_string();
|
||||
assert!(usage.starts_with("Panel(content: [\n") && usage.contains("Badge()"), "{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}");
|
||||
}
|
||||
@@ -359,10 +398,22 @@ fn identifiers_are_escaped_and_unique() {
|
||||
] {
|
||||
assert!(leptos.contains(want), "Leptos has {want:?}:\n{leptos}");
|
||||
}
|
||||
// Dart: lowerCamel members, keywords suffixed, never a type the widget tree names.
|
||||
let dart = code_for(doc, c, CodeLang::Flutter);
|
||||
for want in [
|
||||
"class TextComponent extends StatelessWidget {",
|
||||
"enum TextComponentColorVariant { red }\n",
|
||||
" this.defaultValue = false,",
|
||||
" this.p2Columns = false,",
|
||||
" final String type;",
|
||||
] {
|
||||
assert!(dart.contains(want), "Flutter has {want:?}:\n{dart}");
|
||||
}
|
||||
// Errors.
|
||||
let r = idv(&ex(&mut s, "shape.rectangle", json!({"x": 0, "y": 0, "width": 10, "height": 10})));
|
||||
assert!(s.execute("inspect.componentCode", &json!({"id": r})).is_err(), "not a component");
|
||||
assert!(s.execute("inspect.componentCode", &json!({"id": c, "language": "html"})).is_err());
|
||||
assert!(s.execute("inspect.componentCode", &json!({"id": c, "language": "flutter"})).is_ok());
|
||||
assert!(s.execute("inspect.componentCode", &json!({"id": 999_999})).is_err());
|
||||
}
|
||||
|
||||
|
||||
@@ -37,8 +37,9 @@ 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::Vue | CodeLang::SwiftUi | CodeLang::Compose | CodeLang::ArkUi))
|
||||
CodeLang::from_id(s).filter(|l| {
|
||||
matches!(l, CodeLang::React | CodeLang::Leptos | CodeLang::Vue | CodeLang::SwiftUi | CodeLang::Compose | CodeLang::ArkUi | CodeLang::Flutter)
|
||||
})
|
||||
}
|
||||
|
||||
/// The layer `id`'s links are stored on: its component set, else its component (an instance's
|
||||
@@ -69,6 +70,7 @@ pub fn import_line(link: &CodeLink, lang: CodeLang) -> String {
|
||||
CodeLang::Leptos => format!("use {path}::{};", link.name.split('.').next().unwrap_or_default()),
|
||||
// A single-file component is a default export.
|
||||
CodeLang::Vue => format!("import {} from {};", link.name.split('.').next().unwrap_or_default(), js_string(path)),
|
||||
CodeLang::Flutter => format!("import '{}';", path.replace(['\'', '\\'], "")),
|
||||
_ => format!("import {path}"),
|
||||
}
|
||||
}
|
||||
@@ -273,6 +275,7 @@ fn expr(doc: &Document, c: &Component, p: &Param, v: &Value, lang: CodeLang) ->
|
||||
(Some(l), CodeLang::Vue) => l.name.clone(),
|
||||
(Some(l), CodeLang::SwiftUi) => format!("{}()", l.name),
|
||||
(Some(l), CodeLang::ArkUi) => format!("() => {{ {}() }}", l.name),
|
||||
(Some(l), CodeLang::Flutter) => format!("{}()", l.name),
|
||||
(Some(l), _) => format!("{{ {}() }}", l.name),
|
||||
(None, _) => value_expr(doc, c, p, v, lang),
|
||||
}
|
||||
@@ -456,7 +459,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::Vue => format!("<{name}{} />", params.iter().map(|p| format!(" :{p}={{{p}}}")).collect::<String>()),
|
||||
CodeLang::SwiftUi => format!("{name}({})", params.iter().map(|p| format!("{p}: {{{p}}}")).collect::<Vec<_>>().join(", ")),
|
||||
CodeLang::SwiftUi | CodeLang::Flutter => 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(", ")),
|
||||
}
|
||||
@@ -478,6 +481,7 @@ pub fn check(c: &Component, link: &CodeLink, lang: CodeLang) -> std::result::Res
|
||||
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 == ':'),
|
||||
CodeLang::Vue => path.chars().all(|ch| ch.is_ascii_alphanumeric() || "_./@-".contains(ch)),
|
||||
CodeLang::Flutter => path.chars().all(|ch| ch.is_ascii_alphanumeric() || "_./:-".contains(ch)),
|
||||
_ => path.chars().all(|ch| ch.is_ascii_alphanumeric() || ch == '_' || ch == '.'),
|
||||
};
|
||||
if path.chars().count() > MAX_IMPORT || !path_ok {
|
||||
@@ -486,6 +490,9 @@ pub fn check(c: &Component, link: &CodeLink, lang: CodeLang) -> std::result::Res
|
||||
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"),
|
||||
CodeLang::Vue => format!("importPath: a module path (letters, digits, _ . / @ and -), up to {MAX_IMPORT} characters"),
|
||||
CodeLang::Flutter => {
|
||||
format!("importPath: a Dart import (package:acme/card.dart; letters, digits, _ . / : and -), up to {MAX_IMPORT} characters")
|
||||
}
|
||||
_ => format!("importPath: a module or package name (letters, digits, _ and .), up to {MAX_IMPORT} characters"),
|
||||
});
|
||||
}
|
||||
@@ -521,7 +528,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|vue|swiftui|compose|arkui")),
|
||||
Some(l) => language(&l).ok_or_else(|| bad(cmd, "language: react|leptos|vue|swiftui|compose|arkui|flutter")),
|
||||
None => Ok(CodeLang::React),
|
||||
}
|
||||
}
|
||||
@@ -551,7 +558,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
spec!(
|
||||
"codeConnect.set",
|
||||
"Connect to code",
|
||||
"{id? (component, set or instance; default the selection), language?: react|leptos|vue|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)}",
|
||||
"{id? (component, set or instance; default the selection), language?: react|leptos|vue|swiftui|compose|arkui|flutter (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";
|
||||
@@ -666,7 +673,10 @@ 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|vue|swiftui|compose|arkui), name and template strings", i + 1));
|
||||
warnings.push(format!(
|
||||
"link {}: needs component, language (react|leptos|vue|swiftui|compose|arkui|flutter), name and template strings",
|
||||
i + 1
|
||||
));
|
||||
continue;
|
||||
};
|
||||
let link = CodeLink {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! 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, 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
|
||||
//! layer itself, HTML or React for its subtree, and SwiftUI, Jetpack Compose, ArkUI (HarmonyOS) or
|
||||
//! Flutter (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.
|
||||
|
||||
@@ -20,6 +20,7 @@ use crate::{bad, spec};
|
||||
mod arkui;
|
||||
mod compose;
|
||||
mod css;
|
||||
mod flutter;
|
||||
mod html;
|
||||
mod swift;
|
||||
|
||||
@@ -30,6 +31,7 @@ pub(crate) use css::Tokens;
|
||||
pub(crate) use css::css_decls;
|
||||
use css::{css, tok};
|
||||
pub use css::{css_var_name, css_var_of};
|
||||
pub(crate) use flutter::{FLUTTER_IMPORT, Place, flutter, flutter_indent};
|
||||
use html::html;
|
||||
pub(crate) use html::{Markup, class_name};
|
||||
pub(crate) use swift::swift;
|
||||
@@ -38,13 +40,15 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
vec![spec!(
|
||||
"inspect.code",
|
||||
"Code for layer",
|
||||
"{id?, language?: css|html|react|leptos|vue|tailwind|swiftui|compose|arkui (default css; html, react, leptos and vue 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; tailwind is React with Tailwind CSS v4 utilities for pages: fluid auto layout boxes, instances inlined with their overrides, bound variables as theme utilities), mobile? (tailwind: a mobile frame with the same layer names; the code is mobile first with the desktop layer's classes behind lg:), unit?: px|rem (CSS, HTML, React, Leptos and Vue; default px), rootFontSize? (rem: pixels per rem, default 16)} → {code, language, connected}",
|
||||
"{id?, language?: css|html|react|leptos|vue|tailwind|swiftui|compose|arkui|flutter (default css; html, react, leptos and vue 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; tailwind is React with Tailwind CSS v4 utilities for pages: fluid auto layout boxes, instances inlined with their overrides, bound variables as theme utilities), mobile? (tailwind: a mobile frame with the same layer names; the code is mobile first with the desktop layer's classes behind lg:), unit?: px|rem (CSS, HTML, React, Leptos and Vue; 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|vue|tailwind|swiftui|compose|arkui"))?,
|
||||
Some(l) => {
|
||||
CodeLang::from_id(&l).ok_or_else(|| bad(cmd, "language: css|html|react|leptos|vue|tailwind|swiftui|compose|arkui|flutter"))?
|
||||
}
|
||||
None => CodeLang::Css,
|
||||
};
|
||||
let rem = match string(p, "unit").map(|u| u.to_ascii_lowercase()).as_deref() {
|
||||
@@ -96,10 +100,12 @@ pub enum CodeLang {
|
||||
Compose,
|
||||
/// HarmonyOS ArkUI: an ArkTS struct (`@Component`, `build`).
|
||||
ArkUi,
|
||||
/// A Flutter `StatelessWidget` (Dart) with its widget tree.
|
||||
Flutter,
|
||||
}
|
||||
|
||||
impl CodeLang {
|
||||
pub const ALL: [CodeLang; 9] = [
|
||||
pub const ALL: [CodeLang; 10] = [
|
||||
CodeLang::Css,
|
||||
CodeLang::Html,
|
||||
CodeLang::React,
|
||||
@@ -109,6 +115,7 @@ impl CodeLang {
|
||||
CodeLang::SwiftUi,
|
||||
CodeLang::Compose,
|
||||
CodeLang::ArkUi,
|
||||
CodeLang::Flutter,
|
||||
];
|
||||
pub fn id(self) -> &'static str {
|
||||
match self {
|
||||
@@ -121,6 +128,7 @@ impl CodeLang {
|
||||
CodeLang::SwiftUi => "swiftui",
|
||||
CodeLang::Compose => "compose",
|
||||
CodeLang::ArkUi => "arkui",
|
||||
CodeLang::Flutter => "flutter",
|
||||
}
|
||||
}
|
||||
pub fn label(self) -> &'static str {
|
||||
@@ -134,6 +142,7 @@ impl CodeLang {
|
||||
CodeLang::SwiftUi => "SwiftUI",
|
||||
CodeLang::Compose => "Compose",
|
||||
CodeLang::ArkUi => "ArkUI",
|
||||
CodeLang::Flutter => "Flutter",
|
||||
}
|
||||
}
|
||||
/// Languages whose lengths are CSS pixels that can be shown in rem (not Tailwind: its lengths
|
||||
@@ -141,6 +150,11 @@ impl CodeLang {
|
||||
pub fn is_web(self) -> bool {
|
||||
matches!(self, CodeLang::Css | CodeLang::Html | CodeLang::React | CodeLang::Leptos | CodeLang::Vue)
|
||||
}
|
||||
/// Languages whose strings interpolate `$` (Kotlin's `$name`, Dart's `$name`), where a literal
|
||||
/// `$` has to be escaped.
|
||||
pub fn escapes_dollar(self) -> bool {
|
||||
matches!(self, CodeLang::Compose | CodeLang::Flutter)
|
||||
}
|
||||
pub fn from_id(s: &str) -> Option<CodeLang> {
|
||||
if s.eq_ignore_ascii_case("tsx") || s.eq_ignore_ascii_case("jsx") {
|
||||
return Some(CodeLang::React);
|
||||
@@ -151,6 +165,9 @@ impl CodeLang {
|
||||
if s.eq_ignore_ascii_case("arkts") {
|
||||
return Some(CodeLang::ArkUi);
|
||||
}
|
||||
if s.eq_ignore_ascii_case("dart") {
|
||||
return Some(CodeLang::Flutter);
|
||||
}
|
||||
CodeLang::ALL.into_iter().find(|l| l.id().eq_ignore_ascii_case(s) || l.label().eq_ignore_ascii_case(s))
|
||||
}
|
||||
}
|
||||
@@ -262,7 +279,7 @@ pub fn code_for(doc: &Document, id: NodeId, lang: CodeLang) -> String {
|
||||
CodeLang::Css => css(doc, id),
|
||||
CodeLang::Html => html(doc, id),
|
||||
CodeLang::Tailwind => crate::tailwind::code(doc, id, None),
|
||||
CodeLang::React | CodeLang::Leptos | CodeLang::Vue | CodeLang::SwiftUi | CodeLang::Compose | CodeLang::ArkUi => {
|
||||
CodeLang::React | CodeLang::Leptos | CodeLang::Vue | CodeLang::SwiftUi | CodeLang::Compose | CodeLang::ArkUi | CodeLang::Flutter => {
|
||||
crate::component_codegen::code(doc, id, lang)
|
||||
}
|
||||
}
|
||||
@@ -549,6 +566,111 @@ mod tests {
|
||||
assert!(ark.contains(&format!(".position({{ x: {}, y: {} }})", num(x), num(y))), "the absolute child keeps its place: {ark}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flutter_builds_a_widget_tree() {
|
||||
let mut s = Session::new();
|
||||
let f = card(&mut s);
|
||||
let doc = s.doc().unwrap();
|
||||
let dart = code_for(doc, f, CodeLang::Flutter);
|
||||
// The card hugs its content, so its width is the layout's, not the 200 it was created with.
|
||||
let w = doc.node(f).map(|n| n.width).unwrap_or_default();
|
||||
assert!(dart.starts_with(&format!("Container(\n width: {},\n height: 100,\n", num(w))), "{dart}");
|
||||
assert!(dart.contains("borderRadius: BorderRadius.circular(12)"), "{dart}");
|
||||
assert!(dart.contains("child: Row(\n"), "{dart}");
|
||||
assert!(dart.contains(" Text(\n \"Say \\\"hi\\\" \\$5\\nnow\",\n"), "escaped: {dart}");
|
||||
assert!(dart.contains("fontWeight: FontWeight.w400,"), "{dart}");
|
||||
assert!(dart.contains("shape: BoxShape.circle"), "{dart}");
|
||||
assert!(dart.ends_with(')'), "a bare expression, like the other native targets: {dart}");
|
||||
// Through the command: `dart` names the same language, and rem is for the web only.
|
||||
let v = s.execute("inspect.code", &json!({"id": f, "language": "flutter"})).unwrap();
|
||||
assert_eq!(v["code"].as_str().unwrap(), dart);
|
||||
assert_eq!(v["language"], "flutter");
|
||||
assert_eq!(s.execute("inspect.code", &json!({"id": f, "language": "dart"})).unwrap()["language"], "flutter");
|
||||
let rem = s.execute("inspect.code", &json!({"id": f, "language": "flutter", "unit": "rem"})).unwrap();
|
||||
assert_eq!(rem["code"].as_str().unwrap(), dart, "rem doesn't apply to Dart");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flutter_positions_free_children_and_bounds_the_tree() {
|
||||
let mut s = Session::new();
|
||||
let root = id_of(&s.execute("frame.create", &json!({"x": 0, "y": 0, "width": 50, "height": 50})).unwrap()["id"]).unwrap();
|
||||
s.execute("shape.rectangle", &json!({"x": 7, "y": 9, "width": 5, "height": 5, "parent": root})).unwrap();
|
||||
let mut parent = root;
|
||||
for _ in 0..20 {
|
||||
parent = id_of(&s.execute("frame.create", &json!({"x": 1, "y": 1, "width": 40, "height": 40, "parent": parent})).unwrap()["id"]).unwrap();
|
||||
}
|
||||
let dart = code_for(s.doc().unwrap(), root, CodeLang::Flutter);
|
||||
assert!(dart.contains("Stack(\n alignment: Alignment.topLeft,\n"), "{dart}");
|
||||
assert!(dart.contains("Positioned(\n left: 7,\n top: 9,\n"), "{dart}");
|
||||
// A wrapper's closing parenthesis sits on its own line, not glued to the widget's.
|
||||
assert!(dart.contains(" )\n ),\n"), "{dart}");
|
||||
assert!(dart.contains("…"), "the depth is capped: {dart}");
|
||||
assert!(dart.contains("const SizedBox()"), "a capped layer is still a widget in the list: {dart}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flutter_keeps_a_capped_layers_name_on_its_comment_line() {
|
||||
let mut s = Session::new();
|
||||
let root = id_of(&s.execute("frame.create", &json!({"x": 0, "y": 0, "width": 50, "height": 50})).unwrap()["id"]).unwrap();
|
||||
let mut parent = root;
|
||||
for _ in 0..20 {
|
||||
let p = json!({"x": 1, "y": 1, "width": 40, "height": 40, "parent": parent, "name": "deep\nvoid main() {}"});
|
||||
parent = id_of(&s.execute("frame.create", &p).unwrap()["id"]).unwrap();
|
||||
}
|
||||
let dart = code_for(s.doc().unwrap(), root, CodeLang::Flutter);
|
||||
assert!(dart.contains("…"), "the depth is capped: {dart}");
|
||||
assert!(!dart.lines().any(|l| l.trim_start().starts_with("void main()")), "a name never leaves its comment: {dart}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flutter_expands_and_stretches_fill_children() {
|
||||
let mut s = Session::new();
|
||||
let row = id_of(&s.execute("frame.create", &json!({"x": 0, "y": 0, "width": 200, "height": 40})).unwrap()["id"]).unwrap();
|
||||
let a = id_of(&s.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 50, "height": 20, "parent": row})).unwrap()["id"]).unwrap();
|
||||
s.execute("shape.rectangle", &json!({"x": 60, "y": 0, "width": 50, "height": 20, "parent": row})).unwrap();
|
||||
s.execute("layout.add", &json!({"ids": [row], "direction": "horizontal"})).unwrap();
|
||||
s.execute("node.set", &json!({"id": a, "props": {"sizingH": "fill", "sizingV": "fill"}})).unwrap();
|
||||
let dart = code_for(s.doc().unwrap(), row, CodeLang::Flutter);
|
||||
assert!(dart.contains("child: Row(\n"), "{dart}");
|
||||
assert!(dart.contains("Expanded(\n"), "a child filling the main axis takes the room left: {dart}");
|
||||
assert!(dart.contains("crossAxisAlignment: CrossAxisAlignment.stretch,"), "a child filling the cross axis stretches the line: {dart}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flutter_lifts_a_child_that_ignores_its_auto_layout_parent_out_of_the_line() {
|
||||
let mut s = Session::new();
|
||||
let row = id_of(&s.execute("frame.create", &json!({"x": 0, "y": 0, "width": 200, "height": 40})).unwrap()["id"]).unwrap();
|
||||
let flow = id_of(&s.execute("shape.rectangle", &json!({"x": 4, "y": 4, "width": 50, "height": 20, "parent": row})).unwrap()["id"]).unwrap();
|
||||
let over = id_of(&s.execute("shape.ellipse", &json!({"x": 150, "y": 6, "width": 30, "height": 30, "parent": row})).unwrap()["id"]).unwrap();
|
||||
s.execute("layout.add", &json!({"ids": [row], "direction": "horizontal"})).unwrap();
|
||||
s.execute("node.set", &json!({"id": row, "props": {"padding": [4, 4, 4, 4]}})).unwrap();
|
||||
// It asks for both axes, which the line would read as "stretch me and take the room left".
|
||||
s.execute("node.set", &json!({"id": over, "props": {"absolute": true, "sizingH": "fill", "sizingV": "fill"}})).unwrap();
|
||||
let doc = s.doc().unwrap();
|
||||
let dart = code_for(doc, row, CodeLang::Flutter);
|
||||
let at = |needle: &str| dart.find(needle).unwrap_or_else(|| panic!("no {needle:?}: {dart}"));
|
||||
// Over the line, in a Stack at the parent's own box, at its own offsets.
|
||||
let [_, _, _, _, x, y] = doc.node(over).unwrap().transform.as_coeffs();
|
||||
assert!(
|
||||
dart.contains(&format!("Positioned(\n left: {},\n top: {},\n", num(x), num(y))),
|
||||
"the absolute child keeps its place: {dart}"
|
||||
);
|
||||
let stack = at("child: Stack(\n");
|
||||
let fill = at("Positioned.fill(\n");
|
||||
let padding = at("padding: EdgeInsets.all(4),");
|
||||
let line = at("child: Row(\n");
|
||||
assert!(stack < fill && fill < padding && padding < line, "the line sits in the Stack, padded: {dart}");
|
||||
assert!(!dart[..padding].contains("padding: "), "the parent's padding would offset the absolute child: {dart}");
|
||||
// Only the flow child is in the line, so neither `Expanded` nor a stretch comes from it.
|
||||
let over_at = at("\n Positioned(\n");
|
||||
let line_part = &dart[line..over_at];
|
||||
assert!(line_part.contains("width: 50,"), "the flow child is in the line: {dart}");
|
||||
assert!(!line_part.contains("width: 30,"), "the absolute child left the line: {dart}");
|
||||
assert!(!dart.contains("Expanded(\n"), "it takes no room in the line either: {dart}");
|
||||
assert!(dart.contains("crossAxisAlignment: CrossAxisAlignment.start,"), "{dart}");
|
||||
assert!(doc.node(flow).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arkui_wraps_a_positioned_instance_swap_so_the_position_lands_on_a_container() {
|
||||
let mut s = Session::new();
|
||||
|
||||
@@ -0,0 +1,364 @@
|
||||
use figcraft_doc::AutoLayout;
|
||||
|
||||
use super::swift::one_line;
|
||||
use super::{
|
||||
CodeLang, CrossAlign, Ctx, LayoutMode, LetterSpacing, LineHeight, MainAlign, Node, NodeId, NodeKind, Param, Sizing, TextAlignH, TextDecoration,
|
||||
drop_shadow, first_fill, kotlin_color, literal, num, radius, stroke_color,
|
||||
};
|
||||
|
||||
// ------------------------------------------------------------------ Flutter (Dart)
|
||||
|
||||
/// The import a generated Flutter unit starts with (a widget tree on its own needs it too).
|
||||
pub(crate) const FLUTTER_IMPORT: &str = "import 'package:flutter/material.dart';\n\n";
|
||||
|
||||
/// Dart indents by two spaces.
|
||||
pub(crate) fn flutter_indent(depth: usize) -> String {
|
||||
" ".repeat(depth)
|
||||
}
|
||||
|
||||
/// Where a child sits in its parent: in a Stack at its position in the frame, taking the room left
|
||||
/// in a Row or Column, or wherever the parent puts it.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub(crate) enum Place {
|
||||
Flow,
|
||||
At(f64, f64),
|
||||
Fill,
|
||||
}
|
||||
|
||||
/// A child and where its parent puts it.
|
||||
type Placed = (NodeId, Place);
|
||||
|
||||
/// Dart's `FontWeight.w600` (a weight is hundreds).
|
||||
fn flutter_weight(w: u16) -> String {
|
||||
format!("FontWeight.w{}", (w.clamp(100, 900) / 100) * 100)
|
||||
}
|
||||
|
||||
/// A Row or Column's main-axis alignment (`None` for the default, the start).
|
||||
fn flutter_main_align(a: MainAlign) -> Option<&'static str> {
|
||||
match a {
|
||||
MainAlign::Min => None,
|
||||
MainAlign::Center => Some("MainAxisAlignment.center"),
|
||||
MainAlign::Max => Some("MainAxisAlignment.end"),
|
||||
MainAlign::SpaceBetween => Some("MainAxisAlignment.spaceBetween"),
|
||||
MainAlign::SpaceAround => Some("MainAxisAlignment.spaceAround"),
|
||||
MainAlign::SpaceEvenly => Some("MainAxisAlignment.spaceEvenly"),
|
||||
}
|
||||
}
|
||||
|
||||
/// A Row or Column's cross-axis alignment; a flow child that fills it stretches the line instead.
|
||||
fn flutter_cross_align(a: CrossAlign, stretch: bool) -> &'static str {
|
||||
if stretch {
|
||||
return "CrossAxisAlignment.stretch";
|
||||
}
|
||||
match a {
|
||||
CrossAlign::Min => "CrossAxisAlignment.start",
|
||||
CrossAlign::Center => "CrossAxisAlignment.center",
|
||||
CrossAlign::Max => "CrossAxisAlignment.end",
|
||||
CrossAlign::Baseline => "CrossAxisAlignment.baseline",
|
||||
}
|
||||
}
|
||||
|
||||
/// A frame's auto layout padding as `EdgeInsets` (`None` when it has none).
|
||||
fn flutter_padding(n: &Node) -> Option<String> {
|
||||
let l = n.layout.as_ref()?;
|
||||
let [t, r, b, left] = l.padding;
|
||||
if ![t, r, b, left].iter().any(|v| *v > 0.0) {
|
||||
return None;
|
||||
}
|
||||
Some(if t == r && r == b && b == left {
|
||||
format!("EdgeInsets.all({})", num(t))
|
||||
} else if t == b && left == r {
|
||||
format!("EdgeInsets.symmetric(vertical: {}, horizontal: {})", num(t), num(left))
|
||||
} else {
|
||||
format!("EdgeInsets.fromLTRB({}, {}, {}, {})", num(left), num(t), num(r), num(b))
|
||||
})
|
||||
}
|
||||
|
||||
/// A layer's fill, shape, corner radius, stroke and shadow as a `BoxDecoration`; `None` when it has
|
||||
/// none of them. `pad` is the indentation of the constructor itself.
|
||||
fn box_decoration(n: &Node, pad: &str) -> Option<String> {
|
||||
let mut parts: Vec<String> = Vec::new();
|
||||
if let Some(c) = first_fill(n) {
|
||||
parts.push(format!("color: {}", kotlin_color(c)));
|
||||
}
|
||||
if matches!(n.kind, NodeKind::Ellipse { .. }) {
|
||||
parts.push("shape: BoxShape.circle".into());
|
||||
} else if radius(n) > 0.0 {
|
||||
parts.push(format!("borderRadius: BorderRadius.circular({})", num(radius(n))));
|
||||
}
|
||||
if let Some(c) = stroke_color(n) {
|
||||
parts.push(format!("border: Border.all(color: {}, width: {})", kotlin_color(c), num(n.stroke.weight)));
|
||||
}
|
||||
if let Some(s) = drop_shadow(n) {
|
||||
parts.push(format!(
|
||||
"boxShadow: [BoxShadow(color: {}, blurRadius: {}, offset: Offset({}, {}))]",
|
||||
kotlin_color(s.color),
|
||||
num(s.radius / 2.0),
|
||||
num(s.offset.0),
|
||||
num(s.offset.1)
|
||||
));
|
||||
}
|
||||
if parts.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let inner = format!("{pad} ");
|
||||
Some(format!("BoxDecoration(\n{}{pad})", parts.iter().map(|p| format!("{inner}{p},\n")).collect::<String>()))
|
||||
}
|
||||
|
||||
/// Flutter for a layer and its subtree at `depth`. A layer a boolean parameter shows sits behind an
|
||||
/// `if (…)`: Dart's lists take conditions, so the element itself carries it.
|
||||
pub(crate) fn flutter(ctx: &Ctx, id: NodeId, depth: usize, out: &mut String, place: Place) {
|
||||
let Some(n) = ctx.doc.node(id) else { return };
|
||||
if id != ctx.root
|
||||
&& let Some(p) = ctx.param(n, "visible")
|
||||
{
|
||||
out.push_str(&format!("{}if ({})\n", flutter_indent(depth), p.ident));
|
||||
flutter_placed(ctx, n, depth + 1, out, place);
|
||||
return;
|
||||
}
|
||||
flutter_placed(ctx, n, depth, out, place);
|
||||
}
|
||||
|
||||
/// A layer at `depth` and what wraps it: where its parent puts it, then its opacity. Every closing
|
||||
/// parenthesis sits at its own level, so the caller can end the element with a comma in a list.
|
||||
fn flutter_placed(ctx: &Ctx, n: &Node, depth: usize, out: &mut String, place: Place) {
|
||||
if ctx.too_deep(depth) {
|
||||
let pad = flutter_indent(depth);
|
||||
out.push_str(&format!("{pad}// {} …\n{pad}const SizedBox()", one_line(&n.name)));
|
||||
return;
|
||||
}
|
||||
let mut depth = depth;
|
||||
let mut close: Vec<String> = Vec::new();
|
||||
match place {
|
||||
Place::Flow => {}
|
||||
Place::At(x, y) => {
|
||||
let (pad, inner) = (flutter_indent(depth), flutter_indent(depth + 1));
|
||||
out.push_str(&format!("{pad}Positioned(\n{inner}left: {},\n{inner}top: {},\n{inner}child: ", num(x), num(y)));
|
||||
close.push(format!("\n{pad})"));
|
||||
depth += 1;
|
||||
}
|
||||
Place::Fill => {
|
||||
let (pad, inner) = (flutter_indent(depth), flutter_indent(depth + 1));
|
||||
out.push_str(&format!("{pad}Expanded(\n{inner}child: "));
|
||||
close.push(format!("\n{pad})"));
|
||||
depth += 1;
|
||||
}
|
||||
}
|
||||
if n.opacity < 1.0 {
|
||||
let (pad, inner) = (flutter_indent(depth), flutter_indent(depth + 1));
|
||||
out.push_str(&format!("{pad}Opacity(\n{inner}opacity: {},\n{inner}child: ", num(f64::from(n.opacity))));
|
||||
close.push(format!("\n{pad})"));
|
||||
depth += 1;
|
||||
}
|
||||
// A wrapper leaves its line open ("child: "), so the widget carries no indentation of its own.
|
||||
let mut body = String::new();
|
||||
flutter_widget(ctx, n, depth, &mut body);
|
||||
out.push_str(if close.is_empty() { body.as_str() } else { body.trim_start() });
|
||||
for c in close.iter().rev() {
|
||||
out.push_str(c);
|
||||
}
|
||||
}
|
||||
|
||||
/// A layer's own look and its subtree as a Flutter widget at `depth`: a complete expression whose
|
||||
/// closing parenthesis sits at `depth`.
|
||||
fn flutter_widget(ctx: &Ctx, n: &Node, depth: usize, out: &mut String) {
|
||||
let (pad, inner) = (flutter_indent(depth), flutter_indent(depth + 1));
|
||||
// Nested instances are calls; an instance swap parameter is a widget.
|
||||
let call = match ctx.param(n, "mainComponent").filter(|_| n.id != ctx.root) {
|
||||
Some(p) => Some(p.ident.clone()),
|
||||
None => ctx.call(n, CodeLang::Flutter, depth).map(|(code, _)| code),
|
||||
};
|
||||
if let Some(call) = call {
|
||||
out.push_str(call.trim_end_matches('\n'));
|
||||
return;
|
||||
}
|
||||
match &n.kind {
|
||||
NodeKind::Text(t) => {
|
||||
let text = ctx.param(n, "characters").map_or_else(|| literal(&t.characters, true), |p| p.ident.clone());
|
||||
out.push_str(&format!("{pad}Text(\n{inner}{text},\n"));
|
||||
match t.align_h {
|
||||
TextAlignH::Left => {}
|
||||
TextAlignH::Center => out.push_str(&format!("{inner}textAlign: TextAlign.center,\n")),
|
||||
TextAlignH::Right => out.push_str(&format!("{inner}textAlign: TextAlign.right,\n")),
|
||||
TextAlignH::Justified => out.push_str(&format!("{inner}textAlign: TextAlign.justify,\n")),
|
||||
}
|
||||
let s = &t.style;
|
||||
let mut style: Vec<String> = Vec::new();
|
||||
if !s.font_family.is_empty() {
|
||||
style.push(format!("fontFamily: {}", literal(&s.font_family, true)));
|
||||
}
|
||||
style.push(format!("fontSize: {}", num(s.font_size)));
|
||||
style.push(format!("fontWeight: {}", flutter_weight(s.weight())));
|
||||
if let Some(c) = first_fill(n) {
|
||||
style.push(format!("color: {}", kotlin_color(c)));
|
||||
}
|
||||
if s.italic() {
|
||||
style.push("fontStyle: FontStyle.italic".into());
|
||||
}
|
||||
match s.line_height {
|
||||
// Dart's height is a multiple of the font size, not a length.
|
||||
LineHeight::Pixels(v) if v > 0.0 && s.font_size > 0.0 => style.push(format!("height: {}", num(v / s.font_size))),
|
||||
LineHeight::Percent(p) if p > 0.0 => style.push(format!("height: {}", num(p / 100.0))),
|
||||
_ => {}
|
||||
}
|
||||
match s.letter_spacing {
|
||||
LetterSpacing::Pixels(v) if v != 0.0 => style.push(format!("letterSpacing: {}", num(v))),
|
||||
LetterSpacing::Percent(p) if p != 0.0 && s.font_size > 0.0 => style.push(format!("letterSpacing: {}", num(p / 100.0 * s.font_size))),
|
||||
_ => {}
|
||||
}
|
||||
match s.decoration {
|
||||
TextDecoration::None => {}
|
||||
TextDecoration::Underline => style.push("decoration: TextDecoration.underline".into()),
|
||||
TextDecoration::Strikethrough => style.push("decoration: TextDecoration.lineThrough".into()),
|
||||
}
|
||||
let sp = flutter_indent(depth + 2);
|
||||
out.push_str(&format!("{inner}style: TextStyle(\n{}{inner}),\n{pad})", style.iter().map(|x| format!("{sp}{x},\n")).collect::<String>()));
|
||||
}
|
||||
NodeKind::Ellipse { .. } | NodeKind::Rectangle => {
|
||||
out.push_str(&format!("{pad}Container(\n{inner}width: {},\n{inner}height: {},\n", num(n.width), num(n.height)));
|
||||
if let Some(d) = box_decoration(n, &inner) {
|
||||
out.push_str(&format!("{inner}decoration: {d},\n"));
|
||||
}
|
||||
out.push_str(&format!("{pad})"));
|
||||
}
|
||||
k if k.is_frame_like() || matches!(k, NodeKind::Group) => {
|
||||
let children = ctx.kids(n);
|
||||
let slot = ctx.param(n, "slot");
|
||||
// The children block says whether it kept the padding for itself (an absolute child
|
||||
// puts the line in a Stack at the parent's own box).
|
||||
let mut kids = String::new();
|
||||
let padded_inside = !(children.is_empty() && slot.is_none()) && flutter_children(ctx, n, &children, slot, depth + 1, &mut kids);
|
||||
out.push_str(&format!("{pad}Container(\n{inner}width: {},\n{inner}height: {},\n", num(n.width), num(n.height)));
|
||||
if !padded_inside && let Some(p) = flutter_padding(n) {
|
||||
out.push_str(&format!("{inner}padding: {p},\n"));
|
||||
}
|
||||
if let Some(d) = box_decoration(n, &inner) {
|
||||
out.push_str(&format!("{inner}decoration: {d},\n"));
|
||||
}
|
||||
if children.is_empty() && slot.is_none() {
|
||||
out.push_str(&format!("{pad})"));
|
||||
return;
|
||||
}
|
||||
out.push_str(&format!("{inner}alignment: Alignment.topLeft,\n{inner}child: "));
|
||||
out.push_str(kids.trim_start());
|
||||
out.push_str(&format!(",\n{pad})"));
|
||||
}
|
||||
_ => out.push_str(&format!(
|
||||
"{pad}// {}: export as an image asset\n{pad}Image.asset({}, width: {}, height: {})",
|
||||
n.kind.label(),
|
||||
literal(&format!("{}.png", n.name), true),
|
||||
num(n.width),
|
||||
num(n.height)
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// The Row, Column or Stack holding a frame-like layer's children, at `depth`. Returns whether it
|
||||
/// took the parent's padding over (see [`flutter_widget`]).
|
||||
fn flutter_children(ctx: &Ctx, n: &Node, children: &[NodeId], slot: Option<&Param>, depth: usize, out: &mut String) -> bool {
|
||||
let doc = ctx.doc;
|
||||
let (pad, inner) = (flutter_indent(depth), flutter_indent(depth + 1));
|
||||
// Where each child sits, and whether one of the children in the line asks it to stretch.
|
||||
let mut placed: Vec<Placed> = Vec::new();
|
||||
let mut stretch = false;
|
||||
for c in children {
|
||||
let Some(cn) = doc.node(*c) else { continue };
|
||||
let place = match n.layout.as_ref() {
|
||||
// A child that ignores its auto layout parent keeps its own offsets: it leaves the
|
||||
// line, so it neither stretches it nor takes the room left in it.
|
||||
Some(_) if cn.child_layout.absolute => {
|
||||
let [_, _, _, _, x, y] = cn.transform.as_coeffs();
|
||||
Place::At(x, y)
|
||||
}
|
||||
Some(l) => {
|
||||
let vertical = l.mode == LayoutMode::Vertical;
|
||||
let (main, cross) = if vertical {
|
||||
(cn.child_layout.sizing_v, cn.child_layout.sizing_h)
|
||||
} else {
|
||||
(cn.child_layout.sizing_h, cn.child_layout.sizing_v)
|
||||
};
|
||||
stretch |= cross == Sizing::Fill;
|
||||
if main == Sizing::Fill { Place::Fill } else { Place::Flow }
|
||||
}
|
||||
// Free children sit at their position in the frame.
|
||||
None => {
|
||||
let [_, _, _, _, x, y] = cn.transform.as_coeffs();
|
||||
Place::At(x, y)
|
||||
}
|
||||
};
|
||||
placed.push((*c, place));
|
||||
}
|
||||
let Some(l) = n.layout.as_ref() else {
|
||||
out.push_str(&format!("{pad}Stack(\n{inner}alignment: Alignment.topLeft,\n{inner}children: [\n"));
|
||||
flutter_items(ctx, &placed, slot, depth + 2, out);
|
||||
out.push_str(&format!("{inner}],\n{pad})"));
|
||||
return false;
|
||||
};
|
||||
let (over, flow): (Vec<Placed>, Vec<Placed>) = placed.into_iter().partition(|(_, p)| matches!(p, Place::At(..)));
|
||||
if over.is_empty() {
|
||||
out.push_str(&flutter_line(ctx, l, &flow, slot, stretch, depth));
|
||||
return false;
|
||||
}
|
||||
// A child at its own offsets goes over the line, in a Stack at the parent's own box, so its
|
||||
// offsets stay the parent's and the line keeps the parent's padding.
|
||||
let pad_edges = flutter_padding(n);
|
||||
let list = flutter_indent(depth + 2);
|
||||
let line_depth = depth + 3 + usize::from(pad_edges.is_some());
|
||||
out.push_str(&format!("{pad}Stack(\n{inner}alignment: Alignment.topLeft,\n{inner}children: [\n"));
|
||||
if !flow.is_empty() || slot.is_some() {
|
||||
// Filling the Stack gives the line the parent's size, which `Expanded` children need.
|
||||
out.push_str(&format!("{list}Positioned.fill(\n{list} child: "));
|
||||
match pad_edges {
|
||||
Some(p) => {
|
||||
out.push_str(&format!("Padding(\n{list} padding: {p},\n{list} child: "));
|
||||
out.push_str(flutter_line(ctx, l, &flow, slot, stretch, line_depth).trim_start());
|
||||
out.push_str(&format!(",\n{list} )"));
|
||||
}
|
||||
None => out.push_str(flutter_line(ctx, l, &flow, slot, stretch, line_depth).trim_start()),
|
||||
}
|
||||
out.push_str(&format!(",\n{list}),\n"));
|
||||
}
|
||||
flutter_items(ctx, &over, None, depth + 2, out);
|
||||
out.push_str(&format!("{inner}],\n{pad})"));
|
||||
true
|
||||
}
|
||||
|
||||
/// The Row or Column holding a frame's flow children, at `depth`: a complete widget expression.
|
||||
fn flutter_line(ctx: &Ctx, l: &AutoLayout, flow: &[Placed], slot: Option<&Param>, stretch: bool, depth: usize) -> String {
|
||||
let (pad, inner) = (flutter_indent(depth), flutter_indent(depth + 1));
|
||||
let mut out = format!("{pad}{}(\n", if l.mode == LayoutMode::Vertical { "Column" } else { "Row" });
|
||||
if let Some(m) = flutter_main_align(l.main_align) {
|
||||
out.push_str(&format!("{inner}mainAxisAlignment: {m},\n"));
|
||||
}
|
||||
out.push_str(&format!("{inner}crossAxisAlignment: {},\n", flutter_cross_align(l.cross_align, stretch)));
|
||||
if !stretch && matches!(l.cross_align, CrossAlign::Baseline) {
|
||||
out.push_str(&format!("{inner}textBaseline: TextBaseline.alphabetic,\n"));
|
||||
}
|
||||
// The gap between the children; the space distributions already spread them.
|
||||
if l.gap > 0.0 && flutter_main_align(l.main_align).is_none_or(|m| !m.contains("Space")) {
|
||||
out.push_str(&format!("{inner}spacing: {},\n", num(l.gap)));
|
||||
}
|
||||
out.push_str(&format!("{inner}children: [\n"));
|
||||
flutter_items(ctx, flow, slot, depth + 2, &mut out);
|
||||
out.push_str(&format!("{inner}],\n{pad})"));
|
||||
out
|
||||
}
|
||||
|
||||
/// A children list (`[` already open) at `depth`; a slot shows the instance's content, or else the
|
||||
/// main's.
|
||||
fn flutter_items(ctx: &Ctx, kids: &[Placed], slot: Option<&Param>, depth: usize, out: &mut String) {
|
||||
let list = flutter_indent(depth);
|
||||
if let Some(p) = slot {
|
||||
let name = &p.ident;
|
||||
out.push_str(&format!("{list}...?{name},\n"));
|
||||
out.push_str(&format!("{list}if ({name} == null) ...[\n"));
|
||||
}
|
||||
let kid = depth + usize::from(slot.is_some());
|
||||
for (id, place) in kids {
|
||||
flutter(ctx, *id, kid, out, *place);
|
||||
out.push_str(",\n");
|
||||
}
|
||||
if slot.is_some() {
|
||||
out.push_str(&format!("{list}],\n"));
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,17 @@
|
||||
//! Component code: a component (or component set) becomes a React, Leptos, Vue, 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.
|
||||
//! Compose, ArkUI or Flutter 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::{Document, NodeId, NodeKind, PropDef};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use crate::cmd::CommandSpec;
|
||||
use crate::codegen::{CodeLang, Ctx, Markup, arkui, class_name, compose, indent, literal, swift};
|
||||
use crate::codegen::{CodeLang, Ctx, FLUTTER_IMPORT, Markup, Place, arkui, class_name, compose, flutter, flutter_indent, indent, literal, swift};
|
||||
use crate::components::{prop_default_value, slot_edited, slots_of, variant_options, variant_props, variant_set};
|
||||
use crate::params::{id_of, string, targets};
|
||||
use crate::{bad, spec};
|
||||
@@ -68,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|vue|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}",
|
||||
"{id? (component, set or instance; default the selection), language?: react|leptos|vue|swiftui|compose|arkui|flutter (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") {
|
||||
// One list of code languages, shared with codeConnect (see `code_connect::language`).
|
||||
Some(l) => crate::code_connect::language(&l).ok_or_else(|| bad(cmd, "language: react|leptos|vue|swiftui|compose|arkui"))?,
|
||||
Some(l) => crate::code_connect::language(&l).ok_or_else(|| bad(cmd, "language: react|leptos|vue|swiftui|compose|arkui|flutter"))?,
|
||||
None => CodeLang::React,
|
||||
};
|
||||
let doc = s.doc()?;
|
||||
@@ -108,10 +108,10 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
)]
|
||||
}
|
||||
|
||||
/// Code for any layer in React, Leptos, Vue, 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), with the imports of the connected components it calls
|
||||
/// at the top.
|
||||
/// Code for any layer in React, Leptos, Vue, SwiftUI, Compose, ArkUI or Flutter: 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), with the imports of the connected components it
|
||||
/// calls at the top.
|
||||
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)) {
|
||||
@@ -119,7 +119,15 @@ pub fn code(doc: &Document, id: NodeId, lang: CodeLang) -> String {
|
||||
}
|
||||
let comp = main_of(doc, id).and_then(|m| describe(doc, m));
|
||||
let code = match (&n.kind, comp) {
|
||||
(NodeKind::Instance { .. }, Some(c)) => format!("{}\n{}", usage(doc, id, lang, 0).unwrap_or_default(), definition(doc, &c, lang)),
|
||||
(NodeKind::Instance { .. }, Some(c)) => {
|
||||
let call = usage(doc, id, lang, 0).unwrap_or_default();
|
||||
let def = definition(doc, &c, lang);
|
||||
match def.strip_prefix(FLUTTER_IMPORT) {
|
||||
// Dart's imports come before the code that uses them.
|
||||
Some(rest) if lang == CodeLang::Flutter => format!("{FLUTTER_IMPORT}{call}\n{rest}"),
|
||||
_ => format!("{call}\n{def}"),
|
||||
}
|
||||
}
|
||||
(NodeKind::Component { .. } | NodeKind::ComponentSet { .. }, Some(c)) => definition(doc, &c, lang),
|
||||
_ => match lang {
|
||||
CodeLang::React | CodeLang::Leptos | CodeLang::Vue => {
|
||||
@@ -140,6 +148,11 @@ pub fn code(doc: &Document, id: NodeId, lang: CodeLang) -> String {
|
||||
arkui(&Ctx::new(doc, id), id, 0, &mut out, &[]);
|
||||
out
|
||||
}
|
||||
CodeLang::Flutter => {
|
||||
let mut out = String::new();
|
||||
flutter(&Ctx::new(doc, id), id, 0, &mut out, Place::Flow);
|
||||
out
|
||||
}
|
||||
_ => {
|
||||
let mut out = String::new();
|
||||
compose(&Ctx::new(doc, id), id, 0, &mut out);
|
||||
@@ -328,24 +341,33 @@ const RESERVED: &[&str] = &[
|
||||
"yield",
|
||||
];
|
||||
|
||||
/// Types the generated SwiftUI, Compose and ArkUI code names: a variant enum may not shadow them.
|
||||
/// Types the generated SwiftUI, Compose, ArkUI and Flutter code names: a variant enum may not shadow them.
|
||||
const TYPES: &[&str] = &[
|
||||
"Alignment",
|
||||
"AnyView",
|
||||
"Arrangement",
|
||||
"Bool",
|
||||
"Boolean",
|
||||
"Border",
|
||||
"BorderRadius",
|
||||
"BorderSide",
|
||||
"Box",
|
||||
"BoxDecoration",
|
||||
"BoxShadow",
|
||||
"BoxShape",
|
||||
"BuildContext",
|
||||
"CaseIterable",
|
||||
"Circle",
|
||||
"CircleShape",
|
||||
"Color",
|
||||
"Column",
|
||||
"Composable",
|
||||
"CrossAxisAlignment",
|
||||
"Edge",
|
||||
"EdgeInsets",
|
||||
"Ellipse",
|
||||
"EmptyView",
|
||||
"Expanded",
|
||||
"FlexAlign",
|
||||
"Font",
|
||||
"FontStyle",
|
||||
@@ -354,19 +376,28 @@ const TYPES: &[&str] = &[
|
||||
"HStack",
|
||||
"HorizontalAlign",
|
||||
"Image",
|
||||
"MainAxisAlignment",
|
||||
"Modifier",
|
||||
"Offset",
|
||||
"Opacity",
|
||||
"Positioned",
|
||||
"Rect",
|
||||
"RectangleShape",
|
||||
"RoundedCornerShape",
|
||||
"RoundedRectangle",
|
||||
"Row",
|
||||
"Shape",
|
||||
"SizedBox",
|
||||
"Stack",
|
||||
"StatelessWidget",
|
||||
"String",
|
||||
"Text",
|
||||
"TextAlign",
|
||||
"TextBaseline",
|
||||
"TextCase",
|
||||
"TextDecoration",
|
||||
"TextDecorationType",
|
||||
"TextStyle",
|
||||
"Unit",
|
||||
"VStack",
|
||||
"VerticalAlign",
|
||||
@@ -481,6 +512,13 @@ fn cases(values: &[String], swift: bool) -> Vec<String> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Enum member names for variant values in Dart: lowerCamel, unique, never a keyword (a `default`
|
||||
/// value becomes `defaultValue`).
|
||||
fn dart_cases(values: &[String]) -> Vec<String> {
|
||||
let mut used = Vec::new();
|
||||
values.iter().map(|v| ident(v, &mut used)).collect()
|
||||
}
|
||||
|
||||
/// A Rust string literal.
|
||||
pub(crate) fn rust_string(s: &str) -> String {
|
||||
format!("{:?}", s.chars().take(10_000).collect::<String>())
|
||||
@@ -533,7 +571,7 @@ pub(crate) fn value_expr(doc: &Document, c: &Component, p: &Param, value: &Value
|
||||
let i = opts.iter().position(|o| Some(o.as_str()) == value.as_str()).unwrap_or(0);
|
||||
format!("{}::{}", leptos_enum(c, p), rust_cases(opts).get(i).cloned().unwrap_or_else(|| "Unknown".into()))
|
||||
}
|
||||
(ParamKind::Text, l) => literal(value.as_str().unwrap_or(""), l == CodeLang::Compose),
|
||||
(ParamKind::Text, l) => literal(value.as_str().unwrap_or(""), l.escapes_dollar()),
|
||||
(ParamKind::Variant(opts), l) => {
|
||||
let v = value.as_str().unwrap_or("");
|
||||
if l == CodeLang::Vue {
|
||||
@@ -542,10 +580,12 @@ pub(crate) fn value_expr(doc: &Document, c: &Component, p: &Param, value: &Value
|
||||
if l == CodeLang::React {
|
||||
return js_string(v);
|
||||
}
|
||||
let swift = l == CodeLang::SwiftUi;
|
||||
let i = opts.iter().position(|o| o == v).unwrap_or(0);
|
||||
let case = cases(opts, swift).get(i).cloned().unwrap_or_else(|| "unknown".into());
|
||||
if swift { format!(".{case}") } else { format!("{}.{case}", enum_name(c, p, false)) }
|
||||
match l {
|
||||
CodeLang::SwiftUi => format!(".{}", cases(opts, true).get(i).cloned().unwrap_or_else(|| "unknown".into())),
|
||||
CodeLang::Flutter => format!("{}.{}", enum_name(c, p, false), dart_cases(opts).get(i).cloned().unwrap_or_else(|| "unknown".into())),
|
||||
_ => format!("{}.{}", enum_name(c, p, false), cases(opts, false).get(i).cloned().unwrap_or_else(|| "Unknown".into())),
|
||||
}
|
||||
}
|
||||
(ParamKind::InstanceSwap, l) => {
|
||||
let t = id_of(value).and_then(|i| component_type(doc, i));
|
||||
@@ -556,12 +596,14 @@ 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}())"),
|
||||
(Some(t), CodeLang::Flutter) => format!("{t}()"),
|
||||
// ArkUI takes a builder: `@BuilderParam` is a `() => void`.
|
||||
(Some(t), CodeLang::ArkUi) => format!("() => {{ {t}() }}"),
|
||||
(None, CodeLang::ArkUi) => "() => {}".into(),
|
||||
(Some(t), _) => format!("{{ {t}() }}"),
|
||||
(None, _) if js => "null".into(),
|
||||
(None, CodeLang::SwiftUi) => "AnyView(EmptyView())".into(),
|
||||
(None, CodeLang::Flutter) => "const SizedBox.shrink()".into(),
|
||||
(None, _) => "{}".into(),
|
||||
}
|
||||
}
|
||||
@@ -616,7 +658,7 @@ pub fn usage(doc: &Document, id: NodeId, lang: CodeLang, depth: usize) -> Option
|
||||
pub(crate) fn usage_with_styles(doc: &Document, id: NodeId, lang: CodeLang, depth: usize) -> Option<(String, String)> {
|
||||
let c = main_of(doc, id).and_then(|m| describe(doc, m))?;
|
||||
let values = instance_values(doc, id, &c)?;
|
||||
let pad = if matches!(lang, CodeLang::React | CodeLang::Vue) { " ".repeat(depth) } else { indent(depth) };
|
||||
let pad = if matches!(lang, CodeLang::React | CodeLang::Vue | CodeLang::Flutter) { " ".repeat(depth) } else { indent(depth) };
|
||||
// A connected component: its real snippet (its import is at the top of the code: `code`).
|
||||
if let Some(link) = crate::code_connect::link(doc, id, lang) {
|
||||
return Some((crate::code_connect::render(doc, &c, &values, link, lang, &pad), String::new()));
|
||||
@@ -684,7 +726,9 @@ pub(crate) fn usage_with_styles(doc: &Document, id: NodeId, lang: CodeLang, dept
|
||||
.collect();
|
||||
if slots.is_empty() { format!("{pad}<{t}{attrs} />\n") } else { format!("{pad}<{t}{attrs}>\n{slots}{pad}</{t}>\n") }
|
||||
}
|
||||
CodeLang::SwiftUi => format!("{pad}{t}({})\n", args.iter().map(|(n, e, _)| format!("{n}: {e}")).collect::<Vec<_>>().join(", ")),
|
||||
CodeLang::SwiftUi | CodeLang::Flutter => {
|
||||
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.
|
||||
@@ -743,6 +787,15 @@ fn slot_content(doc: &Document, slot: NodeId, lang: CodeLang, depth: usize, pref
|
||||
}
|
||||
(format!("() => {{\n{out}{}}}", indent(depth + 1)), String::new())
|
||||
}
|
||||
CodeLang::Flutter => {
|
||||
// A slot holds a list of widgets, so it takes however many layers the slot has.
|
||||
let mut out = String::new();
|
||||
for k in kids {
|
||||
flutter(&ctx, k, depth + 1, &mut out, Place::Flow);
|
||||
out.push_str(",\n");
|
||||
}
|
||||
(format!("[\n{out}{}]", flutter_indent(depth)), String::new())
|
||||
}
|
||||
_ => {
|
||||
let mut out = String::new();
|
||||
for k in kids {
|
||||
@@ -760,6 +813,7 @@ pub fn definition(doc: &Document, c: &Component, lang: CodeLang) -> String {
|
||||
match lang {
|
||||
CodeLang::SwiftUi => swift_definition(doc, c),
|
||||
CodeLang::Compose => compose_definition(doc, c),
|
||||
CodeLang::Flutter => flutter_definition(doc, c),
|
||||
CodeLang::Leptos => leptos(doc, c),
|
||||
CodeLang::Vue => vue(doc, c),
|
||||
CodeLang::ArkUi => arkui_definition(doc, c),
|
||||
@@ -994,6 +1048,66 @@ fn compose_definition(doc: &Document, c: &Component) -> String {
|
||||
out
|
||||
}
|
||||
|
||||
/// A Flutter `StatelessWidget`: variant enums, a const constructor with defaults, and the widget
|
||||
/// tree in `build`.
|
||||
fn flutter_definition(doc: &Document, c: &Component) -> String {
|
||||
let ctx = Ctx { doc, root: c.comp, comp: Some(c), base: 2 };
|
||||
let mut out = String::from(FLUTTER_IMPORT);
|
||||
for p in &c.params {
|
||||
if let ParamKind::Variant(opts) = &p.kind {
|
||||
out.push_str(&format!("enum {} {{ {} }}\n\n", enum_name(c, p, false), dart_cases(opts).join(", ")));
|
||||
}
|
||||
}
|
||||
let t = &c.type_name;
|
||||
out.push_str(&format!("class {t} extends StatelessWidget {{\n"));
|
||||
if c.params.is_empty() {
|
||||
out.push_str(&format!(" const {t}({{super.key}});\n"));
|
||||
} else {
|
||||
out.push_str(&format!(" const {t}({{\n super.key,\n"));
|
||||
for p in &c.params {
|
||||
match flutter_default(doc, c, p) {
|
||||
Some(d) => out.push_str(&format!(" this.{} = {d},\n", p.ident)),
|
||||
None => out.push_str(&format!(" this.{},\n", p.ident)),
|
||||
}
|
||||
}
|
||||
out.push_str(" });\n\n");
|
||||
for p in &c.params {
|
||||
out.push_str(&format!(" final {} {};\n", flutter_type(c, p), p.ident));
|
||||
}
|
||||
}
|
||||
out.push_str("\n @override\n Widget build(BuildContext context) {\n return ");
|
||||
let mut body = String::new();
|
||||
flutter(&ctx, c.comp, 2, &mut body, Place::Flow);
|
||||
out.push_str(body.trim_start());
|
||||
out.push_str(";\n }\n}\n");
|
||||
out
|
||||
}
|
||||
|
||||
/// A parameter's Dart type.
|
||||
fn flutter_type(c: &Component, p: &Param) -> String {
|
||||
match &p.kind {
|
||||
ParamKind::Variant(_) => enum_name(c, p, false),
|
||||
ParamKind::Boolean => "bool".into(),
|
||||
ParamKind::Text => "String".into(),
|
||||
ParamKind::InstanceSwap => "Widget".into(),
|
||||
// A slot holds however many layers it was filled with.
|
||||
ParamKind::Slot => "List<Widget>?".into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// A parameter's default as a Dart constant (`None` for a slot, which has none): Dart wants its
|
||||
/// defaults to be constants, so an instance swap is a `const` widget.
|
||||
fn flutter_default(doc: &Document, c: &Component, p: &Param) -> Option<String> {
|
||||
match &p.kind {
|
||||
ParamKind::Slot => None,
|
||||
ParamKind::InstanceSwap => Some(match id_of(&p.default).and_then(|i| component_type(doc, i)) {
|
||||
Some(t) => format!("const {t}()"),
|
||||
None => "const SizedBox.shrink()".into(),
|
||||
}),
|
||||
_ => Some(value_expr(doc, c, p, &p.default, CodeLang::Flutter)),
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
//! Inspect behaviour suite, written from the public documentation of design tools' inspect and
|
||||
//! developer handoff modes: the reported layout (size, position, rotation, radius, opacity, blend),
|
||||
//! colour formats, units, typography, the generated CSS, SwiftUI and Compose for common layers,
|
||||
//! the asset list and redline measurements. Each rule is a table row or an assert with a one-line
|
||||
//! derivation; where the documentation is silent the row says so and pins today's behaviour.
|
||||
//! colour formats, units, typography, the generated CSS, SwiftUI, Compose and Flutter for common
|
||||
//! layers, the asset list and redline measurements. Each rule is a table row or an assert with a
|
||||
//! one-line derivation; where the documentation is silent the row says so and pins today's
|
||||
//! behaviour.
|
||||
|
||||
// The tables read best as tuples.
|
||||
#![allow(clippy::type_complexity)]
|
||||
@@ -241,12 +242,16 @@ fn native_code_for_a_simple_layer() {
|
||||
set(&mut s, &[r], json!({"cornerRadius": 8}));
|
||||
let swift = code(&mut s, r, json!({"language": "swiftui"}));
|
||||
let compose = code(&mut s, r, json!({"language": "compose"}));
|
||||
let flutter = code(&mut s, r, json!({"language": "flutter"}));
|
||||
// (code, expected fragment, derivation)
|
||||
let rows: [(&str, &str, &str); 4] = [
|
||||
let rows: [(&str, &str, &str); 7] = [
|
||||
(&swift, "frame(width: 100, height: 50)", "SwiftUI sizes a view with frame, in points"),
|
||||
(&swift, "Color(red: 1, green: 0, blue: 0, opacity: 1)", "docs silent: the fill as a SwiftUI colour"),
|
||||
(&compose, "Modifier.size(100.dp, 50.dp)", "Compose sizes in dp"),
|
||||
(&compose, "RoundedCornerShape(8.dp)", "docs silent: corner radius as a rounded shape"),
|
||||
(&flutter, "width: 100,\n height: 50,", "Flutter sizes a Container in logical pixels"),
|
||||
(&flutter, "Color(0xFFFF0000)", "docs silent: the fill as a Flutter colour"),
|
||||
(&flutter, "borderRadius: BorderRadius.circular(8)", "docs silent: corner radius as a rounded border"),
|
||||
];
|
||||
for (c, want, why) in rows {
|
||||
assert!(c.contains(want), "{want}: {why}\n{c}");
|
||||
|
||||
@@ -258,7 +258,7 @@ status = "draft — needs native review"
|
||||
"Changes on main" = "التغييرات في الملف الرئيسي"
|
||||
"Changes on the branch" = "التغييرات في الفرع"
|
||||
"Changes unknown" = "التغييرات غير معروفة"
|
||||
"Choose React, Leptos, Vue, SwiftUI, Compose or ArkUI above to connect this component to its code." = "اختر React أو Leptos أو Vue أو SwiftUI أو Compose أو ArkUI أعلاه لربط هذا المكوّن بكوده."
|
||||
"Choose React, Leptos, Vue, SwiftUI, Compose, ArkUI or Flutter above to connect this component to its code." = "اختر React أو Leptos أو Vue أو SwiftUI أو Compose أو ArkUI أو Flutter أعلاه لربط هذا المكوّن بكوده."
|
||||
"Choose another profile to change it." = "اختر ملف تعريف آخر لتغييره."
|
||||
"Choose collection" = "اختيار مجموعة"
|
||||
"Choose image" = "اختيار صورة"
|
||||
|
||||
@@ -245,7 +245,7 @@ status = "draft — needs native review"
|
||||
"Changes on main" = "প্রধান (main)-এ পরিবর্তন"
|
||||
"Changes on the branch" = "ব্রাঞ্চে পরিবর্তন"
|
||||
"Changes unknown" = "পরিবর্তন অজ্ঞাত"
|
||||
"Choose React, Leptos, Vue, SwiftUI, Compose or ArkUI above to connect this component to its code." = "এই কম্পোনেন্টটিকে কোডের সাথে যুক্ত করতে উপরে React, Leptos, Vue, SwiftUI, Compose বা ArkUI বেছে নিন।"
|
||||
"Choose React, Leptos, Vue, SwiftUI, Compose, ArkUI or Flutter above to connect this component to its code." = "এই কম্পোনেন্টটিকে কোডের সাথে যুক্ত করতে উপরে React, Leptos, Vue, SwiftUI, Compose, ArkUI বা Flutter বেছে নিন।"
|
||||
"Choose another profile to change it." = "পরিবর্তন করতে অন্য একটি প্রোফাইল বেছে নিন।"
|
||||
"Choose collection" = "সংগ্রহ (collection) বেছে নিন"
|
||||
"Choose image" = "ছবি বেছে নিন"
|
||||
|
||||
@@ -235,7 +235,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, Vue, 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, Vue, SwiftUI, Compose nebo ArkUI."
|
||||
"Choose React, Leptos, Vue, SwiftUI, Compose, ArkUI or Flutter above to connect this component to its code." = "Pro propojení této komponenty s jejím kódem vyberte nahoře React, Leptos, Vue, SwiftUI, Compose, ArkUI nebo Flutter."
|
||||
"Choose another profile to change it." = "Pro změnu vyberte jiný profil."
|
||||
"Choose collection" = "Vybrat kolekci"
|
||||
"Choose image" = "Vybrat obrázek"
|
||||
|
||||
@@ -230,7 +230,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, Vue, SwiftUI, Compose or ArkUI above to connect this component to its code." = "Wählen Sie oben React, Leptos, Vue, SwiftUI, Compose oder ArkUI, um diese Komponente mit ihrem Code zu verbinden."
|
||||
"Choose React, Leptos, Vue, SwiftUI, Compose, ArkUI or Flutter above to connect this component to its code." = "Wählen Sie oben React, Leptos, Vue, SwiftUI, Compose, ArkUI oder Flutter, 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"
|
||||
|
||||
@@ -252,7 +252,7 @@ name = "English"
|
||||
"Changes on main" = "Changes on main"
|
||||
"Changes on the branch" = "Changes on the branch"
|
||||
"Changes unknown" = "Changes unknown"
|
||||
"Choose React, Leptos, Vue, SwiftUI, Compose or ArkUI above to connect this component to its code." = "Choose React, Leptos, Vue, SwiftUI, Compose or ArkUI above to connect this component to its code."
|
||||
"Choose React, Leptos, Vue, SwiftUI, Compose, ArkUI or Flutter above to connect this component to its code." = "Choose React, Leptos, Vue, SwiftUI, Compose, ArkUI or Flutter 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"
|
||||
|
||||
@@ -254,7 +254,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, Vue, SwiftUI, Compose or ArkUI above to connect this component to its code." = "Elige React, Leptos, Vue, SwiftUI, Compose o ArkUI arriba para conectar este componente con su código."
|
||||
"Choose React, Leptos, Vue, SwiftUI, Compose, ArkUI or Flutter above to connect this component to its code." = "Elige React, Leptos, Vue, SwiftUI, Compose, ArkUI o Flutter 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"
|
||||
|
||||
@@ -236,7 +236,7 @@ status = "draft — needs native review"
|
||||
"Check" = "Valintamerkki"
|
||||
"Checking the fonts installed on this computer…" = "Tarkistetaan tämän tietokoneen asennettuja fontteja…"
|
||||
"Checking the server…" = "Tarkistetaan palvelinta…"
|
||||
"Choose React, Leptos, Vue, SwiftUI, Compose or ArkUI above to connect this component to its code." = "Valitse yllä React, Leptos, Vue, SwiftUI, Compose tai ArkUI yhdistääksesi tämän komponentin sen koodiin."
|
||||
"Choose React, Leptos, Vue, SwiftUI, Compose, ArkUI or Flutter above to connect this component to its code." = "Valitse yllä React, Leptos, Vue, SwiftUI, Compose, ArkUI tai Flutter yhdistääksesi tämän komponentin sen koodiin."
|
||||
"Chevron" = "Kulmanuoli"
|
||||
"Choose another profile to change it." = "Vaihda valitsemalla toinen profiili."
|
||||
"Choose collection" = "Valitse kokoelma"
|
||||
|
||||
@@ -239,7 +239,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, Vue, SwiftUI, Compose or ArkUI above to connect this component to its code." = "Choisissez React, Leptos, Vue, SwiftUI, Compose ou ArkUI ci-dessus pour relier ce composant à son code."
|
||||
"Choose React, Leptos, Vue, SwiftUI, Compose, ArkUI or Flutter above to connect this component to its code." = "Choisissez React, Leptos, Vue, SwiftUI, Compose, ArkUI ou Flutter 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"
|
||||
|
||||
@@ -242,7 +242,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, Vue, SwiftUI, Compose or ArkUI above to connect this component to its code." = "Pilih React, Leptos, Vue, SwiftUI, Compose, atau ArkUI di atas untuk menghubungkan component ini ke kodenya."
|
||||
"Choose React, Leptos, Vue, SwiftUI, Compose, ArkUI or Flutter above to connect this component to its code." = "Pilih React, Leptos, Vue, SwiftUI, Compose, ArkUI, atau Flutter 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"
|
||||
|
||||
@@ -228,7 +228,7 @@ status = "draft — needs native review"
|
||||
"Changes on main" = "Modifiche su main"
|
||||
"Changes on the branch" = "Modifiche sul ramo"
|
||||
"Changes unknown" = "Modifiche sconosciute"
|
||||
"Choose React, Leptos, Vue, SwiftUI, Compose or ArkUI above to connect this component to its code." = "Scegli React, Leptos, Vue, SwiftUI, Compose o ArkUI qui sopra per collegare questo componente al suo codice."
|
||||
"Choose React, Leptos, Vue, SwiftUI, Compose, ArkUI or Flutter above to connect this component to its code." = "Scegli React, Leptos, Vue, SwiftUI, Compose, ArkUI o Flutter qui sopra per collegare questo componente al suo codice."
|
||||
"Choose another profile to change it." = "Scegli un altro profilo per cambiarlo."
|
||||
"Choose collection" = "Scegli collezione"
|
||||
"Choose image" = "Scegli immagine"
|
||||
|
||||
@@ -240,7 +240,7 @@ status = "draft — needs native review"
|
||||
"Changes on main" = "メインの変更"
|
||||
"Changes on the branch" = "ブランチの変更"
|
||||
"Changes unknown" = "変更内容は不明"
|
||||
"Choose React, Leptos, Vue, SwiftUI, Compose or ArkUI above to connect this component to its code." = "上で React、Leptos、Vue、SwiftUI、Compose、ArkUI のいずれかを選び、このコンポーネントをコードに接続。"
|
||||
"Choose React, Leptos, Vue, SwiftUI, Compose, ArkUI or Flutter above to connect this component to its code." = "上で React、Leptos、Vue、SwiftUI、Compose、ArkUI、Flutter のいずれかを選び、このコンポーネントをコードに接続。"
|
||||
"Choose another profile to change it." = "変更するには別のプロファイルを選択。"
|
||||
"Choose collection" = "コレクションを選択"
|
||||
"Choose image" = "画像を選択"
|
||||
|
||||
@@ -249,7 +249,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, Vue, SwiftUI, Compose or ArkUI above to connect this component to its code." = "ამ კომპონენტის კოდთან დასაკავშირებლად ზემოთ აირჩიეთ React, Leptos, Vue, SwiftUI, Compose ან ArkUI."
|
||||
"Choose React, Leptos, Vue, SwiftUI, Compose, ArkUI or Flutter above to connect this component to its code." = "ამ კომპონენტის კოდთან დასაკავშირებლად ზემოთ აირჩიეთ React, Leptos, Vue, SwiftUI, Compose, ArkUI ან Flutter."
|
||||
"Choose another profile to change it." = "მის შესაცვლელად აირჩიეთ სხვა პროფილი."
|
||||
"Choose collection" = "კოლექციის არჩევა"
|
||||
"Choose image" = "გამოსახულების არჩევა"
|
||||
|
||||
@@ -230,7 +230,7 @@ status = "draft — needs native review"
|
||||
"Changes on main" = "메인의 변경 사항"
|
||||
"Changes on the branch" = "브랜치의 변경 사항"
|
||||
"Changes unknown" = "변경 내용 알 수 없음"
|
||||
"Choose React, Leptos, Vue, SwiftUI, Compose or ArkUI above to connect this component to its code." = "위에서 React, Leptos, Vue, SwiftUI, Compose 또는 ArkUI를 선택해 이 컴포넌트를 코드에 연결하세요."
|
||||
"Choose React, Leptos, Vue, SwiftUI, Compose, ArkUI or Flutter above to connect this component to its code." = "위에서 React, Leptos, Vue, SwiftUI, Compose, ArkUI 또는 Flutter를 선택해 이 컴포넌트를 코드에 연결하세요."
|
||||
"Choose another profile to change it." = "변경하려면 다른 프로필을 선택하세요."
|
||||
"Choose collection" = "컬렉션 선택"
|
||||
"Choose image" = "이미지 선택"
|
||||
|
||||
@@ -236,7 +236,7 @@ status = "draft — needs native review"
|
||||
"Check" = "Vinkje"
|
||||
"Checking the fonts installed on this computer…" = "De lettertypen op deze computer worden gecontroleerd…"
|
||||
"Checking the server…" = "Server controleren…"
|
||||
"Choose React, Leptos, Vue, SwiftUI, Compose or ArkUI above to connect this component to its code." = "Kies hierboven React, Leptos, Vue, SwiftUI, Compose of ArkUI om dit component aan zijn code te koppelen."
|
||||
"Choose React, Leptos, Vue, SwiftUI, Compose, ArkUI or Flutter above to connect this component to its code." = "Kies hierboven React, Leptos, Vue, SwiftUI, Compose, ArkUI of Flutter om dit component aan zijn code te koppelen."
|
||||
"Chevron" = "Punthaak"
|
||||
"Choose another profile to change it." = "Kies een ander profiel om dit te wijzigen."
|
||||
"Choose collection" = "Kies een verzameling"
|
||||
|
||||
@@ -245,7 +245,7 @@ name = "Polski"
|
||||
"Changes on main" = "Zmiany w gałęzi głównej"
|
||||
"Changes on the branch" = "Zmiany w gałęzi"
|
||||
"Changes unknown" = "Nieznane zmiany"
|
||||
"Choose React, Leptos, Vue, SwiftUI, Compose or ArkUI above to connect this component to its code." = "Wybierz React, Leptos, Vue, SwiftUI, Compose lub ArkUI powyżej, aby połączyć ten komponent z jego kodem."
|
||||
"Choose React, Leptos, Vue, SwiftUI, Compose, ArkUI or Flutter above to connect this component to its code." = "Wybierz React, Leptos, Vue, SwiftUI, Compose, ArkUI lub Flutter 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"
|
||||
|
||||
@@ -228,7 +228,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, Vue, SwiftUI, Compose or ArkUI above to connect this component to its code." = "Escolha React, Leptos, Vue, SwiftUI, Compose ou ArkUI acima para conectar este componente ao código dele."
|
||||
"Choose React, Leptos, Vue, SwiftUI, Compose, ArkUI or Flutter above to connect this component to its code." = "Escolha React, Leptos, Vue, SwiftUI, Compose, ArkUI ou Flutter 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"
|
||||
|
||||
@@ -242,7 +242,7 @@ name = "Русский"
|
||||
"Changes on main" = "Изменения в основной ветке"
|
||||
"Changes on the branch" = "Изменения в ветке"
|
||||
"Changes unknown" = "Изменения неизвестны"
|
||||
"Choose React, Leptos, Vue, SwiftUI, Compose or ArkUI above to connect this component to its code." = "Выберите выше React, Leptos, Vue, SwiftUI, Compose или ArkUI, чтобы связать компонент с его кодом."
|
||||
"Choose React, Leptos, Vue, SwiftUI, Compose, ArkUI or Flutter above to connect this component to its code." = "Выберите выше React, Leptos, Vue, SwiftUI, Compose, ArkUI или Flutter, чтобы связать компонент с его кодом."
|
||||
"Choose another profile to change it." = "Для смены выберите другой профиль."
|
||||
"Choose collection" = "Выбрать коллекцию"
|
||||
"Choose image" = "Выбрать изображение"
|
||||
|
||||
@@ -252,7 +252,7 @@ name = "Svenska"
|
||||
"Changes on main" = "Ändringar i main"
|
||||
"Changes on the branch" = "Ändringar i grenen"
|
||||
"Changes unknown" = "Okända ändringar"
|
||||
"Choose React, Leptos, Vue, SwiftUI, Compose or ArkUI above to connect this component to its code." = "Välj React, Leptos, Vue, SwiftUI, Compose eller ArkUI ovan för att koppla den här komponenten till dess kod."
|
||||
"Choose React, Leptos, Vue, SwiftUI, Compose, ArkUI or Flutter above to connect this component to its code." = "Välj React, Leptos, Vue, SwiftUI, Compose, ArkUI eller Flutter 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"
|
||||
|
||||
@@ -243,7 +243,7 @@ status = "draft — needs native review"
|
||||
"Changes on main" = "การเปลี่ยนแปลงบนเมน"
|
||||
"Changes on the branch" = "การเปลี่ยนแปลงบนแบรนช์"
|
||||
"Changes unknown" = "ไม่ทราบการเปลี่ยนแปลง"
|
||||
"Choose React, Leptos, Vue, SwiftUI, Compose or ArkUI above to connect this component to its code." = "เลือก React, Leptos, Vue, SwiftUI, Compose หรือ ArkUI ด้านบนเพื่อเชื่อมคอมโพเนนต์นี้กับโค้ด"
|
||||
"Choose React, Leptos, Vue, SwiftUI, Compose, ArkUI or Flutter above to connect this component to its code." = "เลือก React, Leptos, Vue, SwiftUI, Compose, ArkUI หรือ Flutter ด้านบนเพื่อเชื่อมคอมโพเนนต์นี้กับโค้ด"
|
||||
"Choose another profile to change it." = "เลือกโปรไฟล์อื่นเพื่อเปลี่ยน"
|
||||
"Choose collection" = "เลือกคอลเลกชัน"
|
||||
"Choose image" = "เลือกรูปภาพ"
|
||||
|
||||
@@ -239,7 +239,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, Vue, SwiftUI, Compose or ArkUI above to connect this component to its code." = "Bu bileşeni koduna bağlamak için yukarıdan React, Leptos, Vue, SwiftUI, Compose veya ArkUI seçin."
|
||||
"Choose React, Leptos, Vue, SwiftUI, Compose, ArkUI or Flutter above to connect this component to its code." = "Bu bileşeni koduna bağlamak için yukarıdan React, Leptos, Vue, SwiftUI, Compose, ArkUI veya Flutter 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ç"
|
||||
|
||||
@@ -223,7 +223,7 @@ status = "draft — needs native review"
|
||||
"Changes unknown" = "Không rõ thay đổi"
|
||||
"Checking the fonts installed on this computer…" = "Đang kiểm tra các phông chữ cài đặt trên máy tính này…"
|
||||
"Checking the server…" = "Đang kiểm tra máy chủ…"
|
||||
"Choose React, Leptos, Vue, SwiftUI, Compose or ArkUI above to connect this component to its code." = "Hãy chọn React, Leptos, Vue, SwiftUI, Compose hoặc ArkUI ở trên để kết nối thành phần này với mã nguồn."
|
||||
"Choose React, Leptos, Vue, SwiftUI, Compose, ArkUI or Flutter above to connect this component to its code." = "Hãy chọn React, Leptos, Vue, SwiftUI, Compose, ArkUI hoặc Flutter ở trên để kết nối thành phần này với mã nguồn."
|
||||
"Choose another profile to change it." = "Hãy chọn hồ sơ khác để thay đổi."
|
||||
"Choose collection" = "Chọn bộ sưu tập"
|
||||
"Choose image" = "Chọn hình ảnh"
|
||||
|
||||
@@ -231,7 +231,7 @@ status = "draft — needs native review"
|
||||
"Changes on main" = "主文件上的更改"
|
||||
"Changes on the branch" = "分支上的更改"
|
||||
"Changes unknown" = "更改未知"
|
||||
"Choose React, Leptos, Vue, SwiftUI, Compose or ArkUI above to connect this component to its code." = "在上方选择 React、Leptos、Vue、SwiftUI、Compose 或 ArkUI,将此组件关联到其代码。"
|
||||
"Choose React, Leptos, Vue, SwiftUI, Compose, ArkUI or Flutter above to connect this component to its code." = "在上方选择 React、Leptos、Vue、SwiftUI、Compose、ArkUI 或 Flutter,将此组件关联到其代码。"
|
||||
"Choose another profile to change it." = "选择其他配置文件以更改。"
|
||||
"Choose collection" = "选择集合"
|
||||
"Choose image" = "选择图片"
|
||||
|
||||
@@ -445,7 +445,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, Vue, SwiftUI, Compose or ArkUI above to connect this component to its code.")
|
||||
tr("Choose React, Leptos, Vue, SwiftUI, Compose, ArkUI or Flutter above to connect this component to its code.")
|
||||
};
|
||||
widgets::note(ui, text, t.text_secondary);
|
||||
return;
|
||||
|
||||
@@ -65,7 +65,7 @@ updates with overrides kept: `library_tests.rs`, `components_scale_tests.rs`.
|
||||
| Variables: colour, number, string, boolean; collections, modes, aliases, scopes, groups | done | behaviour suite: `variables_behaviour_tests.rs`; aliases same type, no loops (also on create); scopes filter pickers only |
|
||||
| Modes: add, duplicate, rename, delete, set as default / reorder | done | layers on Auto follow a new default; explicit modes stay |
|
||||
| Deleting a bound variable or its collection | done | layers keep their last value and are unbound; aliases to it take its values |
|
||||
| Code syntax per platform (Web, iOS, Android) | done | generated CSS uses the Web name; in the variables JSON; SwiftUI/Compose code doesn't name variables yet |
|
||||
| Code syntax per platform (Web, iOS, Android, Flutter) | done | generated CSS uses the Web name; in the variables JSON; SwiftUI/Compose/ArkUI/Flutter code doesn't name variables yet |
|
||||
| Bindings: fills, strokes, effects, opacity, padding, gaps, sizes, radius, font family/style, font size/weight, line height, letter spacing, paragraph spacing/indent, text, visibility, layout grids, instance properties | done | type-checked per field; number variables fill text content; the Design panel's font family and font style menus start with Apply variable (string variables), and a bound one shows its variable in place of the menu; a colour variable bound to a solid paint brings its whole colour, alpha included, once (the paint's opacity restarts at 100% on binding, an opacity set while bound multiplies on top; unbinding moves the alpha back into the opacity) |
|
||||
| Explicit modes per frame, library collections' modes | done | |
|
||||
| Gradient stops and variant properties bound to variables | done | a stop's binding follows it when earlier stops are removed |
|
||||
|
||||
@@ -12,13 +12,13 @@ repository, third-party integrations. Remaining: 1–2 h.
|
||||
|---|---|---|
|
||||
| Dev Mode toggle (Shift+D), Inspect panel | done | the canvas only inspects: presses select, pan and comment but never move, resize, draw or edit text and vectors; menus and shortcuts refuse design edits with a note (Dev Mode's annotations, statuses, dev resources, measurements, code connections and comments still work); a drawing tool chosen in the toolbar goes back to the design. The layers panel only selects and expands (no renaming, reordering, hiding or locking; a double click says why), and the Pages list only opens pages (no adding, renaming, reordering, duplicating or deleting). Files dropped on the canvas aren't imported (a dropped document still opens as a file of its own), and the Assets panel places, swaps and applies nothing, doesn't change Hide when publishing, and doesn't accept library updates or add, remove or swap libraries; each says why |
|
||||
| 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, Vue 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; class names from layer names transliterate non-ASCII scripts into readable ASCII (로그인 화면 → `rogeuin-hwamyeon`, кнопка → `knopka`, über → `uber`; symbols-only names take the node's kind, `.text`), and the 40-character cut lands on a word boundary (#1235) |
|
||||
| Code: CSS, iOS (SwiftUI), Android (Compose), Flutter | done | plus HTML, React, Vue and Leptos (ours; Leptos: `#[component]` functions with `#[prop]` defaults, variant enums and `view!` markup; Flutter is ours too: a `StatelessWidget` for a component, a widget tree for a layer); components take properties as parameters, instances are calls; CSS in px or rem (`inspect.code {unit, rootFontSize}`), with rotation, blend modes, gradient directions and kinds (linear, radial, conic), text alignment, case and decoration; class names from layer names transliterate non-ASCII scripts into readable ASCII (로그인 화면 → `rogeuin-hwamyeon`, кнопка → `knopka`, über → `uber`; symbols-only names take the node's kind, `.text`), and the 40-character cut lands on a word boundary (#1235) |
|
||||
| Assets list with export | done | layers with export settings, icons (small, vector shapes only: SVG) and images (PNG); the icon rule is ours |
|
||||
| Annotations: pinned properties, categories, coloured callouts, filter | done | |
|
||||
| Ready for dev list, statuses, "Changed" after edits | done | the list filters Ready for dev, Changed, Completed or any, and badges changed ones (`inspect.readyForDev {status: changed}`, `changed` per item); only sections and frames at the top of a page or section take a status (others are refused) |
|
||||
| Focus view (one frame, previous / next) | done | |
|
||||
| Compare changes (before/after side by side or overlaid, property diffs) | done | against the snapshot taken when a frame was first changed after it was marked ready, or any saved version (`dev.compare {version}`; the view's Compare with menu); a frame that isn't ready for dev compares with versions |
|
||||
| Code connect (map components to repository code) | done | per language (React, Leptos, Vue, 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; generated layer trees bring the import lines of the connected components they call (also in definitions, slots and swapped-in instances) once each at the top, per language. No CLI publishing mappings from the code repository |
|
||||
| Code connect (map components to repository code) | done | per language (React, Leptos, Vue, SwiftUI, Compose, ArkUI, Flutter): 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; generated layer trees bring the import lines of the connected components they call (also in definitions, slots and swapped-in instances) once each at the top, per language. No CLI publishing mappings from the code repository |
|
||||
| Plugins in Dev Mode (Jira, GitHub, etc.) | partial | FigCraft's own WASM plugins that only read run from the Inspect tab, with their panels (callbacks can only read); no third-party integrations |
|
||||
| Measurements (redlines on hover, Alt in the design) | done | distances between the selection and the hovered layer, its parent or a guide, auto layout padding and gaps (Shift+Alt); measurements kept on the canvas (`devMode.addMeasurement` between two layers, along a side or to the layer around it; stored on the layer, saved, synced, undoable) drawn in Dev Mode as labelled redlines that follow the layers, listed, placed and removed in the Inspect tab; a measurement whose other layer was deleted isn't drawn until an undo brings it back |
|
||||
| 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 |
|
||||
@@ -31,6 +31,7 @@ repository, third-party integrations. Remaining: 1–2 h.
|
||||
| 2026-10-26 | minor | Class names from layer names transliterate non-ASCII scripts (#1235): `class_name` runs names through `deunicode` (new dependency: pure Rust tables, zero deps, BSD-3-Clause), so Korean, Cyrillic, Latin-diacritic and CJK names become readable ASCII classes in HTML/CSS/React/Leptos output, component roots and `data-*` variant attributes (`rogeuin-hwamyeon`, `knopka`, `uber`, `an-niu`; before, every one of them came out `layer-`); symbols-only names take the node's kind (`.text`, `.frame`) instead of `layer-`, and the 40-character cut lands on a word boundary. ASCII-only names are unchanged. `html_codegen_tests` updated (its diacritics expectation improves from `layer-1-n-code-name` to `layer-1-unicode-name`) and extended. Percentages and hours unchanged |
|
||||
| 2026-10-24 | minor | Dev Mode refuses design edits from files dropped on the canvas (a dropped document still opens as a file of its own, and the drop overlay shows the Dev Mode note for other files), from the Pages list's add, rename, reorder, duplicate and delete, and from the Assets panel's place, drag, Alt-drop swap, style, Hide when publishing, Update, and adding, removing and swapping libraries. Percentages and hours unchanged |
|
||||
| 2026-10-24 | minor | Measurements kept on the canvas (gap 16): `devMode.addMeasurement {from?, to?, side?}` places one between two layers, along one side of a layer or (neither) to the layer around it, stored on the layer it starts from (saved in `.figcraft`, synced, undoable; old files open without them); Dev Mode draws them as labelled redlines that follow the layers, the Inspect tab lists them (those of the selected layer, or every one on the page), places and removes them; `devMode.removeMeasurement` and `devMode.measurements` remove and list them; a copy keeps those to layers copied with it. The layers panel is read-only in Dev Mode: rows select and expand, but don't rename, reorder, hide or lock. Breadth 97% → 98%, ready 76% → 78%, 2–4 h → 1–2 h |
|
||||
| 2026-10-16 | minor | Flutter code generation (ours: the incumbent generates CSS, iOS and Android only): `inspect.code {language: flutter}`, with `dart` as an alias, writes a Dart widget tree for a layer — `Container` for a frame's box (size, padding, `BoxDecoration` with colour, radius, border and shadows), `Row` / `Column` for auto layout (`spacing`, `crossAxisAlignment`, `textBaseline`; `stretch` where a child fills), `Expanded` for a child that fills the main axis, `Stack` + `Positioned` for free children, `Text` with `TextStyle` (family, size, weight, style, height, letter spacing, decoration, alignment), `Image.asset` for images, `Opacity` for layer opacity, `SizedBox` for a capped subtree — and a `StatelessWidget` for a component (properties become constructor parameters with their defaults, instances are calls passing only what they change, variants become a Dart enum, swaps a `Widget` and slots a `List<Widget>?`). Code connect takes `flutter` too, with a Dart `import '…';` line. The Inspect panel's language picker (which iterates `CodeLang::ALL`) and the Connect to code note list it, and the 13 UI catalogues follow. Dev Mode breadth (~96%) and ready for real work (~72%) unchanged at this rounding; hours unchanged |
|
||||
| 2026-10-15 | minor | Dev Mode tail (gap 16): generated layer trees (React, Leptos, SwiftUI, Compose) start with the import lines of the connected components they call, once each, also those in a component's definition, in slots an instance fills and placed by a connected template's instance swaps; Compare changes takes any saved version as the before (`dev.compare {version}`, the view's Compare with menu; frames that aren't ready for dev compare with versions). Behaviour suite (5 tests; 4 failed before the fixes): the Dev Mode canvas only inspects (no moving, resizing, drawing, text or vector editing; menus, shortcuts, nudges, opacity keys and paste refuse design edits with a note; toolbar drawing tools go back to the design), the ready list filters and badges Changed items, statuses are refused on layers inside frames. Read-only plugins run from the Inspect tab show their panels (the runtime's panels, reused). Breadth 96% → 97% (measurements kept on the canvas and the read-only layers panel counted as missing), ready 72% → 76%, 0–1 h → 2–4 h |
|
||||
| 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 |
|
||||
|
||||
@@ -57,7 +57,7 @@ export limit, not a native document nesting limit.
|
||||
| WebP | — | yes (ours) | | — |
|
||||
| Android Vector Drawable (XML) | — | yes (ours) | an `.xml` drawable resource (`file.export {format: "xml"}`, export settings, File ▸ Export…, the CLI and MCP), pretty-printed, sized in dp with the viewport in layer units, for AndroidX (VectorDrawableCompat: gradients and even-odd need API 24 without it). Shapes, booleans and text as path data with transforms baked in; solid fills and linear, radial and sweep gradients (`aapt:attr`, exact for any handles: radial and sweep in a `<group>` that maps the unit circle); centred strokes as stroke attributes where Android draws them the same (uniform scale, one cap, no dashes or variable width), other strokes as filled outlines; clipping frames and masks as `<group>` with `<clip-path>`; `android:name` from layer names with the id attribute option. What the format can't hold goes by the compatibility mode (`vdCompat`): best effort (default) applies layer opacity to each shape, rewrites even-odd fills and clip outlines as non-zero, draws diamond gradients as radial and alpha masks as outline clips, each with a warning; exact leaves out what it can't draw exactly, with a warning; ignore writes what maps without warnings: layer opacity over overlapping shapes is dropped (drawn opaque), even-odd fills keep `fillType="evenOdd"`, even-odd clip outlines clip as non-zero, diamond gradients are left out and alpha masks clip to their outline. Effects, image and pattern fills and blend modes are always left out (warned except in ignore); an export that draws nothing writes one empty path (Android refuses a drawable without a path), with a warning. Checked by parsing the XML, against FigCraft's renderer (gradients pixel by pixel), with `aapt2 compile` and `link`, and by rendering samples (linear, skewed radial and sweep gradients, a sweep with mirrored handles, even-odd booleans, centred and inside strokes, rotated text, a clipping frame, an alpha mask, a translucent group, each compatibility mode) with Android 13's (API 33) VectorDrawable on an emulator, next to FigCraft's PNG export: they match, including where the sweep starts and which way it turns, except what the compatibility mode and its warnings say (best effort: overlapping shapes in a translucent group look denser; exact and ignore leave out or draw opaque what they list). Not checked on API 21–23 (VectorDrawableCompat) | — |
|
||||
| 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, React, Leptos and Vue | see [dev-mode-parity](dev-mode-parity.md) | — |
|
||||
| Dev Mode code (CSS, iOS SwiftUI, Android Compose) | yes | yes, plus HTML, React, Vue, Leptos, ArkUI and Flutter | 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
@@ -382,7 +382,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, Leptos, Vue, SwiftUI, Compose, ArkUI): an import path, the name and a template whose `{property}`
|
||||
language (React, Leptos, Vue, SwiftUI, Compose, ArkUI, Flutter): 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
|
||||
|
||||
+7
-7
@@ -314,7 +314,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, Vue, 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, Leptos and Vue, `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, Vue, SwiftUI, Jetpack Compose, ArkUI or Flutter 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, Leptos and Vue, `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: name it with `"as": "card"` and `{"$ref": "card"}` anywhere in a later step's params becomes the `id` in its result (`{"$step": n}` does the same by zero-based position); `"field"` takes another key or a slash path such as `ids/0` (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. |
|
||||
@@ -1529,16 +1529,16 @@ permissions: without it nothing is downloaded); `plugin.directory.publish {path
|
||||
same layer names, mobile layout) the code is mobile first: the mobile frame's classes, the
|
||||
desktop frame's that differ behind `lg:`, and layers only on desktop `hidden` plus `lg:flex`
|
||||
(auto layout frames) or `lg:block` (other layers).
|
||||
- **Component code:** `inspect.code {id, language: react|leptos|vue|swiftui|compose|arkui}` on a component,
|
||||
- **Component code:** `inspect.code {id, language: react|leptos|vue|swiftui|compose|arkui|flutter}` 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), Vue as a
|
||||
single-file component (`<template>`, a `<script setup>` with `defineProps` and its defaults, a
|
||||
`<style scoped>`; a slot layer is Vue's own `<slot>`, so it takes no prop), SwiftUI
|
||||
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
|
||||
struct (`@Prop` parameters, `@BuilderParam` for swaps and slots), Flutter with a `StatelessWidget`. Its component properties are the
|
||||
parameters, with their defaults: variant properties become string unions (React, Vue) or enums,
|
||||
booleans become flags around the layers they show, text properties strings, instance swaps views
|
||||
(`React.ReactNode`, Leptos `ViewFn`, `AnyView`, a composable lambda, an ArkUI builder) and slots
|
||||
(`React.ReactNode`, Leptos `ViewFn`, `AnyView`, a composable lambda, an ArkUI builder, a `Widget`) and slots
|
||||
optional content.
|
||||
Every variant of a set shares one definition; in React, Vue and Leptos the other variants'
|
||||
differences become CSS rules on `data-*` attributes. A Vue template holds no arbitrary
|
||||
@@ -1549,13 +1549,13 @@ permissions: without it nothing is downloaded); `plugin.directory.publish {path
|
||||
state="Hover" label="Buy" />`, `<Button state=ButtonState::Hover label="Buy" />`,
|
||||
`<Button state="Hover" />`, `Button(state:
|
||||
.hover, label: "Buy")`, `Button(state = ButtonState.Hover, label = "Buy")`, `Button({ state:
|
||||
ButtonState.Hover, label: "Buy" })`. Names are escaped and
|
||||
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
|
||||
are snake_case, Dart enum members lowerCamel; 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`, `vue`, `swiftui`, `compose`, `arkui`): `codeConnect.set {id?, language?, importPath?,
|
||||
as, per language (`react`, `leptos`, `vue`, `swiftui`, `compose`, `arkui`, `flutter`): `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
|
||||
|
||||
@@ -623,7 +623,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/Vue/SwiftUI/Compose/ArkUI
|
||||
was marked ready (before / after side by side or overlaid, property-level diffs), CSS/HTML/React/Vue/Leptos/SwiftUI/Compose/ArkUI/Flutter
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user