Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changepacks/changepack_log_feedback_interactions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"changes": {
"crates/devup-mcp/Cargo.toml": "Minor"
},
"note": "Expose bounded interaction, prototype, state, shared-component and widget evidence in Figma exports, with required wiring review and completion evidence. Merge existing UI/route inventories and environment key names without values; validate semantic controls and static destinations, report source/rendered href coverage and compare explicit prototype navigation bindings in feature traces. Add frontend guidance for accessible links, unresolved destinations, visual verification and Windows tooling.",
"date": "2026-10-14T09:45:00+00:00"
}
9 changes: 9 additions & 0 deletions .changepacks/changepack_log_feedback_release_integration.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"changes": {
"crates/devup-mcp/Cargo.toml": "Patch",
"crates/devup-mcp-figma/Cargo.toml": "Patch",
"crates/devup-mcp-devup-ui/Cargo.toml": "Patch"
},
"note": "Integrate the settled theme, nested asset and interaction feedback work. Flag constant navigation underlines as unverified info, preserve MCP schema compatibility, bound text diagnostic excerpts, deliver complete bulky diagnostics/verdict/layout metadata through hash-verified resources with explicit preview omissions and required review, and reserve context headroom for final MCP wire sizing. Discover tall named breakpoint wrappers and provide valid direct-frame responsive guidance without claiming implementation. Verify final code/theme/file/resource hashes and enforce nested asset/async identity source regressions in existing CI; include a read-only Prime verification driver and exact coverage/limitations.",
"date": "2026-10-14T10:15:00Z"
}
8 changes: 8 additions & 0 deletions .changepacks/changepack_log_feedback_theme_responsive.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"changes": {
"crates/devup-mcp/Cargo.toml": "Patch",
"crates/devup-mcp-devup-ui/Cargo.toml": "Patch"
},
"note": "Keep independent theme tokens when a text style is unreadable, report named style/node diagnostics, support used-only themes and existing-theme missing-token diffs, elide proven typography duplicates, and discover responsive frame groups with breakpoint completion guidance while preserving percentage and fill/hug mappings.",
"date": "2026-10-14T18:39:56+09:00"
}
8 changes: 8 additions & 0 deletions .changepacks/changepack_log_nested_asset_exports.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"changes": {
"crates/devup-mcp-figma/Cargo.toml": "Patch",
"crates/devup-mcp/Cargo.toml": "Patch"
},
"note": "Nested instance IDs now parse and round-trip through encoded Figma and bridge URLs, enabling an asset-only export of an instance descendant. Asset exports retain the real lookup/render exception, node type, elapsed time and bounded native/fallback stage diagnostics; deterministic renderer failures are no longer advertised as retryable and failed manifests omit path/outputPath. DEVUP_ASSET_TIMEOUT_MS configures a bounded per-asset lookup/render budget (default 90000 ms, range 1000–600000), also carried through export fragment reads; polling reports elapsed time on running calls. A hung SVG renderer leaves time for a read-only fallback using the requested subtree's actual rendered fill geometry, centered stroke outlines, colors, opacity, winding rules and transforms. Unsupported masks, clipping, gradients, effects, text, non-center/dashed strokes and ambiguous bounds fail explicitly rather than changing the picture; native export and normal PNG/PDF assets stay unchanged. Fragment re-exports use the same fallback and preserve its original failure evidence. Rebuild and reopen the plugin bundle, and update the bridge host, to enable the new script and timeout behavior. Instance snapshots additionally collect async main-component identity and preserve lookup errors for shared-component review. Live Prime SVG, PNG at scale 2 and PDF exports were byte-identical before/after the server change; the historical repeated renderer failure did not recur in this session and its cause remains unproven.",
"date": "2026-10-14T18:50:00+09:00"
}
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,6 @@
# directory, which was cleared overnight and took the previous copies with it.
/fixtures/local-tools/

/docs/
/feedback.txt
/devup-mcp-feedback/
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 14 additions & 8 deletions README.md

Large diffs are not rendered by default.

