//! 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 //! [`crate::component_codegen`]); variables bound to a layer become CSS custom properties. The //! output is a starting point, not a pixel-exact translation. use figcraft_color::{Color, PaintKind}; use figcraft_doc::{ ContentAlign, CrossAlign, Document, EffectKind, LayoutMode, LetterSpacing, LineHeight, MainAlign, Node, NodeId, NodeKind, Shadow, Sizing, TextAlignH, TextCase, TextDecoration, Variable, }; use serde_json::json; use crate::cmd::CommandSpec; use crate::component_codegen::{Component, Param}; use crate::params::{get, id_of, num_or, string, targets}; use crate::{bad, spec}; mod arkui; mod compose; mod css; mod html; mod swift; pub(crate) use arkui::arkui; pub(crate) use compose::{compose, kotlin_color}; pub(crate) use css::Tokens; #[cfg(test)] pub(crate) use css::css_decls; use css::{css, tok}; pub use css::{css_var_name, css_var_of}; use html::html; pub(crate) use html::{Markup, class_name}; pub(crate) use swift::swift; pub fn specs() -> Vec { 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}", 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"))?, None => CodeLang::Css, }; let rem = match string(p, "unit").map(|u| u.to_ascii_lowercase()).as_deref() { None | Some("px") => None, Some("rem") => Some(num_or(p, "rootFontSize", 16.0)).filter(|r| r.is_finite() && *r > 0.0 && *r <= 1000.0), Some(_) => return Err(bad(cmd, "unit: px|rem")), }; if string(p, "unit").is_some_and(|u| u.eq_ignore_ascii_case("rem")) && rem.is_none() { return Err(bad(cmd, "rootFontSize: a positive number of pixels (up to 1000)")); } let doc = s.doc()?; if !doc.contains(id) { return Err(bad(cmd, "no such layer")); } let connected = crate::code_connect::link(doc, id, lang).is_some(); let mobile = match get(p, "mobile") { None | Some(serde_json::Value::Null) => None, Some(v) => Some(id_of(v).filter(|m| doc.contains(*m)).ok_or_else(|| bad(cmd, "mobile: the id of a mobile frame"))?), }; let code = match (lang, mobile) { (CodeLang::Tailwind, Some(m)) => crate::tailwind::code(doc, id, Some(m)), _ => code_for(doc, id, lang), }; let code = match rem { Some(root) if lang.is_web() => px_to_rem(&code, root), _ => code, }; Ok(json!({"code": code, "language": lang.id(), "connected": connected})) } )] } /// Languages the Inspect panel offers. #[derive(Clone, Copy, Debug, PartialEq, Eq, Default)] pub enum CodeLang { #[default] Css, /// The whole layer tree as nested HTML elements with a stylesheet. Html, /// A React function component (TSX) with its stylesheet. React, /// A Leptos component (`view!` markup) with its stylesheet. Leptos, /// A Vue single-file component (`