mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-26 11:26:50 -06:00
Merge pull request #1011 from lucasmartins-ai/feat/tailwind-output
Dev Mode: React + Tailwind page code and Tailwind theme export
This commit is contained in:
@@ -18,7 +18,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
vec![spec!(
|
||||
"inspect.code",
|
||||
"Code for layer",
|
||||
"{id?, language?: css|html|react|leptos|swiftui|compose (default css; html, react and leptos are the whole layer tree; components take their properties as parameters, instances pass their values; a component connected to code (codeConnect.set) gives its import and snippet instead), unit?: px|rem (CSS, HTML, React and Leptos; default px), rootFontSize? (rem: pixels per rem, default 16)} → {code, language, connected}",
|
||||
"{id?, language?: css|html|react|leptos|tailwind|swiftui|compose (default css; html, react and leptos are the whole layer tree; components take their properties as parameters, instances pass their values; a component connected to code (codeConnect.set) gives its import and snippet instead; 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 and Leptos; default px), rootFontSize? (rem: pixels per rem, default 16)} → {code, language, connected}",
|
||||
false,
|
||||
|s, p| {
|
||||
let cmd = "inspect.code";
|
||||
@@ -40,7 +40,14 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
return Err(bad(cmd, "no such layer"));
|
||||
}
|
||||
let connected = crate::code_connect::link(doc, id, lang).is_some();
|
||||
let code = code_for(doc, id, lang);
|
||||
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,
|
||||
@@ -61,18 +68,22 @@ pub enum CodeLang {
|
||||
React,
|
||||
/// A Leptos component (`view!` markup) with its stylesheet.
|
||||
Leptos,
|
||||
/// A React function component styled with Tailwind CSS v4 utilities (no stylesheet).
|
||||
Tailwind,
|
||||
SwiftUi,
|
||||
Compose,
|
||||
}
|
||||
|
||||
impl CodeLang {
|
||||
pub const ALL: [CodeLang; 6] = [CodeLang::Css, CodeLang::Html, CodeLang::React, CodeLang::Leptos, CodeLang::SwiftUi, CodeLang::Compose];
|
||||
pub const ALL: [CodeLang; 7] =
|
||||
[CodeLang::Css, CodeLang::Html, CodeLang::React, CodeLang::Leptos, CodeLang::Tailwind, CodeLang::SwiftUi, CodeLang::Compose];
|
||||
pub fn id(self) -> &'static str {
|
||||
match self {
|
||||
CodeLang::Css => "css",
|
||||
CodeLang::Html => "html",
|
||||
CodeLang::React => "react",
|
||||
CodeLang::Leptos => "leptos",
|
||||
CodeLang::Tailwind => "tailwind",
|
||||
CodeLang::SwiftUi => "swiftui",
|
||||
CodeLang::Compose => "compose",
|
||||
}
|
||||
@@ -83,11 +94,13 @@ impl CodeLang {
|
||||
CodeLang::Html => "HTML + CSS",
|
||||
CodeLang::React => "React",
|
||||
CodeLang::Leptos => "Leptos",
|
||||
CodeLang::Tailwind => "React + Tailwind",
|
||||
CodeLang::SwiftUi => "SwiftUI",
|
||||
CodeLang::Compose => "Compose",
|
||||
}
|
||||
}
|
||||
/// Languages whose lengths are CSS pixels (they can be shown in rem).
|
||||
/// Languages whose lengths are CSS pixels that can be shown in rem (not Tailwind: its lengths
|
||||
/// are utility classes).
|
||||
pub fn is_web(self) -> bool {
|
||||
matches!(self, CodeLang::Css | CodeLang::Html | CodeLang::React | CodeLang::Leptos)
|
||||
}
|
||||
@@ -199,6 +212,7 @@ pub fn code_for(doc: &Document, id: NodeId, lang: CodeLang) -> String {
|
||||
match lang {
|
||||
CodeLang::Css => css(doc, id),
|
||||
CodeLang::Html => html(doc, id),
|
||||
CodeLang::Tailwind => crate::tailwind::code(doc, id, None),
|
||||
CodeLang::React | CodeLang::Leptos | CodeLang::SwiftUi | CodeLang::Compose => crate::component_codegen::code(doc, id, lang),
|
||||
}
|
||||
}
|
||||
@@ -321,25 +335,32 @@ pub(crate) fn indent(depth: usize) -> String {
|
||||
|
||||
/// The CSS custom properties generated code uses: name → fallback value, in first-use order.
|
||||
#[derive(Default)]
|
||||
pub(crate) struct Tokens(pub Vec<(String, String)>);
|
||||
pub(crate) struct Tokens {
|
||||
vars: Vec<(String, String)>,
|
||||
/// Name variables as Tailwind theme variables (`--color-accent`), not by their CSS name.
|
||||
pub tailwind: bool,
|
||||
/// The file's theme names (computed on first use).
|
||||
theme: Option<crate::tailwind::Theme>,
|
||||
}
|
||||
|
||||
impl Tokens {
|
||||
/// `var(--name, value)`, remembering the variable.
|
||||
fn var(&mut self, variable: &Variable, value: String) -> String {
|
||||
let name = css_var_of(variable);
|
||||
fn var(&mut self, doc: &Document, variable: &Variable, value: String) -> String {
|
||||
let name =
|
||||
if self.tailwind { self.theme.get_or_insert_with(|| crate::tailwind::Theme::new(doc)).name(variable) } else { css_var_of(variable) };
|
||||
let out = format!("var({name}, {value})");
|
||||
if !self.0.iter().any(|(n, _)| *n == name) {
|
||||
self.0.push((name, value));
|
||||
if !self.vars.iter().any(|(n, _)| *n == name) {
|
||||
self.vars.push((name, value));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// A `:root` rule declaring the variables (empty without any).
|
||||
pub fn root_rule(&self) -> String {
|
||||
if self.0.is_empty() {
|
||||
if self.vars.is_empty() {
|
||||
return String::new();
|
||||
}
|
||||
let decls: String = self.0.iter().map(|(n, v)| format!(" {n}: {v};\n")).collect();
|
||||
let decls: String = self.vars.iter().map(|(n, v)| format!(" {n}: {v};\n")).collect();
|
||||
format!(":root {{\n{decls}}}\n")
|
||||
}
|
||||
}
|
||||
@@ -370,7 +391,7 @@ pub fn css_var_of(v: &Variable) -> String {
|
||||
/// `value`, or a reference to the variable bound to the first of `fields` that has one.
|
||||
fn tok(doc: &Document, n: &Node, fields: &[&str], value: String, t: &mut Tokens) -> String {
|
||||
match fields.iter().find_map(|f| crate::devinspect::bound_variable(doc, n, f)) {
|
||||
Some(v) => t.var(v, value),
|
||||
Some(v) => t.var(doc, v, value),
|
||||
None => value,
|
||||
}
|
||||
}
|
||||
@@ -664,6 +685,14 @@ pub(crate) struct Markup<'c, 'a> {
|
||||
/// Each layer's path in the tree (names with sibling counts), class and declarations, so
|
||||
/// variants can be compared layer by layer.
|
||||
pub rules: Vec<(String, String, String)>,
|
||||
/// Instances as their own (overridden) layers instead of component calls, except those
|
||||
/// connected to code (page code).
|
||||
pub inline: bool,
|
||||
/// Auto layout frames and text as fluid boxes (`width: 100%` up to their width) for pages.
|
||||
pub fluid: bool,
|
||||
/// With `fluid`, other layers at least this share of the root's width are fluid too (a
|
||||
/// divider, a banner image); smaller ones (icons) keep their size. Default: none.
|
||||
pub fluid_leaves: f64,
|
||||
}
|
||||
|
||||
impl<'c, 'a> Markup<'c, 'a> {
|
||||
@@ -678,6 +707,9 @@ impl<'c, 'a> Markup<'c, 'a> {
|
||||
root_class: None,
|
||||
root_attrs: String::new(),
|
||||
rules: Vec::new(),
|
||||
inline: false,
|
||||
fluid: false,
|
||||
fluid_leaves: f64::INFINITY,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -759,7 +791,10 @@ impl<'c, 'a> Markup<'c, 'a> {
|
||||
}
|
||||
}
|
||||
// Nested instances are calls (component code), in a wrapper that places them when needed.
|
||||
if self.code() && !root && matches!(n.kind, NodeKind::Instance { .. }) {
|
||||
// Page code inlines instances, except those connected to code (codeConnect.set): their
|
||||
// snippet, e.g. a design-system `<Button>`.
|
||||
let connected = self.inline && crate::code_connect::link(doc, n.id, CodeLang::React).is_some();
|
||||
if self.code() && (!self.inline || connected) && !root && matches!(n.kind, NodeKind::Instance { .. }) {
|
||||
let at = depth + usize::from(!decls.is_empty());
|
||||
let run = if self.lang == CodeLang::Leptos { ".run()" } else { "" };
|
||||
let inner = match self.ctx.param(n, "mainComponent") {
|
||||
@@ -779,6 +814,11 @@ impl<'c, 'a> Markup<'c, 'a> {
|
||||
let hugs = n.layout.is_some() || matches!(n.kind, NodeKind::Text(_));
|
||||
let hug = (hugs && n.child_layout.sizing_h == Sizing::Hug, hugs && n.child_layout.sizing_v == Sizing::Hug);
|
||||
let mut own = css_decls(doc, n, &mut self.tokens, hug);
|
||||
let page = self.ctx.doc.node(self.ctx.root).map_or(f64::INFINITY, |r| r.width);
|
||||
if self.fluid && (n.layout.is_some() || matches!(n.kind, NodeKind::Text(_)) || (!root && n.width >= page * self.fluid_leaves)) {
|
||||
// Full width, or sized by the flex row (`flex: 1`): no cap needed.
|
||||
own = fluid(&own, n.width >= page - 0.5 || decls.contains("flex: 1 1 0;"), n.layout.is_some());
|
||||
}
|
||||
if n.layout.is_none() && !n.children.is_empty() && !root && in_flow {
|
||||
own.push_str("position: relative;\n");
|
||||
}
|
||||
@@ -845,6 +885,28 @@ impl<'c, 'a> Markup<'c, 'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// `decls` for a box that follows the page: a fixed width becomes `width: 100%` capped at that
|
||||
/// width (or just the full width for boxes as wide as the page), and a frame's fixed height a
|
||||
/// minimum, so content can wrap and grow.
|
||||
// ponytail: width-based heuristic; fixed-size leaves (icons, images, shapes) keep their pixels.
|
||||
fn fluid(decls: &str, full: bool, frame: bool) -> String {
|
||||
let mut out = String::with_capacity(decls.len() + 32);
|
||||
for line in decls.lines() {
|
||||
if let Some(w) = line.strip_prefix("width: ") {
|
||||
out.push_str("width: 100%;\n");
|
||||
if !full {
|
||||
out.push_str(&format!("max-width: {w}\n"));
|
||||
}
|
||||
} else if let Some(h) = line.strip_prefix("height: ").filter(|_| frame) {
|
||||
out.push_str(&format!("min-height: {h}\n"));
|
||||
} else {
|
||||
out.push_str(line);
|
||||
out.push('\n');
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ SwiftUI
|
||||
|
||||
fn swift_color(c: Color) -> String {
|
||||
|
||||
Reference in New Issue
Block a user