37 changes: 37 additions & 0 deletions crates/devup-mcp-devup-ui/src/codegen/component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ pub struct CodegenOptions {
pub include_diagnostics: bool,
pub inline_instances: bool,
pub text_style_tokens: std::collections::BTreeMap<String, String>,
/// Materialized typography values from the same resources as devup.json.
/// Names alone cannot prove that a local font property is redundant.
pub typography_values: BTreeMap<String, serde_json::Value>,
pub variable_tokens: std::collections::BTreeMap<String, String>,
pub root_layout: RootLayout,
/// Name every asset after the node it came from, rather than after its
Expand Down Expand Up @@ -74,6 +77,36 @@ impl CodegenOptions {
self.text_style_tokens = named_tokens(styles, "styles");
self.variable_tokens = named_tokens(variables, "variables");
self.length_tokens = length_tokens(variables);
let mut snapshot = variables
.and_then(|result| crate::theme::variable_snapshot_from_result(result).ok())
.unwrap_or_else(|| {
serde_json::from_value(serde_json::json!({})).expect("empty variable snapshot")
});
if let Some(styles) =
styles.and_then(|result| crate::theme::variable_snapshot_from_result(result).ok())
{
for style in styles.styles {
if !snapshot
.styles
.iter()
.any(|existing| existing.id == style.id)
{
snapshot.styles.push(style);
}
}
}
self.typography_values =
crate::theme::generate_devup_json(&snapshot, crate::theme::ThemeScope::File)
.ok()
.and_then(|output| serde_json::from_str::<serde_json::Value>(&output.json).ok())
.and_then(|value| {
value
.pointer("/theme/typography")
.and_then(serde_json::Value::as_object)
.cloned()
})
.map(|values| values.into_iter().collect())
.unwrap_or_default();
self
}
}
Expand Down Expand Up @@ -1073,6 +1106,7 @@ fn generate_node_marked(
asset_names_per_node: options.asset_names_per_node,
inline_instances: options.inline_instances,
text_style_tokens: options.text_style_tokens.clone(),
typography_values: options.typography_values.clone(),
variable_tokens: options.variable_tokens.clone(),
length_tokens: options.length_tokens.clone(),
root_layout: options.root_layout,
Expand Down Expand Up @@ -1419,6 +1453,7 @@ struct Context {
inline_instances: bool,
instance_boolean_properties: Vec<BTreeMap<String, bool>>,
text_style_tokens: std::collections::BTreeMap<String, String>,
typography_values: BTreeMap<String, serde_json::Value>,
variable_tokens: std::collections::BTreeMap<String, String>,
length_tokens: BTreeMap<String, LengthToken>,
root_layout: RootLayout,
Expand Down Expand Up @@ -1697,6 +1732,7 @@ fn render_node(
text::push_text_props(
&view,
&context.text_style_tokens,
&context.typography_values,
&context.variable_tokens,
&mut context.used_tokens,
&mut props,
Expand Down Expand Up @@ -1794,6 +1830,7 @@ fn render_node(
let children = text::render_text_children(
&view,
&context.text_style_tokens,
&context.typography_values,
&context.variable_tokens,
&mut context.used_tokens,
depth + 1,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ pub(crate) fn property_derivations(
"text" => text::push_text_props(
&node.typed_view(),
&options.text_style_tokens,
&options.typography_values,
&options.variable_tokens,
&mut tokens,
&mut props,
Expand Down
23 changes: 19 additions & 4 deletions crates/devup-mcp-devup-ui/src/codegen/responsive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,7 @@
//! therefore a report, not a switch.
//!
//! The rules here are ported from `devup-figma-plugin` at the commit this
//! repo's corpus pins — `src/codegen/responsive/` — and `docs/
//! responsive-merge-rules.md` says which function each came from and where
//! this repo deliberately differs.
//! repo's corpus pins — `src/codegen/responsive/`.

use std::collections::{BTreeMap, BTreeSet, VecDeque};

Expand Down Expand Up @@ -284,7 +282,24 @@ impl DivergenceReason {

fn rank_of(name: &str) -> Option<usize> {
let name = name.trim().to_ascii_lowercase();
BREAKPOINT_NAMES.iter().position(|known| *known == name)
BREAKPOINT_NAMES
.iter()
.position(|known| *known == name)
.or_else(|| {
// Some Sections label their middle design by a width range instead
// of "tablet". Recognize only explicit numeric ranges; a number in
// an unrelated screen name is not a breakpoint declaration.
let (start, end) = name.split_once(['~', '–'])?;
let (start, end) = (
start.trim().parse::<u32>().ok()?,
end.trim().parse::<u32>().ok()?,
);
(start > 0 && end >= start).then_some(1)
})
}

pub fn is_breakpoint_name(name: &str) -> bool {
rank_of(name).is_some()
}

/// Whether roots with these names and widths could be a breakpoint family.
Expand Down
73 changes: 67 additions & 6 deletions crates/devup-mcp-devup-ui/src/codegen/text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use super::{
pub(super) fn push_text_props(
view: &TypedNode<'_>,
text_style_tokens: &BTreeMap<String, String>,
typography_values: &BTreeMap<String, Value>,
variable_tokens: &BTreeMap<String, String>,
used_tokens: &mut BTreeSet<String>,
props: &mut Vec<Prop>,
Expand All @@ -22,6 +23,7 @@ pub(super) fn push_text_props(
let segment = default_segment(view);
let typography = segment
.and_then(|value| value.get("textStyleId"))
.or_else(|| view.value("textStyleId"))
.and_then(Value::as_str)
.and_then(|id| text_style_tokens.get(id));
let value = |field: &str| {
Expand Down Expand Up @@ -146,6 +148,9 @@ pub(super) fn push_text_props(
);
string_prop(props, "paintOrder", "stroke fill");
}
if let Some(value) = typography.and_then(|token| typography_values.get(token)) {
elide_typography_duplicates(props, value);
}
if let Some(list) = segment
.and_then(|value| value.get("listOptions"))
.and_then(Value::as_object)
Expand All @@ -162,6 +167,40 @@ pub(super) fn push_text_props(
}
}

fn elide_typography_duplicates(props: &mut Vec<Prop>, typography: &Value) {
// A responsive property can be omitted only when every effective slot
// defines that same value, including the first width. Never select an
// active viewport from a text node's own width.
let values = match typography {
Value::Object(value) => vec![value],
Value::Array(slots) if slots.first().is_some_and(Value::is_object) => {
let Some(values) = slots
.iter()
.filter(|slot| !slot.is_null())
.map(Value::as_object)
.collect::<Option<Vec<_>>>()
else {
return;
};
values
}
_ => return,
};
props.retain(|(name, PropValue::String(value))| {
if !matches!(name.as_str(), "fontSize" | "fontWeight" | "lineHeight") {
return true;
}
!values.iter().all(|values| {
values.get(name).is_some_and(|expected| {
expected.as_str().is_some_and(|expected| expected == value)
|| expected
.as_f64()
.is_some_and(|expected| value.parse::<f64>().ok() == Some(expected))
})
})
});
}

fn is_resolved_value(value: &Value) -> bool {
!value.as_object().is_some_and(|object| {
["$unsupported", "$undefined", "$truncated", "$error"]
Expand Down Expand Up @@ -279,6 +318,7 @@ fn replace_prop(props: &mut Vec<Prop>, name: &str, value: String) {
pub(super) fn render_text_children(
view: &TypedNode<'_>,
text_style_tokens: &BTreeMap<String, String>,
typography_values: &BTreeMap<String, Value>,
variable_tokens: &BTreeMap<String, String>,
used_tokens: &mut BTreeSet<String>,
depth: usize,
Expand Down Expand Up @@ -337,6 +377,18 @@ pub(super) fn render_text_children(
.iter()
.any(|(default_name, default_value)| default_name == name && default_value == value)
});
// A different run style establishes its own metrics. Equal metrics
// can be omitted there; same-style runs keep local overrides relative
// to the parent, even when a run happens to equal the token again.
if segment_props.iter().any(|(name, _)| name == "typography")
&& let Some(value) = segment
.get("textStyleId")
.and_then(Value::as_str)
.and_then(|id| text_style_tokens.get(id))
.and_then(|token| typography_values.get(token))
{
elide_typography_duplicates(&mut segment_props, value);
}
let content = escape_jsx_text(value);
if segment_props.is_empty() {
if content.is_empty() {
Expand Down Expand Up @@ -648,19 +700,28 @@ pub(super) fn whitespace_collapse_diagnostic(
if !collapses {
return None;
}
let mut details = serde_json::json!({
"originalValue": characters.chars().take(800).collect::<String>(),
"classification": "css-whitespace-collapse",
"whiteSpace": if view.number("maxLines") == Some(1.0) { "nowrap" } else { "normal" },
"nextAction": "Review intentional spaces or tabs in Figma and choose an explicit whitespace-preserving style if required."
});
let count = characters.chars().count();
if count > 800 {
details["originalValueTruncated"] = serde_json::json!(true);
details["originalValueCharCount"] = serde_json::json!(count);
details["fullValueSource"] = serde_json::json!(
"The original node characters in rawSnapshot; generated TSX retains the complete text."
);
}
Some(devup_mcp_figma::Diagnostic {
code: "DEVUP_CODEGEN_TEXT_WHITESPACE_COLLAPSE".into(),
node_id: Some(view.id().to_owned()),
property: Some("characters".into()),
message: "Text characters are retained in JSX, but CSS collapses source spaces or tabs."
.into(),
fidelity_impact: Some(devup_mcp_figma::FidelityImpact::Lossy),
details: Some(serde_json::json!({
"originalValue": characters,
"classification": "css-whitespace-collapse",
"whiteSpace": if view.number("maxLines") == Some(1.0) { "nowrap" } else { "normal" },
"nextAction": "Review intentional spaces or tabs in Figma and choose an explicit whitespace-preserving style if required."
})),
details: Some(details),
..devup_mcp_figma::Diagnostic::default()
})
}
Expand Down
2 changes: 2 additions & 0 deletions crates/devup-mcp-devup-ui/src/codegen/variant.rs
Original file line number Diff line number Diff line change
Expand Up @@ -546,6 +546,7 @@ fn project_tree_inner(
text::push_text_props(
&view,
&options.text_style_tokens,
&options.typography_values,
&options.variable_tokens,
&mut used_tokens,
&mut props,
Expand Down Expand Up @@ -596,6 +597,7 @@ fn project_tree_inner(
text::render_text_children(
&view,
&options.text_style_tokens,
&options.typography_values,
&options.variable_tokens,
&mut used_tokens,
0,
Expand Down
Loading
Loading